feat: static/object layer — Tree off terrain to sparse statics (#89) #144

Merged
marco merged 1 commit from feat/static-object-layer into main 2026-07-22 14:41:38 +02:00
Owner

Summary

The object/static layer for the map (#89), built on top of the biomes foundation (#142) — the piece #142 deliberately deferred (and closes #139 "Biomes D: fixtures object-layer").

Andrea's #142 did the terrain half (biome grounds + TerrainInfo), keeping Tree as an interim terrain type. This PR does the object half: a sparse static layer and moving Tree off terrain onto it, so a tile's semantics come purely from flagsis it walkable? does it block sight? — never from "is it a tree".

  • TileMap gains a sparse IReadOnlyDictionary<GridPosition, StaticTile> (StaticTile = id + blocks) + StaticAt, and a TileMap.Load(mapText, staticsText) seam (Parse stays for terrain-only tests). IsWalkable/HasLineOfSight now combine TerrainInfo flags (Andrea's, untouched) with a static's blocks.
  • Tree leaves the terrain model (TerrainType / TerrainInfo / BiomeVisuals) and becomes a blocking tree static. # in the grid is shorthand for one; a sibling content/maps/world.statics (x y staticId rows) places named statics (walls/pillars for the arena #109). blocks comes from StaticCatalog (adding a static kind = one entry + static/<id> art — extend by data).
  • Client render: statics fold into the existing depth-sorted pass and draw generically by static/<id> (GridRenderer.VisibleStatics/DrawStatic), no per-type logic; the tree art is renamed static/tree. Occlusion-fade generalised to any static.
  • Size-agnostic: statics are sparse (dict keyed by tile), collision is O(1), render is O(viewport).
  • Assets-required policy: updated the invariant — the committed pack is required (like UO); no PlaceholderArt shapes as content, new content extracts real UO art. Sole documented exception: the per-biome ground tints (BiomeVisuals) until the biome art import (#137).

Follow-ups filed: #141 (fold marsh scatter into passable statics — remove the last terrain-type render branch), #143 (express terrain+static defs as a data file, tiledata-style — coordinate with @andrea). #142's biomes/terrain are untouched.

Screenshots / recording

Piloted from a fresh database via the debug harness. Below: walked south from spawn across the biome bands — the tree line renders from the static layer (depth-sorted, the player fades behind it via occlusion), the biomes (#142) still render as distinct bands, and the marsh scatter (top-right) is unchanged. No regression; trees now come from the object layer, not terrain.

world

Commands: login test; goto 24 26; screenshot.

How it was tested

  • Unit tests: TileMap statics + Load (named static blocks movement + sight; blank/comment lines ignored; bad/unknown/out-of-bounds rows rejected), Parse classifies # as a blocking tree static on grass; TerrainInfo every-row + no-terrain-blocks-sight (opacity is now static-only). StaticCatalog.
  • just lint (CSharpier + analyzers, zero warnings) and just test (402 tests) green; whole solution builds incl. client + tools.
  • Ran the client from a fresh DB (screenshot above): trees render/collide from the static layer, biomes render, no regression.

Checklist

  • just lint passes (CSharpier + analyzers, zero warnings)
  • just test is green
  • The whole solution builds (client and tools included)
  • Multi-platform preserved (server on Win/macOS/Linux, client on Win/macOS)
  • Tests added/updated for this change
  • Linked the related issue (#89) and its Definition of Done is met (closes #139)

⚠️ Touches assets.isoa (binary — tree art renamed static/tree). If another biome-track PR touching the pack is in flight, merge promptly to avoid a binary conflict.

🤖 Generated with Claude Code

## Summary The object/static layer for the map (#89), built **on top of the biomes foundation (#142)** — the piece #142 deliberately deferred (and closes **#139** "Biomes D: fixtures object-layer"). Andrea's #142 did the **terrain** half (biome grounds + `TerrainInfo`), keeping `Tree` as an interim *terrain* type. This PR does the **object** half: a **sparse static layer** and moving `Tree` off terrain onto it, so a tile's semantics come purely from **flags** — *is it walkable? does it block sight?* — never from "is it a tree". - **`TileMap`** gains a sparse `IReadOnlyDictionary<GridPosition, StaticTile>` (`StaticTile = id + blocks`) + `StaticAt`, and a `TileMap.Load(mapText, staticsText)` seam (`Parse` stays for terrain-only tests). `IsWalkable`/`HasLineOfSight` now combine `TerrainInfo` flags (Andrea's, untouched) **with** a static's `blocks`. - **`Tree` leaves the terrain model** (`TerrainType` / `TerrainInfo` / `BiomeVisuals`) and becomes a blocking **`tree` static**. `#` in the grid is shorthand for one; a sibling **`content/maps/world.statics`** (`x y staticId` rows) places named statics (walls/pillars for the arena #109). `blocks` comes from **`StaticCatalog`** (adding a static kind = one entry + `static/<id>` art — extend by data). - **Client render**: statics fold into the existing depth-sorted pass and draw **generically** by `static/<id>` (`GridRenderer.VisibleStatics`/`DrawStatic`), no per-type logic; the tree art is renamed `static/tree`. Occlusion-fade generalised to any static. - **Size-agnostic**: statics are sparse (dict keyed by tile), collision is O(1), render is O(viewport). - **Assets-required policy**: updated the invariant — the committed pack is required (like UO); no `PlaceholderArt` shapes as content, new content extracts real UO art. **Sole documented exception**: the per-biome ground tints (`BiomeVisuals`) until the biome art import (#137). Follow-ups filed: **#141** (fold marsh scatter into passable statics — remove the last terrain-type render branch), **#143** (express terrain+static defs as a data file, tiledata-style — coordinate with @andrea). #142's biomes/terrain are untouched. ## Screenshots / recording Piloted from a **fresh database** via the debug harness. Below: walked south from spawn across the biome bands — the **tree line renders from the static layer** (depth-sorted, the player fades behind it via occlusion), the **biomes** (#142) still render as distinct bands, and the **marsh scatter** (top-right) is unchanged. No regression; trees now come from the object layer, not terrain. ![world](https://git.homelab.devncode.it/attachments/ef9cd7f1-9690-4492-b3bd-a4276a67750b) Commands: `login test`; `goto 24 26`; `screenshot`. ## How it was tested - **Unit tests**: `TileMap` statics + `Load` (named static blocks movement + sight; blank/comment lines ignored; bad/unknown/out-of-bounds rows rejected), `Parse` classifies `#` as a blocking tree static on grass; `TerrainInfo` every-row + no-terrain-blocks-sight (opacity is now static-only). `StaticCatalog`. - `just lint` (CSharpier + analyzers, zero warnings) and `just test` (**402 tests**) green; whole solution builds incl. client + tools. - Ran the client from a fresh DB (screenshot above): trees render/collide from the static layer, biomes render, no regression. ## Checklist - [x] `just lint` passes (CSharpier + analyzers, zero warnings) - [x] `just test` is green - [x] The whole solution builds (client and tools included) - [x] Multi-platform preserved (server on Win/macOS/Linux, client on Win/macOS) - [x] Tests added/updated for this change - [x] Linked the related issue (#89) and its Definition of Done is met (closes #139) > ⚠️ Touches `assets.isoa` (binary — tree art renamed `static/tree`). If another biome-track PR touching the pack is in flight, merge promptly to avoid a binary conflict. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(world): object/static layer — move Tree off terrain to sparse statics (#89)
All checks were successful
ci / Lint & Test (pull_request) Successful in 1m33s
0e521fa1a8
On top of the biomes foundation (#142): add the sparse static/object layer
the biome commit deferred (closes #139). TileMap gains a sparse Statics dict
+ StaticAt + TileMap.Load(map, statics); IsWalkable/HasLineOfSight combine
TerrainInfo flags with a static's blocks. Tree leaves TerrainType (+ TerrainInfo
+ BiomeVisuals) and becomes a blocking 'tree' static — '#' in the grid is
shorthand for one; world.statics places named statics. Client draws statics
generically via static/<id> (no per-type logic); tree art renamed static/tree.
Assets-required policy (no placeholder-as-content, biome-tint the sole
exception). Biomes/terrain of #142 untouched. 402 tests green.
marco merged commit 23cdc24fa5 into main 2026-07-22 14:41:38 +02:00
marco deleted branch feat/static-object-layer 2026-07-22 14:41:38 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
marco/IsoMmo!144
No description provided.