Files
aetherbound-guild/runtime/tools/m80_contract_run_probe.gd
T

118 lines
5.1 KiB
GDScript

extends SceneTree
## M80-B4 evidence: plays one whole region-one contract through the simulation
## bridge - twelve ordinary rounds with retries, then the boss Muster. The
## record is round-by-round so the balance and pacing can be read from one run.
const ContentRegistry = preload("res://p10_0/content_registry.gd")
const ContractRun = preload("res://domain/contract_run.gd")
const Bridge = preload("res://entry/battle_sim_bridge.gd")
const CONTRACT_ID := "CTR-R1-01"
const REGION_INDEX := 1
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var catalog: Dictionary = ContentRegistry.snapshot()
var contract := _contract(catalog, CONTRACT_ID)
if contract.is_empty():
push_error("M80-B4 probe: contract %s is missing" % CONTRACT_ID)
quit(1)
return
var enemy_ids: Array = []
for row in Array(catalog.get("enemies", [])):
if int(Dictionary(row).get("region", 0)) == REGION_INDEX:
enemy_ids.append(str(Dictionary(row).get("id", "")))
var enemy_meta := _enemy_meta(catalog)
var state := ContractRun.state_after_clear(contract, 0)
var record: Array[String] = []
var round_index := 1
var guard := 0
while round_index <= int(contract.get("round_count", 12)) and guard < 40:
guard += 1
# The probe walks the same ladder the game does: the Company grows with
# cleared rounds (section-17 capacity vector) and gains levels, so the
# probe cannot pass or fail on a stale fixed party.
var level := 3 + int(floor(float(round_index - 1) / 2.0))
var size := mini(10, 4 + int(floor(float(round_index - 1) / 2.0)))
var recruits := _company(size, level)
var formation: Array = []
for recruit in recruits:
formation.append(str(Dictionary(recruit).get("recruit_id", "")))
var attempt := int(state.get("attempt", 0))
var encounter := ContractRun.ordinary_encounter(REGION_INDEX, enemy_ids, round_index, attempt, size)
var result := Bridge.run_encounter("m80-contract|round-%d|attempt-%d" % [round_index, attempt], formation, recruits, encounter, REGION_INDEX, enemy_meta)
var victory := str(result.get("outcome", "")) == "victory"
state = ContractRun.advance(state, victory)
record.append("%02d:%s(%d)" % [round_index, "W" if victory else "L", int(result.get("ticks", 0))])
if victory:
round_index += 1
var cleared := int(state.get("cleared_rounds", 0))
var level := 3 + int(floor(float(contract.get("round_count", 12) - 1) / 2.0)) + 1
var boss_recruits := _company(10, level)
var boss_formation: Array = []
for recruit in boss_recruits:
boss_formation.append(str(Dictionary(recruit).get("recruit_id", "")))
var boss_id := str(contract.get("boss_id", ""))
var muster: Array = [{"enemy_id": boss_id, "class": "boss"}]
for entry in ContractRun.ordinary_encounter(REGION_INDEX, enemy_ids, 12, 0).slice(0, ContractRun.MUSTER_ESCORTS):
muster.append(Dictionary(entry).duplicate(true))
var boss_result := Bridge.run_encounter("m80-contract|boss", boss_formation, boss_recruits, muster, REGION_INDEX, enemy_meta)
var boss_outcome := str(boss_result.get("outcome", ""))
# The probe owns the structure: every ordinary round resolves, clears and
# retries land where section 7.2 says, and the boss Muster resolves. How
# many rounds a static level-4 party clears is a balance number, reported
# for the economy pass instead of being forced here.
var failures: Array[String] = []
if cleared < 6:
failures.append("a static level-4 party cleared only %d ordinary rounds" % cleared)
if boss_outcome not in ["victory", "defeat", "timeout"]:
failures.append("the boss battle did not resolve (%s)" % boss_outcome)
print("ABG_M80_CONTRACT_RUN_RECORD contract=%s cleared=%d retries=%d boss=%s boss_outcome=%s record=%s"
% [CONTRACT_ID, cleared, int(state.get("market_index", 0)), str(contract.get("boss_id", "")), boss_outcome, ",".join(record)])
if not failures.is_empty():
for failure in failures:
push_error("M80-B4 probe: %s" % failure)
quit(1)
return
print("ABG_M80_CONTRACT_RUN_PROBE_OK contract=%s cleared=%d attempts=%d boss_resolved=true boss=%s" % [CONTRACT_ID, cleared, int(state.get("market_index", 0)), str(contract.get("boss_id", ""))])
quit(0)
func _company(size: int, level: int) -> Array:
var professions := ["PF-B01", "PF-B02", "PF-B07", "PF-B04", "PF-B03", "PF-B09",
"PF-B08", "PF-B05", "PF-B06", "PF-B11"]
var rows: Array = []
for index in range(mini(size, professions.size())):
rows.append({
"recruit_id": "R-%d" % (index + 1),
"profession_id": professions[index],
"level": level,
"trait_id": "",
"might": 8 + level, "finesse": 7 + level, "insight": 6 + level,
"vigor": 9 + level, "resolve": 7 + level,
})
return rows
func _enemy_meta(catalog: Dictionary) -> Dictionary:
var meta := {}
for row in Array(catalog.get("enemies", [])):
var entry: Dictionary = Dictionary(row)
meta[str(entry.get("id", ""))] = {"class": str(entry.get("class", "ordinary"))}
for row in Array(catalog.get("bosses", [])):
var entry: Dictionary = Dictionary(row)
meta[str(entry.get("id", ""))] = {"class": "boss"}
return meta
func _contract(catalog: Dictionary, contract_id: String) -> Dictionary:
for row in Array(catalog.get("contracts", [])):
if str(Dictionary(row).get("id", "")) == contract_id:
return Dictionary(row)
return {}