From ba9f80f222874b5cf6362eec02cd9f37b5b308a4 Mon Sep 17 00:00:00 2001 From: Aldric Date: Sun, 21 Jun 2026 17:32:50 +0200 Subject: [PATCH] feat(forest): wire watchtower landmark Add the watchtower GLB to the forest streaming manifest and register it as a large static landmark so the first map gets the requested tower silhouette. Cover the placement and registry contract in Vitest, and document the asset as wired for FLO-476. Co-Authored-By: Aldric Co-Authored-By: GPT-5.5 --- docs/guide/assets.md | 12 ++++++++---- src/game/streaming/index.ts | 1 + src/game/streaming/zoneManifests.test.ts | 6 +++++- src/game/streaming/zoneManifests.ts | 7 +++++++ src/scenes/forestScene.test.ts | 9 +++++++-- src/scenes/forestScene.ts | 6 ++++++ 6 files changed, 34 insertions(+), 7 deletions(-) diff --git a/docs/guide/assets.md b/docs/guide/assets.md index e2e2bad..7f5491a 100644 --- a/docs/guide/assets.md +++ b/docs/guide/assets.md @@ -65,7 +65,7 @@ any Community/showcase page. | **Empire toll gate** | `public/models/toll-gate.glb` | **1947** | 4.1 MiB | static | preview `019ee748-205e-763a-a883-bdda11e91c7e` · retexture `019ee749-ae59-7680-aa94-e6a8842c7bd5` | | **Caravan wagon (Salt Road)** *(wired: caravan enemy visual + forest static prop, FLO-470)* | `public/models/caravan-wagon.glb` | **2827** | 394 KiB | static | `019ee749-d4cf-79b9-b813-d98be2201197` (preview) / `019ee74d-2300-772c-8a22-612c27dd99dc` (retexture) | | **Cargo crate (Salt Road)** *(wired: forest cargo decor, FLO-470)* | `public/models/cargo-crate.glb` | **1022** | 296 KiB | static | `019ee743-a02c-7878-a22e-7b67cdfbafa6` (preview) | -| **Ruined watchtower (Salt Road)** | `public/models/watchtower.glb` | **2564** | 379 KiB | static | `019ee749-6051-7990-b286-98be4ecf82b4` (retexture) | +| **Ruined watchtower (Salt Road)** *(wired: forest landmark, FLO-476)* | `public/models/watchtower.glb` | **2564** | 379 KiB | static | `019ee749-6051-7990-b286-98be4ecf82b4` (retexture) | | **Ranged enemy (archer)** | `public/models/ranged-archer.glb` | **2589** | 258 KiB | static (no skeleton) | preview `019ee91a-3128-705b-8acc-3d10e03e9930` · retexture `019ee91c-7315-78b1-8841-a90f77930d17` | | **Empire palace-guard** | `public/models/empire-palace-guard.glb` | **2793** | 357 KiB | static (no skeleton) | retexture `019ee91c-7f15-78b4-9665-5f7c18268eec` (of soldier `019ee601-93f0-7988-86f8-e35ce1067881`) | @@ -113,9 +113,11 @@ in `src/scenes/humanLandsScene.ts`. - **Rig:** ships **static** (no skeleton) — it is scenery, not animated. - **Verification:** loads headless via `node tools/meshy-3d/smoke_load_glb.mjs public/models/watchtower.glb` → 2 meshes, 2564 tris, no errors. -- **Handoff:** wiring the GLB into the scene (replacing the procedural - watchtower box at `humanLandsScene.ts:60`) is engineering's job — for - Daedalus/CTO, not done here. +- **Wiring:** FLO-476 places the GLB in the first forest map as + `landmark.forest-watchtower`, streamed through `defaultLoadGlb()` with the + matte/faceted conform. The canonical Salt Road / human-lands landmark remains a + separate scene-wiring concern; this forest placement is the board-requested + first-map landmark. ### Empire soldier (enemy) — Phase 2 @@ -314,6 +316,8 @@ the same streaming manifest used by the tree/hut props: prop; also the live `CaravanEnemy` visual. - `korovany_hero_player-default.glb` → `npc.forest-static-elf`, two static decorative forest elves. +- `watchtower.glb` → `landmark.forest-watchtower`, one large static ruined tower + placed near the first forest map as a visible silhouette landmark (FLO-476). All streamed GLBs go through `defaultLoadGlb`, which normalizes them with `loadModel()` and applies the shared `flatShade()` matte/faceted conform before diff --git a/src/game/streaming/index.ts b/src/game/streaming/index.ts index b6e8cbf..464a08a 100644 --- a/src/game/streaming/index.ts +++ b/src/game/streaming/index.ts @@ -19,6 +19,7 @@ export { FOREST_CHEST_ASSET_ID, FOREST_STATIC_ELF_ASSET_ID, FOREST_TREE_ASSET_ID, + FOREST_WATCHTOWER_ASSET_ID, WOODEN_HUT_ASSET_ID, } from './zoneManifests' export { diff --git a/src/game/streaming/zoneManifests.test.ts b/src/game/streaming/zoneManifests.test.ts index ef717a5..688ae89 100644 --- a/src/game/streaming/zoneManifests.test.ts +++ b/src/game/streaming/zoneManifests.test.ts @@ -7,6 +7,7 @@ import { FOREST_CHEST_ASSET_ID, FOREST_STATIC_ELF_ASSET_ID, WOODEN_HUT_ASSET_ID, + FOREST_WATCHTOWER_ASSET_ID, ZONE_MANIFESTS, getZoneManifest, } from './zoneManifests' @@ -19,12 +20,15 @@ describe('zone manifests', () => { } }) - it('streams the forest props plus leftover decor (12 trees, 3 huts, 5 leftovers)', () => { + it('streams the forest props plus leftover decor (12 trees, 3 huts, 6 leftovers)', () => { const forest = getZoneManifest('forest') const trees = forest.placements.filter((p) => p.assetId === FOREST_TREE_ASSET_ID) const huts = forest.placements.filter((p) => p.assetId === WOODEN_HUT_ASSET_ID) expect(trees).toHaveLength(12) expect(huts).toHaveLength(3) + const watchtower = forest.placements.find((p) => p.assetId === FOREST_WATCHTOWER_ASSET_ID) + expect(watchtower?.position).toEqual({ x: 12.5, y: 0, z: -13.5 }) + expect(watchtower?.rotationY).toBeCloseTo(-0.55) expect(forest.placements.filter((p) => p.assetId === FOREST_CHEST_ASSET_ID)).toHaveLength(1) expect(forest.placements.filter((p) => p.assetId === FOREST_CARGO_CRATE_ASSET_ID)).toHaveLength(1) expect(forest.placements.filter((p) => p.assetId === FOREST_CARAVAN_WAGON_ASSET_ID)).toHaveLength(1) diff --git a/src/game/streaming/zoneManifests.ts b/src/game/streaming/zoneManifests.ts index 45ce4b9..81fff80 100644 --- a/src/game/streaming/zoneManifests.ts +++ b/src/game/streaming/zoneManifests.ts @@ -29,6 +29,8 @@ export const FOREST_CARGO_CRATE_ASSET_ID = 'prop.forest-cargo-crate' export const FOREST_CARAVAN_WAGON_ASSET_ID = 'prop.forest-caravan-wagon' /** Retired hero GLB reused as static forest elf NPC decor (FLO-470). */ export const FOREST_STATIC_ELF_ASSET_ID = 'npc.forest-static-elf' +/** Ruined watchtower placed in the first forest map as a landmark (FLO-476). */ +export const FOREST_WATCHTOWER_ASSET_ID = 'landmark.forest-watchtower' /** Tree positions: (x, z) pairs in scene units. Keeps a 4-unit clearing. */ const FOREST_TREE_POSITIONS: readonly [number, number][] = [ @@ -54,6 +56,11 @@ const FOREST_HUT_POSITIONS: readonly [number, number][] = [ ] const FOREST_LEFTOVER_PLACEMENTS: readonly ZoneAssetPlacement[] = [ + { + assetId: FOREST_WATCHTOWER_ASSET_ID, + position: { x: 12.5, y: 0, z: -13.5 }, + rotationY: -0.55, + }, { assetId: FOREST_CARAVAN_WAGON_ASSET_ID, position: { x: -5.5, y: 0, z: -9 }, diff --git a/src/scenes/forestScene.test.ts b/src/scenes/forestScene.test.ts index dacc3d3..3a875eb 100644 --- a/src/scenes/forestScene.test.ts +++ b/src/scenes/forestScene.test.ts @@ -9,6 +9,7 @@ import { FOREST_CHEST_ASSET_ID, FOREST_STATIC_ELF_ASSET_ID, FOREST_TREE_ASSET_ID, + FOREST_WATCHTOWER_ASSET_ID, FOREST_SPAWN_PROP_SPECS, FOREST_ZONE_ID, SAFE_SPAWN_BUFFER, @@ -57,14 +58,18 @@ describe('seedForestAssets', () => { expect(registry.resolve(FOREST_STATIC_ELF_ASSET_ID).url).toBe( '/models/korovany_hero_player-default.glb', ) + expect(registry.resolve(FOREST_WATCHTOWER_ASSET_ID).url).toBe('/models/watchtower.glb') }) - it('gives the tree a larger targetSize than the hut', () => { + it('gives landmark-scale assets a larger targetSize than ordinary props', () => { const registry = new AssetRegistry() seedForestAssets(registry) expect(registry.resolve(FOREST_TREE_ASSET_ID).metadata.targetSize).toBeGreaterThan( registry.resolve(WOODEN_HUT_ASSET_ID).metadata.targetSize ?? 0, ) + expect(registry.resolve(FOREST_WATCHTOWER_ASSET_ID).metadata.targetSize).toBeGreaterThan( + registry.resolve(FOREST_TREE_ASSET_ID).metadata.targetSize ?? 0, + ) }) // The forest manifest (E3.2 wiring) and the registry seeding live in separate @@ -417,7 +422,7 @@ describe('forest safe spawn & difficulty curve (FLO-412)', () => { expect(onPlayerDamaged).not.toHaveBeenCalled() game.dispose() - }) + }, 15_000) // Acceptance (FLO-412): the player can walk to a caravan without dying. The // nearest caravan (`caravan-1`, (-8,-6), 10 m) sits inside the soldier-free diff --git a/src/scenes/forestScene.ts b/src/scenes/forestScene.ts index 168fb42..5c57193 100644 --- a/src/scenes/forestScene.ts +++ b/src/scenes/forestScene.ts @@ -26,6 +26,7 @@ import { FOREST_CHEST_ASSET_ID, FOREST_STATIC_ELF_ASSET_ID, FOREST_TREE_ASSET_ID, + FOREST_WATCHTOWER_ASSET_ID, WOODEN_HUT_ASSET_ID, ZoneStreamingManager, defaultLoadGlb, @@ -104,6 +105,7 @@ export { FOREST_CHEST_ASSET_ID, FOREST_STATIC_ELF_ASSET_ID, FOREST_TREE_ASSET_ID, + FOREST_WATCHTOWER_ASSET_ID, WOODEN_HUT_ASSET_ID, } @@ -154,6 +156,10 @@ export function seedForestAssets(registry: AssetRegistry): void { url: '/models/korovany_hero_player-default.glb', metadata: { label: 'Static forest elf', targetSize: 1.8 }, }) + registry.register(FOREST_WATCHTOWER_ASSET_ID, { + url: '/models/watchtower.glb', + metadata: { label: 'Forest watchtower', targetSize: 9 }, + }) } function propMaterial(scene: Scene, name: string, color: Color3): StandardMaterial {