feat: continue Windglass Market through three rounds

This commit is contained in:
2026-08-15 01:32:21 +08:00
parent af3ec4ec76
commit a77400a8d2
8 changed files with 519 additions and 5 deletions
+52 -4
View File
@@ -4,6 +4,8 @@ const FacadeScript = preload("res://compat/p9_0_root_facade.gd")
const P92BattleServiceScript = preload("res://domain/p9_2/battle_service.gd")
const P92BattlePresenterScript = preload("res://presentation/p9_2_battle_presenter.gd")
const P93CoreLoopControllerScript = preload("res://p9_3/core_loop_controller.gd")
const P97RuntimeControllerScript = preload("res://p9_7/runtime_controller.gd")
const P97SaveServiceScript = preload("res://p9_7/save_service.gd")
const AppPage = FacadeScript.AppPage
const ANIMATION_SPECS = FacadeScript.ANIMATION_SPECS
const BATTLE_PHASE_DURATIONS = FacadeScript.BATTLE_PHASE_DURATIONS
@@ -23,6 +25,7 @@ var authoritative_last_command_code := ""
var authoritative_last_events: Array[Dictionary] = []
@export var p93_experience_enabled := true
var p93_experience
var p97_experience
var active_battle_feedback:
get: return facade.active_battle_feedback
@@ -197,8 +200,10 @@ var victory_events:
set(value): facade.victory_events = value
func _ready() -> void:
var should_start_p93: bool = auto_boot and p93_experience_enabled
if should_start_p93:
var p97_resume_snapshot := _p97_resume_snapshot()
var should_start_p97: bool = auto_boot and p93_experience_enabled and not p97_resume_snapshot.is_empty()
var should_start_p93: bool = auto_boot and p93_experience_enabled and not should_start_p97
if should_start_p93 or should_start_p97:
facade.auto_boot = false
facade.name = "RuntimeFacade"
facade.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
@@ -208,6 +213,8 @@ func _ready() -> void:
authoritative_battle_presenter = P92BattlePresenterScript.new(self)
if should_start_p93:
call_deferred("start_p93_experience")
elif should_start_p97:
call_deferred("start_p97_experience", p97_resume_snapshot)
func _get(property: StringName) -> Variant:
return facade.get(property)
@@ -232,7 +239,7 @@ func _get_property_list() -> Array[Dictionary]:
return result
func _process(delta: float) -> void:
if is_instance_valid(p93_experience):
if is_instance_valid(p93_experience) or is_instance_valid(p97_experience):
return
if authoritative_battle_active:
_process_authoritative_battle(delta)
@@ -240,14 +247,55 @@ func _process(delta: float) -> void:
facade._process(delta)
func start_p93_experience() -> void:
if is_instance_valid(p93_experience):
if is_instance_valid(p93_experience) or is_instance_valid(p97_experience):
return
facade.visible = false
p93_experience = P93CoreLoopControllerScript.new()
p93_experience.name = "P93CoreLoop"
p93_experience.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
p93_experience.windglass_market_requested.connect(start_p97_experience)
add_child(p93_experience)
func start_p97_experience(snapshot: Dictionary) -> void:
if snapshot.is_empty() or is_instance_valid(p97_experience):
return
var source_locale := facade.locale
var source_text_scale := facade.text_scale
var source_reduced_motion := facade.reduced_motion
var source_muted := facade.muted
var source_low_power := false
if is_instance_valid(p93_experience):
source_locale = p93_experience.locale
source_text_scale = p93_experience.text_scale
source_reduced_motion = p93_experience.reduced_motion
source_muted = p93_experience.muted
source_low_power = p93_experience.low_power
p93_experience.visible = false
p93_experience.process_mode = Node.PROCESS_MODE_DISABLED
p93_experience.queue_free()
p93_experience = null
facade.visible = false
p97_experience = P97RuntimeControllerScript.new()
p97_experience.name = "P97WindglassRuntime"
p97_experience.locale = source_locale
p97_experience.text_scale = source_text_scale
p97_experience.reduced_motion = source_reduced_motion
p97_experience.muted = source_muted
p97_experience.low_power = source_low_power
p97_experience.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
p97_experience.configure_entry(snapshot)
add_child(p97_experience)
func _p97_resume_snapshot() -> Dictionary:
if not auto_boot or not p93_experience_enabled:
return {}
var loaded: Dictionary = P97SaveServiceScript.new().load_snapshot()
if not loaded.ok or int(loaded.get("source_version", -1)) != 2:
return {}
return Dictionary(loaded.snapshot).duplicate(true)
func start_authoritative_battle() -> bool:
var members: Array[String] = facade.session_state.current_member_ids()
if members.is_empty():
+13
View File
@@ -1,6 +1,8 @@
class_name P93CoreLoopController
extends Control
signal windglass_market_requested(snapshot: Dictionary)
const Service = preload("res://p9_3/core_loop_service.gd")
const State = preload("res://p9_3/core_loop_state.gd")
const Rules = preload("res://domain/p9_2/battle_rules.gd")
@@ -170,6 +172,13 @@ func recover_injured_company() -> void:
_queue_render_current()
func enter_windglass_market() -> void:
if service.state.phase != State.Phase.HALL or service.state.recovery_required or service.state.reward_receipts.size() < 4:
return
_save_stable()
windglass_market_requested.emit(service.state.snapshot())
func tap_encounter(encounter_id: String) -> void:
if service.state.phase != State.Phase.ENCOUNTER:
return
@@ -659,6 +668,10 @@ func _render_rift_tide_return_hall() -> void:
display_root.add_child(condition)
_add_member_status(member_id, actor_position + Vector2(-12, actor_height - 30), Vector2(actor_height + 24, 42), "P96FinalStatus_%s" % member_id, true)
_add_equipped_icon(member_id, actor_position + Vector2(actor_height - 18, actor_height * 0.38))
var continue_button := _action_button("P97EnterMarketButton", _tr("前往风晶集市", "ENTER WINDGLASS MARKET"), enter_windglass_market)
continue_button.position = Vector2(size.x - 232, 156)
continue_button.size = Vector2(216, 52)
display_root.add_child(continue_button)
func _render_encounters() -> void:
+1
View File
@@ -134,6 +134,7 @@ func _bootstrap_market() -> void:
last_code = "market_ready"
battle_active = false
battle_result_visible = false
_save_stable()
_queue_render()
@@ -11,6 +11,7 @@ const BattleService = preload("res://p9_7/battle_service.gd")
const FIXTURES := [
{"name": "market-small-zh", "size": Vector2i(760, 360), "state": "market", "locale": "zh_CN", "scale": 1.0, "reduced": false, "low_power": false},
{"name": "market-reference-en-130", "size": Vector2i(844, 390), "state": "market", "locale": "en", "scale": 1.3, "reduced": true, "low_power": false},
{"name": "market-reference-zh-130", "size": Vector2i(844, 390), "state": "market", "locale": "zh_CN", "scale": 1.3, "reduced": true, "low_power": false},
{"name": "market-large-low-power", "size": Vector2i(1280, 720), "state": "market", "locale": "zh_CN", "scale": 1.0, "reduced": false, "low_power": true},
{"name": "recovery-zh-130", "size": Vector2i(844, 390), "state": "recovery", "locale": "zh_CN", "scale": 1.3, "reduced": true, "low_power": true},
{"name": "relay-en-130", "size": Vector2i(844, 390), "state": "relay", "locale": "en", "scale": 1.3, "reduced": true, "low_power": true},
@@ -38,7 +39,7 @@ func run_test() -> void:
await verify_fixture(fixture, snapshots[String(fixture.state)])
if failures.is_empty():
print("ABG_P9_7_RUNTIME_CONTROLLER_COMPILE_OK viewports=760x360,844x390,1280x720,390x844 landscape=world portrait=rotate")
print("ABG_P9_7_RUNTIME_LAYOUT_OK fixtures=6 states=market,recovery,relay viewports=760x360,844x390,1280x720,390x844 locales=zh_CN,en text_130=true reduced_motion=true muted=true low_power=true controls=48 cards=0 captures=%d" % capture_count)
print("ABG_P9_7_RUNTIME_LAYOUT_OK fixtures=7 states=market,recovery,relay viewports=760x360,844x390,1280x720,390x844 locales=zh_CN,en text_130=true reduced_motion=true muted=true low_power=true controls=48 cards=0 captures=%d" % capture_count)
quit(0)
else:
for failure in failures:
@@ -0,0 +1,212 @@
extends SceneTree
const MainScene = preload("res://main.tscn")
const LegacyService = preload("res://p9_3/core_loop_service.gd")
const LegacyState = preload("res://p9_3/core_loop_state.gd")
const P97SaveService = preload("res://p9_7/save_service.gd")
const VIEWPORT_SIZE := Vector2i(844, 390)
const ROUND_STEPS := [
{"market": 5, "recruit": "P97-M5-R2", "route": "P97-R5-Q0"},
{"market": 6, "recruit": "P97-M6-R2", "route": "P97-R6-Q0"},
{"market": 7, "recruit": "P97-M7-R3", "route": "P97-R7-Q0"},
]
var failures: Array[String] = []
var parsed_touches := 0
var capture_root := ""
var game
var save_service = P97SaveService.new()
func _initialize() -> void:
call_deferred("run_test")
func run_test() -> void:
capture_root = OS.get_environment("ABG_P9_7_CONTINUATION_CAPTURE_ROOT")
expect(save_service.clear().ok, "test-owned v2 save state clears before the opening handoff")
root.content_scale_mode = Window.CONTENT_SCALE_MODE_CANVAS_ITEMS
root.content_scale_aspect = Window.CONTENT_SCALE_ASPECT_EXPAND
root.content_scale_size = VIEWPORT_SIZE
root.size = VIEWPORT_SIZE
game = MainScene.instantiate()
game.auto_boot = true
game.p93_experience_enabled = true
root.add_child(game)
await settle_frames(5)
expect(game.p93_experience != null, "main starts the accepted P9.3-P9.6 controller when no v2 save exists")
if game.p93_experience == null:
await finish()
return
game.p93_experience.persistence_enabled = false
game.p93_experience.service = complete_p96()
game.p93_experience.returned_from_battle = true
game.p93_experience._render_current()
await settle_frames()
expect(find_control(game, "P96FinalHeadline") != null and find_control(game, "P97EnterMarketButton") != null, "settled Rift Tide Hall exposes one Windglass continuation")
await capture("01-p96-windglass-handoff")
await tap_named("P97EnterMarketButton")
await settle_frames(4)
var experience = game.p97_experience
expect(experience != null and game.p93_experience == null, "handoff replaces the old controller with P9.7")
if experience == null:
await finish()
return
experience.battle_auto_advance = false
expect(int(experience.market.state.current_market) == 5 and int(experience.market.state.cleared_rounds) == 4, "handoff preserves the exact settled P9.6 state")
await capture("02-market5-entry")
for step in ROUND_STEPS:
await play_round(experience, step)
expect(int(experience.market.state.current_market) == 8 and int(experience.market.state.cleared_rounds) == 7, "three rounds reach Market 8")
expect(experience.market.state.relay_restored and experience.market.state.continuation_locked, "Round 7 restores the Relay and opens the explicit threshold")
expect(experience.market.state.route_history == ["P97-R5-Q0", "P97-R6-Q0", "P97-R7-Q0"], "all three committed routes remain ordered")
expect(experience.market.state.battle_settlement_receipts.size() == 3, "three battle settlements commit exactly once")
expect(experience.market.state.company.size() == 6 and experience.market.state.deployed_recruit_ids.size() == 6 and experience.market.state.line_capacity == 7, "Company, Line and post-Round-6 capacity remain distinct")
await capture("09-relay-restored")
var loaded: Dictionary = save_service.load_snapshot()
expect(loaded.ok and int(loaded.source_version) == 2 and loaded.snapshot == experience.market.state.snapshot(), "Market 8 save reloads without reroll or offline progress")
var saved_snapshot: Dictionary = experience.market.state.snapshot()
game.queue_free()
await settle_frames(4)
game = MainScene.instantiate()
game.auto_boot = true
game.p93_experience_enabled = true
root.add_child(game)
await settle_frames(6)
expect(game.p97_experience != null and game.p93_experience == null, "main routes a valid v2 save directly to P9.7")
if game.p97_experience != null:
expect(game.p97_experience.market.state.snapshot() == saved_snapshot, "reopened Market 8 preserves Company, HP, Coin, gear and route history")
expect(find_control(game, "P97RelayTitle") != null, "reopened Market 8 renders the restored Relay")
await capture("10-market8-reloaded")
await finish()
func play_round(experience, step: Dictionary) -> void:
var market_index := int(step.market)
var recruit_id := String(step.recruit)
var route_id := String(step.route)
expect(int(experience.market.state.current_market) == market_index, "Market %d opens before its preparation" % market_index)
await tap_named("P97RecruitTarget_%s" % recruit_id)
await tap_named("P97RecruitTarget_%s" % recruit_id)
expect(recruit_id in experience.market.state.reserve_recruit_ids, "Market %d hire stages %s in Reserve" % [market_index, recruit_id])
if market_index == 5:
await tap_named("P97EquipmentTarget_P97-M5-EQ-021")
await tap_named("P97EquipmentTarget_P97-M5-EQ-021")
await tap_named("P97CompanyTarget_%s" % recruit_id)
expect(experience.market.state.equipped_by_recruit[recruit_id] == "P97-M5-EQ-021-ITEM", "Market 5 equips the purchased Threatglass to the new Recruit")
await tap_named("P97CompanyTarget_%s" % recruit_id)
await tap_named("P97CompanyTarget_%s" % recruit_id)
await tap_named("P97CommitLineButton")
expect(recruit_id in experience.market.state.deployed_recruit_ids and recruit_id not in experience.market.state.reserve_recruit_ids, "Market %d commits %s to the Line" % [market_index, recruit_id])
await capture("%02d-market%d-prepared" % [market_index - 2, market_index])
await tap_named("P97RouteTarget_%s" % route_id)
await tap_named("P97RouteTarget_%s" % route_id)
expect(experience.battle_active and experience.battle.state.route_id == route_id, "Market %d starts the chosen route" % market_index)
var guard := 0
while not experience.battle.state.terminal and guard < 2200:
experience.advance_battle_ticks(1)
if guard % 8 == 0:
await process_frame
guard += 1
await settle_frames()
expect(guard < 2200 and experience.battle.state.outcome == "victory", "Round %d reaches deterministic victory" % market_index)
await capture("%02d-round%d-result" % [market_index - 1, market_index])
await tap_named("P97SettleButton")
expect(experience.market.state.battle_settlement_receipts.size() == market_index - 4, "Round %d settlement commits once" % market_index)
if experience.market.state.recovery_required:
expect(find_control(game, "P97RecoveryButton") != null, "Round %d exposes recovery for fallen members" % market_index)
await tap_named("P97RecoveryButton")
expect(not experience.market.state.recovery_required, "Round %d recovery restores the next decision state" % market_index)
await settle_frames()
func tap_named(node_name: String) -> void:
var control := find_control(game, node_name)
expect(control != null, "%s exists before parsed touch" % node_name)
if control == null:
return
var point := root.get_final_transform() * control.get_global_rect().get_center()
var press := InputEventScreenTouch.new()
press.index = 0
press.position = point
press.pressed = true
Input.parse_input_event(press)
parsed_touches += 1
await process_frame
var release := InputEventScreenTouch.new()
release.index = 0
release.position = point
release.pressed = false
Input.parse_input_event(release)
await settle_frames()
func find_control(root_node: Node, node_name: String) -> Control:
return root_node.find_child(node_name, true, false) as Control
func capture(name: String) -> void:
if capture_root.is_empty():
return
DirAccess.make_dir_recursive_absolute(capture_root)
await settle_frames()
await RenderingServer.frame_post_draw
var error := root.get_texture().get_image().save_png(capture_root.path_join(name + ".png"))
expect(error == OK, "%s capture writes" % name)
await process_frame
func complete_p96():
var service = LegacyService.new()
service.enter_hall()
service.focus_offer("Farstring")
service.hire("Farstring")
complete_legacy_battle(service, "open_road")
service.focus_offer("Kindhand")
service.hire("Kindhand")
complete_legacy_battle(service, "crosswind")
service.focus_equipment("EQ-002")
service.equip_focused("Hearthguard")
complete_legacy_battle(service, "bell_breach")
service.choose_rift_tide_plan("press_on")
complete_legacy_battle(service, "rift_tide")
return service
func complete_legacy_battle(service, encounter_id: String) -> void:
service.select_encounter(encounter_id)
service.start_battle()
var guard := 0
while service.state.phase == LegacyState.Phase.BATTLE and guard < 4000:
service.advance_battle_ticks(1)
guard += 1
service.claim_and_return()
func settle_frames(count := 2) -> void:
for _index in range(count):
await process_frame
func finish() -> void:
if game != null and is_instance_valid(game):
game.queue_free()
game = null
expect(save_service.clear().ok, "test-owned v2 save state clears after the reload proof")
await settle_frames(120)
if failures.is_empty():
print("ABG_P9_7_THREE_ROUND_CONTINUATION_OK touch=%d fallback=0 direct=0 handoff=p9.6_to_market5 rounds=5,6,7 routes=steady,steady,steady settlements=3 company=6 line=6/7 relay=true market8=reload_exact v2_resume=direct" % parsed_touches)
quit(0)
else:
for failure in failures:
push_error(failure)
quit(1)
func expect(condition: bool, message: String) -> void:
if not condition:
failures.append("P9.7 three-round continuation: %s" % message)
@@ -0,0 +1 @@
uid://d9p97threerounds
@@ -0,0 +1,237 @@
extends SceneTree
const LegacyService = preload("res://p9_3/core_loop_service.gd")
const LegacyState = preload("res://p9_3/core_loop_state.gd")
const MarketService = preload("res://p9_7/market_service.gd")
const MarketState = preload("res://p9_7/market_state.gd")
const BattleService = preload("res://p9_7/battle_service.gd")
const SaveService = preload("res://p9_7/save_service.gd")
const PROFILES := {
"normal": {
"recruits": ["P97-M5-R2", "P97-M6-R2", "P97-M7-R3"],
"routes": ["P97-R5-Q0", "P97-R6-Q0", "P97-R7-Q0"],
"gear": "EQ-021", "wearer": "P97-M5-R2", "speed": 1,
},
"active": {
"recruits": ["P97-M5-R3", "P97-M6-R1", "P97-M7-R3"],
"routes": ["P97-R5-Q1", "P97-R6-Q1", "P97-R7-Q2"],
"gear": "EQ-036", "wearer": "P97-M5-R3", "speed": 2,
},
"efficient": {
"recruits": ["P97-M5-R3", "P97-M6-R2", "P97-M7-R1"],
"routes": ["P97-R5-Q0", "P97-R6-Q0", "P97-R7-Q1"],
"gear": "", "wearer": "", "speed": 2,
},
"adversarial": {
"recruits": ["P97-M5-R2", "P97-M6-R3", "P97-M7-R1"],
"routes": ["P97-R5-Q0", "P97-R6-Q2", "P97-R7-Q2"],
"gear": "EQ-028", "wearer": "P97-M5-R2", "speed": 1,
},
"returning": {
"recruits": ["P97-M5-R2", "P97-M6-R2", "P97-M7-R3"],
"routes": ["P97-R5-Q1", "P97-R6-Q0", "P97-R7-Q1"],
"gear": "EQ-023", "wearer": "P97-M5-R2", "speed": 1,
},
}
var failures: Array[String] = []
var represented_route_risks: Dictionary = {}
var represented_gear: Dictionary = {}
var longest_gap := 0.0
var timestamp_count := 0
var defeat_recoveries := 0
var interruption_reloads := 0
var invalid_mutations := 0
var save_root := ""
func _initialize() -> void:
call_deferred("run_test")
func run_test() -> void:
save_root = OS.get_environment("ABG_P9_7_PROFILE_SAVE_ROOT")
if save_root.is_empty():
save_root = OS.get_user_data_dir().path_join("p9_7_profiles")
DirAccess.make_dir_recursive_absolute(save_root)
for profile_name in ["normal", "active", "efficient", "adversarial", "returning"]:
run_profile(profile_name)
expect(represented_route_risks.size() == 3, "profiles cover steady, pressured and elite routes")
expect(represented_gear.size() == 5 and represented_gear.has("none"), "profiles cover four distinct purchases plus no purchase")
expect(defeat_recoveries == 1, "adversarial profile proves exactly one defeat/recovery/replan")
expect(interruption_reloads == 1, "returning profile proves exactly one Encounter interruption reload")
expect(longest_gap <= 25.0, "longest automatic battle gap remains at most 25 seconds")
if failures.is_empty():
print("ABG_P9_7_THREE_ROUND_FIVE_PROFILE_OK profiles=5 route_risks=steady,pressured,elite gear=EQ-021,EQ-023,EQ-028,EQ-036,none timestamps=%d longest_gap=%.2f defeat_recoveries=%d interruption_reloads=%d offline_rewards=0 starvation=0 invalid_mutations=%d relay=5/5" % [timestamp_count, longest_gap, defeat_recoveries, interruption_reloads, invalid_mutations])
quit(0)
else:
for failure in failures:
push_error(failure)
quit(1)
func run_profile(profile_name: String) -> void:
var profile: Dictionary = PROFILES[profile_name]
var market = MarketService.new(complete_p96())
var labels: Array[String] = ["market5_read"]
var times: Array[float] = [0.0]
var profile_defeats := 0
var profile_interruptions := 0
represented_gear[String(profile.gear) if not String(profile.gear).is_empty() else "none"] = true
for round_index in range(3):
var market_index := 5 + round_index
var recruit_id := String(profile.recruits[round_index])
var route_id := String(profile.routes[round_index])
var route: Dictionary = market.content.routes[route_id]
represented_route_risks[int(route.risk)] = true
append_time(labels, times, "m%d_focus_recruit" % market_index, 0.7)
expect(market.focus_recruit_offer(recruit_id).ok, "%s Market %d focuses Recruit" % [profile_name, market_index])
append_time(labels, times, "m%d_hire" % market_index, 0.5)
expect(market.hire("%s-hire-%d" % [profile_name, market_index], recruit_id).ok, "%s Market %d hires Recruit" % [profile_name, market_index])
if round_index == 0 and not String(profile.gear).is_empty():
buy_profile_gear(market, profile_name, profile, labels, times)
var deployment: Array[String] = []
deployment.assign(market.auto_order_candidate())
if profile_name == "adversarial" and market_index == 6:
deployment.assign(["Hearthguard", "Farstring", "Kindhand", "P97-M6-R3"])
append_time(labels, times, "m%d_commit_line" % market_index, 0.7)
expect(market.commit_deployment("%s-deploy-%d" % [profile_name, market_index], deployment).ok, "%s Market %d commits Line" % [profile_name, market_index])
append_time(labels, times, "m%d_focus_route" % market_index, 1.0)
expect(market.focus_route(route_id).ok, "%s Market %d focuses route" % [profile_name, market_index])
append_time(labels, times, "m%d_commit_route" % market_index, 0.5)
expect(market.commit_route("%s-route-%d" % [profile_name, market_index], route_id).ok, "%s Market %d commits route" % [profile_name, market_index])
if profile_name == "returning" and market_index == 6:
market = interrupt_and_reload(market, profile_name, market_index, route_id, labels, times)
profile_interruptions += 1
var battle_result: Dictionary = run_battle(market, int(profile.speed))
append_time(labels, times, "m%d_%s" % [market_index, battle_result.outcome], float(battle_result.ticks) * 0.08 / float(profile.speed))
var settlement: Dictionary = market.settle_battle("%s-settle-%d-%d" % [profile_name, market_index, profile_defeats], battle_result.snapshot)
expect(settlement.ok, "%s Round %d settles" % [profile_name, market_index])
append_time(labels, times, "m%d_settle" % market_index, 0.5)
if battle_result.outcome == "defeat":
profile_defeats += 1
defeat_recoveries += 1
expect(profile_name == "adversarial" and market_index == 6, "only the declared adversarial Round 6 may defeat")
append_time(labels, times, "m%d_recover8" % market_index, 0.8)
expect(market.recover_injured_company("%s-defeat-recover-%d" % [profile_name, market_index]).ok, "%s Round %d recovers defeat" % [profile_name, market_index])
append_time(labels, times, "m%d_replan_line" % market_index, 0.7)
expect(market.commit_deployment("%s-redeploy-%d" % [profile_name, market_index], market.auto_order_candidate()).ok, "%s Round %d commits recovered full Line" % [profile_name, market_index])
append_time(labels, times, "m%d_retry_route" % market_index, 0.8)
expect(market.focus_route(route_id).ok and market.commit_route("%s-retry-route-%d" % [profile_name, market_index], route_id).ok, "%s Round %d recommits route" % [profile_name, market_index])
var retry: Dictionary = run_battle(market, int(profile.speed))
append_time(labels, times, "m%d_retry_%s" % [market_index, retry.outcome], float(retry.ticks) * 0.08 / float(profile.speed))
expect(retry.outcome == "victory", "%s Round %d recovered full Line wins" % [profile_name, market_index])
expect(market.settle_battle("%s-retry-settle-%d" % [profile_name, market_index], retry.snapshot).ok, "%s Round %d retry settles" % [profile_name, market_index])
append_time(labels, times, "m%d_retry_settle" % market_index, 0.5)
if market.state.recovery_required:
append_time(labels, times, "m%d_injury_recovery" % market_index, 0.6)
expect(market.recover_injured_company("%s-injury-recover-%d" % [profile_name, market_index]).ok, "%s Round %d recovers fallen members" % [profile_name, market_index])
if market_index < 7:
append_time(labels, times, "market%d_read" % (market_index + 1), 0.9)
var snapshot: Dictionary = market.state.snapshot()
expect(snapshot.current_market == 8 and snapshot.relay_restored and snapshot.continuation_locked, "%s reaches the Relay threshold" % profile_name)
expect(int(snapshot.coin) >= 0, "%s never starves below zero Coin" % profile_name)
expect(snapshot.battle_settlement_receipts.size() == 3 + profile_defeats, "%s settlement count includes only committed attempts" % profile_name)
var duplicate_before := snapshot.duplicate(true)
var duplicate_recovery: Dictionary = market.recover_injured_company("%s-last-recovery" % profile_name)
expect(not duplicate_recovery.ok and market.state.snapshot() == duplicate_before, "%s invalid final recovery mutates nothing" % profile_name)
invalid_mutations += 1
var profile_gap := max_gap(times)
longest_gap = maxf(longest_gap, profile_gap)
timestamp_count += times.size()
interruption_reloads += profile_interruptions
expect(profile_defeats == (1 if profile_name == "adversarial" else 0), "%s has the declared defeat count" % profile_name)
print("P97_PROFILE_RESULT profile=%s routes=%s gear=%s speed=%dx timestamps=%s final_coin=%d settlements=%d max_gap=%.2f" % [profile_name, ",".join(profile.routes), String(profile.gear) if not String(profile.gear).is_empty() else "none", int(profile.speed), format_times(labels, times), int(snapshot.coin), snapshot.battle_settlement_receipts.size(), profile_gap])
func buy_profile_gear(market, profile_name: String, profile: Dictionary, labels: Array[String], times: Array[float]) -> void:
var catalog_id := String(profile.gear)
var offer_id := "P97-M5-%s" % catalog_id
append_time(labels, times, "m5_focus_%s" % catalog_id, 0.4)
expect(market.focus_equipment_offer(offer_id).ok, "%s focuses %s" % [profile_name, catalog_id])
append_time(labels, times, "m5_buy_%s" % catalog_id, 0.4)
var purchase: Dictionary = market.buy_equipment("%s-buy-%s" % [profile_name, catalog_id], offer_id)
expect(purchase.ok, "%s buys %s" % [profile_name, catalog_id])
if not purchase.ok:
return
append_time(labels, times, "m5_equip_%s" % catalog_id, 0.5)
expect(market.equip("%s-equip-%s" % [profile_name, catalog_id], String(purchase.receipt.instance_id), String(profile.wearer)).ok, "%s equips %s" % [profile_name, catalog_id])
func interrupt_and_reload(market, profile_name: String, market_index: int, route_id: String, labels: Array[String], times: Array[float]):
var saves = SaveService.new()
saves.save_path = save_root.path_join("%s.json" % profile_name)
saves.clear()
expect(saves.save_snapshot(market.state.snapshot(), 2000).ok, "%s saves committed Encounter" % profile_name)
append_time(labels, times, "m%d_interrupted" % market_index, 2.5)
var loaded: Dictionary = saves.load_snapshot(2015)
expect(loaded.ok and loaded.battle_interrupted and loaded.offline_coin == 0 and loaded.offline_xp == 0 and loaded.offline_battle_ticks == 0, "%s interruption restores pre-battle Market without offline progress" % profile_name)
var restored = MarketService.new()
expect(restored.state.restore_snapshot(loaded.snapshot), "%s restores interruption checkpoint" % profile_name)
expect(int(restored.state.current_market) == market_index and restored.state.phase == MarketState.Phase.MARKET, "%s returns to the same Market decision" % profile_name)
append_time(labels, times, "m%d_resume_route" % market_index, 0.8)
expect(restored.focus_route(route_id).ok and restored.commit_route("%s-resume-route-%d" % [profile_name, market_index], route_id).ok, "%s recommits route after reload" % profile_name)
return restored
func run_battle(market, speed: int) -> Dictionary:
var battle = BattleService.new()
expect(battle.start_from_market(market.state).ok, "%s battle starts" % market.state.selected_route)
var guard := 0
while not battle.state.terminal and guard < 2400:
battle.advance_ticks(1)
guard += 1
expect(guard < 2400 and battle.state.outcome in ["victory", "defeat"], "%s battle terminates" % market.state.selected_route)
return {"outcome": battle.state.outcome, "ticks": guard, "speed": speed, "snapshot": battle.state.snapshot()}
func complete_p96() -> Dictionary:
var service = LegacyService.new()
service.enter_hall()
service.focus_offer("Farstring")
service.hire("Farstring")
complete_legacy_battle(service, "open_road")
service.focus_offer("Kindhand")
service.hire("Kindhand")
complete_legacy_battle(service, "crosswind")
service.focus_equipment("EQ-002")
service.equip_focused("Hearthguard")
complete_legacy_battle(service, "bell_breach")
service.choose_rift_tide_plan("press_on")
complete_legacy_battle(service, "rift_tide")
return service.state.snapshot()
func complete_legacy_battle(service, encounter_id: String) -> void:
service.select_encounter(encounter_id)
service.start_battle()
var guard := 0
while service.state.phase == LegacyState.Phase.BATTLE and guard < 4000:
service.advance_battle_ticks(1)
guard += 1
service.claim_and_return()
func append_time(labels: Array[String], times: Array[float], label: String, delta: float) -> void:
labels.append(label)
times.append(times[-1] + delta)
func max_gap(times: Array[float]) -> float:
var result := 0.0
for index in range(1, times.size()):
result = maxf(result, times[index] - times[index - 1])
return result
func format_times(labels: Array[String], times: Array[float]) -> String:
var values: Array[String] = []
for index in range(times.size()):
values.append("%s@%.2f" % [labels[index], times[index]])
return ",".join(values)
func expect(condition: bool, message: String) -> void:
if not condition:
failures.append("P9.7 five profile: %s" % message)
@@ -0,0 +1 @@
uid://d9p97fiveprofiles