-

+
+
LVL 0
`;
@@ -197,7 +190,7 @@ export class HudC {
const card = document.createElement("div");
card.className = "hud-endcard";
- card.innerHTML = `

`;
+ card.innerHTML = `

`;
this.root.appendChild(card);
const zombie = card.querySelector(".hud-endcard__zombie") as HTMLElement;
diff --git a/src/controllers/LootC.ts b/src/controllers/LootC.ts
index b7e3ffc..04d599e 100644
--- a/src/controllers/LootC.ts
+++ b/src/controllers/LootC.ts
@@ -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)
diff --git a/src/controllers/LootableC.ts b/src/controllers/LootableC.ts
index 2a7ec72..f8d7076 100644
--- a/src/controllers/LootableC.ts
+++ b/src/controllers/LootableC.ts
@@ -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 };
diff --git a/src/controllers/PayZoneC.ts b/src/controllers/PayZoneC.ts
index 722a41c..d71001f 100644
--- a/src/controllers/PayZoneC.ts
+++ b/src/controllers/PayZoneC.ts
@@ -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;`;
diff --git a/src/controllers/PhysicsC.ts b/src/controllers/PhysicsC.ts
index b4f684b..c86cfd6 100644
--- a/src/controllers/PhysicsC.ts
+++ b/src/controllers/PhysicsC.ts
@@ -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);
}
}
diff --git a/src/controllers/PlayerC.ts b/src/controllers/PlayerC.ts
index 4d0d616..496d46a 100644
--- a/src/controllers/PlayerC.ts
+++ b/src/controllers/PlayerC.ts
@@ -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
= {
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).
diff --git a/src/controllers/TestSceneC.ts b/src/controllers/TestSceneC.ts
index 89e9850..0f4a65c 100644
--- a/src/controllers/TestSceneC.ts
+++ b/src/controllers/TestSceneC.ts
@@ -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);
}
diff --git a/src/controllers/TriggerC.ts b/src/controllers/TriggerC.ts
index a6f3120..993c005 100644
--- a/src/controllers/TriggerC.ts
+++ b/src/controllers/TriggerC.ts
@@ -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;
}
diff --git a/src/core/GameEvents.ts b/src/core/GameEvents.ts
new file mode 100644
index 0000000..bfd28cf
--- /dev/null
+++ b/src/core/GameEvents.ts
@@ -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 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();
+
+ // Crate destroyed: physics removed, loot will drop
+ static onCrateBroken = new EasyEvent();
+
+ // Health bar shown/updated for a crate
+ static onHealthDisplay = new EasyEvent();
+
+ // Health bar hidden
+ static onHealthHide = new EasyEvent();
+
+ // Loot drops at a position (may be from crate break or state change)
+ static onLootSpawn = new EasyEvent();
+
+ // Wood piece collected into UI counter
+ static onWoodCollected = new EasyEvent();
+
+ // Wood spent from counter (e.g., plank flying to pay zone)
+ static onWoodSpent = new EasyEvent();
+}
diff --git a/src/core/GameLoop.ts b/src/core/GameLoop.ts
new file mode 100644
index 0000000..9fecb0b
--- /dev/null
+++ b/src/core/GameLoop.ts
@@ -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);
+ }
+ }
+}
diff --git a/src/css/ui.scss b/src/css/ui.scss
index c101bde..2a0c2ac 100644
--- a/src/css/ui.scss
+++ b/src/css/ui.scss
@@ -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;
diff --git a/src/fonts/customFont.ts b/src/fonts/customFont.ts
index da1041a..3ab82e5 100644
--- a/src/fonts/customFont.ts
+++ b/src/fonts/customFont.ts
@@ -1,3 +1,3 @@
import { FontFamily, formFontFamily } from "@24tools/ads_common";
-export const customFont: undefined | Promise = undefined
+export const customFont: Promise = formFontFamily("PassionOne", "./PassionOne-Black.otf");
diff --git a/src/fonts/passionOne.ts b/src/fonts/passionOne.ts
deleted file mode 100644
index 55ee2dc..0000000
--- a/src/fonts/passionOne.ts
+++ /dev/null
@@ -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);
-}
diff --git a/src/resources/OnbordingUI/Tool_15.webp b/src/resources/OnbordingUI/Tool_15.webp
deleted file mode 100644
index 5a7d3b7..0000000
Binary files a/src/resources/OnbordingUI/Tool_15.webp and /dev/null differ
diff --git a/src/resources/OnbordingUI/Tool_2.webp b/src/resources/OnbordingUI/Tool_2.webp
deleted file mode 100644
index a0d8071..0000000
Binary files a/src/resources/OnbordingUI/Tool_2.webp and /dev/null differ
diff --git a/src/resources/OnbordingUI/ZombiePunk_Button-Install.webp b/src/resources/OnbordingUI/ZombiePunk_Button-Install.webp
deleted file mode 100644
index 0c6b664..0000000
Binary files a/src/resources/OnbordingUI/ZombiePunk_Button-Install.webp and /dev/null differ
diff --git a/src/resources/OnbordingUI/ZombiePunk_Icon-Bottom.webp b/src/resources/OnbordingUI/ZombiePunk_Icon-Bottom.webp
deleted file mode 100644
index d0cc0c9..0000000
Binary files a/src/resources/OnbordingUI/ZombiePunk_Icon-Bottom.webp and /dev/null differ
diff --git a/src/resources/OnbordingUI/ZombiePunk_Icon-Top.webp b/src/resources/OnbordingUI/ZombiePunk_Icon-Top.webp
deleted file mode 100644
index 1315173..0000000
Binary files a/src/resources/OnbordingUI/ZombiePunk_Icon-Top.webp and /dev/null differ
diff --git a/src/resources/OnbordingUI/onboardingUI.ts b/src/resources/OnbordingUI/onboardingUI.ts
deleted file mode 100644
index 1f06511..0000000
--- a/src/resources/OnbordingUI/onboardingUI.ts
+++ /dev/null
@@ -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
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
diff --git a/src/resources/images/CharacterSelectionUI_Logo.webp b/src/resources/images/CharacterSelectionUI_Logo.webp
deleted file mode 100644
index 2a6687f..0000000
Binary files a/src/resources/images/CharacterSelectionUI_Logo.webp and /dev/null differ
diff --git a/src/resources/OnbordingUI/Icon_Zombie_Head.webp b/src/resources/images/Icon_Zombie_Head.webp
similarity index 100%
rename from src/resources/OnbordingUI/Icon_Zombie_Head.webp
rename to src/resources/images/Icon_Zombie_Head.webp
diff --git a/src/resources/OnbordingUI/REF/REF_1.png b/src/resources/images/REF/REF_1.png
similarity index 100%
rename from src/resources/OnbordingUI/REF/REF_1.png
rename to src/resources/images/REF/REF_1.png
diff --git a/src/resources/OnbordingUI/REF/REF_2.png b/src/resources/images/REF/REF_2.png
similarity index 100%
rename from src/resources/OnbordingUI/REF/REF_2.png
rename to src/resources/images/REF/REF_2.png
diff --git a/src/resources/OnbordingUI/ResourceBackground_Metal.webp b/src/resources/images/ResourceBackground_Metal.webp
similarity index 100%
rename from src/resources/OnbordingUI/ResourceBackground_Metal.webp
rename to src/resources/images/ResourceBackground_Metal.webp
diff --git a/src/resources/OnbordingUI/ResourceBackground_Wood.webp b/src/resources/images/ResourceBackground_Wood.webp
similarity index 100%
rename from src/resources/OnbordingUI/ResourceBackground_Wood.webp
rename to src/resources/images/ResourceBackground_Wood.webp
diff --git a/src/resources/OnbordingUI/Tool_1.webp b/src/resources/images/Tool_1.webp
similarity index 100%
rename from src/resources/OnbordingUI/Tool_1.webp
rename to src/resources/images/Tool_1.webp
diff --git a/src/resources/OnbordingUI/Tool_Backgtound.webp b/src/resources/images/Tool_Backgtound.webp
similarity index 100%
rename from src/resources/OnbordingUI/Tool_Backgtound.webp
rename to src/resources/images/Tool_Backgtound.webp
diff --git a/src/resources/images/banner_button.webp b/src/resources/images/banner_button.webp
deleted file mode 100644
index a92e3f2..0000000
Binary files a/src/resources/images/banner_button.webp and /dev/null differ
diff --git a/src/resources/images/icon.webp b/src/resources/images/icon.webp
deleted file mode 100644
index aef4bb9..0000000
Binary files a/src/resources/images/icon.webp and /dev/null differ
diff --git a/src/resources/images/images.ts b/src/resources/images/images.ts
new file mode 100644
index 0000000..3be1700
--- /dev/null
+++ b/src/resources/images/images.ts
@@ -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,
+};
diff --git a/src/resources/images/logo24.png b/src/resources/images/logo24.png
deleted file mode 100644
index 00056ee..0000000
Binary files a/src/resources/images/logo24.png and /dev/null differ
diff --git a/src/resources/images/woodIcon.ts b/src/resources/images/woodIcon.ts
deleted file mode 100644
index 2ba0874..0000000
--- a/src/resources/images/woodIcon.ts
+++ /dev/null
@@ -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");
diff --git a/src/resources/resources.ts b/src/resources/resources.ts
index e60cfcf..3f3816a 100644
--- a/src/resources/resources.ts
+++ b/src/resources/resources.ts
@@ -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 };
diff --git a/src/resources/vfx/VfxManager.ts b/src/resources/vfx/VfxManager.ts
index f4b46b5..684def4 100644
--- a/src/resources/vfx/VfxManager.ts
+++ b/src/resources/vfx/VfxManager.ts
@@ -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) {
diff --git a/src/resources/vfx/files/test.json b/src/resources/vfx/files/test.json
deleted file mode 100644
index 32c50a2..0000000
--- a/src/resources/vfx/files/test.json
+++ /dev/null
@@ -1 +0,0 @@
-{"metadata":{"version":4.6,"type":"Object","generator":"Object3D.toJSON"},"geometries":[{"uuid":"f5ca51c8-abf9-435d-84ee-fdf7860d7569","type":"PlaneGeometry","name":"_geometry","width":1,"height":1,"widthSegments":1,"heightSegments":1}],"materials":[{"uuid":"23fd987d-6965-492d-8b94-5f1577a50984","type":"MeshBasicMaterial","color":16777215,"map":"0eba679e-9eec-43a0-bea5-06c1316765f8","envMapRotation":[0,0,0,"XYZ"],"reflectivity":1,"refractionRatio":0.98,"transparent":true,"blendColor":0}],"textures":[{"uuid":"0eba679e-9eec-43a0-bea5-06c1316765f8","name":"IcePeace1.webp","image":"8e527362-6b8b-41a1-8c90-6fe53e32065b","mapping":300,"channel":0,"repeat":[1,1],"offset":[0,0],"center":[0,0],"rotation":0,"wrap":[1001,1001],"format":1023,"internalFormat":null,"type":1009,"colorSpace":"","minFilter":1008,"magFilter":1006,"anisotropy":1,"flipY":true,"generateMipmaps":true,"premultiplyAlpha":false,"unpackAlignment":4}],"images":[{"uuid":"8e527362-6b8b-41a1-8c90-6fe53e32065b","url":"data:image/webp;base64,UklGRvoEAABXRUJQVlA4WAoAAAAQAAAATwAAJwAAQUxQSHQCAAAN1+SwbdtAesq9//13vrtfISLy43GY29C4llx+67jveMaQW4IOohiCCIe0ZiupVdvgfLvyB7Jl2zZtp899bduMr22bsW3bto5Pbvae33uzFb9F9H8CcNh0Kf/WkQf8MpN218DOqczDV+K/hvKLbFo93Xj+1KdfQM8MP84f6X9w5MVPlnEC+yntU4Ejt8yfqO4UzhumKk+eC/wkxsI2XC0db71x7N3PUHgB19P7x14fve+7Xet4aeyYzDl8Oe4k5+uGB6mHCvC6arrp7G6117nn3NlPblWex49JD3GYbQ4NPD/+xJ1n+PFND07zw9A8VHr0asSFVD/cW8FxThhFskda7u/94siPJ2/hPC/EptJcdtx336ctXMwNgYqqSMvdsM++dONqbhBQMSyk87K/3vbhamJSVAHFUAYvmn66v4y7WREVAAWMLXcc3NnqxambuJwTAQXBEljc72D0QJpr5swGbueHDBVUVERV6p/a+3hqr1tfO3A/K2yJCioooqtr9jhRW+/O+y48zA8jimEilgEpxe/smSPHk914MI+XOSFDRS0DFVBZ2mePV7cXXDhzHW/CWIgaCipo0cZne+xpqXBizkbwNi+igoqigkLC+FEH8YnTYu9rF17nRkBQFVFExap5H7TH41cjtt734HleBFFERUFFxei+5IDF8SIbDxfwPjcKiogFgqLS9HDDQWDp+I/OXsN7yYiAoCKoIoKVtP2mA27FWjcx5wP4MCsmqKgooqKIitV2XR0wtZoJBLrxZXYMQcEKh6PhaOhrLBL5lmg6+XTwIHzoxp+feoOhYAyvz5c0PprDp4GX+FGHa27wa31/iP/TVlA4IAoCAACQCgCdASpQACgAPi0ShkKhoQ1Vm5gMAWJYgCD/+VatRtqOQAtWZnxAiP/xJySr81ANA7Zb2v/8irT+tqtXW42V3mQDf4aBR+FefL2eHjdWMTvPbZHJLcwFhg6vN0AA/v+5UjfH6MtA9ReSqowrhxlTDDAa5Sb2pg9FeXZTBAWGPGXOZ/+Jh2Wnq2zYs3SK0nUwSLntyJjgEeFoO7tNkPl76bHNPP+eDeO38o/xL3kV63G7/iTsXP/+eAY2+wr0wne0jq0VO0u01KNA6atlDEAAU04KEqpiD2MTjelfqs/QPZbMG5c6Y/k3Gpt61DhkpZjDU8uQovLwBATZQIIZHbPfPWcHbKiWv87grXusL+FfRFprxagHtzSyImwaqu/pz/9riH/r8f/w5tlf8U356YZeYiDMFj/sZVzxKW+Wus3/h6iL7TC/gk32bxqnPN+01OfGxfz8E686eD8B6SKBwn6J6w528/6mwA5jiJLbNFaP26+oh4ijcbV9XPStfkei4leMgL5W94CJDf/k0K3y7z/CmwQder53NFetyQt8XgCZOaT2f7OdiXqpaDZhesJSz9ck8cGwYtluRbKbFCQLgTq1u+flme4Tp/mBYhbybf5vzARJpKgt+rO+KP1qd9Cev/G1yX+E2ZAGP/49KWmyub1qTlEbbvFqh/Le+kJqmzVrZxoOEEjDFgAAAABQU0FJTgAAADhCSU0D7QAAAAAAEABIAAAAAQACAEgAAAABAAI4QklNBCgAAAAAAAwAAAACP/AAAAAAAAA4QklNBEMAAAAAAA5QYmVXARAABgBaAAAAAA=="}],"object":{"uuid":"9f5762a0-0edd-4440-9b9f-6b2d1b7c7860","type":"Group","name":"Ice","layers":1,"matrix":[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],"up":[0,1,0],"children":[{"uuid":"c4a36eaf-745d-4175-ba13-2467f4af94e8","type":"ParticleEmitter","name":"small","userData":{"script":" // Randomize tile index for spawning\n const tileColumns = 3;\n const tileRows = 3;\n const randomTileIndex = Math.floor(Math.random() * (tileColumns * tileRows));\n \n // Check and set tile index (if uniforms are available)\n if (this.material && this.material.uniforms) {\n if (!this.material.uniforms.tileIndex) {\n this.material.uniforms.tileIndex = { value: 0 };\n }\n this.material.uniforms.tileIndex.value = randomTileIndex;\n }\n // Alternative: if using a particle attribute or property\n else if (this.material && this.material.map) {\n // For MeshBasicMaterial, try setting via user data\n this.userData.tileIndex = randomTileIndex;\n }"},"layers":1,"matrix":[1,0,0,0,0,2.220446049250313e-16,-1,0,0,1,2.220446049250313e-16,0,0,0,0,1],"up":[0,1,0],"ps":{"version":"3.0","autoDestroy":false,"looping":false,"prewarm":false,"duration":0.1,"shape":{"type":"point"},"startLife":{"type":"ConstantValue","value":0.6},"startSpeed":{"type":"ConstantValue","value":1.5},"startRotation":{"type":"IntervalValue","a":0,"b":360},"startSize":{"type":"IntervalValue","a":0.6,"b":0.9},"startColor":{"type":"ConstantColor","color":{"r":1,"g":1,"b":1,"a":1}},"emissionOverTime":{"type":"ConstantValue","value":0},"emissionOverDistance":{"type":"ConstantValue","value":0},"emissionBursts":[{"time":0,"count":{"type":"ConstantValue","value":15},"probability":1,"interval":0,"cycle":0}],"onlyUsedByOther":false,"instancingGeometry":"f5ca51c8-abf9-435d-84ee-fdf7860d7569","renderOrder":0,"renderMode":0,"rendererEmitterSettings":{},"material":"23fd987d-6965-492d-8b94-5f1577a50984","layers":1,"startTileIndex":{"type":"ConstantValue","value":1},"uTileCount":2,"vTileCount":1,"blendTiles":false,"softParticles":false,"softFarFade":0,"softNearFade":0,"behaviors":[{"type":"SizeOverLife","size":{"type":"PiecewiseBezier","functions":[{"function":{"p0":1,"p1":1,"p2":1.084211735305272,"p3":0.5676373167042756},"start":0},{"function":{"p0":0.5676373167042756,"p1":0.5071181379560418,"p2":0,"p3":0},"start":0.8357291666666666}]}},{"type":"ApplyForce","direction":[0,1,0],"magnitude":{"type":"ConstantValue","value":-2}}],"worldSpace":true}},{"uuid":"67f2841c-b97b-4678-9bb4-576951006a38","type":"ParticleEmitter","name":"big","userData":{"script":" // Randomize tile index for spawning\n const tileColumns = 3;\n const tileRows = 3;\n const randomTileIndex = Math.floor(Math.random() * (tileColumns * tileRows));\n \n // Check and set tile index (if uniforms are available)\n if (this.material && this.material.uniforms) {\n if (!this.material.uniforms.tileIndex) {\n this.material.uniforms.tileIndex = { value: 0 };\n }\n this.material.uniforms.tileIndex.value = randomTileIndex;\n }\n // Alternative: if using a particle attribute or property\n else if (this.material && this.material.map) {\n // For MeshBasicMaterial, try setting via user data\n this.userData.tileIndex = randomTileIndex;\n }"},"layers":1,"matrix":[1,0,0,0,0,2.220446049250313e-16,-1,0,0,1,2.220446049250313e-16,0,0,0,0,1],"up":[0,1,0],"ps":{"version":"3.0","autoDestroy":false,"looping":false,"prewarm":false,"duration":0.1,"shape":{"type":"point"},"startLife":{"type":"ConstantValue","value":0.6},"startSpeed":{"type":"ConstantValue","value":1.5},"startRotation":{"type":"IntervalValue","a":0,"b":360},"startSize":{"type":"IntervalValue","a":0.2,"b":0.5},"startColor":{"type":"ConstantColor","color":{"r":1,"g":1,"b":1,"a":1}},"emissionOverTime":{"type":"ConstantValue","value":0},"emissionOverDistance":{"type":"ConstantValue","value":0},"emissionBursts":[{"time":0,"count":{"type":"ConstantValue","value":15},"probability":1,"interval":0,"cycle":0}],"onlyUsedByOther":false,"instancingGeometry":"f5ca51c8-abf9-435d-84ee-fdf7860d7569","renderOrder":0,"renderMode":0,"rendererEmitterSettings":{},"material":"23fd987d-6965-492d-8b94-5f1577a50984","layers":1,"startTileIndex":{"type":"ConstantValue","value":0},"uTileCount":2,"vTileCount":1,"blendTiles":false,"softParticles":false,"softFarFade":0,"softNearFade":0,"behaviors":[{"type":"SizeOverLife","size":{"type":"PiecewiseBezier","functions":[{"function":{"p0":1,"p1":1,"p2":0.9344334039973435,"p3":0.6430620376937233},"start":0},{"function":{"p0":0.6430620376937233,"p1":0.46121988403206604,"p2":0,"p3":0},"start":0.7935069444444445}]}},{"type":"ApplyForce","direction":[0,1,0],"magnitude":{"type":"ConstantValue","value":-2}}],"worldSpace":true}}]}}
\ No newline at end of file
diff --git a/src/resources/vfx/vfx_json.ts b/src/resources/vfx/vfx_json.ts
index 1d776d4..18bf1fb 100644
--- a/src/resources/vfx/vfx_json.ts
+++ b/src/resources/vfx/vfx_json.ts
@@ -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,
};
diff --git a/src/templateConfig/beforeResourcesLoadedCb.ts b/src/templateConfig/beforeResourcesLoadedCb.ts
index 3d9d941..51c3814 100644
--- a/src/templateConfig/beforeResourcesLoadedCb.ts
+++ b/src/templateConfig/beforeResourcesLoadedCb.ts
@@ -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;
}