210 lines
9.6 KiB
GDScript
210 lines
9.6 KiB
GDScript
extends SceneTree
|
|
|
|
const Adapter = preload("res://campaign/p100_adapter.gd")
|
|
const Controller = preload("res://campaign/fg2_graybox_controller.gd")
|
|
const Fixture = preload("res://campaign/fg2_fixture.gd")
|
|
|
|
const VIEWPORT := Vector2i(844, 390)
|
|
|
|
var failures: Array[String] = []
|
|
var experience
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("run_test")
|
|
|
|
|
|
func run_test() -> void:
|
|
root.size = VIEWPORT
|
|
root.content_scale_mode = Window.CONTENT_SCALE_MODE_CANVAS_ITEMS
|
|
root.content_scale_aspect = Window.CONTENT_SCALE_ASPECT_EXPAND
|
|
root.content_scale_size = VIEWPORT
|
|
await verify_full_presentation()
|
|
await verify_resource_variants()
|
|
await verify_recovery_presentation()
|
|
if is_instance_valid(experience):
|
|
experience.queue_free()
|
|
for _index in 45:
|
|
await process_frame
|
|
if failures.is_empty():
|
|
print("ABG_FG2_MOTION_RUNTIME_OK events=source-bound choreography=attack_then_fallen casualty_delay_max=0.12 scribe=entry+attack sprout=entry+attack aegis=guard damage=signed mitigation=derived fallen=cue+label result=transient recovery=receipt-bound hit_pause=0.055 shake=2 transients=8 labels=4 rest=true reduced_motion=true reduced_flashes=true low_power=true event_hash=exact fallback=0 direct=0")
|
|
quit(0)
|
|
else:
|
|
for failure in failures:
|
|
push_error(failure)
|
|
quit(1)
|
|
|
|
|
|
func verify_full_presentation() -> void:
|
|
await mount(false, false, false)
|
|
var expected = Adapter.new()
|
|
expect(bool(expected.configure(Fixture.canonical_encounter_state()).ok), "expected adapter configures")
|
|
var expected_terminal: Dictionary = expected.run_battle_to_terminal()
|
|
expect(bool(expected_terminal.get("ok", false)), "expected battle reaches terminal")
|
|
var expected_hash := String(expected_terminal.get("event_log_hash", ""))
|
|
|
|
var saw_scribe_entry := false
|
|
var saw_scribe_attack := false
|
|
var saw_sprout_entry := false
|
|
var saw_sprout_attack := false
|
|
var saw_guard := false
|
|
var saw_damage_value := false
|
|
var saw_fallen_cue := false
|
|
var saw_fallen_label := false
|
|
var last_fallen_target_id := ""
|
|
var last_events: Array = []
|
|
var guard := 0
|
|
while not experience.battle_result_visible and guard < 2200:
|
|
var result: Dictionary = experience.advance_battle_ticks(1)
|
|
last_events = result.get("events", [])
|
|
for event in last_events:
|
|
var kind := String(event.get("kind", ""))
|
|
var source_id := String(event.get("source_id", ""))
|
|
var target_id := String(event.get("target_id", ""))
|
|
var identity := String(experience.event_presenter.actor_identities.get(source_id, ""))
|
|
var snapshot: Dictionary = experience.event_presenter.snapshot()
|
|
if kind == "wave_arrived" and identity == "EN-R1-09":
|
|
saw_scribe_entry = cue_is(snapshot, source_id, "entry")
|
|
elif kind == "wave_arrived" and identity == "EN-R1-08":
|
|
saw_sprout_entry = cue_is(snapshot, source_id, "entry")
|
|
elif kind == "damage":
|
|
if identity == "EN-R1-09":
|
|
saw_scribe_attack = cue_is(snapshot, source_id, "attack")
|
|
elif identity == "EN-R1-08":
|
|
saw_sprout_attack = cue_is(snapshot, source_id, "attack")
|
|
var amount := int(event.get("detail", {}).get("effective", 0))
|
|
saw_damage_value = saw_damage_value or has_text(snapshot.get("transients", []), "-%d" % amount)
|
|
var raw := int(event.get("detail", {}).get("raw", 0))
|
|
if int(event.get("detail", {}).get("guard_before", 0)) > 0 and raw > amount:
|
|
saw_guard = cue_is(snapshot, target_id, "guard") and has_text(snapshot.get("transients", []), "%d BLOCKED" % (raw - amount))
|
|
elif kind == "fallen":
|
|
expect(cue_is(snapshot, target_id, "fallen") or queued_motion_is(snapshot, target_id, "fallen"), "fallen target keeps a source-bound current or queued cue after authority removes it")
|
|
saw_fallen_cue = saw_fallen_cue or cue_is(snapshot, target_id, "fallen") or queued_motion_is(snapshot, target_id, "fallen")
|
|
saw_fallen_label = saw_fallen_label or has_text(snapshot.get("transients", []), "FALLEN")
|
|
last_fallen_target_id = target_id
|
|
await process_frame
|
|
expect(experience.feedback_labels.size() <= 4, "native feedback labels stay capped")
|
|
guard += 1
|
|
expect(guard < 2200 and String(experience.view.battle.outcome) == "victory", "canonical battle reaches the real victory")
|
|
expect(saw_scribe_entry and saw_scribe_attack, "Crosswind Scribe shows entry and attack")
|
|
expect(saw_sprout_entry and saw_sprout_attack, "Glassroot Sprout shows entry and attack")
|
|
expect(saw_guard, "Aegis mitigation shows guard pose and exact blocked amount")
|
|
expect(saw_damage_value, "effective damage appears as an actor-local signed value")
|
|
expect(saw_fallen_cue and saw_fallen_label, "fallen identity keeps a bounded cue and explicit label")
|
|
expect(has_text(experience.event_presenter.snapshot().transients, "VICTORY"), "terminal outcome exposes a result transient")
|
|
experience.event_presenter.advance(0.13)
|
|
expect(cue_is(experience.event_presenter.snapshot(), last_fallen_target_id, "fallen"), "same-tick final attacker reaches fallen motion within 130 ms")
|
|
var runtime_hash := String(experience.view.battle.event_log_hash)
|
|
expect(runtime_hash == expected_hash, "presentation preserves the exact authoritative event hash")
|
|
var seen_before := int(experience.event_presenter.snapshot().seen_event_count)
|
|
experience.event_presenter.consume(last_events)
|
|
expect(int(experience.event_presenter.snapshot().seen_event_count) == seen_before, "runtime presenter ignores duplicate event delivery")
|
|
experience.event_presenter.advance(1.2)
|
|
var rested: Dictionary = experience.event_presenter.snapshot()
|
|
expect(rested.actor_cues.is_empty() and rested.transients.is_empty(), "runtime presentation returns to rest")
|
|
expect(experience.fallback_input_count == 0 and experience.direct_command_count == 0, "presentation creates no command fallback")
|
|
|
|
|
|
func verify_resource_variants() -> void:
|
|
await mount(true, true, true)
|
|
var damage_event: Dictionary = {}
|
|
var guard := 0
|
|
while damage_event.is_empty() and guard < 1200:
|
|
var result: Dictionary = experience.advance_battle_ticks(1)
|
|
for event in result.get("events", []):
|
|
if String(event.get("kind", "")) == "damage":
|
|
damage_event = event
|
|
break
|
|
guard += 1
|
|
var snapshot: Dictionary = experience.event_presenter.snapshot()
|
|
expect(not damage_event.is_empty(), "resource fixture reaches damage")
|
|
expect(float(snapshot.hit_pause) == 0.0 and Vector2i(snapshot.shake) == Vector2i.ZERO, "resource variants remove pause and shake")
|
|
for transient in snapshot.transients:
|
|
expect(not bool(transient.get("flash", false)), "resource variants remove flashes")
|
|
var effective := int(damage_event.get("detail", {}).get("effective", 0))
|
|
expect(has_text(snapshot.transients, "-%d" % effective), "resource variants preserve the damage fact")
|
|
expect(experience.reduced_motion and experience.reduced_flashes and experience.low_power, "all resource variants stay active")
|
|
|
|
|
|
func verify_recovery_presentation() -> void:
|
|
await mount_snapshot(Fixture.recovery_market_state(), false, false, false)
|
|
var company_before: int = experience.view.get("company", []).size()
|
|
await tap("FG2RecoveryButton")
|
|
var snapshot: Dictionary = experience.event_presenter.snapshot()
|
|
expect(experience.screen == Controller.SCREEN_MARKET and experience.view.company.size() == company_before + 1, "successful protected hire returns to the changed Market")
|
|
expect(int(snapshot.event_counts.get("recovery", 0)) == 1 and has_text(snapshot.transients, "RECOVERED"), "recovery cue is bound to the successful receipt")
|
|
expect(has_label_text("补员完成"), "zh_CN recovery feedback is visible at the new Recruit")
|
|
expect(experience.input_touch_count == 1 and experience.fallback_input_count == 0 and experience.direct_command_count == 0, "recovery uses one parsed touch and no fallback")
|
|
|
|
|
|
func mount(reduced: bool, flashes: bool, power: bool) -> void:
|
|
await mount_snapshot(Fixture.canonical_encounter_state(), reduced, flashes, power)
|
|
|
|
|
|
func mount_snapshot(snapshot_value: Dictionary, reduced: bool, flashes: bool, power: bool) -> void:
|
|
if is_instance_valid(experience):
|
|
experience.queue_free()
|
|
for _index in 12:
|
|
await process_frame
|
|
experience = Controller.new()
|
|
experience.battle_auto_advance = false
|
|
experience.reduced_motion = reduced
|
|
experience.reduced_flashes = flashes
|
|
experience.low_power = power
|
|
experience.size = Vector2(VIEWPORT)
|
|
experience.configure_entry(snapshot_value)
|
|
root.add_child(experience)
|
|
for _index in 8:
|
|
await process_frame
|
|
|
|
|
|
func tap(node_name: String) -> void:
|
|
var control := experience.find_child(node_name, true, false) as Control
|
|
expect(control != null, "%s exists" % node_name)
|
|
if control == null:
|
|
return
|
|
var point := root.get_final_transform() * control.get_global_rect().get_center()
|
|
var down := InputEventScreenTouch.new()
|
|
down.index = 0
|
|
down.position = point
|
|
down.pressed = true
|
|
Input.parse_input_event(down)
|
|
await process_frame
|
|
var up := InputEventScreenTouch.new()
|
|
up.index = 0
|
|
up.position = point
|
|
up.pressed = false
|
|
Input.parse_input_event(up)
|
|
for _index in 4:
|
|
await process_frame
|
|
|
|
|
|
func has_label_text(expected: String) -> bool:
|
|
for label in experience.feedback_labels.values():
|
|
if is_instance_valid(label) and String(label.text) == expected:
|
|
return true
|
|
return false
|
|
|
|
|
|
func cue_is(snapshot: Dictionary, actor_id: String, motion: String) -> bool:
|
|
return String(snapshot.get("actor_cues", {}).get(actor_id, {}).get("motion", "")) == motion
|
|
|
|
|
|
func queued_motion_is(snapshot: Dictionary, actor_id: String, motion: String) -> bool:
|
|
for cue in snapshot.get("pending_actor_cues", {}).get(actor_id, []):
|
|
if String(cue.get("motion", "")) == motion:
|
|
return true
|
|
return false
|
|
|
|
|
|
func has_text(transients: Array, expected: String) -> bool:
|
|
for transient in transients:
|
|
if String(transient.get("text", "")) == expected:
|
|
return true
|
|
return false
|
|
|
|
|
|
func expect(condition: bool, message: String) -> void:
|
|
if not condition:
|
|
failures.append("FG-2 motion runtime: %s" % message)
|