Structures Implementation Plan¶
Purpose¶
This plan covers full implementation of map-space Structures.
Always update this plan when changing Structure data contracts, runtime ownership, occupancy behavior, rendering, sidecar load/save, commands, UI/debug tooling, pathing integration, or persistence behavior.
Structures are persistent or semi-persistent stateful map objects such as campfires, tents, caches, cairns, shelters, drying racks, and buildings. They can span multiple terrain pixels, have clear placement footprints, and may hide visual information below them. Structure-to-structure overlap is disallowed.
Structures are not sprite details and are not decals. They may reuse the same low-level atlas-backed quad rendering path as decals and agents, but their runtime owner, data model, placement validation, persistence, and gameplay contracts are separate.
Dependencies¶
- Sprite rendering design context:
docs/SPRITE_DETAILS_RENDERING_DESIGN.md - Existing map lifecycle and sidecar patterns:
src/gameplay/mapSidecarLoader.jssrc/gameplay/mapDataSaveController.js - Existing command ownership rules:
src/core/registerMainCommands.jssrc/gameplay/interactionCommands.js - Existing render pass architecture:
src/render/renderPipelineRuntime.jssrc/render/frameSwarmRenderRuntime.js - Existing overlay/debug UI conventions:
docs/RD_UI_ARCHITECTURE.mddocs/UI_LAYOUT_GRID.md
Current Status¶
The first structure slice is implemented and manually smoke-tested.
Implemented:
- optional
assets/<mapName>/structures.jsonsidecar load/save structureRuntimeowner with validation, occupancy, placement, removal, state updates, render snapshots, and query APIs- runtime-generated map-sprite atlas from individual
spriteSrcPNGs - shared map-sprite atlas generation treats structure sources as whole-image
sprites unless a render item explicitly provides source-frame crop metadata;
this preserves the
128x128tent source while agents can still use32x32or64x64strip frames in the same renderer implementation - WebGL structure pass after terrain with terrain/point-light lighting
RD > Sprites > Structurescontrols for render visibility, type selection, place-at-player, repeated cursor place mode, nearest selection, removal, occupancy overlay, and selected-state readout- binary green/red footprint placement preview
- local pathfinding and movement blocking for types with
blocksMovement: true - focused and full JS test coverage for serializer, runtime, sidecar, commands, UI, rendering, movement, and pathfinding integration
Still intentionally future work:
- player-facing structure placement through gameplay actions
- route-planning obstacle projection
- authored-default vs mutable-savegame separation
- container/rest/heat/crafting/event/audio gameplay integrations
- performance profiling with large structure counts
Phase 0: Scope Lock¶
- Decide first proof structure type.
- Recommended:
cacheif inventory/container integration is next. - Alternative:
campfireif lighting/rest/event integration is next. - Chosen v1 proof:
nomadic_tentinassets/map3/structures.json. - Decide first structure sprite source location.
- Reusable structure sprites live under
assets/sprites/structures/default/. - Scoped structure sets can live under
assets/sprites/structures/<mapOrSetName>/. - Artists provide individual PNGs; the runtime can build the atlas.
- Decide initial proof atlas slot size.
- First proof uses one
128x128source sprite rendered over4x4map pixels. - Structure slot size remains per-map/per-sidecar metadata, not hardcoded globally.
- Decide first render order.
- Recommended v1: terrain -> material detail -> sprite details -> ground decals -> structures -> agents -> UI.
- Defer shared agent/structure y-sort until tall structures prove the need.
- Decide whether v1 structures block pathfinding.
- Occupancy grid exists in v1.
- Local pathfinding and movement blocking are gated per structure type with
blocksMovement.
Phase 1: Data Contract¶
- Create
docs/STRUCTURE_DATA_CONTRACT.md. - Define authored sidecar file name:
structures.json. - Define versioned root shape.
- Define structure type registry shape.
- Define structure instance shape.
- Define footprint mask shape.
- Define visual bounds vs footprint bounds.
- Define interaction radius or interaction points.
- Define state payload rules.
- Define backward-tolerant unknown-field behavior.
- Define minimum v1 sidecar example.
- Include one structure type.
- Include one placed structure instance.
- Include atlas metadata.
- Define ID rules.
- Stable unique instance IDs.
- Stable structure type IDs.
- Type IDs must not depend on atlas slot numbers.
- Define coordinate rules.
- Use
pixelXandpixelYfor current single-map compatibility. - Keep contract ready for future
worldXandworldY. - Define anchor meaning as footprint origin for v1.
- Define no-overlap rule.
- Structure footprints cannot overlap occupied structure cells.
- Visual quads may cover terrain/details/decals.
- Visual quads may extend beyond footprint bounds.
Phase 2: Pure Structure Model¶
- Add
src/gameplay/structureDataSerializer.js. - Export default/empty structure data.
- Normalize raw
structures.json. - Validate version.
- Normalize atlas metadata.
- Normalize type definitions.
- Normalize instances.
- Clamp numeric fields.
- Preserve unknown state fields where safe.
- Reject duplicate type IDs.
- Reject duplicate instance IDs.
- Reject missing type references.
- Reject invalid footprint masks.
- Reject configured data caps.
- Add unit tests for serializer.
- Empty data normalizes.
- Valid example normalizes.
- Missing optional fields use defaults.
- Duplicate IDs fail.
- Invalid footprint masks fail.
- Unknown future fields are tolerated where intended.
- Add pure footprint helpers.
- Convert footprint mask to occupied map cells.
- Compute visual bounds.
- Compute footprint bounds.
- Test anchor/origin behavior.
Phase 3: Structure Runtime Owner¶
- Add
src/gameplay/structureRuntime.js. - Own structure type registry.
- Own structure instance state.
- Provide
applyStructureData(rawData). - Provide
serializeStructureData(). - Provide
getStructureSnapshot(). - Provide
getStructureRenderSnapshot(). - Provide
getStructureAtPixel(pixelX, pixelY). - Provide
canPlaceStructure(typeId, pixelX, pixelY, options). - Provide
placeStructure(typeId, pixelX, pixelY, state). - Provide
removeStructure(instanceId). - Provide
updateStructureState(instanceId, patch). - Keep runtime mutation explicit.
- Do not mutate from renderer.
- Do not mutate from event handlers.
- Commands call structure runtime APIs.
- Snapshot rules.
- Snapshots must be clone-safe or immutable enough for UI/render reads.
- Renderer snapshots include only render-relevant fields.
- Gameplay snapshots include type/state/footprint information.
- Add unit tests for runtime.
- Apply and serialize round trip.
- Snapshot isolation.
- Placement succeeds on empty cells.
- Placement rejects occupied footprint cells.
- Removal frees occupancy.
- State updates are explicit and preserve unknown state keys.
Phase 4: Occupancy Grid¶
- Add occupancy grid ownership inside
structureRuntimeor a small helper module. - Use a typed array for occupancy.
-
0means empty. - Non-zero value maps to structure runtime index or compact handle.
- Build occupancy from normalized structures.
- Rebuild on apply/load.
- Update on place/remove.
- Detect overlaps during rebuild.
- Add occupancy query APIs.
-
isStructureOccupied(pixelX, pixelY). -
getStructureIdAt(pixelX, pixelY). -
getOccupiedCells(instanceId). - Add tests.
- Occupancy indexes match footprint masks.
- Out-of-bounds footprints are rejected or clipped by explicit rule.
- Removing a structure clears only its cells.
- Rebuilding occupancy catches sidecar overlaps and skips invalid loaded instances while keeping valid earlier instances.
- Defer pathfinding integration behind a clear follow-up task unless v1 proof requires blocking.
Phase 5: Render Backend¶
- Decide shared renderer naming.
- Recommended:
src/render/mapSpriteRenderer.jsfor common atlas-backed quad drawing. - Structure-specific wrapper:
src/render/structureRenderer.jsonly if needed. - Add shader sources for map-space quads.
- Continuous map-space anchor.
- Visual width/height in map pixels.
- Atlas slot lookup.
- Tint and opacity support.
- Optional cardinal rotation support can be deferred.
- Add renderer resource setup.
- Program creation.
- VAO/VBO or instance buffer.
- Placeholder atlas texture upload.
- Nearest filtering.
- Correct source sprite vertical orientation through atlas UV packing.
- Apply first-pass terrain lighting to structures.
- Sample terrain normal texture at structure fragment map UV.
- Sample terrain shadow texture for sun/moon attenuation.
- Sample point-light texture.
- Use terrain sun/moon/ambient frame lighting params.
- Add render pass integration.
- Register structure pass after main terrain.
- Ensure alpha blending state is set and restored intentionally.
- Draw only when terrain is shown.
- Add render snapshot consumption.
- Structure runtime produces compact render list.
- Renderer does not know gameplay state.
- Renderer does not allocate per structure every frame where avoidable.
- Add focused render unit tests where practical.
- Buffer packing contains expected instance attributes.
- Empty render list skips draw.
- Atlas slot math is deterministic.
Phase 6: Atlas And Assets¶
- Add default structure sprite folder.
- Create
assets/sprites/structures/default/. - Keep the folder tracked with
.gitkeepuntil real art is committed. - Add runtime atlas generation from individual source PNGs.
- Use per-type
spriteSrcpaths. - Draw source images into fixed atlas slots.
- Use nearest filtering by default.
- Preserve whole-source image packing for structures that do not provide explicit source-frame crop metadata.
- Add default structure metadata.
- Define slot IDs.
- Define type-to-sprite mapping.
- Point map3 proof type at
assets/sprites/structures/default/nomadic_tent_01.png. - Add graceful fallback behavior.
- Missing atlas uses placeholder texture.
- Missing structure sidecar means no structures.
- Invalid sidecar surfaces a visible startup/map-load error if it should block.
- Decide whether default atlas lives globally or map-locally.
- V1 uses individual global/default sprite sources and a runtime-generated atlas.
- Map-local/scoped source folders can be referenced by
structures.jsonthroughspriteSrc.
Phase 7: Map Sidecar Load And Save¶
- Extend
mapSidecarLoader. - Load optional
structures.jsonfrom URL maps. - Load optional
structures.jsonfrom selected folder files. - Apply through
structureRuntime.applyStructureData. - Missing sidecar applies empty structure data.
- Invalid sidecar errors should be visible on title/map-load status if blocking.
- Extend
mapDataSaveController. - Include
structures.jsoninSave Allonly if we decide authored defaults should save through map data. - Consider a separate save action if mutable run structures should not overwrite authored defaults.
- Add tests.
- Save All includes or intentionally excludes
structures.jsonper decision. - Missing structures sidecar is tolerated.
- File-folder loading applies structures.
- Update
AI_CONTEXT.mdif sidecar behavior is implemented.
Phase 8: Commands¶
- Register structure commands in command routing.
-
structure/place. -
structure/remove. -
structure/updateState. -
structure/selectif selection is part of v1. - Command rules.
- Commands validate inputs.
- Commands call structure runtime mutation APIs.
- Commands return or surface failure reason where practical.
- Events remain post-change refresh/invalidation only.
- Add command tests.
- Place command forwards valid placement and reports success.
- Place command reports invalid placement without redraw.
- Remove command routes through structure runtime and reports status.
- Update command routes through structure runtime and reports status.
Phase 9: Interaction And Selection¶
- Decide v1 interaction surface.
- Debug-only placement through RD.
- Terrain-click placement mode.
- Local activity menu action.
- Recommended v1.
- RD debug placement/removal first.
- Defer player-facing placement until structure gameplay is defined.
- Add selection support if needed.
- Use nearest-by-type query for first debug selection.
- Store selected structure ID in UI owner, not structure runtime.
- Draw selection gizmo through overlay canvas, not structure renderer.
- Add placement preview if terrain-click placement is included.
- Show footprint validity.
- Green valid, red invalid.
- Keep place mode and cursor preview active after successful placement for repeated authoring.
- Do not resize fixed HUD/side panels for placement details.
- V1 uses binary works/does-not-work feedback.
- Future player-skill levels can expose richer per-cell reasons in the overlay/report.
- Add tests for interaction helpers where practical.
Phase 10: RD Debug UI¶
- Add RD panel controls in the appropriate tab.
- Use
RD > Sprites > Structuresfor first debug controls. - Keep panel content compact per
docs/UI_LAYOUT_GRID.md. - Controls.
- Toggle structure render visibility.
- Select proof structure type.
- Place at player.
- Place mode with cursor footprint preview.
- Select nearest by selected type.
- Remove selected.
- Show occupancy debug overlay.
- Show selected structure state.
- UI ownership.
- UI dispatches commands.
- UI reflects structure runtime snapshots.
- UI does not mutate structure data directly.
- Add UI tests if adding standalone runtime helpers.
Phase 11: Gameplay Integration Hooks¶
- Define structure capability tags.
-
container. -
rest. -
heat. -
crafting. -
ritual. -
blocksMovement. - Add minimal query APIs.
-
getStructuresNear(pixelX, pixelY, radiusPx). -
getStructuresByCapability(capability). -
getNearestStructureByType(typeId, pixelX, pixelY). - Defer deep integrations until needed.
- Inventory/container runtime integration.
- Rest/campfire effects.
- Pathfinding blocking.
- Event triggers.
- Audio/lighting coupling.
- Add tests for query APIs.
Phase 12: Pathfinding Integration¶
- Decide whether v1 blocks movement.
- V1 supports type-gated blocking through
blocksMovement. - Add pathfinding cost/block integration.
- If blocking is enabled.
- Structure runtime exposes
isMovementBlocked(pixelX, pixelY). - Structure runtime exposes bounded blocked-cell queries for local Dijkstra windows.
- Pathfinding preview/runtime writes structure obstacles into the local Dijkstra field.
- RD Pathing exposes structure and terrain diagonal corner-cutting toggles.
- Route planning either ignores structures or receives a low-res obstacle projection.
- Movement execution validates target cells against current occupancy.
- Add tests.
- Local pathfinding avoids blocking structures.
- Movement fails or reroutes when target becomes blocked.
- Non-blocking structures do not affect paths.
Phase 13: Persistence Strategy¶
- Separate authored defaults from mutable run state.
-
structures.jsonas map/scenario defaults. - Future savegame state for player-built, damaged, moved, or removed structures.
- Decide v1 persistence behavior.
- Authored-only structures loaded from sidecar.
- Save All writes current structures back as map data for now.
- Or RD changes are debug/runtime-only until savegame architecture exists.
- Document decision in
AI_CONTEXT.md. - Add serialization tests matching the chosen behavior.
Phase 14: Performance And Limits¶
- Define initial caps.
- Maximum structure types per map.
- Maximum structure instances per map.
- Maximum footprint dimensions.
- Maximum visual sprite dimensions in map pixels.
- Recommended initial caps.
- Structure types:
256. - Instances:
4096or lower if UI/debug only. - Footprint dimensions:
64x64max. - Visual dimensions:
128x128map pixels max. - Add validation for caps.
- Add performance smoke scenario with many structures.
- Profile render pass timing if many structures are visible.
Phase 15: Documentation¶
- Update
docs/SPRITE_DETAILS_RENDERING_DESIGN.mdif implementation choices differ from planning. - Add
docs/STRUCTURE_DATA_CONTRACT.md. - Update
AI_CONTEXT.md. - New structure owner module.
- New sidecar behavior.
- Render order.
- Structure occupancy/pathing behavior.
- Save/load behavior.
- Update
README.mdonly if run steps or asset requirements change. - Document required asset files if default maps use structures.
Phase 16: Validation¶
- Run focused JS syntax checks.
-
node --check src\gameplay\structureDataSerializer.js -
node --check src\gameplay\structureRuntime.js -
node --check src\render\mapSpriteRenderer.js -
node --check src\render\structureRenderer.jsif created. - Run focused tests.
-
node --test tests\structureDataSerializer.test.js -
node --test tests\structureRuntime.test.js -
node --test tests\mapDataSaveController.test.js -
node --test tests\mapSidecarLoader.test.jsif added. - Run broader tests after integration.
-
node --test tests\*.test.js - Run docs lint.
-
npm run lint:md - Manual smoke test.
- Load default map.
- Confirm missing
structures.jsonis harmless. - Confirm proof structure renders.
- Confirm occupancy debug matches footprint.
- Confirm placement rejects overlap.
- Confirm save/load behavior matches the chosen persistence strategy.
Suggested Implementation Order¶
- Phase 1: data contract.
- Phase 2: serializer and pure tests.
- Phase 3: runtime owner and tests.
- Phase 4: occupancy grid and tests.
- Phase 5: renderer proof with hardcoded render snapshot.
- Phase 6: atlas/assets fallback.
- Phase 7: sidecar load/save.
- Phase 8: commands.
- Phase 10: RD debug controls.
- Phase 9: interaction/selection if needed.
- Phase 11: gameplay query hooks.
- Phase 12: pathfinding integration only when explicitly needed.
- Phase 13: persistence strategy before player-built structures.
- Phase 15: documentation updates.
- Phase 16: validation.