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

250 lines
11 KiB
GDScript

extends SceneTree
## M66 strict visual probe for the DEV-4R preparation pages: Party Line,
## Risk Board and Risk Commitment. Single authoritative size (owner policy),
## both shipped locales, static-only / dynamic-only / final composite views.
## Any ink violation fails the probe.
const PartyLine = preload("res://entry/views/party_line_page_view.gd")
const RiskBoard = preload("res://entry/views/risk_board_page_view.gd")
const RiskCommitment = preload("res://entry/views/risk_commitment_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("M66 ink self-test expected at least 3 violations, got %d" % ink_self.size())
quit(1)
return
print("ABG_M66_INK_SELF_TEST_OK violations=%d" % ink_self.size())
ink_scanned = 0
ink_violations = []
var pages: Array = [
{"script": PartyLine, "name": "party_line", "data": Callable(self, "_party_data")},
{"script": RiskBoard, "name": "risk_board", "data": Callable(self, "_risk_board_data")},
{"script": RiskCommitment, "name": "risk_commitment", "data": Callable(self, "_risk_commitment_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_M66_PARTY_RISK_INK_OK scanned=%d violations=0" % ink_scanned)
else:
for violation in ink_violations:
push_error("M66 ink violation %s %s %s %s: %s" % [
violation.get("page", "?"), violation.get("locale", "?"),
violation.get("fixture", "?"), violation.get("kind", "?"),
violation.get("detail", ""),
])
print("ABG_M66_PARTY_RISK_INK_FAIL scanned=%d violations=%d" % [ink_scanned, ink_violations.size()])
quit(1)
return
print("ABG_M66_PARTY_RISK_VISUAL_PROBE_OK pages=party_line,risk_board,risk_commitment 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 = "M66ProbeView"
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: the authored master with no runtime content.
# dynamic-only: runtime content on a neutral field (slot overflow, contrast,
# alignment defects are visible here). Product code carries no test switches.
var master_names: Array = {
"party_line": ["PartyMasterImage", "PartyMasterMatte"],
"risk_board": ["RiskBoardStaticMaster", "RiskMasterMatte"],
"risk_commitment": ["RiskCommitmentStaticMaster", "RiskMasterMatte"],
}.get(page_name, [])
var overlay_names: Array = {
"party_line": ["PartyMasterOverlay"],
"risk_board": ["RiskBoardOverlay", "RiskMasterOverlay"],
"risk_commitment": ["RiskCommitmentOverlay", "RiskMasterOverlay"],
}.get(page_name, [])
if mode == "static":
for name in overlay_names:
var overlay := view.find_child(name, true, false) as Control
if overlay != null:
overlay.visible = false
elif mode == "dynamic":
for name in master_names:
var master := view.find_child(name, true, false) as Control
if master != null:
master.visible = false
var neutral := ColorRect.new()
neutral.name = "M66NeutralProbeBackground"
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 _locale_of(locale: String) -> bool:
return locale == "zh_CN"
func _party_data(locale: String) -> Dictionary:
var is_zh := _locale_of(locale)
var names := ["阿里", "布兰", "塞拉", "戴恩"] if is_zh else ["Ari", "Bram", "Cera", "Dain"]
var ids := ["r-1", "r-2", "r-3", "r-4"]
var ordered: Array = []
for index in range(ids.size()):
ordered.append({
"position": index,
"recruit_id": ids[index],
"recruit": {"recruit_id": ids[index], "profession_id": "PF-B01", "rendered_name_snapshot": {locale: names[index]}},
"relation": {"position": index, "recruit_id": ids[index], "first_threat_target": index == 0, "expected_target_rank": index + 1, "protected_by_recruit_ids": ["r-1"] if index == 1 else [], "reach_id": "full_line" if index > 0 else "front_two"},
})
return {
"ok": true,
"page_id": "party_line",
"locale": locale,
"company": {"company_id": "company-001", "owned_count": 4, "line_capacity": 4},
"contract": {"contract_id": "CTR-R1-01", "difficulty": "stormbound", "line_capacity": 4},
"formation": {"status": "draft", "orientation": "rear_left_front_right", "front_position": 0, "line_capacity": 4, "base_front_to_rear_ids": ["r-4", "r-3", "r-2", "r-1"], "first_threat": {"target_position": 0, "target_recruit_id": "r-1", "enemy_count": 3, "wave_count": 2}},
"order": {"front_to_rear_ids": ids, "ordered_recruits": ordered, "selected_recruit_id": "r-2", "selection_reason": "selected"},
"preview": {"ok": true, "order_hash": "hash-001", "coverage": {"protection": 2, "long_reach": 2, "sustain": 1, "control": 1}, "relations": ordered.map(func(row): return row.relation)},
"commands": {"undo_enabled": true, "reset_enabled": true, "save_enabled": true, "back_enabled": true},
}
func _risk_board_data(locale: String) -> Dictionary:
return {
"ok": true,
"page_id": "risk_board",
"locale": locale,
"party": {
"snapshot_id": "snapshot-001",
"snapshot_hash": "snapshot-hash-001",
"front_to_rear_ids": ["r-1", "r-2", "r-3", "r-4"],
"ordered_recruits": [
{"position": 0, "recruit_id": "r-1", "recruit": _recruit("r-1", "PF-B01", "阿里", "Ari", locale)},
{"position": 1, "recruit_id": "r-2", "recruit": _recruit("r-2", "PF-B05", "布兰", "Bram", locale)},
{"position": 2, "recruit_id": "r-3", "recruit": _recruit("r-3", "PF-B07", "塞拉", "Cera", locale)},
{"position": 3, "recruit_id": "r-4", "recruit": _recruit("r-4", "PF-B03", "戴恩", "Dain", locale)},
],
"coverage": {"protection": 2, "long_reach": 2, "sustain": 1, "control": 1},
},
"options": {"items": [
_option("risk-1", 0, "steady", 2, 1, "disclosed", 0, "standard_supplies", "low"),
_option("risk-2", 1, "pressured", 3, 2, "one_intent_unresolved", 2, "enhanced_choice", "moderate"),
_option("risk-3", 2, "elite", 4, 3, "elite_response_unresolved", 4, "elite_cache", "high"),
], "ids": ["risk-1", "risk-2", "risk-3"], "count": 3},
"selection": {"selected_risk_id": "risk-2", "state": "selected", "auto_selected": false},
"commands": {"review_enabled": true, "back_enabled": true, "settings_enabled": true},
}
func _option(option_id: String, risk: int, tier: String, enemies: int, waves: int, uncertainty: String, cost: int, reward: String, recovery: String) -> Dictionary:
return {
"risk_option_id": option_id,
"risk": risk,
"tier_id": tier,
"known_threat": {"enemy_count": enemies, "wave_count": waves},
"uncertainty_id": uncertainty,
"entry_cost": cost,
"reward_class_id": reward,
"recovery_exposure_id": recovery,
}
func _risk_commitment_data(locale: String) -> Dictionary:
var facts: Array = []
for item in [
["威胁", "3 · 2 波", "!", Color("#a74337")],
["不确定", "一项意图未定", "?", Color("#5d3f91")],
["入场金币", "4 · 剩 34", "¢", Color("#a07816")],
["奖励", "27 · 强化", "◆", Color("#3d7449")],
["恢复", "中等", "!", Color("#a44a2e")],
["撤退", "下阶段", "↩", Color("#2c67a3")],
]:
facts.append({"label": item[0], "value": item[1], "text": item[1], "glyph": item[2], "color": item[3]})
return {
"ok": true,
"page_id": "risk_commitment",
"locale": locale,
"contract": {"contract_id": "CTR-R1-01", "coin": 38},
"risk": {"risk_option_id": "risk-2", "option": {
"risk_option_id": "risk-2", "tier_id": "pressured", "risk": 1,
"option_hash": "option-hash-001",
"known_threat": {"enemy_count": 3, "wave_count": 2},
"uncertainty_id": "one_intent_unresolved", "entry_cost": 4,
"reward_coin": 27, "reward_class_id": "enhanced_choice", "recovery_exposure_id": "moderate",
}},
"consequences": {"facts": facts, "count": facts.size()},
"party": {
"snapshot_id": "snapshot-001",
"snapshot_hash": "snapshot-hash-001",
"front_to_rear_ids": ["r-1", "r-2", "r-3", "r-4"],
"ordered_recruits": [
{"position": 0, "recruit": _recruit("r-1", "PF-B01", "阿里", "Ari", locale), "relation": {"exposure_id": "highest", "reach_id": "front_two"}},
{"position": 1, "recruit": _recruit("r-2", "PF-B05", "布兰", "Bram", locale), "relation": {"exposure_id": "high", "reach_id": "front_three"}},
{"position": 2, "recruit": _recruit("r-3", "PF-B07", "塞拉", "Cera", locale), "relation": {"exposure_id": "guarded", "reach_id": "full_line"}},
{"position": 3, "recruit": _recruit("r-4", "PF-B03", "戴恩", "Dain", locale), "relation": {"exposure_id": "rear", "reach_id": "limited"}},
],
"coverage": {"protection": 2, "long_reach": 2, "sustain": 1, "control": 1},
},
"dependency": {"ok": true, "code": "READY", "facts": {"ok": true, "code": "READY", "coin": 38, "cost": 4}},
"confirmation": {"acknowledged": false, "state": "required"},
"commands": {
"settings_enabled": true, "selected_details_enabled": true,
"consequences_review_enabled": true, "cancel_enabled": true,
"acknowledge_enabled": true, "begin_enabled": false,
},
"notice": "",
}
func _recruit(recruit_id: String, profession_id: String, zh_name: String, en_name: String, locale: String) -> Dictionary:
return {"recruit_id": recruit_id, "profession_id": profession_id, "rendered_name_snapshot": {locale: zh_name if _locale_of(locale) else en_name}}