refactor:
- add single point of entry for all per-frame updates (GameLoop), add centralized event system (GameEvents). Controllers emit events instead of calling each other directly. - remove unused images, optimize path and imports - rework font usege with using template method
@@ -1,8 +1,9 @@
|
|||||||
import { UpdateController } from "@24tools/playable_template";
|
|
||||||
import { Vector3 } from "three";
|
import { Vector3 } from "three";
|
||||||
import { PlayerC } from "./PlayerC";
|
import { PlayerC } from "./PlayerC";
|
||||||
import { Crate, LootableC } from "./LootableC";
|
import { Crate, LootableC } from "./LootableC";
|
||||||
import { Trigger } from "./TriggerC";
|
import { Trigger } from "./TriggerC";
|
||||||
|
import { GameEvents } from "../core/GameEvents";
|
||||||
|
import { GameLoop } from "../core/GameLoop";
|
||||||
|
|
||||||
const ATTACK_DAMAGE = 10; // damage per bat-tip touch
|
const ATTACK_DAMAGE = 10; // damage per bat-tip touch
|
||||||
const CONTACT_DIST = 0.8; // bat tip → crate distance that counts as a touch
|
const CONTACT_DIST = 0.8; // bat tip → crate distance that counts as a touch
|
||||||
@@ -35,7 +36,7 @@ export class CombatC {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateController.Instance.onUpdate.addDelegate(() => this.update());
|
GameLoop.register(() => this.update());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static update() {
|
private static update() {
|
||||||
@@ -85,7 +86,7 @@ export class CombatC {
|
|||||||
crate.root.getWorldPosition(_scratch);
|
crate.root.getWorldPosition(_scratch);
|
||||||
if (Math.hypot(tip.x - _scratch.x, tip.z - _scratch.z) <= CONTACT_DIST) {
|
if (Math.hypot(tip.x - _scratch.x, tip.z - _scratch.z) <= CONTACT_DIST) {
|
||||||
this.hitThisSwing.add(crate);
|
this.hitThisSwing.add(crate);
|
||||||
LootableC.damageCrate(crate, ATTACK_DAMAGE);
|
GameEvents.onCrateHit.Invoke({ crate, damage: ATTACK_DAMAGE });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.pruneBroken();
|
this.pruneBroken();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { CameraC_internal, UpdateController } from "@24tools/playable_template";
|
import { CameraC_internal } from "@24tools/playable_template";
|
||||||
import { Object3D, Vector3 } from "three";
|
import { Object3D, Vector3 } from "three";
|
||||||
|
import { GameLoop } from "../core/GameLoop";
|
||||||
|
|
||||||
const _targetWorldPos = new Vector3();
|
const _targetWorldPos = new Vector3();
|
||||||
const _lookAheadTarget = new Vector3();
|
const _lookAheadTarget = new Vector3();
|
||||||
@@ -29,7 +30,7 @@ export class FollowCameraC {
|
|||||||
// Immediately place camera relative to the actual character position
|
// Immediately place camera relative to the actual character position
|
||||||
// (not config origin) so the character is visible from frame one.
|
// (not config origin) so the character is visible from frame one.
|
||||||
camera.position.copy(_targetWorldPos).add(this.offset);
|
camera.position.copy(_targetWorldPos).add(this.offset);
|
||||||
UpdateController.Instance.onUpdate.addDelegate((delta) => this.update(delta));
|
GameLoop.register((delta) => this.update(delta));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-syncs base offset after a config-driven camera move (e.g. resize).
|
// Re-syncs base offset after a config-driven camera move (e.g. resize).
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Box3, DoubleSide, Group, Material, Mesh, Object3D, Vector3 } from "three";
|
import { Box3, DoubleSide, Group, Material, Mesh, Object3D, Vector3 } from "three";
|
||||||
import * as TWEEN from "@tweenjs/tween.js";
|
import * as TWEEN from "@tweenjs/tween.js";
|
||||||
import { UpdateController, CameraC_internal } from "@24tools/playable_template";
|
import { CameraC_internal } from "@24tools/playable_template";
|
||||||
import { ThreeC } from "./ThreeC";
|
import { ThreeC } from "./ThreeC";
|
||||||
|
import { GameEvents } from "../core/GameEvents";
|
||||||
|
import { GameLoop } from "../core/GameLoop";
|
||||||
import type { Crate } from "./LootableC";
|
import type { Crate } from "./LootableC";
|
||||||
|
|
||||||
// --- Placement ---
|
// --- Placement ---
|
||||||
@@ -54,7 +56,12 @@ export class HealthBarC {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.protoNode = prototype;
|
this.protoNode = prototype;
|
||||||
UpdateController.Instance.onUpdate.addDelegate((delta: number) => this.update(delta));
|
|
||||||
|
// Listen for health display events
|
||||||
|
GameEvents.onHealthDisplay.addDelegate(({ crate, healthFraction }) => this.showDamage(crate, healthFraction));
|
||||||
|
GameEvents.onHealthHide.addDelegate((crate) => this.hide(crate));
|
||||||
|
|
||||||
|
GameLoop.register((delta: number) => this.update(delta));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Show/refresh the bar for a crate at the given health fraction (0..1). */
|
/** Show/refresh the bar for a crate at the given health fraction (0..1). */
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
import { UpdateController } from "@24tools/playable_template";
|
|
||||||
import { ensurePassionOne } from "../fonts/passionOne";
|
|
||||||
import { PlayerC } from "./PlayerC";
|
import { PlayerC } from "./PlayerC";
|
||||||
import * as TWEEN from "@tweenjs/tween.js";
|
import * as TWEEN from "@tweenjs/tween.js";
|
||||||
|
import { GameLoop } from "../core/GameLoop";
|
||||||
import {
|
import {
|
||||||
zombieHeadUrl,
|
images
|
||||||
woodPanelUrl,
|
} from "../resources/images/images";
|
||||||
metalPanelUrl,
|
|
||||||
toolPanelUrl,
|
|
||||||
toolIconUrl,
|
|
||||||
} from "../resources/OnbordingUI/onboardingUI";
|
|
||||||
|
|
||||||
// Onboarding/invasion pacing.
|
// Onboarding/invasion pacing.
|
||||||
const MOVE_TO_HURRY_S = 5; // after the player starts moving, wait this long…
|
const MOVE_TO_HURRY_S = 5; // after the player starts moving, wait this long…
|
||||||
@@ -51,8 +46,6 @@ export class HudC {
|
|||||||
private static tweens = new TWEEN.Group();
|
private static tweens = new TWEEN.Group();
|
||||||
private static buttonTween: TWEEN.Tween<{ x: number; y: number }> | null = null;
|
private static buttonTween: TWEEN.Tween<{ x: number; y: number }> | null = null;
|
||||||
static init() {
|
static init() {
|
||||||
ensurePassionOne();
|
|
||||||
|
|
||||||
const hud = document.createElement("div");
|
const hud = document.createElement("div");
|
||||||
hud.id = "hud";
|
hud.id = "hud";
|
||||||
hud.innerHTML = this.markup();
|
hud.innerHTML = this.markup();
|
||||||
@@ -71,7 +64,7 @@ export class HudC {
|
|||||||
this.showTutorial("DRAG TO MOVE");
|
this.showTutorial("DRAG TO MOVE");
|
||||||
this.invasion?.classList.add("hud-invasion--hidden", "hud-invasion--hurry-phase");
|
this.invasion?.classList.add("hud-invasion--hidden", "hud-invasion--hurry-phase");
|
||||||
|
|
||||||
UpdateController.Instance.onUpdate.addDelegate((delta: number) => {
|
GameLoop.register((delta: number) => {
|
||||||
this.tick(delta);
|
this.tick(delta);
|
||||||
this.tweens.update();
|
this.tweens.update();
|
||||||
});
|
});
|
||||||
@@ -90,22 +83,22 @@ export class HudC {
|
|||||||
<div class="hud-invasion__row">
|
<div class="hud-invasion__row">
|
||||||
<div class="hud-invasion__bar">
|
<div class="hud-invasion__bar">
|
||||||
<div class="hud-invasion__track"><div class="hud-invasion__fill"></div></div>
|
<div class="hud-invasion__track"><div class="hud-invasion__fill"></div></div>
|
||||||
<div class="hud-invasion__head-wrap"><img class="hud-invasion__head" src="${zombieHeadUrl}" /></div>
|
<div class="hud-invasion__head-wrap"><img class="hud-invasion__head" src="${images.zombieHeadUrl}" /></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hud-resources">
|
<div class="hud-resources">
|
||||||
<div id="wood-ui" class="hud-resources__panel" style="background-image:url(${woodPanelUrl})">
|
<div id="wood-ui" class="hud-resources__panel" style="background-image:url(${images.woodPanelUrl})">
|
||||||
<div id="wood-count" class="hud-resources__count">0</div>
|
<div id="wood-count" class="hud-resources__count">0</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hud-resources__panel" style="background-image:url(${metalPanelUrl})">
|
<div class="hud-resources__panel" style="background-image:url(${images.metalPanelUrl})">
|
||||||
<div class="hud-resources__count">0</div>
|
<div class="hud-resources__count">0</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hud-weapon" style="background-image:url(${toolPanelUrl})">
|
<div class="hud-weapon" style="background-image:url(${images.toolPanelUrl})">
|
||||||
<img class="hud-weapon__icon" src="${toolIconUrl}" />
|
<img class="hud-weapon__icon" src="${images.toolIconUrl}" />
|
||||||
<div class="hud-weapon__lvl">LVL 0</div>
|
<div class="hud-weapon__lvl">LVL 0</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -197,7 +190,7 @@ export class HudC {
|
|||||||
|
|
||||||
const card = document.createElement("div");
|
const card = document.createElement("div");
|
||||||
card.className = "hud-endcard";
|
card.className = "hud-endcard";
|
||||||
card.innerHTML = `<img class="hud-endcard__zombie" src="${zombieHeadUrl}" />`;
|
card.innerHTML = `<img class="hud-endcard__zombie" src="${images.zombieHeadUrl}" />`;
|
||||||
this.root.appendChild(card);
|
this.root.appendChild(card);
|
||||||
|
|
||||||
const zombie = card.querySelector(".hud-endcard__zombie") as HTMLElement;
|
const zombie = card.querySelector(".hud-endcard__zombie") as HTMLElement;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { Sprite, SpriteMaterial, Texture, TextureLoader, SRGBColorSpace, Vector3 } from "three";
|
import { Sprite, SpriteMaterial, Texture, TextureLoader, SRGBColorSpace, Vector3 } from "three";
|
||||||
import * as TWEEN from "@tweenjs/tween.js";
|
import * as TWEEN from "@tweenjs/tween.js";
|
||||||
import { UpdateController, CameraC_internal } from "@24tools/playable_template";
|
import { CameraC_internal } from "@24tools/playable_template";
|
||||||
import { ThreeC } from "./ThreeC";
|
import { ThreeC } from "./ThreeC";
|
||||||
import { TestSceneC } from "./TestSceneC"; // for groundY (ground level)
|
import { TestSceneC } from "./TestSceneC"; // for groundY (ground level)
|
||||||
import { woodIconUrl } from "../resources/images/woodIcon";
|
import { GameEvents } from "../core/GameEvents";
|
||||||
|
import { GameLoop } from "../core/GameLoop";
|
||||||
|
import { images } from "../resources/resources";
|
||||||
import { worldToScreen } from "../utils/screen";
|
import { worldToScreen } from "../utils/screen";
|
||||||
|
|
||||||
// Tunables — tweak here
|
// Tunables — tweak here
|
||||||
@@ -46,7 +48,7 @@ export class LootC {
|
|||||||
private static tweens = new TWEEN.Group();
|
private static tweens = new TWEEN.Group();
|
||||||
|
|
||||||
static init() {
|
static init() {
|
||||||
this.texture = new TextureLoader().load(woodIconUrl);
|
this.texture = new TextureLoader().load(images.woodIconUrl);
|
||||||
this.texture.colorSpace = SRGBColorSpace; // correct color
|
this.texture.colorSpace = SRGBColorSpace; // correct color
|
||||||
|
|
||||||
// The wood icon/count live in the HUD (built by HudC). We just reference
|
// The wood icon/count live in the HUD (built by HudC). We just reference
|
||||||
@@ -55,8 +57,11 @@ export class LootC {
|
|||||||
this.countEl = document.getElementById("wood-count");
|
this.countEl = document.getElementById("wood-count");
|
||||||
this.renderCount();
|
this.renderCount();
|
||||||
|
|
||||||
|
// Listen for loot spawns (from crate breaks or state changes)
|
||||||
|
GameEvents.onLootSpawn.addDelegate(({ position, count }) => this.spawn(position, count));
|
||||||
|
|
||||||
// ⚠️ Key: pump our group every frame, otherwise the tweens don't advance.
|
// ⚠️ Key: pump our group every frame, otherwise the tweens don't advance.
|
||||||
UpdateController.Instance.onUpdate.addDelegate(() => this.tweens.update());
|
GameLoop.register(() => this.tweens.update());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Current spendable wood. */
|
/** Current spendable wood. */
|
||||||
@@ -69,6 +74,9 @@ export class LootC {
|
|||||||
const taken = Math.min(amount, this.balance);
|
const taken = Math.min(amount, this.balance);
|
||||||
this.balance -= taken;
|
this.balance -= taken;
|
||||||
this.renderCount();
|
this.renderCount();
|
||||||
|
if (taken > 0) {
|
||||||
|
GameEvents.onWoodSpent.Invoke({ amount: taken, balance: this.balance });
|
||||||
|
}
|
||||||
return taken;
|
return taken;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +217,7 @@ export class LootC {
|
|||||||
this.remove(piece);
|
this.remove(piece);
|
||||||
|
|
||||||
const flier = document.createElement("img");
|
const flier = document.createElement("img");
|
||||||
flier.src = woodIconUrl;
|
flier.src = images.woodIconUrl;
|
||||||
flier.style.cssText =
|
flier.style.cssText =
|
||||||
`position:fixed; left:0; top:0; width:${sizePx}px; height:${sizePx}px;` +
|
`position:fixed; left:0; top:0; width:${sizePx}px; height:${sizePx}px;` +
|
||||||
// above #hud (z-index 9999) so the wood clearly flies on top of, and into, the icon
|
// above #hud (z-index 9999) so the wood clearly flies on top of, and into, the icon
|
||||||
@@ -218,7 +226,7 @@ export class LootC {
|
|||||||
|
|
||||||
// White "glint" copy that rides on top of the flier and fades out as it moves.
|
// White "glint" copy that rides on top of the flier and fades out as it moves.
|
||||||
const flash = document.createElement("img");
|
const flash = document.createElement("img");
|
||||||
flash.src = woodIconUrl;
|
flash.src = images.woodIconUrl;
|
||||||
flash.style.cssText = flier.style.cssText;
|
flash.style.cssText = flier.style.cssText;
|
||||||
flash.style.filter = "brightness(0) invert(1)"; // solid white silhouette
|
flash.style.filter = "brightness(0) invert(1)"; // solid white silhouette
|
||||||
flash.style.zIndex = "10001";
|
flash.style.zIndex = "10001";
|
||||||
@@ -255,7 +263,14 @@ export class LootC {
|
|||||||
.to({ progress: 1 }, FLY_MS)
|
.to({ progress: 1 }, FLY_MS)
|
||||||
.easing(TWEEN.Easing.Quadratic.InOut)
|
.easing(TWEEN.Easing.Quadratic.InOut)
|
||||||
.onUpdate(apply)
|
.onUpdate(apply)
|
||||||
.onComplete(() => { flier.remove(); flash.remove(); this.balance++; this.renderCount(); this.pulseUiIcon(); });
|
.onComplete(() => {
|
||||||
|
flier.remove();
|
||||||
|
flash.remove();
|
||||||
|
this.balance++;
|
||||||
|
this.renderCount();
|
||||||
|
this.pulseUiIcon();
|
||||||
|
GameEvents.onWoodCollected.Invoke({ amount: 1, balance: this.balance });
|
||||||
|
});
|
||||||
// Shrink — SAME duration as the flight, so the two start AND finish together
|
// Shrink — SAME duration as the flight, so the two start AND finish together
|
||||||
// (no "shrink first"); Sinusoidal makes the size change extra smooth.
|
// (no "shrink first"); Sinusoidal makes the size change extra smooth.
|
||||||
const shrink = new TWEEN.Tween(anim, this.tweens)
|
const shrink = new TWEEN.Tween(anim, this.tweens)
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { Mesh, Object3D, Vector3 } from "three";
|
import { Mesh, Object3D, Vector3 } from "three";
|
||||||
import * as TWEEN from "@tweenjs/tween.js";
|
import * as TWEEN from "@tweenjs/tween.js";
|
||||||
import { UpdateController } from "@24tools/playable_template";
|
|
||||||
import { PhysicsBody, PhysicsLayer } from "./PhysicsC";
|
import { PhysicsBody, PhysicsLayer } from "./PhysicsC";
|
||||||
import { Trigger } from "./TriggerC";
|
import { Trigger } from "./TriggerC";
|
||||||
import { LootC } from "./LootC";
|
import { GameEvents } from "../core/GameEvents";
|
||||||
import { HealthBarC } from "./HealthBarC";
|
import { GameLoop } from "../core/GameLoop";
|
||||||
import { VfxManager } from "../resources/vfx/VfxManager";
|
import { VfxManager } from "../resources/vfx/VfxManager";
|
||||||
|
|
||||||
// Crate health, split evenly across 3 damage states (S1/S2/S3), broken at 0.
|
// Crate health, split evenly across 3 damage states (S1/S2/S3), broken at 0.
|
||||||
@@ -50,8 +49,11 @@ export class LootableC {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listen for crate hits from combat system
|
||||||
|
GameEvents.onCrateHit.addDelegate(({ crate, damage }) => this.damageCrate(crate, damage));
|
||||||
|
|
||||||
// Drive our juice tweens (hit punch, flash, break) every frame.
|
// Drive our juice tweens (hit punch, flash, break) every frame.
|
||||||
UpdateController.Instance.onUpdate.addDelegate(() => this.tweens.update());
|
GameLoop.register(() => this.tweens.update());
|
||||||
|
|
||||||
lootableGroup.visible = true;
|
lootableGroup.visible = true;
|
||||||
lootableGroup.updateWorldMatrix(true, true); // collider world positions must be current
|
lootableGroup.updateWorldMatrix(true, true); // collider world positions must be current
|
||||||
@@ -116,7 +118,7 @@ export class LootableC {
|
|||||||
this.flashCrate(crate);
|
this.flashCrate(crate);
|
||||||
|
|
||||||
// Show/refresh the floating health bar above the crate.
|
// Show/refresh the floating health bar above the crate.
|
||||||
HealthBarC.showDamage(crate, crate.health / crate.maxHealth);
|
GameEvents.onHealthDisplay.Invoke({ crate, healthFraction: crate.health / crate.maxHealth });
|
||||||
|
|
||||||
// Map remaining health to a level, never below where this crate started.
|
// Map remaining health to a level, never below where this crate started.
|
||||||
let level = Math.floor((1 - crate.health / crate.maxHealth) * LEVELS);
|
let level = Math.floor((1 - crate.health / crate.maxHealth) * LEVELS);
|
||||||
@@ -125,7 +127,8 @@ export class LootableC {
|
|||||||
crate.level = level;
|
crate.level = level;
|
||||||
crate.statesByLevel.forEach((state, lvl) => { if (state) state.visible = lvl === level; });
|
crate.statesByLevel.forEach((state, lvl) => { if (state) state.visible = lvl === level; });
|
||||||
// Loot drops on every state change, not only on destruction.
|
// Loot drops on every state change, not only on destruction.
|
||||||
LootC.spawn(crate.root.getWorldPosition(new Vector3()));
|
const position = crate.root.getWorldPosition(new Vector3());
|
||||||
|
GameEvents.onLootSpawn.Invoke({ position });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +138,7 @@ export class LootableC {
|
|||||||
crate.broken = true;
|
crate.broken = true;
|
||||||
|
|
||||||
// Drop the floating health bar.
|
// Drop the floating health bar.
|
||||||
HealthBarC.hide(crate);
|
GameEvents.onHealthHide.Invoke(crate);
|
||||||
|
|
||||||
// Stop the hit-punch so it doesn't fight the break animation.
|
// Stop the hit-punch so it doesn't fight the break animation.
|
||||||
crate.hitTween?.stop();
|
crate.hitTween?.stop();
|
||||||
@@ -147,7 +150,8 @@ export class LootableC {
|
|||||||
crate.trigger = null;
|
crate.trigger = null;
|
||||||
|
|
||||||
// Drop loot at the crate's spot.
|
// Drop loot at the crate's spot.
|
||||||
LootC.spawn(crate.root.getWorldPosition(new Vector3()));
|
const position = crate.root.getWorldPosition(new Vector3());
|
||||||
|
GameEvents.onLootSpawn.Invoke({ position });
|
||||||
|
|
||||||
// Shrink to nothing while spinning, then hide the meshes.
|
// Shrink to nothing while spinning, then hide the meshes.
|
||||||
const anim = { progress: 0 };
|
const anim = { progress: 0 };
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Object3D, Raycaster, Vector3 } from "three";
|
import { Object3D, Raycaster, Vector3 } from "three";
|
||||||
import * as TWEEN from "@tweenjs/tween.js";
|
import * as TWEEN from "@tweenjs/tween.js";
|
||||||
import { UpdateController, CameraC_internal } from "@24tools/playable_template";
|
import { CameraC_internal } from "@24tools/playable_template";
|
||||||
import { Trigger } from "./TriggerC";
|
import { Trigger } from "./TriggerC";
|
||||||
import { TestSceneC } from "./TestSceneC";
|
import { TestSceneC } from "./TestSceneC";
|
||||||
import { LootC } from "./LootC";
|
import { LootC } from "./LootC";
|
||||||
import { woodIconUrl } from "../resources/images/woodIcon";
|
import { GameLoop } from "../core/GameLoop";
|
||||||
|
import { images } from "../resources/resources";
|
||||||
import { worldToScreen } from "../utils/screen";
|
import { worldToScreen } from "../utils/screen";
|
||||||
|
|
||||||
// --- Tunables ---
|
// --- Tunables ---
|
||||||
@@ -75,7 +76,7 @@ export class PayZoneC {
|
|||||||
{ onEnter: () => { this.inside = true; }, onExit: () => { this.inside = false; } },
|
{ onEnter: () => { this.inside = true; }, onExit: () => { this.inside = false; } },
|
||||||
);
|
);
|
||||||
|
|
||||||
UpdateController.Instance.onUpdate.addDelegate((delta) => this.update(delta));
|
GameLoop.register((delta) => this.update(delta));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static update(delta: number) {
|
private static update(delta: number) {
|
||||||
@@ -103,7 +104,7 @@ export class PayZoneC {
|
|||||||
if (!to) return;
|
if (!to) return;
|
||||||
|
|
||||||
const img = document.createElement("img");
|
const img = document.createElement("img");
|
||||||
img.src = woodIconUrl;
|
img.src = images.woodIconUrl;
|
||||||
img.style.cssText =
|
img.style.cssText =
|
||||||
`position:fixed; left:0; top:0; width:${PLANK_PX}px; height:${PLANK_PX}px;` +
|
`position:fixed; left:0; top:0; width:${PLANK_PX}px; height:${PLANK_PX}px;` +
|
||||||
`z-index:1000; pointer-events:none; transform:translate(-50%,-50%); will-change:left,top;`;
|
`z-index:1000; pointer-events:none; transform:translate(-50%,-50%); will-change:left,top;`;
|
||||||
|
|||||||
@@ -12,6 +12,36 @@ export enum PhysicsLayer {
|
|||||||
Enemy = 8,
|
Enemy = 8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Physics world API facade. All direct Physics_internal access goes through here.
|
||||||
|
*/
|
||||||
|
export class PhysicsC {
|
||||||
|
/** Add a body to the physics world. */
|
||||||
|
static addBody(body: Body) {
|
||||||
|
Physics_internal.physicsWorld?.addBody(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove a body from the physics world. */
|
||||||
|
static removeBody(body: Body) {
|
||||||
|
Physics_internal.physicsWorld?.removeBody(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Listen for collision begin events. */
|
||||||
|
static onContactBegin(callback: (event: any) => void) {
|
||||||
|
Physics_internal.physicsWorld?.addEventListener("beginContact", callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Listen for collision end events. */
|
||||||
|
static onContactEnd(callback: (event: any) => void) {
|
||||||
|
Physics_internal.physicsWorld?.addEventListener("endContact", callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the raw physics world (for advanced usage). */
|
||||||
|
static get world() {
|
||||||
|
return Physics_internal.physicsWorld;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps a three.js object in a cannon-es rigid body. The shape is derived from
|
* Wraps a three.js object in a cannon-es rigid body. The shape is derived from
|
||||||
* the object: the player gets a Sphere (rolls smoothly along walls/floor),
|
* the object: the player gets a Sphere (rolls smoothly along walls/floor),
|
||||||
@@ -57,7 +87,7 @@ export class PhysicsBody {
|
|||||||
"XYZ",
|
"XYZ",
|
||||||
);
|
);
|
||||||
|
|
||||||
Physics_internal.physicsWorld?.addBody(this.body);
|
PhysicsC.addBody(this.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The underlying cannon body (for direct velocity/position control). */
|
/** The underlying cannon body (for direct velocity/position control). */
|
||||||
@@ -67,6 +97,6 @@ export class PhysicsBody {
|
|||||||
|
|
||||||
/** Remove the body from the physics world (e.g. when a crate breaks). */
|
/** Remove the body from the physics world (e.g. when a crate breaks). */
|
||||||
destroy() {
|
destroy() {
|
||||||
Physics_internal.physicsWorld?.removeBody(this.body);
|
PhysicsC.removeBody(this.body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { CameraC_internal, JoystickC, ThreeC_internal, UpdateController } from "@24tools/playable_template";
|
import { CameraC_internal, JoystickC, ThreeC_internal } from "@24tools/playable_template";
|
||||||
import { AnimationAction, AnimationMixer, LoopOnce, Mesh, Object3D, Raycaster, Vector3 } from "three";
|
import { AnimationAction, AnimationMixer, LoopOnce, Mesh, Object3D, Raycaster, Vector3 } from "three";
|
||||||
import { Body } from "cannon-es";
|
import { Body } from "cannon-es";
|
||||||
import { PhysicsBody, PhysicsLayer } from "./PhysicsC";
|
import { PhysicsBody, PhysicsLayer } from "./PhysicsC";
|
||||||
|
import { GameLoop } from "../core/GameLoop";
|
||||||
|
|
||||||
const ANIM_NAMES: Record<string, string[]> = {
|
const ANIM_NAMES: Record<string, string[]> = {
|
||||||
idle: ["idle", "Idle", "IDLE", "stand", "Stand"],
|
idle: ["idle", "Idle", "IDLE", "stand", "Stand"],
|
||||||
@@ -92,7 +93,7 @@ export class PlayerC {
|
|||||||
this.setupWeapon();
|
this.setupWeapon();
|
||||||
this.setupAnimations();
|
this.setupAnimations();
|
||||||
this.setupJoystick();
|
this.setupJoystick();
|
||||||
UpdateController.Instance.onUpdate.addDelegate((delta) => this.update(delta));
|
GameLoop.register((delta) => this.update(delta));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle the bat between the back (idle/normal) and the hand (attack).
|
// Toggle the bat between the back (idle/normal) and the hand (attack).
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { ThreeC } from "./ThreeC";
|
import { ThreeC } from "./ThreeC";
|
||||||
import { InputC, Physics_internal } from "@24tools/playable_template";
|
import { InputC } from "@24tools/playable_template";
|
||||||
import { Box3, Mesh, Object3D, Vector3 } from "three";
|
import { Box3, Mesh, Object3D, Vector3 } from "three";
|
||||||
import { Body, Box, Vec3 } from "cannon-es";
|
import { Body, Box, Vec3 } from "cannon-es";
|
||||||
import { PhysicsBody, PhysicsLayer } from "./PhysicsC";
|
import { PhysicsBody, PhysicsC, PhysicsLayer } from "./PhysicsC";
|
||||||
|
|
||||||
export class TestSceneC {
|
export class TestSceneC {
|
||||||
static mapObject: Object3D;
|
static mapObject: Object3D;
|
||||||
@@ -136,7 +136,7 @@ export class TestSceneC {
|
|||||||
// player in. Bounds are read from the floor proxy so the walls always match
|
// player in. Bounds are read from the floor proxy so the walls always match
|
||||||
// the authored map, even if it changes.
|
// the authored map, even if it changes.
|
||||||
private static buildBoundaryWalls() {
|
private static buildBoundaryWalls() {
|
||||||
if (!this.colliderGroup || !Physics_internal.physicsWorld) return;
|
if (!this.colliderGroup || !PhysicsC.world) return;
|
||||||
|
|
||||||
const bounds = new Box3().setFromObject(this.colliderGroup);
|
const bounds = new Box3().setFromObject(this.colliderGroup);
|
||||||
if (bounds.isEmpty()) return;
|
if (bounds.isEmpty()) return;
|
||||||
@@ -169,7 +169,7 @@ export class TestSceneC {
|
|||||||
collisionFilterMask: PhysicsLayer.Player,
|
collisionFilterMask: PhysicsLayer.Player,
|
||||||
});
|
});
|
||||||
body.position.set(wallX, midY, wallZ);
|
body.position.set(wallX, midY, wallZ);
|
||||||
Physics_internal.physicsWorld.addBody(body);
|
PhysicsC.addBody(body);
|
||||||
this.boundaryBodies.push(body);
|
this.boundaryBodies.push(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Physics_internal } from "@24tools/playable_template";
|
|
||||||
import { Body, Box, Vec3 } from "cannon-es";
|
import { Body, Box, Vec3 } from "cannon-es";
|
||||||
import { PhysicsLayer } from "./PhysicsC";
|
import { PhysicsC, PhysicsLayer } from "./PhysicsC";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A single invisible trigger zone.
|
* A single invisible trigger zone.
|
||||||
@@ -32,13 +31,13 @@ export class Trigger {
|
|||||||
});
|
});
|
||||||
this.body.position.set(center.x, center.y, center.z);
|
this.body.position.set(center.x, center.y, center.z);
|
||||||
|
|
||||||
Physics_internal.physicsWorld?.addBody(this.body);
|
PhysicsC.addBody(this.body);
|
||||||
TriggerC.register(this);
|
TriggerC.register(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
TriggerC.unregister(this);
|
TriggerC.unregister(this);
|
||||||
Physics_internal.physicsWorld?.removeBody(this.body);
|
PhysicsC.removeBody(this.body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,10 +59,9 @@ export class TriggerC {
|
|||||||
this.playerBody = playerBody;
|
this.playerBody = playerBody;
|
||||||
if (this.started) return;
|
if (this.started) return;
|
||||||
|
|
||||||
const world = Physics_internal.physicsWorld;
|
if (!PhysicsC.world) return;
|
||||||
if (!world) return;
|
PhysicsC.onContactBegin(this.onBegin);
|
||||||
world.addEventListener("beginContact", this.onBegin);
|
PhysicsC.onContactEnd(this.onEnd);
|
||||||
world.addEventListener("endContact", this.onEnd);
|
|
||||||
this.started = true;
|
this.started = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { EasyEvent } from "@24tools/playable_template";
|
||||||
|
import { Vector3 } from "three";
|
||||||
|
import type { Crate } from "../controllers/LootableC";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralized event system. Controllers emit events instead of calling each other directly.
|
||||||
|
* This decouples systems and makes it easy to add new listeners (e.g., sound, particles, analytics).
|
||||||
|
*
|
||||||
|
* Uses EasyEvent<Type> from @24tools/playable_template (same pattern as UpdateController, JoystickC).
|
||||||
|
* Subscribe: event.addDelegate(callback)
|
||||||
|
* Emit: event.Invoke(data)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Event data types
|
||||||
|
export interface CrateHitData {
|
||||||
|
crate: Crate;
|
||||||
|
damage: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CrateBrokenData {
|
||||||
|
crate: Crate;
|
||||||
|
position: Vector3;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LootSpawnData {
|
||||||
|
position: Vector3;
|
||||||
|
count?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HealthDisplayData {
|
||||||
|
crate: Crate;
|
||||||
|
healthFraction: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WoodCollectedData {
|
||||||
|
amount: number;
|
||||||
|
balance: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WoodSpentData {
|
||||||
|
amount: number;
|
||||||
|
balance: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global game events. Use like:
|
||||||
|
* GameEvents.onCrateHit.Invoke({ crate, damage: 10 });
|
||||||
|
* GameEvents.onCrateHit.addDelegate(({ crate, damage }) => { ... });
|
||||||
|
*/
|
||||||
|
export class GameEvents {
|
||||||
|
// Crate hit: damage applied, state may change
|
||||||
|
static onCrateHit = new EasyEvent<CrateHitData>();
|
||||||
|
|
||||||
|
// Crate destroyed: physics removed, loot will drop
|
||||||
|
static onCrateBroken = new EasyEvent<CrateBrokenData>();
|
||||||
|
|
||||||
|
// Health bar shown/updated for a crate
|
||||||
|
static onHealthDisplay = new EasyEvent<HealthDisplayData>();
|
||||||
|
|
||||||
|
// Health bar hidden
|
||||||
|
static onHealthHide = new EasyEvent<Crate>();
|
||||||
|
|
||||||
|
// Loot drops at a position (may be from crate break or state change)
|
||||||
|
static onLootSpawn = new EasyEvent<LootSpawnData>();
|
||||||
|
|
||||||
|
// Wood piece collected into UI counter
|
||||||
|
static onWoodCollected = new EasyEvent<WoodCollectedData>();
|
||||||
|
|
||||||
|
// Wood spent from counter (e.g., plank flying to pay zone)
|
||||||
|
static onWoodSpent = new EasyEvent<WoodSpentData>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { UpdateController } from "@24tools/playable_template";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralized game loop. Instead of each controller registering separately with UpdateController,
|
||||||
|
* they register with GameLoop. This gives us:
|
||||||
|
* - Single point of entry for all per-frame updates
|
||||||
|
* - Explicit ordering of controller ticks
|
||||||
|
* - Easy to add/remove/reorder without touching UpdateController
|
||||||
|
*/
|
||||||
|
|
||||||
|
type UpdateFn = (delta: number) => void;
|
||||||
|
|
||||||
|
export class GameLoop {
|
||||||
|
private static updates: UpdateFn[] = [];
|
||||||
|
private static started = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register an update function to be called each frame.
|
||||||
|
* The first call to register() sets up the UpdateController listener.
|
||||||
|
*/
|
||||||
|
static register(fn: UpdateFn) {
|
||||||
|
this.updates.push(fn);
|
||||||
|
if (!this.started) {
|
||||||
|
UpdateController.Instance.onUpdate.addDelegate((delta) => this.tick(delta));
|
||||||
|
this.started = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static tick(delta: number) {
|
||||||
|
for (const fn of this.updates) {
|
||||||
|
fn(delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,7 +61,9 @@
|
|||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
font-family: "PassionOne", "Roboto", sans-serif;
|
// "gameFont" is the family name the template registers our custom font under
|
||||||
|
// (set in src/fonts/customFont.ts → PassionOne Black, inlined on release).
|
||||||
|
font-family: "gameFont", "Roboto", sans-serif;
|
||||||
|
|
||||||
// Portrait: every fluid size scales with viewport width.
|
// Portrait: every fluid size scales with viewport width.
|
||||||
--fluid-unit: 1vw;
|
--fluid-unit: 1vw;
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
import { FontFamily, formFontFamily } from "@24tools/ads_common";
|
import { FontFamily, formFontFamily } from "@24tools/ads_common";
|
||||||
|
|
||||||
export const customFont: undefined | Promise<FontFamily> = undefined
|
export const customFont: Promise<FontFamily> = formFontFamily("PassionOne", "./PassionOne-Black.otf");
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import { ConvertToBase64WhenRelease } from "@24tools/ads_common";
|
|
||||||
|
|
||||||
// Heavy display font used for the HUD labels (matches the REF look). Inlined as
|
|
||||||
// base64 on release. We inject an @font-face at runtime so it works in dev too.
|
|
||||||
const passionOneUrl = ConvertToBase64WhenRelease("./PassionOne-Black.otf");
|
|
||||||
|
|
||||||
export const PASSION_ONE = "PassionOne";
|
|
||||||
|
|
||||||
let injected = false;
|
|
||||||
|
|
||||||
/** Register the PassionOne font once. Safe to call multiple times. */
|
|
||||||
export function ensurePassionOne() {
|
|
||||||
if (injected) return;
|
|
||||||
injected = true;
|
|
||||||
const style = document.createElement("style");
|
|
||||||
style.textContent =
|
|
||||||
`@font-face{font-family:'${PASSION_ONE}';` +
|
|
||||||
`src:url(${passionOneUrl}) format('opentype');font-weight:900;font-display:swap;}`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 814 B |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 30 KiB |
@@ -1,9 +0,0 @@
|
|||||||
import { ConvertToBase64WhenRelease } from "@24tools/ads_common";
|
|
||||||
|
|
||||||
// HUD image URLs. In a release build each is inlined as base64 (like the loot
|
|
||||||
// icon). Paths are relative to this file. Usable directly in <img src> or CSS url().
|
|
||||||
export const zombieHeadUrl = ConvertToBase64WhenRelease("./Icon_Zombie_Head.webp"); // invasion bar + death end-card
|
|
||||||
export const woodPanelUrl = ConvertToBase64WhenRelease("./ResourceBackground_Wood.webp");
|
|
||||||
export const metalPanelUrl = ConvertToBase64WhenRelease("./ResourceBackground_Metal.webp");
|
|
||||||
export const toolPanelUrl = ConvertToBase64WhenRelease("./Tool_Backgtound.webp"); // weapon panel background
|
|
||||||
export const toolIconUrl = ConvertToBase64WhenRelease("./Tool_1.webp"); // weapon icon
|
|
||||||
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 430 KiB After Width: | Height: | Size: 430 KiB |
|
Before Width: | Height: | Size: 427 KiB After Width: | Height: | Size: 427 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
@@ -0,0 +1,17 @@
|
|||||||
|
import { ConvertToBase64WhenRelease } from "@24tools/ads_common";
|
||||||
|
|
||||||
|
const woodIconUrl = ConvertToBase64WhenRelease("./Icon_Wood.webp");
|
||||||
|
const zombieHeadUrl = ConvertToBase64WhenRelease("./Icon_Zombie_Head.webp"); // invasion bar + death end-card
|
||||||
|
const woodPanelUrl = ConvertToBase64WhenRelease("./ResourceBackground_Wood.webp");
|
||||||
|
const metalPanelUrl = ConvertToBase64WhenRelease("./ResourceBackground_Metal.webp");
|
||||||
|
const toolPanelUrl = ConvertToBase64WhenRelease("./Tool_Backgtound.webp"); // weapon panel background
|
||||||
|
const toolIconUrl = ConvertToBase64WhenRelease("./Tool_1.webp"); // weapon icon
|
||||||
|
|
||||||
|
export const images = {
|
||||||
|
woodIconUrl,
|
||||||
|
zombieHeadUrl,
|
||||||
|
woodPanelUrl,
|
||||||
|
metalPanelUrl,
|
||||||
|
toolPanelUrl,
|
||||||
|
toolIconUrl,
|
||||||
|
};
|
||||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1,5 +0,0 @@
|
|||||||
import { ConvertToBase64WhenRelease } from "@24tools/ads_common";
|
|
||||||
|
|
||||||
// URL of the wood loot icon. In a release build it's inlined as base64 (like
|
|
||||||
// meshes/sounds). Path is relative to this file.
|
|
||||||
export const woodIconUrl = ConvertToBase64WhenRelease("./Icon_Wood.webp");
|
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import { ConvertResourcesType } from "@24tools/playable_template";
|
import { ConvertResourcesType } from "@24tools/playable_template";
|
||||||
import { meshes } from "./meshes/meshes";
|
import { meshes } from "./meshes/meshes";
|
||||||
import { sounds } from "./sounds/sounds";
|
import { sounds } from "./sounds/sounds";
|
||||||
import { vfx_json } from "./vfx/vfx_json";
|
import { vfx_json } from "./vfx/vfx_json";1
|
||||||
|
import { images } from "./images/images";
|
||||||
|
|
||||||
export const resources: ConvertResourcesType = [meshes, sounds, vfx_json];
|
export const resources: ConvertResourcesType = [meshes, sounds, vfx_json];
|
||||||
|
|
||||||
|
export { images };
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { BatchedRenderer, QuarksLoader, QuarksUtil } from "three.quarks";
|
import { BatchedRenderer, QuarksLoader, QuarksUtil } from "three.quarks";
|
||||||
import { Object3D, Euler, Vector3 } from "three";
|
import { Object3D, Euler, Vector3 } from "three";
|
||||||
import { ResourcesC, UpdateController } from "@24tools/playable_template";
|
import { ResourcesC } from "@24tools/playable_template";
|
||||||
import { ThreeC } from "../../controllers/ThreeC";
|
import { ThreeC } from "../../controllers/ThreeC";
|
||||||
|
import { GameLoop } from "../../core/GameLoop";
|
||||||
|
|
||||||
// Resource "type" under which Quark VFX JSONs are registered (see resources.ts).
|
// Resource "type" under which Quark VFX JSONs are registered (see resources.ts).
|
||||||
const VFX_RESOURCE_TYPE = "vfx_json";
|
const VFX_RESOURCE_TYPE = "vfx_json";
|
||||||
@@ -14,7 +15,7 @@ export class VfxManager {
|
|||||||
this.batchRenderer = new BatchedRenderer();
|
this.batchRenderer = new BatchedRenderer();
|
||||||
this.loader = new QuarksLoader();
|
this.loader = new QuarksLoader();
|
||||||
ThreeC.addToScene(this.batchRenderer);
|
ThreeC.addToScene(this.batchRenderer);
|
||||||
UpdateController.Instance.onUpdate.addDelegate(this.update.bind(this));
|
GameLoop.register(this.update.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
static update(delta: number) {
|
static update(delta: number) {
|
||||||
|
|||||||
@@ -17,10 +17,6 @@ export const vfx_json: ConvertResourceType = {
|
|||||||
"resources/vfx/files/VFX_Lootable_Destroy.json",
|
"resources/vfx/files/VFX_Lootable_Destroy.json",
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "Test",
|
|
||||||
value: ConvertToBase64WhenRelease("resources/vfx/files/test.json"),
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
loader: quarksLoader,
|
loader: quarksLoader,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,8 +28,7 @@ export const beforeResourcesLoadedCb = () => {
|
|||||||
// The player is steered by setting its velocity directly every frame, so
|
// The player is steered by setting its velocity directly every frame, so
|
||||||
// ground friction would only fight the intended motion (it dropped the
|
// ground friction would only fight the intended motion (it dropped the
|
||||||
// effective speed to ~0.84 of 4 m/s). Disable it globally; boundary walls
|
// effective speed to ~0.84 of 4 m/s). Disable it globally; boundary walls
|
||||||
// still block the player via the normal contact constraint. When crates are
|
// still block the player via the normal contact constraint
|
||||||
// added later, give them their own ContactMaterial if they need friction.
|
|
||||||
if (Physics_internal.physicsWorld) {
|
if (Physics_internal.physicsWorld) {
|
||||||
Physics_internal.physicsWorld.defaultContactMaterial.friction = 0;
|
Physics_internal.physicsWorld.defaultContactMaterial.friction = 0;
|
||||||
}
|
}
|
||||||
|
|||||||