feat: continue Windglass Market through three rounds
This commit is contained in:
+52
-4
@@ -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():
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user