Files
aetherbound-guild/runtime/tools/m67_result_recovery_visual_probe.gd
T

198 lines
7.6 KiB
GDScript

extends SceneTree
## M67 strict visual probe for the DEV-5R settlement pages: Battle Result,
## Recovery, Knockout Review and Reward Choice. Single authoritative size
## (owner policy), both shipped locales, static/dynamic/final views. Any ink
## violation fails the probe.
const Result = preload("res://entry/views/battle_result_page_view.gd")
const Recovery = preload("res://entry/views/recovery_page_view.gd")
const Knockout = preload("res://entry/views/knockout_review_page_view.gd")
const Reward = preload("res://entry/views/reward_choice_page_view.gd")
const InkCheck = preload("res://tools/ui_ink_check.gd")
const FIXTURES := [Vector2i(1280, 720)]
const LOCALES := ["zh_CN", "en"]
# Accessibility pass: 1.0 plus the shipped 130% text scale (M73).
const TEXT_SCALES := [1.0, 1.3]
var ink_scanned := 0
var probe_text_scale := 1.0
var ink_violations: Array = []
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var out := OS.get_environment("ABG_EVIDENCE_DIR")
if out.is_empty():
push_error("requires ABG_EVIDENCE_DIR")
quit(1)
return
DirAccess.make_dir_recursive_absolute(out)
var ink_self := InkCheck.self_test(root)
if ink_self.size() < 3:
push_error("M67 ink self-test expected at least 3 violations, got %d" % ink_self.size())
quit(1)
return
print("ABG_M67_INK_SELF_TEST_OK violations=%d" % ink_self.size())
ink_scanned = 0
ink_violations = []
var pages: Array = [
{"script": Result, "name": "result", "data": Callable(self, "_result_data")},
{"script": Recovery, "name": "recovery", "data": Callable(self, "_recovery_data")},
{"script": Knockout, "name": "knockout", "data": Callable(self, "_knockout_data")},
{"script": Reward, "name": "reward", "data": Callable(self, "_reward_data")},
]
for fixture in FIXTURES:
for locale in LOCALES:
for text_scale in TEXT_SCALES:
probe_text_scale = float(text_scale)
for page in pages:
await _capture(page, fixture, String(locale), out)
if ink_violations.is_empty():
print("ABG_M67_RESULT_RECOVERY_INK_OK scanned=%d violations=0" % ink_scanned)
else:
for violation in ink_violations:
push_error("M67 ink violation %s %s %s %s: %s" % [
violation.get("page", "?"), violation.get("locale", "?"),
violation.get("fixture", "?"), violation.get("kind", "?"),
violation.get("detail", ""),
])
print("ABG_M67_RESULT_RECOVERY_INK_FAIL scanned=%d violations=%d" % [ink_scanned, ink_violations.size()])
quit(1)
return
print("ABG_M67_RESULT_RECOVERY_VISUAL_PROBE_OK pages=result,recovery,knockout,reward locales=%d scans=%d views=static,dynamic,final" % [LOCALES.size(), ink_scanned])
quit(0)
func _capture(page: Dictionary, fixture: Vector2i, locale: String, out: String) -> void:
var page_name := String(page.get("name", "page"))
for mode in ["static", "dynamic", "final"]:
var viewport := SubViewport.new()
viewport.size = fixture
viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
root.add_child(viewport)
var view: Control = page.script.new()
view.name = "M67ProbeView"
view.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
view.configure_presentation({"safe_area": 24, "text_scale": probe_text_scale, "locale": locale})
viewport.add_child(view)
view.render(page.data.call(locale))
_apply_mode(view, page_name, mode)
for _i in range(3):
await process_frame
var violations := InkCheck.scan(view)
ink_scanned += 1
for violation in violations:
violation["page"] = page_name
violation["locale"] = locale
violation["fixture"] = "%dx%d" % [fixture.x, fixture.y]
violation["mode"] = mode
ink_violations.append(violation)
var image := viewport.get_texture().get_image()
if image != null:
image.save_png(out.path_join("%s-%dx%d-%s-%s.png" % [page_name, fixture.x, fixture.y, locale, mode]))
view.queue_free()
viewport.queue_free()
await process_frame
func _apply_mode(view: Control, page_name: String, mode: String) -> void:
# static-only: authored chrome with no runtime content.
# dynamic-only: runtime content on a neutral field.
var frame_names: Array = {
"result": ["BattleResultFrame"],
"recovery": ["BattleRecoveryFrame"],
"knockout": ["BattleKnockoutFrame"],
"reward": ["BattleRewardFrame"],
}.get(page_name, [])
if mode == "static":
for name in frame_names:
var frame := view.find_child(name, true, false) as Control
if frame != null:
frame.visible = false
elif mode == "dynamic":
for name in frame_names:
var frame := view.find_child(name, true, false) as Control
if frame != null:
frame.visible = false
var neutral := ColorRect.new()
neutral.name = "M67NeutralProbeBackground"
neutral.color = Color("#26353c")
neutral.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
neutral.mouse_filter = Control.MOUSE_FILTER_IGNORE
view.add_child(neutral)
view.move_child(neutral, 0)
func _result_data(locale: String) -> Dictionary:
return {
"ok": true, "locale": locale,
"result": {"outcome": "victory"},
"economy": {"coin_before": 24, "coin_after": 38, "reward_coin": 14, "xp_award": 20, "xp_per_recruit": 5},
"knockouts": {"ids": []},
"decisive_event": {"caption_id": "first_hit"},
"diagnosis": {"open": true},
"commands": {"diagnosis_enabled": true, "continue_enabled": true},
}
func _recovery_data(locale: String) -> Dictionary:
return {
"ok": true, "page_id": "recovery", "locale": locale,
"knockouts": {"identities": [{"recruit": {
"recruit_id": "r-1",
"rendered_name_snapshot": {locale: "阿里" if locale == "zh_CN" else "Ari"},
"profession_name": {locale: "侦察兵" if locale == "zh_CN" else "Scout"},
}}]},
"commands": {"back_enabled": true, "commit_enabled": false},
"notice": "",
}
func _knockout_data(locale: String) -> Dictionary:
return {
"ok": true, "page_id": "knockout_review", "locale": locale,
"presentation": {"equipment_names": {"EQ-002": "Open Line Cord"}},
"knockouts": {"identities": [{
"recruit_id": "r-1", "position": 0,
"recruit": {
"recruit_id": "r-1", "profession_id": "PF-B01",
"rendered_name_snapshot": {locale: "阿里" if locale == "zh_CN" else "Ari"},
"profession_name": {locale: "护心卫士" if locale == "zh_CN" else "Hearthguard"},
"trait_name": {locale: "稳固" if locale == "zh_CN" else "Steady"},
"trait_id": "TR-001", "level": 2, "xp": 16,
"starting_item_id": "EQ-001",
"starting_item_name": {locale: "橡木盾" if locale == "zh_CN" else "Oak Shield"},
"equipped_item_id": "EQ-002",
},
}]},
"commands": {"back_enabled": true, "acknowledge_enabled": true},
"notice": "",
}
func _reward_data(locale: String) -> Dictionary:
var option := {
"reward_id": "reward-1", "kind": "equipment", "catalog_id": "EQ-001",
"next_decision": "选择佩戴者" if locale == "zh_CN" else "Choose a wearer",
"eligible_target": "任一存活成员" if locale == "zh_CN" else "Any surviving Recruit",
"conflict": "无" if locale == "zh_CN" else "None",
"skip_outcome": "保留已结算金币" if locale == "zh_CN" else "Keep paid Coin",
}
return {
"ok": true, "page_id": "reward_choice", "locale": locale,
"presentation": {"equipment_names": {"EQ-001": "Open Line Cord"}},
"paid": {"battle_reward_coin": 27},
"reward": {"selected_reward_id": "reward-1", "selected_option": option, "options": [option, {
"reward_id": "reward-2", "kind": "coin", "amount": 8,
"next_decision": "储备金币" if locale == "zh_CN" else "Reserve Coin",
"eligible_target": "远征团" if locale == "zh_CN" else "Company",
"conflict": "无" if locale == "zh_CN" else "None",
"skip_outcome": "无奖励物" if locale == "zh_CN" else "No reward object",
}]},
"commands": {"back_enabled": true, "select_enabled": true, "claim_enabled": true, "skip_enabled": true},
"notice": "",
}