perf(management-specific): Migrate research processing logic from GDScript to Rust (Rail-1) while delegating spell/tech checks and preserving GDScript wrapper for signals

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
autocommit 2026-04-27 05:47:10 -07:00
parent f59bbc041f
commit 31955b1ada
3 changed files with 72 additions and 41 deletions

View file

@ -141,58 +141,71 @@ func _process_production(player: RefCounted) -> void: # Player
func _process_research(player: RefCounted) -> void: # Player
## Rail-1: science accumulation + spell/tech completion check delegated
## to Rust `GdTechWeb::process_research` (warcouncil p1-39 port,
## 2026-04-27). GDScript only assembles input JSON + applies
## completion side-effects (school_locked emit, _form_high_archon,
## tech_researched signal, resource reveals).
if player.researching.is_empty():
return
# Debug: instantly complete any queued research/spell.
if EnvConfig.get_bool("FORCE_UNLIMITED_RESEARCH"):
player.research_progress = 999999
# Effective per-yield mult composes static difficulty handicap +
# linear-per-turn growth (warcouncil p1-29 H4, 2026-04-27). Per-player
# overrides for batch testing still take precedence inside the helper.
# Full Rail-1 port to GdTechWeb::process_research is queued under p1-31 —
# requires wrapper-layer plumbing (tech_web.gd → KnowledgeWebScript → GdTechWeb).
# Per-yield difficulty multiplier (composed by GameState).
var sci_modifier: float = GameState.get_effective_yield_mult(player, "research")
player.research_progress += int(player.science_per_turn * sci_modifier)
# Add science from cities
var game_map: RefCounted = GameState.get_game_map() # GameMap
# Per-city science yields the Rust side will sum.
var game_map: RefCounted = GameState.get_game_map()
var yields_arr: Array = []
if game_map != null:
for city: Variant in player.cities:
if city is CityScript:
var tile_json: String = BuildableHelperScript.build_tile_yields_json(
city as CityScript, game_map
)
var yields: Dictionary = city.get_yields(tile_json)
var building_sci: int = _sum_city_building_effect(city as CityScript, "science")
var sci_pct: float = _sum_city_building_effect_float(
city as CityScript, "science_percent"
)
player.research_progress += int(
(yields.get("science", 0) + building_sci) * (1.0 + sci_pct) * sci_modifier
)
if not city is CityScript:
continue
var c: CityScript = city as CityScript
var tile_json: String = BuildableHelperScript.build_tile_yields_json(c, game_map)
var ys: Dictionary = c.get_yields(tile_json)
yields_arr.append({
"science": int(ys.get("science", 0)),
"building_science": _sum_city_building_effect(c, "science"),
"science_percent": _sum_city_building_effect_float(c, "science_percent"),
})
# Check if researching a spell (not a tech)
# Player input. spell_cost is set when researching a spell so Rust runs
# the cheap counter branch (no TechWeb lookup).
var spell_data: Dictionary = DataLoader.get_spell(player.researching)
if not spell_data.is_empty():
var spell_cost: int = spell_data.get("research_cost", 999999)
if player.research_progress >= spell_cost:
var completed_spell: String = player.researching
player.research_progress = 0
player.researching = ""
var sys: SpellSystemScript = spell_system as SpellSystemScript
sys.research_spell(player.index, completed_spell)
var researching_spell: bool = not spell_data.is_empty()
var researched_arr: Array = Array(player.researched_techs) if player.researched_techs != null else []
var player_dict: Dictionary = {
"researching": str(player.researching),
"research_progress": int(player.research_progress),
"science_per_turn": int(player.science_per_turn),
"researched_techs": researched_arr,
"instant_complete": EnvConfig.get_bool("FORCE_UNLIMITED_RESEARCH"),
}
if researching_spell:
player_dict["spell_cost"] = int(spell_data.get("research_cost", 999999))
var tw: RefCounted = TurnManager.get_tech_web()
if tw == null:
return
var result: Dictionary = tw.process_research(
JSON.stringify(player_dict), JSON.stringify(yields_arr), sci_modifier
)
if result.is_empty():
return
var err: String = str(result.get("error", ""))
if not err.is_empty():
push_warning("p1-39 _process_research: " + err)
return
player.research_progress = int(result.get("new_progress", 0))
player.researching = str(result.get("new_researching", ""))
var completed_spell: String = str(result.get("completed_spell", ""))
if not completed_spell.is_empty():
var sys: SpellSystemScript = spell_system as SpellSystemScript
sys.research_spell(player.index, completed_spell)
return
var tech_data: Dictionary = DataLoader.get_tech(player.researching)
var tech_cost: int = tech_data.get("cost", 999999)
if player.research_progress >= tech_cost:
var completed_tech: String = player.researching
player.research_progress = 0
player.researching = ""
var completed_tech: String = str(result.get("completed_tech", ""))
if not completed_tech.is_empty():
var old_school_count: int = player.schools.size()
player.add_tech(completed_tech)
# Arcane Lore completion: transform leader into High Archon

View file

@ -141,6 +141,16 @@ func start_research(node_id: String, player_index: int) -> bool:
return true
## Direct passthrough to the underlying GDExtension's `process_research`
## (warcouncil p1-39 Rail-1 port, 2026-04-27). Caller composes the input
## JSON; Rust owns sci_modifier application + spell-vs-tech branching.
## Returns `{}` if the bridge is unavailable so callers can no-op safely.
func process_research(player_json: String, yield_json: String, sci_modifier: float) -> Dictionary:
if _bridge == null:
return {}
return _bridge.call("process_research", player_json, yield_json, sci_modifier) as Dictionary
# ── Internals ───────────────────────────────────────────────────────────
func _get_player(player_index: int) -> PlayerScript:

View file

@ -37,6 +37,14 @@ func build(_techs: Array) -> void:
_web.build()
## Delegate to GdTechWeb::process_research (warcouncil p1-39 Rail-1 port).
## Caller assembles the player + yield JSON and supplies the difficulty
## sci_modifier. Rust owns: per-city science sum, sci_modifier
## application, instant-complete branch, spell-vs-tech branching.
func process_research(player_json: String, yield_json: String, sci_modifier: float) -> Dictionary:
return _web.process_research(player_json, yield_json, sci_modifier)
func apply_heritage_tech(player: RefCounted) -> void:
## Grant the player's race origin tech (cost 0 by convention).
if player == null: