diff --git a/src/game/engine/scenes/world_map/world_map_units.gd b/src/game/engine/scenes/world_map/world_map_units.gd index 96c3de16..ee7e766e 100644 --- a/src/game/engine/scenes/world_map/world_map_units.gd +++ b/src/game/engine/scenes/world_map/world_map_units.gd @@ -38,6 +38,12 @@ func create_unit(type_id: String, owner_index: int, pos: Vector2i) -> RefCounted func register_unit(unit: RefCounted, player: RefCounted) -> void: + # Rail-1: a unit enters the world here — push it into the authoritative + # `presentation_units` slot before it lands in the player/layer lists, so all + # downstream position/hp/movement/posture reads route to Rust. Idempotent if + # the unit was already spawned (e.g. a load path). + if unit is UnitScript: + unit.spawn_into_slot() player.units.append(unit) var primary: Dictionary = GameState.get_primary_layer() var layer_units: Array = primary.get("units", []) @@ -46,6 +52,10 @@ func register_unit(unit: RefCounted, player: RefCounted) -> void: func remove_unit(unit: RefCounted, player: RefCounted) -> void: + # Rail-1: drop the unit's entry from the authoritative slot (index-shift-safe) + # before unlinking it from the player/layer lists. + if unit is UnitScript: + unit.remove_from_slot() player.units.erase(unit) var primary: Dictionary = GameState.get_primary_layer() var layer_units: Array = primary.get("units", []) diff --git a/src/game/engine/src/modules/ai/ai_turn_bridge_dispatch.gd b/src/game/engine/src/modules/ai/ai_turn_bridge_dispatch.gd index ef4f788f..58eea2a0 100644 --- a/src/game/engine/src/modules/ai/ai_turn_bridge_dispatch.gd +++ b/src/game/engine/src/modules/ai/ai_turn_bridge_dispatch.gd @@ -228,6 +228,9 @@ static func dispatch_found_city( GameState.turn_number, ) player.cities.append(city) + # Rail-1: the settler is consumed founding the city — drop its slot entry. + if settler is Unit: + settler.remove_from_slot() player.units.erase(settler) var primary: Dictionary = GameState.get_primary_layer() primary.get("units", []).erase(settler) diff --git a/src/game/engine/src/modules/ai/wild_creature_ai.gd b/src/game/engine/src/modules/ai/wild_creature_ai.gd index 79b5fcd9..ffb43cd4 100644 --- a/src/game/engine/src/modules/ai/wild_creature_ai.gd +++ b/src/game/engine/src/modules/ai/wild_creature_ai.gd @@ -72,15 +72,16 @@ func spawn_initial_creatures(game_map: RefCounted) -> void: continue var b_pos: Vector2i = _building_pos(b) - var unit: RefCounted = UnitScript.new() + var unit: RefCounted = UnitScript.new(unit_type_id, -1, b_pos) unit.id = "wild_%d" % _rng.randi() unit.type_id = unit_type_id - unit.owner = -1 - unit.position = b_pos unit.max_hp = unit_data.get("hp", 8) unit.hp = unit.max_hp unit.movement_remaining = unit_data.get("movement", 2) unit.has_attacked = false + # Rail-1: wild creatures land in the dedicated wilds row of the + # authoritative `presentation_units` slot (owner -1 → `wilds_pi()`). + unit.spawn_into_slot() primary_layer.get("units", []).append(unit) EventBus.wild_creature_spawned.emit(unit, b_pos) diff --git a/src/game/engine/src/modules/combat/combat_utils.gd b/src/game/engine/src/modules/combat/combat_utils.gd index 8cd163a8..b38e0e84 100644 --- a/src/game/engine/src/modules/combat/combat_utils.gd +++ b/src/game/engine/src/modules/combat/combat_utils.gd @@ -89,6 +89,12 @@ static func handle_unit_death(unit: RefCounted, killer: RefCounted, all_units: A if unit.owner == -1 and killer != null and killer is UnitScript and killer.owner >= 0: _roll_wild_creature_loot(unit, killer) + # Rail-1: drop the dead unit's entry from the authoritative `presentation_units` + # slot. `remove_from_slot` snapshots its final position/hp into the unit's + # local mirror first, so the `unit_destroyed` subscribers (loot, chronicle) + # still read the unit as it died rather than safe defaults. + unit.remove_from_slot() + all_units.erase(unit) if unit.owner >= 0 and unit.owner < GameState.players.size(): @@ -195,6 +201,7 @@ static func _destroy_high_archon(player: RefCounted, all_units: Array) -> void: for unit: RefCounted in player.units: if unit is UnitScript and unit.type_id == "high_archon": unit.hp = 0 + unit.remove_from_slot() all_units.erase(unit) player.units.erase(unit) var primary_layer: Dictionary = GameState.get_primary_layer() diff --git a/src/game/engine/src/modules/empire/economy.gd b/src/game/engine/src/modules/empire/economy.gd index 1d619a19..aa70bbfc 100644 --- a/src/game/engine/src/modules/empire/economy.gd +++ b/src/game/engine/src/modules/empire/economy.gd @@ -170,6 +170,8 @@ static func _disband_cheapest(player: RefCounted, count: int) -> void: break if victim == null: return + # Rail-1: drop the disbanded unit from the authoritative slot. + victim.remove_from_slot() player.units.erase(victim) var primary: Dictionary = GameState.get_primary_layer() primary.get("units", []).erase(victim) diff --git a/src/game/engine/src/modules/management/prologue_driver.gd b/src/game/engine/src/modules/management/prologue_driver.gd index c51ddb83..81021e90 100644 --- a/src/game/engine/src/modules/management/prologue_driver.gd +++ b/src/game/engine/src/modules/management/prologue_driver.gd @@ -184,6 +184,9 @@ func found_capital_for_tribe( city.owner = pid city.found_with_population(city_name, q, r, true, 1, pop) player.cities.append(city) + # Rail-1: the tribe unit is consumed into the capital — drop its slot entry. + if tribe_unit is Unit: + tribe_unit.remove_from_slot() player.units.erase(tribe_unit) var primary: Dictionary = GameState.get_primary_layer() var layer_units: Array = primary.get("units", []) diff --git a/src/game/engine/src/modules/management/turn_processor.gd b/src/game/engine/src/modules/management/turn_processor.gd index 7b9879e3..c4bcf39f 100644 --- a/src/game/engine/src/modules/management/turn_processor.gd +++ b/src/game/engine/src/modules/management/turn_processor.gd @@ -230,6 +230,9 @@ func _spawn_unit(type_id: String, player: RefCounted, pos: Vector2i) -> UnitScri unit.id = "unit_p%d_%d_%d_%d" % [player.index, pos.x, pos.y, GameState.turn_number] var data: Dictionary = DataLoader.get_unit(type_id) unit.display_name = data.get("name", type_id) + # Rail-1: push the unit into the authoritative `presentation_units` slot. All + # subsequent reads/writes of position/hp/movement/posture route to Rust. + unit.spawn_into_slot() player.units.append(unit) var primary: Dictionary = GameState.get_primary_layer() var layer_units: Array = primary.get("units", [])