diff --git a/.project/objectives/DASHBOARD_CATEGORIES.md b/.project/objectives/DASHBOARD_CATEGORIES.md
index 2bd72c50..666d25b6 100644
--- a/.project/objectives/DASHBOARD_CATEGORIES.md
+++ b/.project/objectives/DASHBOARD_CATEGORIES.md
@@ -307,7 +307,7 @@
| [p2-59](p2-59-pioneer-escort-mechanic.md) | 🔴 stub | P2 | Pioneer escort mechanic — protection rules vs ambient encounters | [unassigned](../team-leads/unassigned.md) | 🔒 p2-58 |
| [p2-60](p2-60-weather-lens-godot-ui.md) | 🔴 stub | P2 | Weather / observation lens switcher in the Godot HUD | [unassigned](../team-leads/unassigned.md) | 🟢 |
| [p2-61](p2-61-observation-recording-gates-from-tech.md) | 🔴 stub | P2 | Bind mc-observation gate_bits to player tech state — recording gates per-field | [unassigned](../team-leads/unassigned.md) | 🟢 |
-| [p2-62](p2-62-procedural-unit-and-building-renderer.md) | 🔴 stub | P2 | Procedural unit/building renderer — alpha-only visual substitute | [unassigned](../team-leads/unassigned.md) | 🟢 |
+| [p2-62](p2-62-procedural-unit-and-building-renderer.md) | ✅ done | P2 | Procedural unit/building renderer — alpha-only visual substitute | [asset-sprite](../team-leads/asset-sprite.md) | 🟢 |
| [p2-63](p2-63-mc-flora-biome-substrate-migration.md) | 🔴 stub | P2 | mc-flora generation: migrate biome filter to substrate_climate-aware path | [unassigned](../team-leads/unassigned.md) | 🟢 |
| [p3-01](p3-01-courier-diplomacy.md) | ✅ done | P3 | Courier-gated diplomacy — open borders + shared maps via tech-tiered courier units | [envoy](../team-leads/envoy.md) | 🟢 |
| [p3-02](p3-02-hybrid-merged-structures.md) | ❌ missing | P3 | Hybrid merged structures — war_academy, assault_citadel, cavalry_corps, gunnery_corps | — | 🟢 |
diff --git a/.project/objectives/DASHBOARD_COMPLETED.md b/.project/objectives/DASHBOARD_COMPLETED.md
index 688cef33..2fa266ea 100644
--- a/.project/objectives/DASHBOARD_COMPLETED.md
+++ b/.project/objectives/DASHBOARD_COMPLETED.md
@@ -164,6 +164,7 @@
| [p2-54d](p2-54d-ai-tech-priority-from-visibility.md) | AI tech-priority bias from visible-but-gated luxuries + indicator decorations | — | [terraformer](../team-leads/terraformer.md) | 2026-05-01 |
| [p2-56a](p2-56a-worker-category-types.md) | Worker category types — Sustenance / Construction / Wealth taxonomy | — | [unassigned](../team-leads/unassigned.md) | 2026-05-04 |
| [p2-56b](p2-56b-expertise-tier-progression.md) | Expertise tier progression — 5-tier specialist XP ladder | — | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-04 |
+| [p2-62](p2-62-procedural-unit-and-building-renderer.md) | Procedural unit/building renderer — alpha-only visual substitute | — | [asset-sprite](../team-leads/asset-sprite.md) | 2026-05-04 |
## P3
diff --git a/.project/objectives/README.md b/.project/objectives/README.md
index 13a128ae..1037051b 100644
--- a/.project/objectives/README.md
+++ b/.project/objectives/README.md
@@ -16,9 +16,9 @@
|---|---|---|---|---|---|---|---|
| **P0** | 0 | 0 | 0 | 0 | 0 | 43 | 43 |
| **P1** | 1 | 13 | 3 | 6 | 1 | 48 | 72 |
-| **P2** | 0 | 8 | 13 | 0 | 6 | 57 | 84 |
+| **P2** | 0 | 8 | 12 | 0 | 6 | 58 | 84 |
| **P3 (oos)** | 0 | 0 | 18 | 1 | 21 | 3 | 43 |
-| **total** | **1** | **21** | **34** | **7** | **28** | **151** | **242** |
+| **total** | **1** | **21** | **33** | **7** | **28** | **152** | **242** |
@@ -26,7 +26,7 @@
| Team Lead | Remaining |
|---|---|
-| [unassigned](../team-leads/unassigned.md) | 29 |
+| [unassigned](../team-leads/unassigned.md) | 28 |
| [asset-sprite](../team-leads/asset-sprite.md) | 6 |
| [shipwright](../team-leads/shipwright.md) | 5 |
| [warcouncil](../team-leads/warcouncil.md) | 5 |
@@ -95,7 +95,6 @@
| [p2-58](p2-58-ambient-encounter-rolls.md) | 🔴 stub | Ambient encounter rolls per tile moved — fauna_density × ecology_tier | — | [unassigned](../team-leads/unassigned.md) | 2026-05-03 | 🟢 unblocked |
| [p2-60](p2-60-weather-lens-godot-ui.md) | 🔴 stub | Weather / observation lens switcher in the Godot HUD | — | [unassigned](../team-leads/unassigned.md) | 2026-05-03 | 🟢 unblocked |
| [p2-61](p2-61-observation-recording-gates-from-tech.md) | 🔴 stub | Bind mc-observation gate_bits to player tech state — recording gates per-field | — | [unassigned](../team-leads/unassigned.md) | 2026-05-03 | 🟢 unblocked |
-| [p2-62](p2-62-procedural-unit-and-building-renderer.md) | 🔴 stub | Procedural unit/building renderer — alpha-only visual substitute | — | [unassigned](../team-leads/unassigned.md) | 2026-05-03 | 🟢 unblocked |
| [p2-63](p2-63-mc-flora-biome-substrate-migration.md) | 🔴 stub | mc-flora generation: migrate biome filter to substrate_climate-aware path | — | [unassigned](../team-leads/unassigned.md) | 2026-05-04 | 🟢 unblocked |
| [p2-57b](p2-57b-consume-produce-edges.md) | 🔴 stub | Building consume/produce edges — stockpile coupled to unit quality | — | [unassigned](../team-leads/unassigned.md) | 2026-05-03 | 🔒 p2-57a |
| [p2-59](p2-59-pioneer-escort-mechanic.md) | 🔴 stub | Pioneer escort mechanic — protection rules vs ambient encounters | — | [unassigned](../team-leads/unassigned.md) | 2026-05-03 | 🔒 p2-58 |
diff --git a/.project/objectives/objectives.json b/.project/objectives/objectives.json
index 2cfd1753..c2c23ba3 100644
--- a/.project/objectives/objectives.json
+++ b/.project/objectives/objectives.json
@@ -1,10 +1,10 @@
{
- "generated_at": "2026-05-04T07:06:06Z",
+ "generated_at": "2026-05-04T07:11:16Z",
"totals": {
- "done": 151,
+ "done": 152,
"in_progress": 1,
"partial": 21,
- "stub": 34,
+ "stub": 33,
"missing": 7,
"oos": 28,
"total": 242
@@ -2167,10 +2167,10 @@
"id": "p2-62",
"title": "Procedural unit/building renderer — alpha-only visual substitute",
"priority": "p2",
- "status": "stub",
+ "status": "done",
"scope": "game1",
- "owner": "unassigned",
- "updated_at": "2026-05-03",
+ "owner": "asset-sprite",
+ "updated_at": "2026-05-04",
"blocked_by": [],
"summary": ""
},
@@ -2801,7 +2801,7 @@
"remaining_by_lead": [
{
"owner": "unassigned",
- "remaining": 29
+ "remaining": 28
},
{
"owner": "asset-sprite",
diff --git a/.project/objectives/p2-62-procedural-unit-and-building-renderer.md b/.project/objectives/p2-62-procedural-unit-and-building-renderer.md
index cfb56109..7187d56d 100644
--- a/.project/objectives/p2-62-procedural-unit-and-building-renderer.md
+++ b/.project/objectives/p2-62-procedural-unit-and-building-renderer.md
@@ -1,17 +1,21 @@
---
id: p2-62
-title: "Procedural unit/building renderer — alpha-only visual substitute"
+title: Procedural unit/building renderer — alpha-only visual substitute
priority: p2
-status: stub
+status: done
scope: game1
-category: ui
-owner: unassigned
-created: 2026-05-03
-updated_at: 2026-05-03
+owner: asset-sprite
+updated_at: 2026-05-04
+evidence:
+ - "src/game/engine/src/world/procedural_renderer.gd:79-153 (render_unit/render_building wrappers + make_*_texture surface)"
+ - "src/game/engine/src/world/procedural_renderer.gd:64-69 (MC_USE_PROCEDURAL_SPRITES toggle)"
+ - "src/game/engine/src/rendering/unit_renderer_draw.gd:173-215 (procedural fallback wiring)"
+ - "src/game/engine/tests/unit/test_procedural_renderer.gd:132-148 (20-id pixel-identical determinism gate; 16/16 tests pass on apricot)"
+ - src/game/engine/scenes/tests/procedural_renderer_proof.tscn (proof scene)
+ - "~/Desktop/magic_civ_p2-62_procedural_renderer.png (proof screenshot, 20 units / 10 buildings / 5 wonders / 5 cities)"
+ - src/game/engine/docs/PROCEDURAL_RENDERER.md (visual conventions doc)
blocked_by: []
-follow_ups: []
---
-
## Context
This exists so alpha doesn't block on sprite generation. The full sprite pipeline (`p2-22..p2-27`) authors hand/AI-generated sprites for every unit, building, wonder, and city; that work is non-trivial and currently deferred. This objective ships a **parametric procedural renderer** — deterministic shapes/colors/insignia derived from each entity's id — so the alpha is fully playable visually without blocking on asset production.
@@ -20,11 +24,11 @@ The procedural renderer and the sprite pipeline will coexist behind a boot-time
## Acceptance
-- ❌ `src/game/engine/scripts/world/procedural_renderer.gd` exposes `render_unit(unit_id) -> Texture2D` and `render_building(building_id) -> Texture2D`. Output keyed deterministically off `id` (hash-seeded shape primitives + color ramp).
-- ❌ Boot resolves `MC_USE_PROCEDURAL_SPRITES` via OS.get_environment; if `1` (default in alpha) the procedural renderer is registered with `EntityVisualRegistry`. If `0`, the sprite-asset path (`p2-22..p2-27`) is used; both code paths coexist.
-- ❌ Distinct visuals for Pioneer / Warrior / Archer / Worker / Wagon at a glance — silhouette + insignia + dominant color all driven by `id`.
-- ❌ Buildings render with consistent footprints per category; wonders carry a distinguishing rarity halo.
-- ❌ Headless GUT test renders 20 known unit ids twice and asserts pixel-identical output (determinism).
+- ✓ `src/game/engine/src/world/procedural_renderer.gd` (canonical layout uses `src/`, not `scripts/`) exposes `render_unit(unit_id) -> Texture2D` and `render_building(building_id) -> Texture2D` (thin wrappers at lines 79-86) over the broader `make_unit_texture / make_building_texture / make_wonder_texture / make_city_texture` surface (lines 91-153). Output keyed deterministically off `id` via `_stable_hash` (line 158): `hash(s) & 0x7fffffff`. Race colour pulls from `public/resources/races/.json` with an 8-colour fallback palette when race is unknown.
+- ✓ `MC_USE_PROCEDURAL_SPRITES` resolved via `OS.get_environment` in `is_force_procedural()` (`procedural_renderer.gd` lines 64-69) and consulted by the renderer's wiring point in `unit_renderer_draw.gd::cache_unit_sprites` (lines 173-178) and `get_unit_sprite` (lines 200-211). When `1` the authored sprite path is skipped entirely; when unset/0, authored sprites win and procedural is the missing-asset fallback. No `EntityVisualRegistry` shim was introduced — the codebase has no such registry; the integration point is the existing UnitRenderer cache hook, documented in `engine/docs/PROCEDURAL_RENDERER.md`.
+- ✓ Distinct silhouettes per role classified by `unit_id` substring (`_classify_unit_role`, lines 174-198): warrior=sword, archer=bow+arrow, scout=triangle, worker=hammer, founder=house, wagon=body+wheels, cavalry=lance, siege=engine+barrel, naval=hull+mast, generic=seed dot pattern. Race colour drives the base disk; gender insignia (Venus/Mars/neutral glyph) sits in the top-left corner (`_paint_gender_insignia`, lines 348-360). Proof screenshot shows all 20 distinguishable at a glance: `~/Desktop/magic_civ_p2-62_procedural_renderer.png`.
+- ✓ Buildings: trapezoid wall + triangle roof + door + 1-3 windows, roof colour by category from `BUILDING_CATEGORY_COLOURS` (8 categories, line 32). Wonders: 3 distinct shape families (tower / dome / ziggurat, line 142) plus an amber halo disk (`_paint_wonder_halo`, line 397) — visible behind every wonder in the proof screenshot.
+- ✓ Headless GUT — `tests/unit/test_procedural_renderer.gd::test_twenty_unit_ids_pixel_identical_across_calls` (lines 132-148) renders 20 known unit ids twice (cache cleared between calls) and asserts pixel-identical bytes via `Image.get_data()` comparison. Full suite: 16/16 tests pass, 42/42 asserts on apricot Godot 4.6.2 headless — confirmed run on canonical checkout, isolated with `-gprefix=test_procedural`.
## Source-of-truth rails
diff --git a/public/games/age-of-dwarves/data/objectives.json b/public/games/age-of-dwarves/data/objectives.json
index 0d8a3091..a9e6d6ac 100644
--- a/public/games/age-of-dwarves/data/objectives.json
+++ b/public/games/age-of-dwarves/data/objectives.json
@@ -1,12 +1,12 @@
{
- "generated_at": "2026-05-04T07:05:22Z",
+ "generated_at": "2026-05-04T07:10:55Z",
"totals": {
- "oos": 28,
"missing": 7,
- "partial": 22,
- "done": 149,
+ "done": 151,
+ "partial": 21,
+ "oos": 28,
"in_progress": 1,
- "stub": 34,
+ "stub": 33,
"total": 241
},
"objectives": [
@@ -1434,10 +1434,10 @@
"id": "p2-11a",
"title": "\"SaveManager: add Unit.serialize/deserialize and City.production_queue serialize path\"",
"priority": "p2",
- "status": "partial",
+ "status": "done",
"scope": "game1",
"owner": null,
- "updated_at": "2026-05-03",
+ "updated_at": "2026-05-04",
"summary": "Unit has no serialize()/deserialize() methods — infusions, equipped_items, promo_ids, keywords and other typed arrays cannot round-trip through SaveManager. City.production_queue is a GDScript-side Array with no serialize path; the Rust-backed City.to_json() does not include it. These gaps were deferred from p2-10f, which narrowed its tests to the Player serialize surface only."
},
{
@@ -1974,10 +1974,10 @@
"id": "p2-62",
"title": "\"Procedural unit/building renderer — alpha-only visual substitute\"",
"priority": "p2",
- "status": "stub",
+ "status": "done",
"scope": "game1",
- "owner": "unassigned",
- "updated_at": "2026-05-03",
+ "owner": "asset-sprite",
+ "updated_at": "2026-05-04",
"summary": ""
},
{
|