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 { PlayerC } from "./PlayerC";
|
||||
import { Crate, LootableC } from "./LootableC";
|
||||
import { Trigger } from "./TriggerC";
|
||||
import { GameEvents } from "../core/GameEvents";
|
||||
import { GameLoop } from "../core/GameLoop";
|
||||
|
||||
const ATTACK_DAMAGE = 10; // damage per bat-tip 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() {
|
||||
@@ -85,7 +86,7 @@ export class CombatC {
|
||||
crate.root.getWorldPosition(_scratch);
|
||||
if (Math.hypot(tip.x - _scratch.x, tip.z - _scratch.z) <= CONTACT_DIST) {
|
||||
this.hitThisSwing.add(crate);
|
||||
LootableC.damageCrate(crate, ATTACK_DAMAGE);
|
||||
GameEvents.onCrateHit.Invoke({ crate, damage: ATTACK_DAMAGE });
|
||||
}
|
||||
}
|
||||
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 { GameLoop } from "../core/GameLoop";
|
||||
|
||||
const _targetWorldPos = new Vector3();
|
||||
const _lookAheadTarget = new Vector3();
|
||||
@@ -29,7 +30,7 @@ export class FollowCameraC {
|
||||
// Immediately place camera relative to the actual character position
|
||||
// (not config origin) so the character is visible from frame one.
|
||||
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).
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Box3, DoubleSide, Group, Material, Mesh, Object3D, Vector3 } from "three";
|
||||
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 { GameEvents } from "../core/GameEvents";
|
||||
import { GameLoop } from "../core/GameLoop";
|
||||
import type { Crate } from "./LootableC";
|
||||
|
||||
// --- Placement ---
|
||||
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 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)
|
||||
|
||||
// --- Fill animation ---
|
||||
const FOREGROUND_MS = 120; // Foreground (current health) drops fast — animates first
|
||||
@@ -54,7 +56,12 @@ export class HealthBarC {
|
||||
return;
|
||||
}
|
||||
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). */
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { UpdateController } from "@24tools/playable_template";
|
||||
import { ensurePassionOne } from "../fonts/passionOne";
|
||||
import { PlayerC } from "./PlayerC";
|
||||
import * as TWEEN from "@tweenjs/tween.js";
|
||||
import { GameLoop } from "../core/GameLoop";
|
||||
import {
|
||||
zombieHeadUrl,
|
||||
woodPanelUrl,
|
||||
metalPanelUrl,
|
||||
toolPanelUrl,
|
||||
toolIconUrl,
|
||||
} from "../resources/OnbordingUI/onboardingUI";
|
||||
images
|
||||
} from "../resources/images/images";
|
||||
|
||||
// Onboarding/invasion pacing.
|
||||
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 buttonTween: TWEEN.Tween<{ x: number; y: number }> | null = null;
|
||||
static init() {
|
||||
ensurePassionOne();
|
||||
|
||||
const hud = document.createElement("div");
|
||||
hud.id = "hud";
|
||||
hud.innerHTML = this.markup();
|
||||
@@ -71,7 +64,7 @@ export class HudC {
|
||||
this.showTutorial("DRAG TO MOVE");
|
||||
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.tweens.update();
|
||||
});
|
||||
@@ -90,22 +83,22 @@ export class HudC {
|
||||
<div class="hud-invasion__row">
|
||||
<div class="hud-invasion__bar">
|
||||
<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 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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="hud-weapon" style="background-image:url(${toolPanelUrl})">
|
||||
<img class="hud-weapon__icon" src="${toolIconUrl}" />
|
||||
<div class="hud-weapon" style="background-image:url(${images.toolPanelUrl})">
|
||||
<img class="hud-weapon__icon" src="${images.toolIconUrl}" />
|
||||
<div class="hud-weapon__lvl">LVL 0</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -197,7 +190,7 @@ export class HudC {
|
||||
|
||||
const card = document.createElement("div");
|
||||
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);
|
||||
|
||||
const zombie = card.querySelector(".hud-endcard__zombie") as HTMLElement;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Sprite, SpriteMaterial, Texture, TextureLoader, SRGBColorSpace, Vector3 } from "three";
|
||||
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 { 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";
|
||||
|
||||
// Tunables — tweak here
|
||||
@@ -46,7 +48,7 @@ export class LootC {
|
||||
private static tweens = new TWEEN.Group();
|
||||
|
||||
static init() {
|
||||
this.texture = new TextureLoader().load(woodIconUrl);
|
||||
this.texture = new TextureLoader().load(images.woodIconUrl);
|
||||
this.texture.colorSpace = SRGBColorSpace; // correct color
|
||||
|
||||
// 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.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.
|
||||
UpdateController.Instance.onUpdate.addDelegate(() => this.tweens.update());
|
||||
GameLoop.register(() => this.tweens.update());
|
||||
}
|
||||
|
||||
/** Current spendable wood. */
|
||||
@@ -69,6 +74,9 @@ export class LootC {
|
||||
const taken = Math.min(amount, this.balance);
|
||||
this.balance -= taken;
|
||||
this.renderCount();
|
||||
if (taken > 0) {
|
||||
GameEvents.onWoodSpent.Invoke({ amount: taken, balance: this.balance });
|
||||
}
|
||||
return taken;
|
||||
}
|
||||
|
||||
@@ -209,7 +217,7 @@ export class LootC {
|
||||
this.remove(piece);
|
||||
|
||||
const flier = document.createElement("img");
|
||||
flier.src = woodIconUrl;
|
||||
flier.src = images.woodIconUrl;
|
||||
flier.style.cssText =
|
||||
`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
|
||||
@@ -218,7 +226,7 @@ export class LootC {
|
||||
|
||||
// White "glint" copy that rides on top of the flier and fades out as it moves.
|
||||
const flash = document.createElement("img");
|
||||
flash.src = woodIconUrl;
|
||||
flash.src = images.woodIconUrl;
|
||||
flash.style.cssText = flier.style.cssText;
|
||||
flash.style.filter = "brightness(0) invert(1)"; // solid white silhouette
|
||||
flash.style.zIndex = "10001";
|
||||
@@ -255,7 +263,14 @@ export class LootC {
|
||||
.to({ progress: 1 }, FLY_MS)
|
||||
.easing(TWEEN.Easing.Quadratic.InOut)
|
||||
.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
|
||||
// (no "shrink first"); Sinusoidal makes the size change extra smooth.
|
||||
const shrink = new TWEEN.Tween(anim, this.tweens)
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { Mesh, Object3D, Vector3 } from "three";
|
||||
import * as TWEEN from "@tweenjs/tween.js";
|
||||
import { UpdateController } from "@24tools/playable_template";
|
||||
import { PhysicsBody, PhysicsLayer } from "./PhysicsC";
|
||||
import { Trigger } from "./TriggerC";
|
||||
import { LootC } from "./LootC";
|
||||
import { HealthBarC } from "./HealthBarC";
|
||||
import { GameEvents } from "../core/GameEvents";
|
||||
import { GameLoop } from "../core/GameLoop";
|
||||
import { VfxManager } from "../resources/vfx/VfxManager";
|
||||
|
||||
// Crate health, split evenly across 3 damage states (S1/S2/S3), broken at 0.
|
||||
@@ -50,8 +49,11 @@ export class LootableC {
|
||||
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.
|
||||
UpdateController.Instance.onUpdate.addDelegate(() => this.tweens.update());
|
||||
GameLoop.register(() => this.tweens.update());
|
||||
|
||||
lootableGroup.visible = true;
|
||||
lootableGroup.updateWorldMatrix(true, true); // collider world positions must be current
|
||||
@@ -116,7 +118,7 @@ export class LootableC {
|
||||
this.flashCrate(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.
|
||||
let level = Math.floor((1 - crate.health / crate.maxHealth) * LEVELS);
|
||||
@@ -125,7 +127,8 @@ export class LootableC {
|
||||
crate.level = level;
|
||||
crate.statesByLevel.forEach((state, lvl) => { if (state) state.visible = lvl === level; });
|
||||
// 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;
|
||||
|
||||
// Drop the floating health bar.
|
||||
HealthBarC.hide(crate);
|
||||
GameEvents.onHealthHide.Invoke(crate);
|
||||
|
||||
// Stop the hit-punch so it doesn't fight the break animation.
|
||||
crate.hitTween?.stop();
|
||||
@@ -147,7 +150,8 @@ export class LootableC {
|
||||
crate.trigger = null;
|
||||
|
||||
// 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.
|
||||
const anim = { progress: 0 };
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Object3D, Raycaster, Vector3 } from "three";
|
||||
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 { TestSceneC } from "./TestSceneC";
|
||||
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";
|
||||
|
||||
// --- Tunables ---
|
||||
@@ -75,7 +76,7 @@ export class PayZoneC {
|
||||
{ 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) {
|
||||
@@ -103,7 +104,7 @@ export class PayZoneC {
|
||||
if (!to) return;
|
||||
|
||||
const img = document.createElement("img");
|
||||
img.src = woodIconUrl;
|
||||
img.src = images.woodIconUrl;
|
||||
img.style.cssText =
|
||||
`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;`;
|
||||
|
||||
@@ -12,6 +12,36 @@ export enum PhysicsLayer {
|
||||
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
|
||||
* the object: the player gets a Sphere (rolls smoothly along walls/floor),
|
||||
@@ -57,7 +87,7 @@ export class PhysicsBody {
|
||||
"XYZ",
|
||||
);
|
||||
|
||||
Physics_internal.physicsWorld?.addBody(this.body);
|
||||
PhysicsC.addBody(this.body);
|
||||
}
|
||||
|
||||
/** 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). */
|
||||
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 { Body } from "cannon-es";
|
||||
import { PhysicsBody, PhysicsLayer } from "./PhysicsC";
|
||||
import { GameLoop } from "../core/GameLoop";
|
||||
|
||||
const ANIM_NAMES: Record<string, string[]> = {
|
||||
idle: ["idle", "Idle", "IDLE", "stand", "Stand"],
|
||||
@@ -92,7 +93,7 @@ export class PlayerC {
|
||||
this.setupWeapon();
|
||||
this.setupAnimations();
|
||||
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).
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
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 { Body, Box, Vec3 } from "cannon-es";
|
||||
import { PhysicsBody, PhysicsLayer } from "./PhysicsC";
|
||||
import { PhysicsBody, PhysicsC, PhysicsLayer } from "./PhysicsC";
|
||||
|
||||
export class TestSceneC {
|
||||
static mapObject: Object3D;
|
||||
@@ -136,7 +136,7 @@ export class TestSceneC {
|
||||
// player in. Bounds are read from the floor proxy so the walls always match
|
||||
// the authored map, even if it changes.
|
||||
private static buildBoundaryWalls() {
|
||||
if (!this.colliderGroup || !Physics_internal.physicsWorld) return;
|
||||
if (!this.colliderGroup || !PhysicsC.world) return;
|
||||
|
||||
const bounds = new Box3().setFromObject(this.colliderGroup);
|
||||
if (bounds.isEmpty()) return;
|
||||
@@ -169,7 +169,7 @@ export class TestSceneC {
|
||||
collisionFilterMask: PhysicsLayer.Player,
|
||||
});
|
||||
body.position.set(wallX, midY, wallZ);
|
||||
Physics_internal.physicsWorld.addBody(body);
|
||||
PhysicsC.addBody(body);
|
||||
this.boundaryBodies.push(body);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Physics_internal } from "@24tools/playable_template";
|
||||
import { Body, Box, Vec3 } from "cannon-es";
|
||||
import { PhysicsLayer } from "./PhysicsC";
|
||||
import { PhysicsC, PhysicsLayer } from "./PhysicsC";
|
||||
|
||||
/**
|
||||
* A single invisible trigger zone.
|
||||
@@ -32,13 +31,13 @@ export class Trigger {
|
||||
});
|
||||
this.body.position.set(center.x, center.y, center.z);
|
||||
|
||||
Physics_internal.physicsWorld?.addBody(this.body);
|
||||
PhysicsC.addBody(this.body);
|
||||
TriggerC.register(this);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
TriggerC.unregister(this);
|
||||
Physics_internal.physicsWorld?.removeBody(this.body);
|
||||
PhysicsC.removeBody(this.body);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,10 +59,9 @@ export class TriggerC {
|
||||
this.playerBody = playerBody;
|
||||
if (this.started) return;
|
||||
|
||||
const world = Physics_internal.physicsWorld;
|
||||
if (!world) return;
|
||||
world.addEventListener("beginContact", this.onBegin);
|
||||
world.addEventListener("endContact", this.onEnd);
|
||||
if (!PhysicsC.world) return;
|
||||
PhysicsC.onContactBegin(this.onBegin);
|
||||
PhysicsC.onContactEnd(this.onEnd);
|
||||
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;
|
||||
z-index: 9999;
|
||||
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.
|
||||
--fluid-unit: 1vw;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
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 { meshes } from "./meshes/meshes";
|
||||
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 { images };
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { BatchedRenderer, QuarksLoader, QuarksUtil } from "three.quarks";
|
||||
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 { GameLoop } from "../../core/GameLoop";
|
||||
|
||||
// Resource "type" under which Quark VFX JSONs are registered (see resources.ts).
|
||||
const VFX_RESOURCE_TYPE = "vfx_json";
|
||||
@@ -14,7 +15,7 @@ export class VfxManager {
|
||||
this.batchRenderer = new BatchedRenderer();
|
||||
this.loader = new QuarksLoader();
|
||||
ThreeC.addToScene(this.batchRenderer);
|
||||
UpdateController.Instance.onUpdate.addDelegate(this.update.bind(this));
|
||||
GameLoop.register(this.update.bind(this));
|
||||
}
|
||||
|
||||
static update(delta: number) {
|
||||
|
||||
@@ -17,10 +17,6 @@ export const vfx_json: ConvertResourceType = {
|
||||
"resources/vfx/files/VFX_Lootable_Destroy.json",
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Test",
|
||||
value: ConvertToBase64WhenRelease("resources/vfx/files/test.json"),
|
||||
},
|
||||
],
|
||||
loader: quarksLoader,
|
||||
};
|
||||
|
||||
@@ -28,8 +28,7 @@ export const beforeResourcesLoadedCb = () => {
|
||||
// The player is steered by setting its velocity directly every frame, so
|
||||
// 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
|
||||
// still block the player via the normal contact constraint. When crates are
|
||||
// added later, give them their own ContactMaterial if they need friction.
|
||||
// still block the player via the normal contact constraint
|
||||
if (Physics_internal.physicsWorld) {
|
||||
Physics_internal.physicsWorld.defaultContactMaterial.friction = 0;
|
||||
}
|
||||
|
||||