Files
aetherbound-guild/runtime/tests/dev5/battle_live_field_test.gd
T

301 lines
14 KiB
GDScript

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_battle_live_field_draft.json"
const COMPANY_PATH := "user://dev5_battle_live_field_company.json"
const SEED := "02468ace13579bdf02468ace13579bdf"
const CONTRACT_OP := "abcdef0123456789abcdef0123456789"
const HIRE_OP_1 := "11111111222222223333333344444444"
const HIRE_OP_2 := "55555555666666667777777788888888"
const ORDER_OP := "99999999aaaaaaaabbbbbbbbcccccccc"
const READY_OP := "ddddddddeeeeeeeeffffffff00000000"
const BATTLE_OP := "1234567890abcdef1234567890abcdef"
var failures: Array[String] = []
var parsed_touches := 0
var capture_count := 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)
var prepared := _create_prepared_company()
if not prepared.is_empty():
await _verify_live_field(prepared)
_cleanup()
if failures.is_empty():
print("ABG_DEV5_BATTLE_LIVE_FIELD_OK handoff=direct no_immediate_commit=true receipt=one contained_16_9=true actors=4 enemies=3 field=layered card_wall=false waiting_copy=exact waiting_density=sparse event_projection=center actor_rail=4 controls=timeline_bound settings_return=true viewports=760x360,844x390,1280x720 locales=zh_CN,en text_100=true targets=48 parsed_touch=%d captures=%d" % [parsed_touches, capture_count])
quit(0)
else:
for failure in failures:
push_error(failure)
quit(1)
func _verify_live_field(company: Dictionary) -> void:
var option_id := String(company.active_contract.formation.risk_options[1].risk_option_id)
var started: Dictionary = _battle_service().begin_battle(BATTLE_OP, option_id)
_expect(started.ok, "fixture starts one battle")
if not started.ok:
return
var settings := SettingsStore.new().defaults()
settings.setup_completed = true
settings.locale = "en"
settings.text_scale = 1.0
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_opening(started.company, false)
await _frames(3)
var start := shell.find_child("BattleStartObservation", true, false) as Button
await _touch(start)
_expect(shell.current_screen == "battle" and not shell.battle_opening_mode, "Start Observation enters BAT-002 directly")
_expect(shell.battle_live_state == "waiting_for_next_event", "BAT-002 opens in the waiting state")
var persisted := _battle_snapshot()
_expect(int(persisted.get("phase_index", -1)) == 0 and int(persisted.get("receipt_count", 0)) == 1, "handoff advances no tick and creates no receipt")
_expect(bool(persisted.get("has_observation", false)), "handoff persists the receipt-bound observation")
shell.toggle_battle_pause()
await _frames(24)
var still_waiting := _battle_snapshot()
_expect(int(still_waiting.get("phase_index", -1)) == 0 and shell.battle_playback_timer == null and shell.battle_paused, "pausing before the first forecast commits no hidden event")
var fixtures := [
{"size": Vector2i(760, 360), "locale": "zh_CN"},
{"size": Vector2i(844, 390), "locale": "en"},
{"size": Vector2i(1280, 720), "locale": "zh_CN"},
{"size": Vector2i(1280, 720), "locale": "en"},
]
for fixture in fixtures:
await _verify_render_fixture(fixture.size, fixture.locale)
shell.settings.locale = "en"
shell.show_battle(shell.guild_company, false)
await _frames(4)
await _touch(shell.find_child("BattleLiveSettings", true, false) as Button)
_expect(shell.current_screen == "settings", "Settings opens from BAT-002")
shell.request_settings_back()
await _frames(4)
_expect(shell.current_screen == "battle" and shell.find_child("BattleLiveCanvas", true, false) != null, "Settings returns to the same BAT-002 observation")
var after_settings := _battle_snapshot()
_expect(int(after_settings.get("phase_index", -1)) == 0 and int(after_settings.get("receipt_count", 0)) == 1, "Settings return changes no tick or receipt")
shell.queue_free()
await _frames(20)
func _verify_render_fixture(viewport_size: Vector2i, locale: String) -> void:
var loaded: Dictionary = _battle_service().load_company()
_expect(loaded.ok, "%s %s loads the persisted observation" % [viewport_size, locale])
if not loaded.ok:
return
var viewport := SubViewport.new()
viewport.name = "BattleLiveFixtureViewport"
viewport.size = viewport_size
viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
root.add_child(viewport)
var shell := EntryShell.new()
shell.auto_start = false
shell.initial_company_service.path = DRAFT_PATH
shell.initial_company_service.company_path = COMPANY_PATH
viewport.add_child(shell)
await _frames(3)
var settings := SettingsStore.new().defaults()
settings.setup_completed = true
settings.locale = locale
settings.text_scale = 1.0
shell.settings = settings
shell.setup_draft = settings.duplicate(true)
shell.show_battle(loaded.company, false)
await _frames(5)
_verify_fixture(shell, viewport_size, locale)
await _capture_fixture(viewport, viewport_size, locale)
shell.queue_free()
viewport.queue_free()
await _frames(8)
func _capture_fixture(viewport: SubViewport, viewport_size: Vector2i, locale: String) -> void:
var evidence_root := OS.get_environment("ABG_EVIDENCE_DIR")
if evidence_root.is_empty() or DisplayServer.get_name() == "headless":
return
var capture_root := evidence_root.path_join("captures")
if DirAccess.make_dir_recursive_absolute(capture_root) != OK:
failures.append("could not create capture root: %s" % capture_root)
return
await RenderingServer.frame_post_draw
var image := viewport.get_texture().get_image()
var path := capture_root.path_join("battle-live-%dx%d-%s-100.png" % [viewport_size.x, viewport_size.y, locale])
if image.is_empty() or image.get_width() != viewport_size.x or image.get_height() != viewport_size.y:
failures.append("invalid capture image for %s %s" % [viewport_size, locale])
return
if image.save_png(path) != OK:
failures.append("capture failed: %s" % path)
return
capture_count += 1
func _verify_fixture(shell: Control, viewport_size: Vector2i, locale: String) -> void:
var canvas := shell.find_child("BattleLiveCanvas", true, false) as Control
_expect(canvas != null, "%s %s has the contained live canvas" % [viewport_size, locale])
if canvas == null:
return
var expected_width := minf(float(viewport_size.x), float(viewport_size.y) * 16.0 / 9.0)
var expected_height := minf(float(viewport_size.y), float(viewport_size.x) * 9.0 / 16.0)
_expect(absf(canvas.size.x - expected_width) <= 2.0 and absf(canvas.size.y - expected_height) <= 2.0, "%s %s preserves contained 16:9 geometry" % [viewport_size, locale])
_expect(shell.find_child("BattleLiveArt", true, false) != null, "%s %s has the engine-drawn field rail layer" % [viewport_size, locale])
_expect(shell.find_child("BattleLayout", true, false) == null and shell.find_child("BattleBody", true, false) == null, "%s %s does not use the historical card/container battle" % [viewport_size, locale])
var actors := shell.find_child("BattleActors", true, false) as Control
var enemies := shell.find_child("BattleEnemies", true, false) as Control
_expect(actors != null and actors.get_child_count() == 4, "%s %s renders the complete Party line" % [viewport_size, locale])
_expect(enemies != null and enemies.get_child_count() == 3, "%s %s renders the complete committed enemy line" % [viewport_size, locale])
var waiting := shell.find_child("BattleWaitingForNextEvent", true, false) as Label
_expect(waiting != null and not waiting.text.is_empty(), "%s %s keeps a nonblank reserved event slot" % [viewport_size, locale])
if locale == "en" and waiting != null:
_expect(waiting.text == "Waiting for next event", "%s English waiting copy is exact" % viewport_size)
if waiting != null:
var waiting_rect := waiting.get_rect()
_expect(waiting_rect.position.x >= canvas.size.x * 0.35 and waiting_rect.end.x <= canvas.size.x * 0.65, "%s %s keeps the waiting state in the central event projection" % [viewport_size, locale])
if viewport_size.y >= 700:
_expect(waiting.get_theme_font_size("font_size") >= 18, "%s %s scales live text with the authored canvas" % [viewport_size, locale])
var event_detail := shell.find_child("BattleLiveEventDetail", true, false) as Label
_expect(event_detail != null and not event_detail.visible, "%s %s suppresses explanatory copy while no event exists" % [viewport_size, locale])
_expect(shell.find_child("BattleLiveOrientation", true, false) == null and shell.find_child("BattleLiveEnemyDirection", true, false) == null, "%s %s avoids redundant field instructions" % [viewport_size, locale])
for index in range(4):
_expect(shell.find_child("BattleLiveStateCell%d" % index, true, false) != null, "%s %s keeps actor-state cell %d" % [viewport_size, locale, index])
for node_name in ["BattleLiveObjective", "BattleLiveNoRespawn", "BattleLiveNoReserve", "BattleLiveSnapshotHash", "BattleLiveRecentEvents"]:
_expect(shell.find_child(node_name, true, false) != null, "%s %s binds %s" % [viewport_size, locale, node_name])
var pause := shell.find_child("BattleLivePause", true, false) as Button
var speed := shell.find_child("BattleLiveSpeed", true, false) as Button
var inspect := shell.find_child("BattleLiveInspect", true, false) as Button
var next := shell.find_child("BattleLiveNextBoundary", true, false) as Button
_expect(pause != null and not pause.disabled and speed != null and not speed.disabled and inspect != null and not inspect.disabled and next != null and not next.disabled, "%s %s binds the active timeline and diagnostic entries" % [viewport_size, locale])
_verify_buttons(shell, viewport_size)
func _verify_buttons(node: Node, viewport_size: Vector2i) -> void:
for child in node.get_children():
if child is BaseButton and child.is_visible_in_tree():
var rect := (child as Control).get_global_rect()
_expect(rect.size.x >= 47.9 and rect.size.y >= 47.9, "button %s is at least 48px at %s" % [child.name, viewport_size])
_expect(rect.position.x >= -1.0 and rect.position.y >= -1.0 and rect.end.x <= viewport_size.x + 1.0 and rect.end.y <= viewport_size.y + 1.0, "button %s stays inside %s" % [child.name, viewport_size])
_verify_buttons(child, viewport_size)
func _battle_snapshot() -> Dictionary:
var loaded: Dictionary = _battle_service().load_company()
if not loaded.ok:
return {}
var battle: Dictionary = loaded.company.active_contract.get("battle_state", {})
return {
"phase_index": int(battle.get("phase_index", -1)),
"receipt_count": loaded.company.active_contract.get("battle_receipts", []).size(),
"has_observation": battle.has("observation") and not Dictionary(battle.get("observation", {})).is_empty(),
}
func _create_prepared_company() -> Dictionary:
var draft: Dictionary = _draft_service().begin_or_load(SEED)
if not draft.ok:
_expect(false, "fixture creates opening draft")
return {}
_draft_service().set_selection([String(draft.draft.offers[0].offer_id), String(draft.draft.offers[2].offer_id)])
if not _draft_service().commit_company().ok:
_expect(false, "fixture commits Company")
return {}
var created: Dictionary = _contract_service().create_contract(CONTRACT_OP, "CTR-R1-01", "standard")
if not created.ok:
_expect(false, "fixture creates Contract")
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:
_expect(false, "fixture hires four-member line")
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))
return ready.company if ready.ok else {}
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 _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 _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("BAT2-4B live field: %s" % message)