feat(@projects/@magic-civilization): ✨ add building entity dictionary conversion helpers
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
86c6eea794
commit
05b10be080
1 changed files with 181 additions and 0 deletions
|
|
@ -965,6 +965,34 @@ impl GdClimateSpecEval {
|
|||
}
|
||||
}
|
||||
|
||||
// ── BuildingEntity Dictionary conversion helper ─────────────────────────
|
||||
//
|
||||
// Shape mirrors `Building.gd::to_dict()` exactly — same key set, same
|
||||
// value types, same `position: [col, row]` array — so renderers and
|
||||
// encyclopedia panels can keep consuming the dict shape during the
|
||||
// Stage 2b → Stage 3 transition. The inverse path (Dictionary →
|
||||
// BuildingEntity) is not needed today; spawn callers pass the field
|
||||
// set positionally to `GdGameState::spawn_npc_building`.
|
||||
|
||||
fn building_entity_to_dict(entity: &mc_core::BuildingEntity) -> Dictionary {
|
||||
let mut d: Dictionary = Dictionary::new();
|
||||
d.set("id", GString::from(entity.id.as_str()));
|
||||
d.set("type_id", GString::from(entity.type_id.as_str()));
|
||||
d.set("name", GString::from(entity.name.as_str()));
|
||||
let placement_str = match entity.placement {
|
||||
mc_core::Placement::Map => "map",
|
||||
mc_core::Placement::City => "city",
|
||||
};
|
||||
d.set("placement", GString::from(placement_str));
|
||||
d.set("owner", entity.owner as i64);
|
||||
let mut position = Array::<i64>::new();
|
||||
position.push(entity.position.0 as i64);
|
||||
position.push(entity.position.1 as i64);
|
||||
d.set("position", position);
|
||||
d.set("visited", entity.visited);
|
||||
d
|
||||
}
|
||||
|
||||
// ── Tile Dictionary conversion helpers ──────────────────────────────────
|
||||
|
||||
fn tile_to_dict(tile: &mc_core::grid::TileState) -> Dictionary {
|
||||
|
|
@ -2981,6 +3009,159 @@ impl GdGameState {
|
|||
true
|
||||
}
|
||||
|
||||
// ── p2-72a Stage 2b: NPC-buildings accessor surface ─────────────────
|
||||
//
|
||||
// Stage 2b makes the Rust-side `npc_buildings: Vec<BuildingEntity>`
|
||||
// mirror of `GameState.npc_buildings` (GDScript) authoritative for
|
||||
// save-format purposes. The full read-path migration (renderer / AI
|
||||
// / encyclopedia / fauna) lands in Stage 3+ — for now GDScript
|
||||
// continues to maintain its own `BuildingScript` array as a view
|
||||
// alongside the Rust mirror, and every spawn / mutate / remove path
|
||||
// routes through these accessors so the mirror stays in lockstep.
|
||||
|
||||
/// Number of NPC buildings currently in the Rust mirror.
|
||||
#[func]
|
||||
fn npc_building_count(&self) -> i64 {
|
||||
self.inner.npc_buildings.len() as i64
|
||||
}
|
||||
|
||||
/// Return the building at `idx` as a `Dictionary` shaped to match
|
||||
/// `Building.gd::to_dict()`. Returns an empty `Dictionary` if `idx`
|
||||
/// is out of range (logged via `godot_error!`). Renderers and
|
||||
/// encyclopedia panels consume this shape today.
|
||||
#[func]
|
||||
fn npc_building_dict(&self, idx: i64) -> Dictionary {
|
||||
if idx < 0 || (idx as usize) >= self.inner.npc_buildings.len() {
|
||||
godot_error!(
|
||||
"GdGameState::npc_building_dict: idx {idx} out of range (len {})",
|
||||
self.inner.npc_buildings.len()
|
||||
);
|
||||
return Dictionary::new();
|
||||
}
|
||||
building_entity_to_dict(&self.inner.npc_buildings[idx as usize])
|
||||
}
|
||||
|
||||
/// Return every NPC building as an `Array<Dictionary>` (one entry
|
||||
/// per building, ordered by insertion). Used by Stage 3 read-path
|
||||
/// migration to drain the Rust mirror into renderer/encyclopedia
|
||||
/// data structures.
|
||||
#[func]
|
||||
fn npc_buildings_all(&self) -> Array<Dictionary> {
|
||||
let mut arr = Array::new();
|
||||
for b in &self.inner.npc_buildings {
|
||||
arr.push(&building_entity_to_dict(b));
|
||||
}
|
||||
arr
|
||||
}
|
||||
|
||||
/// Append a new NPC building to the Rust mirror. Called by every
|
||||
/// GDScript spawn path (`VillageLairPlacer._create_npc_building`)
|
||||
/// in lieu of allocating a `BuildingScript` and appending it to
|
||||
/// `GameState.npc_buildings` directly. Returns the index of the
|
||||
/// newly inserted entity.
|
||||
///
|
||||
/// Parameters mirror `BuildingEntity` field-for-field; `placement`
|
||||
/// accepts the GDScript-shaped lowercase strings `"map"` /
|
||||
/// `"city"` and falls back to `"map"` on any other value (with a
|
||||
/// `godot_error!`).
|
||||
#[func]
|
||||
fn spawn_npc_building(
|
||||
&mut self,
|
||||
id: GString,
|
||||
type_id: GString,
|
||||
name: GString,
|
||||
placement: GString,
|
||||
owner: i64,
|
||||
col: i64,
|
||||
row: i64,
|
||||
visited: bool,
|
||||
) -> i64 {
|
||||
let placement = match placement.to_string().as_str() {
|
||||
"map" => mc_core::Placement::Map,
|
||||
"city" => mc_core::Placement::City,
|
||||
other => {
|
||||
godot_error!(
|
||||
"GdGameState::spawn_npc_building: unknown placement '{other}', defaulting to 'map'"
|
||||
);
|
||||
mc_core::Placement::Map
|
||||
}
|
||||
};
|
||||
let entity = mc_core::BuildingEntity {
|
||||
id: id.to_string(),
|
||||
type_id: type_id.to_string(),
|
||||
name: name.to_string(),
|
||||
placement,
|
||||
owner: owner as i32,
|
||||
position: (col as i32, row as i32),
|
||||
visited,
|
||||
};
|
||||
let idx = self.inner.npc_buildings.len() as i64;
|
||||
self.inner.npc_buildings.push(entity);
|
||||
idx
|
||||
}
|
||||
|
||||
/// Drop an NPC building from the mirror by instance id. Returns
|
||||
/// `true` if a matching entity was removed. Used by Stage 3+
|
||||
/// remove-paths (raid-clear, lair-destroyed). Linear scan because
|
||||
/// `Vec<BuildingEntity>` keeps the on-disk save shape simple — the
|
||||
/// spatial-index migration that would replace this with a
|
||||
/// `BTreeMap<AxialPos, Vec<usize>>` is Stage 3 scope.
|
||||
#[func]
|
||||
fn remove_npc_building_by_id(&mut self, id: GString) -> bool {
|
||||
let target = id.to_string();
|
||||
if let Some(pos) = self.inner.npc_buildings.iter().position(|b| b.id == target) {
|
||||
self.inner.npc_buildings.remove(pos);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark an NPC building as visited / unvisited by instance id.
|
||||
/// Returns `true` if a matching entity was found and updated.
|
||||
#[func]
|
||||
fn set_npc_building_visited(&mut self, id: GString, visited: bool) -> bool {
|
||||
let target = id.to_string();
|
||||
if let Some(b) = self.inner.npc_buildings.iter_mut().find(|b| b.id == target) {
|
||||
b.visited = visited;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an NPC building's `type_id` (and `name`) in-place by
|
||||
/// instance id. Used by the lair-abandonment / ruin-conversion
|
||||
/// paths in `ecological_event_handlers_b.gd::_abandon_lair` and
|
||||
/// `fauna.gd::_abandon_lair`, which today mutate the GDScript
|
||||
/// `BuildingScript` instance directly. The Rust mirror tracks the
|
||||
/// same mutation so the save round-trip emits the post-conversion
|
||||
/// state. Returns `true` if a matching entity was found.
|
||||
#[func]
|
||||
fn convert_npc_building_type(
|
||||
&mut self,
|
||||
id: GString,
|
||||
new_type_id: GString,
|
||||
new_name: GString,
|
||||
) -> bool {
|
||||
let target = id.to_string();
|
||||
if let Some(b) = self.inner.npc_buildings.iter_mut().find(|b| b.id == target) {
|
||||
b.type_id = new_type_id.to_string();
|
||||
b.name = new_name.to_string();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset the NPC-buildings mirror to empty. Called by
|
||||
/// `GameState.initialize_game` to clear stale entities between
|
||||
/// games / save loads before fresh spawn paths run.
|
||||
#[func]
|
||||
fn clear_npc_buildings(&mut self) {
|
||||
self.inner.npc_buildings.clear();
|
||||
}
|
||||
|
||||
/// p2-71c — load the runtime `UnitsCatalog` (id → `UnitStats`) from a
|
||||
/// JSON array harvested from `public/resources/units/*.json`. GDScript
|
||||
/// reads every per-unit file, collects them into an `Array`, and
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue