extends SceneTree const EntryShell = preload("res://entry/entry_shell.gd") const SettingsStore = preload("res://entry/settings_store.gd") const DraftService = preload("res://entry/initial_company_draft_service.gd") const ContractService = preload("res://entry/guild_contract_service.gd") const FormationService = preload("res://entry/formation_risk_service.gd") const BattleService = preload("res://entry/battle_service.gd") const DRAFT_PATH := "user://dev5_result_draft.json" const COMPANY_PATH := "user://dev5_result_company.json" const SEED := "00112233445566778899aabbccddeeff" const CONTRACT_OP := "abcdefabcdefabcdefabcdefabcdefab" const HIRE_OP_1 := "11112222333344445555666677778888" const HIRE_OP_2 := "88887777666655554444333322221111" const ORDER_OP := "fedcbafedcbafedcbafedcbafedcbafe" const READY_OP := "1234567890abcdef1234567890abcdef" const BATTLE_OP := "0123456789abcdef0123456789abcdef" var failures: Array[String] = [] var parsed_touches := 0 func _initialize() -> void: call_deferred("run_test") func run_test() -> void: _cleanup() root.content_scale_mode = Window.CONTENT_SCALE_MODE_CANVAS_ITEMS root.content_scale_aspect = Window.CONTENT_SCALE_ASPECT_EXPAND root.size = Vector2i(844, 390) for locale in ["zh_CN", "en"]: _cleanup() var prepared := _create_prepared_company() if not prepared.is_empty(): var result_company := _settle_result() if not result_company.is_empty(): if locale == "zh_CN": verify_result_domain(result_company) await verify_result_ui(result_company, locale) _cleanup() if failures.is_empty(): print("ABG_DEV5_RESULT_OK result=true objective=true decisive_event=damage diagnosis_chain=true reward=27 coin=24,51 xp_total=48 xp_per_survivor=16 knockout_identity=true battle_local=true duplicate_settlement=true locales=zh_CN,en text_130=true") print("ABG_DEV5_RECOVERY_OK result=true objective=true decisive_event=damage diagnosis_chain=true reward=27 coin=24,51 xp_total=48 xp_per_survivor=16 knockout_identity=true battle_local=true continue=knockout_review,recovery,market restart=market duplicate_settlement=true recovery_receipt=one same_identity=true duplicate_recovery=true locales=zh_CN,en text_130=true parsed_touch=%d" % parsed_touches) quit(0) else: for failure in failures: push_error(failure) quit(1) func verify_result_domain(company: Dictionary) -> void: var battle: Dictionary = company.active_contract.battle_state var result: Dictionary = battle.result expect(battle.phase_events.size() == 9, "result keeps exactly nine authored timeline events") expect(String(result.outcome) == "victory" and int(result.reward_coin) == 27, "result reports victory and the pressured reward") expect(int(result.coin_before) == 24 and int(result.coin_after) == 51, "result carries the exact Coin before and after") expect(int(result.xp_award) == 48 and int(result.xp_per_recruit) == 16, "result carries total and per-survivor XP") var decisive := _event_by_hash(battle, String(result.decisive_event_hash)) expect(String(decisive.get("kind", "")) == "damage", "decisive event points to the first decisive damage") var knocked: Array = result.knocked_out_ids expect(knocked.size() == 1, "result carries one battle-local knockout") if knocked.size() == 1: var knocked_recruit := _recruit_by_id(company, String(knocked[0])) expect(int(knocked_recruit.get("xp", -1)) == 0 and int(knocked_recruit.get("level", -1)) == 1, "knocked-out Recruit receives no XP") for recruit in company.owned_recruits: if String(recruit.recruit_id) not in knocked: expect(int(recruit.xp) == 16 and int(recruit.level) == 1, "surviving Recruit receives 16 XP without an invented level-up") var retry: Dictionary = _battle_service().advance_battle(BATTLE_OP) expect(retry.ok and retry.code == "battle_already_settled" and int(retry.company.active_contract.coin) == 51 and retry.company.active_contract.battle_settlement_receipts.size() == 1, "duplicate result settlement keeps one reward receipt") func verify_result_ui(company: Dictionary, locale: String) -> void: var settings := SettingsStore.new().defaults() settings.setup_completed = true settings.locale = locale settings.text_scale = 1.3 var shell := EntryShell.new() shell.auto_start = false shell.initial_company_service.path = DRAFT_PATH shell.initial_company_service.company_path = COMPANY_PATH root.add_child(shell) await _frames(3) shell.settings = settings shell.setup_draft = settings.duplicate(true) shell.show_battle_result(company) await _frames(3) expect(shell.current_screen == "battle_result", "%s result screen opens from settled receipt" % locale) expect(shell.app_services.has_service(&"battle_result_page_controller") and shell.battle_result_view_data.ok, "%s Result View crosses the explicit Controller/AppServices boundary" % locale) expect(shell.battle_result_view_data.decisive_event.kind == "damage" and shell.battle_result_view_data.continuation.target == "battle_knockout_review", "%s Result View consumes controller decisive and Continue facts" % locale) expect(shell.find_child("BattleResultObjective", true, false) != null and shell.find_child("BattleResultDecisive", true, false) != null, "%s result names objective and first decisive event" % locale) expect(shell.find_child("BattleResultReward", true, false) != null and shell.find_child("BattleResultXP", true, false) != null, "%s result exposes Coin and XP totals" % locale) expect(shell.find_child("BattleResultKnockouts", true, false) != null and shell.find_child("BattleResultChain", true, false) != null, "%s result names knockout and causal chain" % locale) var diagnosis := shell.find_child("BattleResultDiagnosis", true, false) as Button var continue_button := shell.find_child("BattleResultContinue", true, false) as Button expect(diagnosis != null and continue_button != null and not continue_button.disabled, "%s result exposes usable diagnosis and Continue" % locale) await _touch(diagnosis) expect(shell.find_child("BattleResultDiagnosisDetail", true, false) != null, "%s diagnosis expands a reviewable causal detail" % locale) var continue_target := shell.find_child("BattleResultContinue", true, false) as Button await _touch(continue_target) expect(shell.current_screen == "battle_knockout_review" and shell.find_child("BattleKnockoutCurrentGap", true, false) != null, "%s Continue reaches knockout review with the current battle gap" % locale) expect(shell.app_services.has_service(&"knockout_review_page_controller") and shell.battle_knockout_view_data.ok, "%s Knockout Review crosses the explicit Controller/AppServices boundary" % locale) expect(shell.battle_knockout_view_data.knockouts.ids == [String(shell.guild_company.active_contract.battle_state.result.knocked_out_ids[0])] and shell.battle_knockout_view_data.acknowledgement.target == "battle_recovery", "%s Knockout Review consumes controller identity and acknowledgement facts" % locale) var acknowledge := shell.find_child("BattleKnockoutAcknowledge", true, false) as Button expect(acknowledge != null and not acknowledge.disabled, "%s knockout review exposes an acknowledge action" % locale) await _touch(acknowledge) expect(shell.current_screen == "battle_recovery" and shell.find_child("BattleRecoveryRule", true, false) != null, "%s acknowledgement reaches recovery commitment" % locale) var commit := shell.find_child("BattleRecoveryCommit", true, false) as Button expect(commit != null and not commit.disabled, "%s recovery exposes a real commit action" % locale) await _touch(commit) expect(shell.current_screen == "market" and shell.find_child("MarketLayout", true, false) != null, "%s recovery returns to next-stage Market" % locale) var recovered: Dictionary = _draft_service().load_company() expect(recovered.ok and recovered.company.active_contract.status == "market" and recovered.company.active_contract.recovery_receipts.size() == 1 and not recovered.company.active_contract.has("formation"), "%s recovery atomically clears stale formation and writes one receipt" % locale) if recovered.ok: expect(_recruit_ids(recovered.company) == _recruit_ids(company), "%s recovery keeps the same Recruit IDs" % locale) for recruit_id in _recruit_ids(company): var before_recruit := _recruit_by_id(company, recruit_id) var after_recruit := _recruit_by_id(recovered.company, recruit_id) expect(String(before_recruit.profession_id) == String(after_recruit.profession_id) and String(before_recruit.trait_id) == String(after_recruit.trait_id) and String(before_recruit.equipped_item_id) == String(after_recruit.equipped_item_id), "%s recovery preserves Profession, Trait and equipment for %s" % [locale, recruit_id]) var recovery_battle: Dictionary = recovered.company.active_contract.battle_state var recovery_operation := ("%s|recovery|%s" % [String(recovery_battle.operation_id), String(recovered.company.company_id)]).sha256_text().substr(0, 32) var duplicate_recovery: Dictionary = _battle_service().recover_next_stage(recovery_operation) expect(duplicate_recovery.ok and duplicate_recovery.code == "recovery_already_applied" and duplicate_recovery.company.active_contract.recovery_receipts.size() == 1, "%s duplicate recovery returns the original receipt" % locale) shell.queue_free() await _timer_seconds(0.2) var resumed := EntryShell.new() resumed.auto_start = false resumed.initial_company_service.path = DRAFT_PATH resumed.initial_company_service.company_path = COMPANY_PATH root.add_child(resumed) await _frames(2) resumed.settings = settings resumed.setup_draft = settings.duplicate(true) resumed._open_company_destination(recovered.company if recovered.ok else company) await _frames(3) expect(resumed.current_screen == "market" and resumed.find_child("MarketLayout", true, false) != null, "%s restart returns to the next-stage Market" % locale) resumed.queue_free() await _timer_seconds(0.2) func _settle_result() -> Dictionary: var company: Dictionary = _draft_service().load_company() if not company.ok: return {} var option_id := String(company.company.active_contract.formation.risk_options[1].risk_option_id) var started: Dictionary = _battle_service().begin_battle(BATTLE_OP, option_id) if not started.ok: expect(false, "result fixture starts battle") return {} var latest: Dictionary = started for _phase in range(9): latest = _battle_service().advance_battle(BATTLE_OP) if not latest.ok: expect(false, "result fixture settles every phase") return {} return latest.company func _create_prepared_company() -> Dictionary: var draft: Dictionary = _draft_service().begin_or_load(SEED) if not draft.ok: return {} _draft_service().set_selection([String(draft.draft.offers[0].offer_id), String(draft.draft.offers[1].offer_id)]) var committed: Dictionary = _draft_service().commit_company() if not committed.ok: return {} var created: Dictionary = _contract_service().create_contract(CONTRACT_OP, "CTR-R1-01", "standard") if not created.ok: return {} var offers: Array = created.company.active_contract.market_board.recruit_offers _contract_service().hire_recruit(HIRE_OP_1, String(offers[0].source_offer_id), 14) var hired: Dictionary = _contract_service().hire_recruit(HIRE_OP_2, String(offers[1].source_offer_id), 14) if not hired.ok: return {} var begun: Dictionary = _formation_service().begin_formation() var saved: Dictionary = _formation_service().save_order(ORDER_OP, begun.formation.base_front_to_rear_ids) var ready: Dictionary = _formation_service().accept_readiness(READY_OP, String(saved.receipt.order_hash)) if not ready.ok: return {} return ready.company func _draft_service() -> RefCounted: var service := DraftService.new() service.path = DRAFT_PATH service.company_path = COMPANY_PATH return service func _contract_service() -> RefCounted: var service := ContractService.new() service.company_path = COMPANY_PATH return service func _formation_service() -> RefCounted: var service := FormationService.new() service.company_path = COMPANY_PATH return service func _battle_service() -> RefCounted: var service := BattleService.new() service.company_path = COMPANY_PATH return service func _event_by_hash(battle: Dictionary, event_hash: String) -> Dictionary: for event in battle.get("phase_events", []): if String(event.get("event_hash", "")) == event_hash: return Dictionary(event) return {} func _recruit_by_id(company: Dictionary, recruit_id: String) -> Dictionary: for recruit in company.get("owned_recruits", []): if String(recruit.recruit_id) == recruit_id: return Dictionary(recruit) return {} func _recruit_ids(company: Dictionary) -> Array[String]: var ids: Array[String] = [] for recruit in company.get("owned_recruits", []): ids.append(String(recruit.recruit_id)) return ids func _touch(control: Control) -> void: if control == null: expect(false, "touch target exists") return var center := control.get_global_rect().get_center() var press := InputEventScreenTouch.new() press.position = center press.pressed = true press.index = 0 Input.parse_input_event(press) await _frames(2) var release := InputEventScreenTouch.new() release.position = center release.pressed = false release.index = 0 Input.parse_input_event(release) parsed_touches += 1 await _frames(5) func _frames(count: int) -> void: for _index in range(count): await process_frame func _timer_seconds(seconds: float) -> void: await create_timer(seconds).timeout func _cleanup() -> void: for path in [DRAFT_PATH, "%s.bak" % DRAFT_PATH, "%s.candidate" % DRAFT_PATH, "%s.bak.candidate" % DRAFT_PATH, COMPANY_PATH, "%s.bak" % COMPANY_PATH, "%s.candidate" % COMPANY_PATH, "%s.bak.candidate" % COMPANY_PATH, "%s.corrupt" % COMPANY_PATH]: if FileAccess.file_exists(path): DirAccess.remove_absolute(path) func expect(condition: bool, message: String) -> void: if not condition: failures.append("DEV-5 result: %s" % message)