feat(rail-1): wire whole-round Rust turn into live end_turn behind RUST_TURN flag (p3-29)
Phase-2b live swap (default OFF). When RUST_TURN=1, the proven GdTurnProcessor.step advances the WHOLE round on live state in one call (sync presentation->inner, step, sync inner->presentation), and the per-player _process_* loop + round-end ecology/climate/wild/diplomacy GDScript passes are gated off to avoid double-processing. step's events[] are translated to EventBus signals (tech/culture/golden-age now; entity- payload kinds deferred). Default path is byte-for-byte the existing turn. Render-proof of the ON path (live game plays a turn through the Rust step) remains the render-gated acceptance item. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
79db241cef
commit
7475daa7f8
1 changed files with 164 additions and 47 deletions
|
|
@ -61,6 +61,14 @@ var climate_effects: RefCounted = ClimateEffectsScript.new() # ClimateEffects
|
||||||
var diplomacy: RefCounted = DiplomacyScript.new() # Diplomacy — relationship state
|
var diplomacy: RefCounted = DiplomacyScript.new() # Diplomacy — relationship state
|
||||||
var marine_harvest: RefCounted = MarineHarvestScript.new() # MarineHarvest — ocean ecology
|
var marine_harvest: RefCounted = MarineHarvestScript.new() # MarineHarvest — ocean ecology
|
||||||
var _processor: RefCounted = null # TurnProcessor — wired in _ready
|
var _processor: RefCounted = null # TurnProcessor — wired in _ready
|
||||||
|
## Rail-1 Phase-2b: the proven whole-round Rust turn (`GdTurnProcessor`).
|
||||||
|
## Instantiated once in _ready when the GDExtension is present. Only used when
|
||||||
|
## the `RUST_TURN` flag is ON (default OFF) — see `end_turn()` / `next_player()`.
|
||||||
|
## Null on headless builds without the dylib (callers guard).
|
||||||
|
var _rust_turn_processor: RefCounted = null # GdTurnProcessor
|
||||||
|
## Cached once-per-process read of the RUST_TURN flag (mirrors AUTO_PLAY pattern).
|
||||||
|
## When false (default) the live turn path is the existing per-player one, untouched.
|
||||||
|
var _rust_turn_enabled: bool = false
|
||||||
## Prologue driver (p0-34). Instantiated when `setup.json:start_turn == -1`
|
## Prologue driver (p0-34). Instantiated when `setup.json:start_turn == -1`
|
||||||
## (i.e. `prologue` group authored by tribe-data-dev is active). Null when
|
## (i.e. `prologue` group authored by tribe-data-dev is active). Null when
|
||||||
## the legacy pop-1 founder path is in effect. Reset by world_map._start_game.
|
## the legacy pop-1 founder path is in effect. Reset by world_map._start_game.
|
||||||
|
|
@ -79,6 +87,22 @@ func _ready() -> void:
|
||||||
proc.climate_effects = climate_effects
|
proc.climate_effects = climate_effects
|
||||||
proc.marine_harvest = marine_harvest
|
proc.marine_harvest = marine_harvest
|
||||||
|
|
||||||
|
# Rail-1 Phase-2b: cache the RUST_TURN flag once and (when present) build the
|
||||||
|
# proven whole-round Rust turn processor. Default OFF → the live game path is
|
||||||
|
# byte-for-byte the existing per-player loop; nothing below runs.
|
||||||
|
_rust_turn_enabled = EnvConfig.get_bool("RUST_TURN")
|
||||||
|
if _rust_turn_enabled:
|
||||||
|
if ClassDB.class_exists("GdTurnProcessor"):
|
||||||
|
_rust_turn_processor = ClassDB.instantiate("GdTurnProcessor") as RefCounted
|
||||||
|
if _rust_turn_processor == null:
|
||||||
|
push_error("TurnManager: GdTurnProcessor registered but instantiate returned null")
|
||||||
|
else:
|
||||||
|
# Mirror the headless harness: load authored fauna/ambient encounter
|
||||||
|
# rates so the in-step fauna pass is live (else encounters are inert).
|
||||||
|
_rust_turn_processor.call("load_authored_encounter_rates")
|
||||||
|
else:
|
||||||
|
push_error("TurnManager: RUST_TURN set but GdTurnProcessor class not registered (gdext build missing?)")
|
||||||
|
|
||||||
|
|
||||||
func _on_deposit_discovered(player_index: int, resource_id: String, _pos: Vector2i) -> void:
|
func _on_deposit_discovered(player_index: int, resource_id: String, _pos: Vector2i) -> void:
|
||||||
## Credit one unit of the strategic resource onto the player's ledger.
|
## Credit one unit of the strategic resource onto the player's ledger.
|
||||||
|
|
@ -244,28 +268,42 @@ func end_turn() -> void:
|
||||||
start_turn()
|
start_turn()
|
||||||
return
|
return
|
||||||
|
|
||||||
var player: RefCounted = GameState.get_current_player() # Player
|
# Rail-1 Phase-2b: when RUST_TURN is ON the proven whole-round `GdTurnProcessor.step`
|
||||||
var game_map: RefCounted = GameState.get_game_map() # GameMap
|
# computes culture/growth/production/economy/research/golden-age/healing/improvements
|
||||||
if player != null and game_map != null:
|
# for ALL players plus the round-end sim glue (fauna/wild/diplomacy/climate/ecology)
|
||||||
# Processing order: culture FIRST so new tiles are available for
|
# in a SINGLE call. So under the flag we SKIP the per-player `_process_*` block below
|
||||||
# citizen assignment during growth. Otherwise new pop can't work
|
# (running both would double-process), and run the Rust step exactly ONCE per round,
|
||||||
# the tile just claimed this turn.
|
# at the round boundary, before `next_player()` rotates the cursor. `next_player()`'s
|
||||||
# 1. Culture (borders) 2. Food (growth) 3. Production
|
# round-end sim glue is correspondingly gated off (see that function). Default OFF →
|
||||||
# 4. Gold (economy) 5. Science 6. Happiness (golden age)
|
# the original per-player path runs untouched.
|
||||||
# 7. Mana 8. Victory 9. Healing 10. Improvements
|
if not _rust_turn_enabled:
|
||||||
var proc: TurnProcessorScript = _processor as TurnProcessorScript
|
var player: RefCounted = GameState.get_current_player() # Player
|
||||||
proc._process_culture(player, game_map)
|
var game_map: RefCounted = GameState.get_game_map() # GameMap
|
||||||
proc._process_culture_research(player)
|
if player != null and game_map != null:
|
||||||
proc._process_growth(player)
|
# Processing order: culture FIRST so new tiles are available for
|
||||||
proc._process_production(player)
|
# citizen assignment during growth. Otherwise new pop can't work
|
||||||
proc._process_economy(player, game_map)
|
# the tile just claimed this turn.
|
||||||
proc._process_research(player)
|
# 1. Culture (borders) 2. Food (growth) 3. Production
|
||||||
proc._process_golden_age(player, game_map)
|
# 4. Gold (economy) 5. Science 6. Happiness (golden age)
|
||||||
proc._process_healing(player)
|
# 7. Mana 8. Victory 9. Healing 10. Improvements
|
||||||
proc._process_city_healing(player)
|
var proc: TurnProcessorScript = _processor as TurnProcessorScript
|
||||||
proc._process_improvements(player)
|
proc._process_culture(player, game_map)
|
||||||
proc._process_loot_decay()
|
proc._process_culture_research(player)
|
||||||
proc._process_government(player)
|
proc._process_growth(player)
|
||||||
|
proc._process_production(player)
|
||||||
|
proc._process_economy(player, game_map)
|
||||||
|
proc._process_research(player)
|
||||||
|
proc._process_golden_age(player, game_map)
|
||||||
|
proc._process_healing(player)
|
||||||
|
proc._process_city_healing(player)
|
||||||
|
proc._process_improvements(player)
|
||||||
|
proc._process_loot_decay()
|
||||||
|
proc._process_government(player)
|
||||||
|
elif GameState.is_last_in_round():
|
||||||
|
# Flag ON + last player of the round just ended → advance the WHOLE round
|
||||||
|
# in Rust on live state: sync presentation→inner, step, sync inner→presentation,
|
||||||
|
# then translate the returned events into EventBus signals.
|
||||||
|
_run_rust_round()
|
||||||
|
|
||||||
EventBus.turn_ended.emit(GameState.turn_number, GameState.current_player_index)
|
EventBus.turn_ended.emit(GameState.turn_number, GameState.current_player_index)
|
||||||
|
|
||||||
|
|
@ -273,6 +311,68 @@ func end_turn() -> void:
|
||||||
next_player()
|
next_player()
|
||||||
|
|
||||||
|
|
||||||
|
## Rail-1 Phase-2b — run the proven whole-round Rust turn on LIVE state.
|
||||||
|
## Syncs the rich presentation slots DOWN into `inner`, runs `GdTurnProcessor.step`
|
||||||
|
## (which advances ALL players + round-end sim glue + increments `state.turn`), then
|
||||||
|
## syncs the results back UP into the presentation slots. Finally translates the
|
||||||
|
## step's `events` array into the existing EventBus signals so renderers/panels
|
||||||
|
## re-read the synced state. Called exactly once per round (at the round boundary).
|
||||||
|
func _run_rust_round() -> void:
|
||||||
|
if _rust_turn_processor == null:
|
||||||
|
push_error("TurnManager: _run_rust_round called with null GdTurnProcessor")
|
||||||
|
return
|
||||||
|
var gs: RefCounted = GameState.get_gd_state() # GdGameState
|
||||||
|
if gs == null:
|
||||||
|
push_error("TurnManager: _run_rust_round — GdGameState is null (gdext missing?)")
|
||||||
|
return
|
||||||
|
gs.call("sync_presentation_to_inner")
|
||||||
|
var result: Dictionary = _rust_turn_processor.call("step", gs)
|
||||||
|
gs.call("sync_inner_to_presentation")
|
||||||
|
_emit_rust_turn_events(result.get("events", []) as Array)
|
||||||
|
# worldsim render hook: nudge the fauna overlay + any turn-number listeners to
|
||||||
|
# re-read the freshly synced state. The normal `turn_ended` emit follows in
|
||||||
|
# `end_turn()`; this mirrors the legacy round-end `worldsim_updated` emit.
|
||||||
|
EventBus.worldsim_updated.emit(GameState.turn_number)
|
||||||
|
|
||||||
|
|
||||||
|
## Translate the `GdTurnProcessor.step` result's `events` array (kind-tagged
|
||||||
|
## Dictionaries from `replay::event_to_dict`) into existing EventBus signals.
|
||||||
|
## Best-effort: only the high-value kinds with a faithful, cheap mapping are
|
||||||
|
## emitted. Entity-payload signals (city_grew/unit_created/…) need a live
|
||||||
|
## CityScript/Unit lookup that is NOT cheap here, so they are intentionally
|
||||||
|
## deferred to a later increment (faithful entity mapping) rather than emitting
|
||||||
|
## null entities into typed signals. State parity (the board advancing via the
|
||||||
|
## synced slots) is the proof target for this phase; the synced presentation
|
||||||
|
## slots already carry the new pop/borders/units, so the board is correct and
|
||||||
|
## only the per-event signal is deferred. Full signal fidelity lands later.
|
||||||
|
func _emit_rust_turn_events(events: Array) -> void:
|
||||||
|
for e: Variant in events:
|
||||||
|
if typeof(e) != TYPE_DICTIONARY:
|
||||||
|
continue
|
||||||
|
var d: Dictionary = e as Dictionary
|
||||||
|
var kind: String = str(d.get("kind", ""))
|
||||||
|
match kind:
|
||||||
|
"TechResearched":
|
||||||
|
EventBus.tech_researched.emit(
|
||||||
|
str(d.get("tech", "")), int(d.get("clan", 0))
|
||||||
|
)
|
||||||
|
"CultureResearched":
|
||||||
|
EventBus.culture_researched.emit(
|
||||||
|
str(d.get("tradition", "")), int(d.get("clan", 0))
|
||||||
|
)
|
||||||
|
"GoldenAgeStarted":
|
||||||
|
EventBus.golden_age_started.emit(int(d.get("clan", 0)))
|
||||||
|
"GoldenAgeEnded":
|
||||||
|
EventBus.golden_age_ended.emit(int(d.get("clan", 0)))
|
||||||
|
_:
|
||||||
|
# Entity-payload and lower-priority kinds (CityGrew,
|
||||||
|
# CityBordersExpanded, UnitCreated, UnitHealed, UnitKilled,
|
||||||
|
# CityFounded, CityCaptured, CityBuildingCompleted,
|
||||||
|
# FloraSuccession, …) are deferred — see the docstring. The
|
||||||
|
# board is already correct via the synced slots.
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
func next_player() -> void:
|
func next_player() -> void:
|
||||||
# p3-15: rotation follows GameState.turn_order (randomized once at game start);
|
# p3-15: rotation follows GameState.turn_order (randomized once at game start);
|
||||||
# falls back to sequential when unset (arena / old saves).
|
# falls back to sequential when unset (arena / old saves).
|
||||||
|
|
@ -284,21 +384,28 @@ func next_player() -> void:
|
||||||
var phase_events: Array = WorldsimState.worldsim.call("end_player_round_phase", GameState.get_gd_state())
|
var phase_events: Array = WorldsimState.worldsim.call("end_player_round_phase", GameState.get_gd_state())
|
||||||
_emit_phase_events(phase_events)
|
_emit_phase_events(phase_events)
|
||||||
# All players have taken their turn — run wild creatures, then advance
|
# All players have taken their turn — run wild creatures, then advance
|
||||||
var proc := _processor as TurnProcessorScript
|
var proc: TurnProcessorScript = _processor as TurnProcessorScript
|
||||||
# Iter 7k: optional parallel Rust fauna encounter pass. No-op unless
|
# Rail-1 Phase-2b: under RUST_TURN the whole-round `GdTurnProcessor.step`
|
||||||
# RUST_FAUNA_ENCOUNTERS env flag is set (off by default).
|
# (already run in end_turn at the round boundary) computes fauna encounters,
|
||||||
proc._process_rust_fauna_encounters()
|
# wild-creature behaviour, diplomacy/trade, climate and ecology. Gate the
|
||||||
proc._process_wild_creatures()
|
# GDScript copies of those passes off so they don't double-process. The
|
||||||
# p3-23 revival step 2: evaluate inter-player trades once per full round,
|
# worldsim carve-out below (world-event dispatch / terraform / contamination
|
||||||
# after all players have moved. Sends PlayerTradeInput records (per-player
|
# / worldsim_updated render hook) is NOT covered by `step` and stays live.
|
||||||
# controlled luxuries + strategics + trade_willingness), applies the
|
if not _rust_turn_enabled:
|
||||||
# resulting ledger — traded luxuries feed happiness, strategics gate unit
|
# Iter 7k: optional parallel Rust fauna encounter pass. No-op unless
|
||||||
# builds. process_turn is internally defensive (guards null game_map,
|
# RUST_FAUNA_ENCOUNTERS env flag is set (off by default).
|
||||||
# missing GdTrade extension, unknown resources) so it cannot abort the
|
proc._process_rust_fauna_encounters()
|
||||||
# round loop the way the old empty-stub call did.
|
proc._process_wild_creatures()
|
||||||
(diplomacy as DiplomacyScript).process_turn(
|
# p3-23 revival step 2: evaluate inter-player trades once per full round,
|
||||||
GameState.players, GameState.turn_number, GameState.get_game_map()
|
# after all players have moved. Sends PlayerTradeInput records (per-player
|
||||||
)
|
# controlled luxuries + strategics + trade_willingness), applies the
|
||||||
|
# resulting ledger — traded luxuries feed happiness, strategics gate unit
|
||||||
|
# builds. process_turn is internally defensive (guards null game_map,
|
||||||
|
# missing GdTrade extension, unknown resources) so it cannot abort the
|
||||||
|
# round loop the way the old empty-stub call did.
|
||||||
|
(diplomacy as DiplomacyScript).process_turn(
|
||||||
|
GameState.players, GameState.turn_number, GameState.get_game_map()
|
||||||
|
)
|
||||||
# DISABLED: EconomyScript.apply_protection_effects — empty stub
|
# DISABLED: EconomyScript.apply_protection_effects — empty stub
|
||||||
# module has no such method; the call aborts next_player and kills
|
# module has no such method; the call aborts next_player and kills
|
||||||
# the arena turn loop. See turn_processor.gd top-of-file out-of-scope
|
# the arena turn loop. See turn_processor.gd top-of-file out-of-scope
|
||||||
|
|
@ -311,7 +418,16 @@ func next_player() -> void:
|
||||||
# Climate processing: weather injects deltas, then physics propagates them.
|
# Climate processing: weather injects deltas, then physics propagates them.
|
||||||
# Must run once per full game turn after all players have moved.
|
# Must run once per full game turn after all players have moved.
|
||||||
if game_map_for_climate != null:
|
if game_map_for_climate != null:
|
||||||
proc._process_climate(game_map_for_climate)
|
# Rail-1 Phase-2b: climate physics + ecology tick are now computed by the
|
||||||
|
# whole-round `GdTurnProcessor.step` (run in end_turn at the round boundary),
|
||||||
|
# so gate the GDScript copies off under RUST_TURN to avoid double-advancing
|
||||||
|
# the living-world grid. Flora-succession surfacing also moves to the Rust
|
||||||
|
# path (step emits FloraSuccession events). The grid is still RESOLVED below
|
||||||
|
# (it persists across turns) so the worldsim carve-out — world-event dispatch,
|
||||||
|
# terraform drain, contamination tick, worldsim_updated render hook — keeps
|
||||||
|
# running, since `step` does NOT cover those (p3-26/p3-27 boundary).
|
||||||
|
if not _rust_turn_enabled:
|
||||||
|
proc._process_climate(game_map_for_climate)
|
||||||
# p1-38 Phase B item #7: tick the shared fauna engine after climate
|
# p1-38 Phase B item #7: tick the shared fauna engine after climate
|
||||||
# so populations evolve turn-over-turn (emergence + Lotka-Volterra
|
# so populations evolve turn-over-turn (emergence + Lotka-Volterra
|
||||||
# dynamics). Reuses Climate's GdGridState — same grid both layers
|
# dynamics). Reuses Climate's GdGridState — same grid both layers
|
||||||
|
|
@ -320,15 +436,16 @@ func next_player() -> void:
|
||||||
if climate_node != null:
|
if climate_node != null:
|
||||||
var fauna_grid: RefCounted = climate_node.get("_grid") as RefCounted
|
var fauna_grid: RefCounted = climate_node.get("_grid") as RefCounted
|
||||||
if fauna_grid != null:
|
if fauna_grid != null:
|
||||||
EcologyState.tick(fauna_grid, GameState.map_seed + GameState.turn_number)
|
if not _rust_turn_enabled:
|
||||||
# g2-07: surface flora succession transitions captured by the
|
EcologyState.tick(fauna_grid, GameState.map_seed + GameState.turn_number)
|
||||||
# ecology tick into the playable game log. One signal per turn
|
# g2-07: surface flora succession transitions captured by the
|
||||||
# carrying every (tile, species) that crossed a tier this turn.
|
# ecology tick into the playable game log. One signal per turn
|
||||||
var flora_transitions: Array = EcologyState.take_flora_transitions()
|
# carrying every (tile, species) that crossed a tier this turn.
|
||||||
if not flora_transitions.is_empty():
|
var flora_transitions: Array = EcologyState.take_flora_transitions()
|
||||||
EventBus.flora_succession.emit(
|
if not flora_transitions.is_empty():
|
||||||
GameState.turn_number, flora_transitions
|
EventBus.flora_succession.emit(
|
||||||
)
|
GameState.turn_number, flora_transitions
|
||||||
|
)
|
||||||
# Increment 3b: world-event dispatch (geological / biological /
|
# Increment 3b: world-event dispatch (geological / biological /
|
||||||
# anomalous) against the SAME live grid, after climate + ecology
|
# anomalous) against the SAME live grid, after climate + ecology
|
||||||
# ticks. Accumulates per-tile eco-damage into WorldsimState's
|
# ticks. Accumulates per-tile eco-damage into WorldsimState's
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue