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
This commit is contained in:
24Play-Mykyta-Slobodianiuk
2026-06-09 12:44:11 +03:00
parent 78d85c3799
commit 2238880bf6
40 changed files with 246 additions and 107 deletions
+4 -3
View File
@@ -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();
+3 -2
View File
@@ -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).
+11 -4
View File
@@ -1,12 +1,14 @@
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 ---
const HEIGHT_ABOVE = 1.4; // world Y offset of the bar above the crate origin const HEIGHT_ABOVE = 1.4; // world Y offset of the bar above the crate origin
const BAR_SCALE = 1.2; // world scale of the cloned bar (prototype is ~1m wide) const BAR_SCALE = 1.2; // world scale of the cloned bar (prototype is ~1m wide)
// --- Fill animation --- // --- Fill animation ---
const FOREGROUND_MS = 120; // Foreground (current health) drops fast — animates first const FOREGROUND_MS = 120; // Foreground (current health) drops fast — animates first
@@ -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). */
+10 -17
View File
@@ -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;
+22 -7
View File
@@ -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)
+12 -8
View File
@@ -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 };
+5 -4
View File
@@ -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;`;
+32 -2
View File
@@ -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);
} }
} }
+3 -2
View File
@@ -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).
+4 -4
View File
@@ -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);
} }
+6 -8
View File
@@ -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;
} }
+71
View File
@@ -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>();
}
+34
View File
@@ -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);
}
}
}
+3 -1
View File
@@ -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 -1
View File
@@ -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");
-20
View File
@@ -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);
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 814 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

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
Binary file not shown.

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

+17
View File
@@ -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,
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

-5
View File
@@ -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");
+4 -1
View File
@@ -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 };
+3 -2
View File
@@ -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) {
File diff suppressed because one or more lines are too long
-4
View File
@@ -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;
} }