127 lines
6.9 KiB
GDScript
127 lines
6.9 KiB
GDScript
extends SceneTree
|
|
|
|
const Controller = preload("res://entry/battle_opening_page_controller.gd")
|
|
|
|
var failures: Array[String] = []
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("run_test")
|
|
|
|
|
|
func run_test() -> void:
|
|
var controller := Controller.new()
|
|
var company := _fixture_company()
|
|
var battle := _fixture_battle()
|
|
var receipt := {"operation_id": "battle-op-001", "receipt_hash": "receipt-hash-001", "snapshot_hash": "snapshot-hash-001"}
|
|
var ready: Dictionary = controller.snapshot(company, battle, "ready_to_observe", "", {}, receipt, "en")
|
|
expect(ready.ok and ready.page_id == "battle_opening", "snapshot identifies Battle Opening")
|
|
expect(ready.company.owned_count == 4 and ready.company.line_capacity == 4, "snapshot copies Company capacity")
|
|
expect(ready.battle.operation_id == "battle-op-001" and ready.battle.phase_index == 0 and ready.battle.total_phases == 9, "snapshot copies phase-zero battle identity")
|
|
expect(ready.receipt.present and ready.receipt.battle.receipt_hash == "receipt-hash-001", "snapshot copies original battle receipt")
|
|
expect(ready.party.front_to_rear_ids == ["r-1", "r-2", "r-3", "r-4"] and ready.party.ordered_recruits[0].recruit.recruit_id == "r-1", "snapshot binds frozen Party order")
|
|
expect(ready.party.ordered_recruits[0].relation.first_threat_target and ready.party.first_threat.target_recruit_id == "r-1", "snapshot binds threat facts")
|
|
expect(ready.enemies.count == 3 and ready.enemies.ids == ["enemy-1", "enemy-2", "enemy-3"], "snapshot copies enemy line")
|
|
expect(ready.state.phase_zero and ready.state.name == "ready_to_observe" and ready.commands.start_observation_enabled, "ready state enables explicit handoff")
|
|
|
|
var interrupted: Dictionary = controller.snapshot(company, battle, "interrupted_resume", "resume notice", {}, receipt, "zh_CN")
|
|
expect(interrupted.ok and interrupted.state.interrupted_resume and interrupted.state.notice == "resume notice", "interrupted resume state is classified")
|
|
var handoff: Dictionary = controller.snapshot(company, battle, "opening_handoff", "handed off", {"handoff_hash": "handoff-001"}, receipt, "en")
|
|
expect(handoff.ok and handoff.state.handoff and not handoff.commands.start_observation_enabled and handoff.receipt.handoff.handoff_hash == "handoff-001", "handoff state disables duplicate start")
|
|
var mismatch: Dictionary = controller.snapshot(company, battle, "snapshot_mismatch", "mismatch", {}, receipt, "en")
|
|
expect(mismatch.ok and mismatch.state.snapshot_mismatch and not mismatch.commands.start_observation_enabled, "mismatch state blocks handoff")
|
|
|
|
var phase_one := _fixture_battle()
|
|
phase_one.phase_index = 1
|
|
var rejected_phase: Dictionary = controller.snapshot(company, phase_one, "ready_to_observe", "", {}, receipt)
|
|
expect(not rejected_phase.ok and rejected_phase.code == "OPENING_PHASE_NOT_ZERO", "non-zero phase is rejected")
|
|
var unknown_enemy := _fixture_battle()
|
|
unknown_enemy.front_to_rear_ids = ["r-1", "r-2", "r-3", "missing"]
|
|
var rejected_party: Dictionary = controller.snapshot(company, unknown_enemy, "ready_to_observe", "", {}, receipt)
|
|
expect(not rejected_party.ok and rejected_party.code == "OPENING_RECRUIT_UNKNOWN", "unknown Party member is rejected")
|
|
var no_enemy := _fixture_battle()
|
|
no_enemy.enemy_ids = []
|
|
var rejected_enemy: Dictionary = controller.snapshot(company, no_enemy, "ready_to_observe", "", {}, receipt)
|
|
expect(not rejected_enemy.ok and rejected_enemy.code == "OPENING_ENEMY_LINE_INVALID", "empty enemy line is rejected")
|
|
var missing: Dictionary = controller.snapshot({}, battle)
|
|
expect(not missing.ok and missing.code == "COMPANY_REQUIRED", "missing Company is rejected")
|
|
|
|
var copied_battle: Dictionary = ready.battle.copy
|
|
copied_battle.phase_events.append({"id": "changed"})
|
|
ready.party.relations[0].protected_by_recruit_ids.append("changed")
|
|
ready.battle.enemy_ids.append("changed")
|
|
expect(battle.phase_events.is_empty(), "battle facts are deep copied")
|
|
expect(company.active_contract.formation.relations[0].protected_by_recruit_ids.is_empty(), "Party relation facts are deep copied")
|
|
expect(battle.enemy_ids.size() == 3, "enemy facts are deep copied")
|
|
|
|
if failures.is_empty():
|
|
print("ABG_M25_BATTLE_OPENING_CONTROLLER_OK company=4 party=4 enemies=3 phase_zero=true states=ready,resume,handoff,mismatch receipt=copied commands=inspect,start copies=true")
|
|
quit(0)
|
|
else:
|
|
for failure in failures:
|
|
push_error(failure)
|
|
quit(1)
|
|
|
|
|
|
func _fixture_company() -> Dictionary:
|
|
return {
|
|
"company_id": "company-001",
|
|
"owned_recruits": [
|
|
{"recruit_id": "r-1", "profession_id": "PF-B01", "rendered_name_snapshot": {"display_name": "Ari"}},
|
|
{"recruit_id": "r-2", "profession_id": "PF-B05", "rendered_name_snapshot": {"display_name": "Bram"}},
|
|
{"recruit_id": "r-3", "profession_id": "PF-B07", "rendered_name_snapshot": {"display_name": "Cera"}},
|
|
{"recruit_id": "r-4", "profession_id": "PF-B03", "rendered_name_snapshot": {"display_name": "Dain"}},
|
|
],
|
|
"active_contract": {
|
|
"contract_id": "CTR-R1-01",
|
|
"run_id": "run-001",
|
|
"line_capacity": 4,
|
|
"contract_hash": "contract-hash-001",
|
|
"formation": {
|
|
"orientation": "rear_left_front_right",
|
|
"front_position": 0,
|
|
"first_threat": {"target_position": 0, "target_recruit_id": "r-1", "enemy_count": 3, "wave_count": 2},
|
|
"relations": [
|
|
{"position": 0, "recruit_id": "r-1", "first_threat_target": true, "protected_by_recruit_ids": []},
|
|
{"position": 1, "recruit_id": "r-2", "first_threat_target": false, "protected_by_recruit_ids": ["r-1"]},
|
|
{"position": 2, "recruit_id": "r-3", "first_threat_target": false, "protected_by_recruit_ids": ["r-1"]},
|
|
{"position": 3, "recruit_id": "r-4", "first_threat_target": false, "protected_by_recruit_ids": []},
|
|
],
|
|
"accepted_party_snapshot": {
|
|
"snapshot_id": "snapshot-001",
|
|
"snapshot_hash": "snapshot-hash-001",
|
|
"front_to_rear_ids": ["r-1", "r-2", "r-3", "r-4"],
|
|
"coverage": {"protection": 2, "long_reach": 2, "sustain": 1, "control": 1},
|
|
"relations": [
|
|
{"position": 0, "recruit_id": "r-1", "first_threat_target": true, "protected_by_recruit_ids": []},
|
|
{"position": 1, "recruit_id": "r-2", "first_threat_target": false, "protected_by_recruit_ids": ["r-1"]},
|
|
{"position": 2, "recruit_id": "r-3", "first_threat_target": false, "protected_by_recruit_ids": ["r-1"]},
|
|
{"position": 3, "recruit_id": "r-4", "first_threat_target": false, "protected_by_recruit_ids": []},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
func _fixture_battle() -> Dictionary:
|
|
return {
|
|
"status": "active",
|
|
"operation_id": "battle-op-001",
|
|
"battle_hash": "battle-hash-001",
|
|
"risk_option_id": "risk-2",
|
|
"risk": 1,
|
|
"snapshot_id": "snapshot-001",
|
|
"snapshot_hash": "snapshot-hash-001",
|
|
"front_to_rear_ids": ["r-1", "r-2", "r-3", "r-4"],
|
|
"enemy_ids": ["enemy-1", "enemy-2", "enemy-3"],
|
|
"phase_index": 0,
|
|
"total_phases": 9,
|
|
"phase_events": [],
|
|
}
|
|
|
|
|
|
func expect(condition: bool, message: String) -> void:
|
|
if not condition:
|
|
failures.append("M25 Battle Opening Controller: %s" % message)
|