118 lines
5.1 KiB
GDScript
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 {}
|