feat(management): ✨ Introduce RustFaunaBridge for fauna state synchronization and management in the game engine
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
f633ab95e5
commit
a4d736172f
1 changed files with 133 additions and 0 deletions
133
src/game/engine/src/modules/management/rust_fauna_bridge.gd
Normal file
133
src/game/engine/src/modules/management/rust_fauna_bridge.gd
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
class_name RustFaunaBridge
|
||||
extends RefCounted
|
||||
## GDScript adapter that drives the Rust mc-turn fauna encounter resolver
|
||||
## from live game state. Iter 7i deliverable.
|
||||
##
|
||||
## Architecture:
|
||||
## 1. The caller hands us a list of live `Unit.gd` instances.
|
||||
## 2. We pack them into the dict shape `GdGameState::set_player_units_from_dicts`
|
||||
## accepts (via Unit.to_bridge_dict()).
|
||||
## 3. We populate a fresh `GdGameState` with the player + units + grid + lairs.
|
||||
## 4. We call `GdTurnProcessor::step` once.
|
||||
## 5. We read back `result["fauna_combat_log"]` and walk it; for each death
|
||||
## event we resolve the (unit_col, unit_row) coordinate back to the live
|
||||
## `Unit` instance, kill it (set hp=0), and emit
|
||||
## `EventBus.unit_destroyed(unit, killer)`.
|
||||
## 6. The bridge is one-shot per call — the caller is responsible for
|
||||
## not feeding the same encounter twice.
|
||||
##
|
||||
## This file is the entire iter 7i GDScript layer for the Rust-fauna
|
||||
## integration. The next iteration (7j) will call into this from the real
|
||||
## `world_map` turn loop.
|
||||
|
||||
const UnitScript: GDScript = preload("res://engine/src/entities/unit.gd")
|
||||
|
||||
|
||||
## Build a fresh GdGameState containing one player + their units + the grid
|
||||
## (cloned from a real GdGridState if provided) + a list of stamped lairs.
|
||||
##
|
||||
## Returns the GdGameState ready to hand to GdTurnProcessor::step.
|
||||
##
|
||||
## `axes`: dict like {"expansion": 2, "production": 5, ...}
|
||||
## `units`: Array[Unit] — live game units to ingest
|
||||
## `cities`: Array[Vector2i] — live city positions (must be Array[Vector2i],
|
||||
## not generic Array; the Rust binding requires
|
||||
## the typed-array variant for typed param slots)
|
||||
## `grid_size`: Vector2i — grid dimensions if no `gridstate` is provided
|
||||
## `gridstate`: optional GdGridState (RefCounted) — if provided, replaces
|
||||
## the blank grid with a clone of this real one
|
||||
static func build_state(
|
||||
axes: Dictionary,
|
||||
units: Array[Unit],
|
||||
cities: Array[Vector2i],
|
||||
grid_size: Vector2i,
|
||||
gridstate: RefCounted = null,
|
||||
) -> RefCounted:
|
||||
var state: RefCounted = ClassDB.instantiate("GdGameState") as RefCounted
|
||||
if state == null:
|
||||
push_error("RustFaunaBridge: GdGameState not registered")
|
||||
return null
|
||||
|
||||
if gridstate != null:
|
||||
state.call("set_grid_from_gridstate", gridstate)
|
||||
else:
|
||||
state.call("create_grid", grid_size.x, grid_size.y)
|
||||
|
||||
var pi: int = int(state.call("add_empty_player_with_axes", axes))
|
||||
state.call("set_player_cities_from_array", pi, cities)
|
||||
|
||||
var unit_dicts: Array[Dictionary] = []
|
||||
for u: Unit in units:
|
||||
unit_dicts.append(u.to_bridge_dict())
|
||||
state.call("set_player_units_from_dicts", pi, unit_dicts)
|
||||
return state
|
||||
|
||||
|
||||
## Stamp lairs into the state's grid from a list of [col, row, tier, species_id]
|
||||
## tuples. Used by proof scenes that don't have a real ecology evolution pass.
|
||||
static func stamp_lairs(state: RefCounted, lairs: Array) -> int:
|
||||
if state == null:
|
||||
return 0
|
||||
var n: int = 0
|
||||
for entry: Array in lairs:
|
||||
var ok: bool = state.call(
|
||||
"stamp_lair", entry[0], entry[1], entry[2], entry[3]
|
||||
)
|
||||
if ok:
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
## Run one Rust step against the prepared state, walk the returned
|
||||
## fauna_combat_log, and apply each death back to the live Unit array.
|
||||
## Returns the per-event log as a Dictionary array (so callers can render
|
||||
## or audit it).
|
||||
##
|
||||
## After this call:
|
||||
## - Dead units have hp=0 (Unit.is_alive() == false)
|
||||
## - EventBus.unit_destroyed signals have been emitted for each death
|
||||
## - The caller can sweep `live_units` and remove dead entries from their
|
||||
## own collections (this method does NOT mutate the input array's length)
|
||||
static func resolve_fauna_encounters(
|
||||
processor: RefCounted,
|
||||
state: RefCounted,
|
||||
live_units: Array[Unit],
|
||||
) -> Array:
|
||||
if processor == null or state == null:
|
||||
return []
|
||||
var result: Dictionary = processor.call("step", state)
|
||||
var log: Array = result.get("fauna_combat_log", [])
|
||||
|
||||
# Build a position → live Unit lookup so we can resolve deaths in O(1).
|
||||
var by_pos: Dictionary = {}
|
||||
for u: Unit in live_units:
|
||||
if u != null and u.is_alive():
|
||||
by_pos[u.position] = u
|
||||
|
||||
for ev: Dictionary in log:
|
||||
if bool(ev.get("unit_survived", true)):
|
||||
continue
|
||||
var unit_col: int = int(ev.get("unit_col", -1))
|
||||
var unit_row: int = int(ev.get("unit_row", -1))
|
||||
var lair_tier: int = int(ev.get("lair_tier", 0))
|
||||
var key: Vector2i = Vector2i(unit_col, unit_row)
|
||||
var dead: Unit = by_pos.get(key, null) as Unit
|
||||
if dead == null:
|
||||
continue
|
||||
# Killing the unit and emitting the signal once per death.
|
||||
# Pre-existing collections still hold the reference; the caller
|
||||
# sweeps them after we return.
|
||||
dead.take_damage(dead.hp) # hp -> 0
|
||||
EventBus.unit_destroyed.emit(dead, _killer_label(lair_tier))
|
||||
# Remove from the lookup so a second event at the same coord
|
||||
# doesn't double-emit.
|
||||
by_pos.erase(key)
|
||||
|
||||
return log
|
||||
|
||||
|
||||
## Synthetic killer label for the EventBus signal. The mc-turn lair
|
||||
## resolution doesn't carry a real killer entity (lairs aren't `Unit`s),
|
||||
## so we use a string tag the UI/log layer can interpret.
|
||||
static func _killer_label(lair_tier: int) -> String:
|
||||
return "wild_lair_t%d" % lair_tier
|
||||
Loading…
Add table
Reference in a new issue