feat(@projects/@magic-civilization): ✨ add fauna fields to tile state
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
9dd3233769
commit
a7ab78751d
3 changed files with 138 additions and 0 deletions
43
.project/objectives/p2-58a-tilestate-fauna-fields.md
Normal file
43
.project/objectives/p2-58a-tilestate-fauna-fields.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
id: p2-58a
|
||||
title: "TileState fauna fields — fauna_density + fauna_index for AmbientTileCtx"
|
||||
priority: p2
|
||||
status: done
|
||||
scope: game1
|
||||
owner: game-systems
|
||||
updated_at: 2026-05-07
|
||||
blocked_by: []
|
||||
evidence:
|
||||
- src/simulator/crates/mc-core/src/grid/mod.rs — fauna_density + fauna_index added to TileState (p2-58a section, lines ~277-290)
|
||||
- "cargo test -p mc-core tilestate_fauna: 3/3 pass (tilestate_fauna_fields_default, tilestate_fauna_roundtrip, tilestate_fauna_density_serde_default_on_missing_field)"
|
||||
- "cargo check --workspace: clean (pre-existing solo_dominion errors only)"
|
||||
---
|
||||
## Summary
|
||||
|
||||
Adds `fauna_density: f32` and `fauna_index: Vec<SpeciesId>` to `TileState` in mc-core
|
||||
so `AmbientTileCtx` (mc-ecology) can be populated from the live GameState in the
|
||||
per-tile-moved encounter hook (p2-58b).
|
||||
|
||||
`SpeciesId` is the existing string newtype from `mc-core::ids` (snake_case fauna species
|
||||
identifier, e.g. `"grey_wolf"`). No new type was created.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- [x] ✓ `TileState.fauna_density: f32` field added with `#[serde(default)]`
|
||||
- [x] ✓ `TileState.fauna_index: Vec<SpeciesId>` field added with `#[serde(default)]`
|
||||
- [x] ✓ `cargo test -p mc-core tilestate_fauna` passes — 3/3 tests green, including
|
||||
backward-compat deserialization (old save files without these fields default cleanly)
|
||||
- [x] ✓ `cargo check --workspace` clean of new errors (pre-existing solo_dominion
|
||||
MapUnit/GameState field errors are pre-existing tech debt, not introduced here)
|
||||
|
||||
## Population note
|
||||
|
||||
`fauna_density` and `fauna_index` default to 0/empty. The ecology pipeline
|
||||
(`mc-ecology::fauna_select::pick_fauna_for_tile`) must be wired to write these fields
|
||||
back onto `TileState` after worldgen — tracked in p2-58b. Until then, encounter rolls
|
||||
will always see density=0 and skip (correct behaviour for uninitialized maps).
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Wiring the ecology pipeline to populate these fields (p2-58b)
|
||||
- The actual `AmbientTileCtx` call site in `mc-turn::movement` (p2-58b)
|
||||
40
.project/objectives/p2-58b-ambient-encounter-hook.md
Normal file
40
.project/objectives/p2-58b-ambient-encounter-hook.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
id: p2-58b
|
||||
title: "Ambient encounter hook — mc-turn::movement calls roll_ambient_encounter per tile step"
|
||||
priority: p2
|
||||
status: stub
|
||||
scope: game1
|
||||
owner: unassigned
|
||||
updated_at: 2026-05-07
|
||||
blocked_by: [p2-58a]
|
||||
---
|
||||
## Summary
|
||||
|
||||
With `TileState.fauna_density` and `TileState.fauna_index` now populated (p2-58a),
|
||||
the per-tile-moved hook in `mc-turn::movement` (or `processor.rs` movement phase)
|
||||
can build `AmbientTileCtx` from the live `GameState` and call
|
||||
`mc_ecology::encounter::roll_ambient_encounter(...)`.
|
||||
|
||||
Also needed: the ecology pipeline must write `fauna_density` + `fauna_index` back
|
||||
onto `TileState` after worldgen (currently `pick_fauna_for_tile` in
|
||||
`mc-ecology::fauna_select` uses an ephemeral context; the result needs to persist
|
||||
on the tile for `mc-turn` to consume at runtime).
|
||||
|
||||
## Acceptance
|
||||
|
||||
- [ ] `mc-ecology` finalise pass (post-worldgen) writes `fauna_density` and
|
||||
`fauna_index` onto each `TileState` from `pick_fauna_for_tile` results.
|
||||
- [ ] `mc-turn::movement` (or `processor.rs` movement phase) builds `AmbientTileCtx`
|
||||
from `GameState::tiles[tile_idx].fauna_density` + `.fauna_index` per step.
|
||||
- [ ] Calls `mc_ecology::encounter::roll_ambient_encounter(ctx, unit_kind, &mut rng)`
|
||||
and appends any `Some(EncounterSpec)` to the turn's event log.
|
||||
- [ ] `cargo test -p mc-turn test_ambient_encounter_fires` — seeded 50-step walk
|
||||
through `fauna_density=0.8` tile yields ≥1 encounter.
|
||||
- [ ] GUT integration test: scout in wilderness, assert `EventBus.encounter_started`
|
||||
fires within 20 moves at density=0.8.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Pioneer escort rules (p2-59).
|
||||
- Lair siege/assault/raid (p3-10).
|
||||
- Encounter narrative text / event cards — separate UI objective.
|
||||
|
|
@ -4,6 +4,7 @@ pub mod biome_registry;
|
|||
// works without drilling through the biome_registry module.
|
||||
pub use biome_registry::terrain_tier_cap;
|
||||
|
||||
use crate::ids::SpeciesId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
|
@ -274,6 +275,16 @@ pub struct TileState {
|
|||
/// BFS distance to nearest river/lake hex. 0 = on water; u8::MAX = beyond MAX_RIPARIAN_DISTANCE.
|
||||
#[serde(default = "default_riparian_distance")]
|
||||
pub riparian_distance: u8,
|
||||
// ── Fauna ambient fields (p2-58a) ─────────────────────────────────────────
|
||||
/// Aggregate ambient fauna density (0.0 = none, 1.0 = saturated). Populated
|
||||
/// by the ecology pipeline after fauna placement; used by AmbientTileCtx in
|
||||
/// the per-tile-moved encounter roll (p2-58b).
|
||||
#[serde(default)]
|
||||
pub fauna_density: f32,
|
||||
/// Candidate species present on this tile (filtered by trophic + domain gates).
|
||||
/// Populated by mc-ecology::fauna_select after worldgen; consumed by encounter roll.
|
||||
#[serde(default)]
|
||||
pub fauna_index: Vec<SpeciesId>,
|
||||
}
|
||||
|
||||
impl Default for TileState {
|
||||
|
|
@ -375,6 +386,8 @@ impl Default for TileState {
|
|||
stream_order: 1,
|
||||
lake_id: None,
|
||||
riparian_distance: u8::MAX,
|
||||
fauna_density: 0.0,
|
||||
fauna_index: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1115,5 +1128,47 @@ mod tests {
|
|||
assert!(f.road, "road feature was wiped by migration");
|
||||
assert!(!f.river, "road edge incorrectly marked as river");
|
||||
}
|
||||
|
||||
// ── p2-58a: TileState fauna fields ──────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn tilestate_fauna_fields_default() {
|
||||
let t = TileState::default();
|
||||
assert_eq!(t.fauna_density, 0.0);
|
||||
assert!(t.fauna_index.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tilestate_fauna_roundtrip() {
|
||||
let mut t = TileState::default();
|
||||
t.fauna_density = 0.75;
|
||||
t.fauna_index = vec![SpeciesId::new("grey_wolf"), SpeciesId::new("dire_bear")];
|
||||
let json = serde_json::to_string(&t).unwrap();
|
||||
let t2: TileState = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(t2.fauna_density, 0.75);
|
||||
assert_eq!(t2.fauna_index, vec![SpeciesId::new("grey_wolf"), SpeciesId::new("dire_bear")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tilestate_fauna_density_serde_default_on_missing_field() {
|
||||
// Tiles serialized before p2-58a won't have fauna_density/fauna_index.
|
||||
// Round-trip a default tile (which has the correct enum variants), strip
|
||||
// the two new keys, and verify they come back as zero/empty.
|
||||
let mut t = TileState::default();
|
||||
t.col = 3;
|
||||
t.row = 5;
|
||||
let full_json = serde_json::to_string(&t).unwrap();
|
||||
// Remove fauna_density and fauna_index from the serialized form to simulate
|
||||
// an old save file that predates p2-58a.
|
||||
let stripped: serde_json::Value = serde_json::from_str(&full_json).unwrap();
|
||||
let mut map = stripped.as_object().unwrap().clone();
|
||||
map.remove("fauna_density");
|
||||
map.remove("fauna_index");
|
||||
let old_json = serde_json::to_string(&map).unwrap();
|
||||
let t2: TileState = serde_json::from_str(&old_json).unwrap();
|
||||
assert_eq!(t2.fauna_density, 0.0, "missing fauna_density should default to 0.0");
|
||||
assert!(t2.fauna_index.is_empty(), "missing fauna_index should default to empty vec");
|
||||
assert_eq!(t2.col, 3);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue