fix(m57-c): per-region readiness chrome and typography

Stop reusing one plaque for every region. The header is now a compartmented
band with dividers, icons and left-aligned values; the bottom is a carved dock
band with four independent command plates (icon above label, wider green
primary) instead of buttons inside a long frame; the warning stack is three
cards with severity icons; the summary is a parchment card with shape markers,
values and pip rows; and typography uses explicit tracking and embolden so
titles, values and captions read as different ranks. View contract names and
z-order are unchanged.
This commit is contained in:
2026-09-13 15:25:55 +08:00
parent dce3640af4
commit 5cf76a07b2
5 changed files with 461 additions and 136 deletions
+180 -92
View File
@@ -3,24 +3,27 @@ extends "res://presentation/page_view.gd"
## UI-only PTY-002 Readiness Review View.
##
## The authored page uses a normalized 16:9 canvas. The View owns the
## parchment, semantic overlays, PartyLineTrack layers, focus and explicit
## intents; all Company/Formation/Warning/Receipt facts come from the copied
## ReadinessPageController snapshot.
##
## M57: the shell uses the shared asset nine-patch material (paper, wood,
## teal command plates) instead of page-local flat rectangles, and the party is
## grounded on one baseline with a contact shadow and a name plate.
## M57: each region owns its own structure instead of one reused plaque.
## The header is a compartmented band, the bottom is a carved dock with four
## independent command plates, warnings are small cards and the summary is an
## icon/value/pip grid. Text and values stay engine-rendered.
const InitialCompanyArt = preload("res://entry/initial_company_art.gd")
const PartyLineTrack = preload("res://entry/party_line_track.gd")
const AssetChrome = preload("res://ui/shared/asset_chrome.gd")
const PageChrome = preload("res://ui/shared/page_chrome.gd")
const UIIcon = preload("res://ui/shared/ui_icon.gd")
const READINESS_STATIC_MASTER_PATH := "res://assets/backgrounds/guild_master_b_background.png"
const M55_READINESS_HEADER = preload("res://ui/m55/readiness_header.gd")
const M55_READINESS_SUMMARY = preload("res://ui/m55/readiness_formation_summary.gd")
const M55_READINESS_ACTION_DOCK = preload("res://ui/m55/readiness_action_dock.gd")
const M55_READINESS_SLOTS = preload("res://ui/m55/readiness_slots.gd")
const HEADER_BAND := Rect2(0.030, 0.018, 0.865, 0.115)
const HEADER_SLOT_COUNT := 5
const DOCK_BAND := Rect2(0.0, 0.838, 1.0, 0.162)
const WARNING_STACK := Rect2(0.700, 0.420, 0.265, 0.222)
const STATUS_CARD := Rect2(0.700, 0.672, 0.265, 0.156)
var design_canvas: Control
static var _shadow_texture_cache: Texture2D
@@ -81,10 +84,6 @@ func _build_overlay(data: Dictionary) -> void:
overlay.mouse_filter = Control.MOUSE_FILTER_PASS
design_canvas.add_child(overlay)
var top_rail := AssetChrome.rail("ReadinessTopRail", Rect2(0.03, 0.018, 0.94, 0.112))
top_rail.z_index = 10
overlay.add_child(top_rail)
var view_formation: Dictionary = Dictionary(data.get("formation", {}))
var view_order: Dictionary = Dictionary(data.get("order", {}))
var view_contract: Dictionary = Dictionary(data.get("contract", {}))
@@ -99,19 +98,16 @@ func _build_overlay(data: Dictionary) -> void:
var contract_name := String(view_contract.get("contract_id", view_contract.get("name", "")))
if contract_name.is_empty():
contract_name = localize("委托", "CONTRACT", data)
var header_values := [
{"name":"ReadinessTitle", "text":localize("出发准备", "DEPARTURE", data), "slot":M55_READINESS_SLOTS.header_value(0)},
{"name":"ReadinessSnapshot", "text":localize("已冻结", "FROZEN", data), "slot":M55_READINESS_SLOTS.header_value(1)},
{"name":"ReadinessContract", "text":contract_name, "slot":M55_READINESS_SLOTS.header_value(2)},
{"name":"ReadinessPressure", "text":_party_pressure_short(String(view_contract.get("difficulty", "standard")), data), "slot":M55_READINESS_SLOTS.header_value(3)},
{"name":"ReadinessCompany", "text":localize("编制 %d/%d", "LINE %d/%d", data) % [int(view_company.get("owned_count", 0)), int(view_company.get("line_capacity", 0))], "slot":M55_READINESS_SLOTS.header_value(4)}
]
var readiness_header := M55_READINESS_HEADER.new().configure(header_values)
readiness_header.name = "ReadinessHeader"
_readiness_place(readiness_header, Rect2(0.0, 0.034, 1.0, 0.080))
readiness_header.z_index = 40
overlay.add_child(readiness_header)
var settings_button := _readiness_action_button("ReadinessSettings", Rect2(0.895, 0.036, 0.066, 0.076), "settings", {}, localize("打开设置", "Open settings", data))
_build_header(overlay, [
{"name":"ReadinessTitle", "text":localize("出发准备", "DEPARTURE", data), "icon":"", "role":"title", "size":11, "tracking":1.6, "embolden":0.38, "align":"left"},
{"name":"ReadinessSnapshot", "text":localize("已冻结", "FROZEN", data), "icon":"pause", "role":"value", "size":9, "tracking":0.6, "embolden":0.0, "align":"left"},
{"name":"ReadinessContract", "text":contract_name, "icon":"inspect", "role":"value", "size":9, "tracking":0.6, "embolden":0.0, "align":"left"},
{"name":"ReadinessPressure", "text":_party_pressure_short(String(view_contract.get("difficulty", "standard")), data), "icon":"risk", "role":"value", "size":9, "tracking":0.6, "embolden":0.0, "align":"left"},
{"name":"ReadinessCompany", "text":localize("编制 %d/%d", "LINE %d/%d", data) % [int(view_company.get("owned_count", 0)), int(view_company.get("line_capacity", 0))], "icon":"company", "role":"value", "size":9, "tracking":0.6, "embolden":0.0, "align":"left"}
])
var settings_button := _readiness_action_button("ReadinessSettings", Rect2(0.903, 0.020, 0.062, 0.078), "settings", {}, localize("打开设置", "Open settings", data))
overlay.add_child(settings_button)
var cord_track := PartyLineTrack.new()
@@ -151,56 +147,35 @@ func _build_overlay(data: Dictionary) -> void:
var summary := M55_READINESS_SUMMARY.new().configure(
localize("首要目标:%s · 四人同时出战 · 无自动替补", "Primary exposure: %s · all four deploy · no automatic Reserve", data) % _recruit_name(front_recruit, data),
[
{"name":"ReadinessProtection", "text":localize("保护位 %d", "GUARD %d", data) % int(view_coverage.get("protection", 0)), "left":0.06, "right":0.22},
{"name":"ReadinessReach", "text":localize("远射位 %d", "RANGED %d", data) % int(view_coverage.get("long_reach", 0)), "left":0.27, "right":0.43},
{"name":"ReadinessPressureCoverage", "text":localize("控场位 %d", "CONTROL %d", data) % int(view_coverage.get("control", 0)), "left":0.47, "right":0.65},
{"name":"ReadinessReserve", "text":localize("预备位 0", "RESERVE 0", data), "left":0.69, "right":0.84}
{"name":"ReadinessProtection", "text":localize("保护", "GUARD", data), "count":int(view_coverage.get("protection", 0)), "left":0.09, "right":0.29},
{"name":"ReadinessReach", "text":localize("远射", "RANGED", data), "count":int(view_coverage.get("long_reach", 0)), "left":0.31, "right":0.51},
{"name":"ReadinessPressureCoverage", "text":localize("控场", "CONTROL", data), "count":int(view_coverage.get("control", 0)), "left":0.53, "right":0.73},
{"name":"ReadinessReserve", "text":localize("预备", "RESERVE", data), "count":0, "left":0.75, "right":0.95}
],
localize("出发摘要", "DEPARTURE SUMMARY", data)
)
summary.name = "ReadinessFormationSummary"
_readiness_place(summary, Rect2(0.04, 0.675, 0.64, 0.16))
_readiness_place(summary, Rect2(0.040, 0.672, 0.620, 0.156))
summary.z_index = 20
overlay.add_child(summary)
var warning_surface := AssetChrome.paper("ReadinessWarningSurface", Rect2(0.69, 0.150, 0.28, 0.200), 0.5, 70.0, 55.0)
warning_surface.z_index = 20
overlay.add_child(warning_surface)
var warning_button := _readiness_warning_button(warning_rows, data)
_readiness_place(warning_button, Rect2(0.69, 0.150, 0.28, 0.200))
warning_button.z_index = 45
overlay.add_child(warning_button)
var status_surface := AssetChrome.paper("ReadinessStatusSurface", Rect2(0.69, 0.365, 0.28, 0.185), 0.5, 70.0, 55.0)
status_surface.z_index = 20
overlay.add_child(status_surface)
var risk_hint := _readiness_label(
overlay,
"ReadinessRiskUnavailable",
Rect2(0.705, 0.378, 0.25, 0.062),
localize("接受准备后可比较风险", "ACCEPT PREPARATION TO COMPARE RISK", data),
8,
Color("#5e4a32") if not bool(command_data.get("risk_enabled", false)) else Color("#3f6b4f")
)
risk_hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
risk_hint.z_index = 21
var knockout := _readiness_label(overlay, "ReadinessKnockoutRule", Rect2(0.705, 0.442, 0.25, 0.062), localize("击倒成员下一关归队", "A KNOCKED-OUT MEMBER RETURNS NEXT STAGE", data), 7, Color("#2f2417"))
knockout.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
knockout.z_index = 21
if bool(Dictionary(data.get("receipt", {})).get("present", false)):
var receipt := _readiness_label(overlay, "ReadinessReceipt", Rect2(0.705, 0.500, 0.25, 0.042), localize("回执已保存", "RECEIPT CONFIRMED", data), 7, Color("#3f6b4f"))
receipt.z_index = 21
receipt.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
_build_status_card(overlay, command_data, data)
var action_dock := M55_READINESS_ACTION_DOCK.new().configure()
action_dock.name = "ReadinessActionDock"
_readiness_place(action_dock, Rect2(0.0, 0.0, 1.0, 1.0))
action_dock.z_index = 50
overlay.add_child(action_dock)
var bottom_rail := AssetChrome.rail("ReadinessBottomRail", Rect2(0.03, 0.842, 0.94, 0.132))
bottom_rail.z_index = 40
action_dock.add_child(bottom_rail)
var revise := _readiness_command_button("ReadinessRevise", localize("修改队列", "REVISE", data), "revise", M55_READINESS_SLOTS.action_hit(0), false, localize("返回队列并重新保存", "Return to the Party Line and save again", data), false)
var dock_band := PageChrome.new().configure("dock_band")
dock_band.name = "ReadinessDockBand"
_readiness_place(dock_band, DOCK_BAND)
dock_band.z_index = 40
action_dock.add_child(dock_band)
var revise := _readiness_command_button("ReadinessRevise", localize("修改队列", "REVISE", data), "revise", M55_READINESS_SLOTS.action_hit(0), false, localize("返回队列并重新调整顺序", "Return to the Party Line and adjust again", data), false)
var back := _readiness_command_button("ReadinessBack", localize("返回公会", "BACK", data), "back", M55_READINESS_SLOTS.action_hit(1), false, localize("返回公会,不改变已保存顺序", "Return to Guild without changing the saved order", data), false)
var accept := _readiness_command_button("ReadinessAccept", localize("接受准备", "ACCEPT", data), "accept", M55_READINESS_SLOTS.action_hit(2), not bool(command_data.get("accept_enabled", false)), localize("冻结四人顺序;不会选择风险或开始战斗", "Freeze the four-member order; no risk or battle starts", data), true)
var risk := _readiness_command_button("ReadinessRisk", localize("比较风险", "RISK", data), "risk", M55_READINESS_SLOTS.action_hit(3), not bool(command_data.get("risk_enabled", false)), localize("接受准备后比较三个风险选项", "Compare three risk options", data), false)
@@ -225,6 +200,89 @@ func _build_overlay(data: Dictionary) -> void:
initial_focus.call_deferred("grab_focus")
func _build_header(parent: Control, items: Array) -> void:
var count := items.size()
var divider_values: Array = []
for index in range(1, count):
divider_values.append(float(index) / float(count))
var band := PageChrome.new().configure("header_band", {"dividers": divider_values})
band.name = "ReadinessHeaderBand"
_readiness_place(band, HEADER_BAND)
band.z_index = 40
parent.add_child(band)
var slots_left := HEADER_BAND.position.x
var width := HEADER_BAND.size.x / float(count)
for index in range(count):
var item: Dictionary = Dictionary(items[index])
var x0 := slots_left + width * float(index)
var icon_kind := String(item.get("icon", ""))
var text_left := 0.06
if not icon_kind.is_empty():
var icon := UIIcon.new().configure(icon_kind, "light")
icon.name = String(item.get("name", "ReadinessHeader%d" % index)) + "Icon"
_readiness_place(icon, Rect2(x0 + width * 0.08, HEADER_BAND.position.y + HEADER_BAND.size.y * 0.30, width * 0.16, HEADER_BAND.size.y * 0.40))
icon.z_index = 41
parent.add_child(icon)
text_left = 0.30
var label := AssetChrome.styled_label(
String(item.get("text", "")),
_readiness_text_size(int(item.get("size", 9))),
PageChrome.INK,
float(item.get("tracking", 0.0)),
float(item.get("embolden", 0.0))
)
label.name = String(item.get("name", "ReadinessHeaderValue%d" % index))
label.anchor_left = x0 + width * text_left
label.anchor_right = x0 + width * 0.94
label.anchor_top = HEADER_BAND.position.y + HEADER_BAND.size.y * 0.08
label.anchor_bottom = HEADER_BAND.position.y + HEADER_BAND.size.y * 0.92
label.offset_left = 0.0
label.offset_top = 0.0
label.offset_right = 0.0
label.offset_bottom = 0.0
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.clip_text = true
label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
label.z_index = 41
parent.add_child(label)
func _build_status_card(parent: Control, command_data: Dictionary, data: Dictionary) -> void:
var card := PageChrome.new().configure("card")
card.name = "ReadinessStatusCard"
_readiness_place(card, STATUS_CARD)
card.z_index = 20
parent.add_child(card)
var rows := [
{"name":"ReadinessRiskState", "text":localize("接受准备后可比较风险", "ACCEPT PREPARATION TO COMPARE RISK", data), "color":Color("#3f6b4f") if bool(command_data.get("risk_enabled", false)) else PageChrome.INK_SOFT},
{"name":"ReadinessKnockoutRule", "text":localize("击倒成员下一关归队", "A KNOCKED-OUT MEMBER RETURNS NEXT STAGE", data), "color":PageChrome.INK}
]
for index in range(rows.size()):
var item: Dictionary = Dictionary(rows[index])
var top := STATUS_CARD.position.y + STATUS_CARD.size.y * (0.16 + float(index) * 0.46)
var marker := PageChrome.new().configure("pip", {"shape": "diamond"})
marker.name = String(item.get("name", "")) + "Marker"
_readiness_place(marker, Rect2(STATUS_CARD.position.x + STATUS_CARD.size.x * 0.05, top + STATUS_CARD.size.y * 0.10, STATUS_CARD.size.x * 0.055, STATUS_CARD.size.y * 0.22))
marker.z_index = 21
parent.add_child(marker)
var label := AssetChrome.styled_label(String(item.get("text", "")), _readiness_text_size(8), Color(item.get("color", PageChrome.INK)), 0.4, 0.0)
label.name = String(item.get("name", "ReadinessStatusRow%d" % index))
label.anchor_left = STATUS_CARD.position.x + STATUS_CARD.size.x * 0.14
label.anchor_right = STATUS_CARD.position.x + STATUS_CARD.size.x * 0.96
label.anchor_top = top
label.anchor_bottom = top + STATUS_CARD.size.y * 0.42
label.offset_left = 0.0
label.offset_top = 0.0
label.offset_right = 0.0
label.offset_bottom = 0.0
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
label.z_index = 21
parent.add_child(label)
func _render_invalid(data: Dictionary) -> void:
var overlay := Control.new()
overlay.name = "ReadinessInvalidOverlay"
@@ -258,12 +316,27 @@ func _add_member_name_plate(parent: Control, index: int, slot: Rect2, name_text:
var width := slot.size.x * 1.02
var cx := slot.position.x + slot.size.x * 0.5
var area := Rect2(cx - width * 0.5, slot.position.y + slot.size.y * 1.005, width, 0.052)
var plate := AssetChrome.market("ReadinessMemberPlate%d" % index, area, "compact_label")
var plate := PageChrome.new().configure("card")
plate.name = "ReadinessMemberPlate%d" % index
_readiness_place(plate, area)
plate.z_index = 31
parent.add_child(plate)
var label := _readiness_label(parent, "ReadinessMember%dIdentity" % index, Rect2(area.position.x, area.position.y, area.size.x, area.size.y), name_text, 8, Color("#2f2417"))
label.z_index = 32
var label := AssetChrome.styled_label(name_text, _readiness_text_size(8), PageChrome.INK, 0.4, 0.16)
label.name = "ReadinessMember%dIdentity" % index
label.anchor_left = area.position.x + 0.012
label.anchor_right = area.end.x - 0.012
label.anchor_top = area.position.y + area.size.y * 0.12
label.anchor_bottom = area.end.y - area.size.y * 0.12
label.offset_left = 0.0
label.offset_top = 0.0
label.offset_right = 0.0
label.offset_bottom = 0.0
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
label.clip_text = true
label.z_index = 32
parent.add_child(label)
func _ground_shadow_texture() -> Texture2D:
@@ -300,8 +373,6 @@ func _readiness_label(parent: Control, node_name: String, area: Rect2, text_valu
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.text_overrun_behavior = TextServer.OVERRUN_NO_TRIMMING
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
if node_name in ["ReadinessFrozenFacts", "ReadinessProtection", "ReadinessReach", "ReadinessPressureCoverage", "ReadinessReserve"]:
label.z_index = 21
_readiness_place(label, area)
parent.add_child(label)
return label
@@ -311,7 +382,7 @@ func _readiness_text_size(design_size: int) -> int:
var canvas_height := design_canvas.size.y if is_instance_valid(design_canvas) else size.y
var canvas_scale := clampf(canvas_height / 360.0, 1.0, 2.0)
var text_scale := float(presentation.get("text_scale", 1.0))
return mini(20, maxi(13, int(round(float(design_size) * canvas_scale * text_scale))))
return mini(30, maxi(12, int(round(float(design_size) * canvas_scale * text_scale))))
func _readiness_action_button(node_name: String, area: Rect2, intent_id: String, payload: Dictionary, tooltip: String) -> Button:
@@ -372,36 +443,52 @@ func _readiness_warning_button(warning_rows: Array, data: Dictionary) -> Button:
button.focus_mode = Control.FOCUS_ALL
button.tooltip_text = localize("查看允许的警告与归属", "Review allowed warnings and ownership", data)
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
## The warning parchment is an authored runtime surface; the control stays
## transparent and owns only input and the localized rows.
for style_name in ["normal", "hover", "pressed", "focus", "disabled", "hover_pressed"]:
button.add_theme_stylebox_override(style_name, StyleBoxEmpty.new())
_readiness_place(button, WARNING_STACK)
button.pressed.connect(func(): emit_intent("review_warnings"), CONNECT_DEFERRED)
var lines: Array[String] = []
var severities: Array[String] = []
for row in warning_rows:
if row is Dictionary:
lines.append(String(row.get("text", "")))
severities.append(String(row.get("severity", "notice")))
else:
lines.append(String(row))
severities.append("notice")
while lines.size() < 3:
lines.append("")
severities.append("notice")
if lines.size() > 3:
lines = lines.slice(0, 3)
severities = severities.slice(0, 3)
for index in range(3):
var row := Control.new()
row.name = "ReadinessWarningRow%d" % index
row.anchor_left = 0.08
row.anchor_right = 0.92
row.anchor_top = 0.20 + float(index) * 0.22
row.anchor_bottom = 0.40 + float(index) * 0.22
row.anchor_left = 0.0
row.anchor_right = 1.0
row.anchor_top = float(index) * 0.347
row.anchor_bottom = float(index) * 0.347 + 0.290
row.mouse_filter = Control.MOUSE_FILTER_IGNORE
button.add_child(row)
var card := PageChrome.new().configure("card")
card.name = "WarningCard"
card.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
row.add_child(card)
var icon := PageChrome.new().configure("warning_icon", {"severity": severities[index]})
icon.name = "WarningIcon"
icon.anchor_left = 0.03
icon.anchor_right = 0.20
icon.anchor_top = 0.14
icon.anchor_bottom = 0.86
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
row.add_child(icon)
var warning_text := _readiness_warning_text(lines[index], data)
var warning_colors := [Color("#7a2f28"), Color("#6a4a1f"), Color("#2b5b60")]
var label := make_label("• " + warning_text if not warning_text.is_empty() else "", _readiness_text_size(8), warning_colors[index])
var label := AssetChrome.styled_label(warning_text, _readiness_text_size(8), PageChrome.INK, 0.4, 0.0)
label.name = "WarningLabel"
label.anchor_left = 0.02
label.anchor_right = 0.95
label.anchor_left = 0.24
label.anchor_right = 0.97
label.anchor_top = 0.0
label.anchor_bottom = 1.0
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
@@ -435,36 +522,37 @@ func _readiness_command_button(node_name: String, text_value: String, intent_id:
button.mouse_default_cursor_shape = Control.CURSOR_FORBIDDEN if disabled else Control.CURSOR_POINTING_HAND
button.set_meta("ui_factory_control", true)
button.set_meta("ui_semantic_variant", "primary" if primary else "secondary")
button.set_meta("ui_shape", "asset")
button.set_meta("ui_shape", "dock_plate")
button.set_meta("ui_family", "company")
for style_name in ["normal", "hover", "pressed", "focus", "disabled", "hover_pressed"]:
button.add_theme_stylebox_override(style_name, StyleBoxEmpty.new())
_readiness_place(button, area)
var plate := TextureRect.new()
var state := "disabled" if disabled else "primary" if primary else "secondary"
var plate := PageChrome.new().configure("command_plate", {"state": state})
plate.name = node_name + "Plate"
plate.texture = AssetChrome.scaled(AssetChrome.ACTION_PLATE, 0.5)
plate.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
plate.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
plate.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
plate.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
plate.mouse_filter = Control.MOUSE_FILTER_IGNORE
if disabled:
plate.modulate = Color(0.58, 0.58, 0.60, 0.72)
elif primary:
plate.modulate = Color(0.80, 1.16, 0.94, 1.0)
else:
plate.modulate = Color(0.86, 0.90, 1.0, 1.0)
button.add_child(plate)
var icon_kind := "party" if intent_id == "revise" else "back" if intent_id == "back" else "confirm" if intent_id == "accept" else "risk"
UIFactory.attach_icon(button, icon_kind, "CommandIcon", "light", Rect2(0.20, 0.20, 0.16, 0.60))
var label := make_label(text_value, _readiness_text_size(11 if primary else 10), Color("#f6ecd4"))
var icon := UIIcon.new().configure(icon_kind, "dark" if (primary or disabled) else "light")
icon.name = "CommandIcon"
icon.anchor_left = 0.34
icon.anchor_right = 0.66
icon.anchor_top = 0.14
icon.anchor_bottom = 0.54
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
button.add_child(icon)
var label_color := Color("#e8e3d5") if disabled else PageChrome.PAPER_LIGHT if primary else PageChrome.INK
var label := AssetChrome.styled_label(text_value, _readiness_text_size(10 if primary else 9), label_color, 0.6, 0.20)
label.name = "ButtonLabel"
label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
label.offset_left = 52.0
label.offset_right = -16.0
label.anchor_left = 0.06
label.anchor_right = 0.94
label.anchor_top = 0.52
label.anchor_bottom = 0.92
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
label.clip_text = true
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
button.add_child(label)
button.pressed.connect(func(): emit_intent(intent_id), CONNECT_DEFERRED)
+68 -33
View File
@@ -1,66 +1,101 @@
class_name ABGM55ReadinessFormationSummary
extends Control
const UIFactory = preload("res://ui/shared/ui_factory.gd")
## Departure summary: one parchment card with a purpose title, the exposure
## sentence and four aligned coverage columns. Each column is a shape marker,
## a value and a pip row, so the numbers read as gauges rather than prose.
const AssetChrome = preload("res://ui/shared/asset_chrome.gd")
const PageChrome = preload("res://ui/shared/page_chrome.gd")
const COLUMN_SHAPES := ["shield", "circle", "diamond", "circle"]
var facts := ""
var metrics: Array = []
var title := "准备摘要"
func configure(next_facts: String, next_metrics: Array, next_title := "准备摘要") -> ABGM55ReadinessFormationSummary:
facts = next_facts
metrics = next_metrics
title = next_title
return self
func _ready() -> void:
clip_contents = true
## The summary is the sole owner of its parchment surface; the shared asset
## nine-patch replaces the previous page-local flat rectangle.
var matte := AssetChrome.paper("ReadinessSummaryBand", Rect2(0.0, 0.0, 1.0, 1.0), 0.5, 70.0, 55.0)
add_child(matte)
move_child(matte, 0)
var title_label := UIFactory.label(title, "label", 1.0, Color("#6a4a1f"))
var card := PageChrome.new().configure("card")
card.name = "ReadinessSummaryBand"
card.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
add_child(card)
move_child(card, 0)
var title_label := AssetChrome.styled_label(title, 15, PageChrome.INK_SOFT, 1.2, 0.34)
title_label.name = "ReadinessSummaryTitle"
title_label.anchor_left = 0.05
title_label.anchor_right = 0.24
title_label.anchor_top = 0.22
title_label.anchor_bottom = 0.46
title_label.anchor_left = 0.03
title_label.anchor_right = 0.20
title_label.anchor_top = 0.10
title_label.anchor_bottom = 0.34
title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
title_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
title_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
title_label.clip_text = true
add_child(title_label)
var divider := ColorRect.new()
divider.name = "ReadinessSummaryDivider"
divider.anchor_left = 0.05
divider.anchor_right = 0.95
divider.anchor_top = 0.50
divider.anchor_bottom = 0.515
divider.color = Color("#9f8252", 0.48)
divider.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(divider)
var facts_label := UIFactory.label(facts, "body", 1.0, Color("#2b241b"))
var facts_label := AssetChrome.styled_label(facts, 17, PageChrome.INK, 0.3, 0.0)
facts_label.name = "ReadinessFrozenFacts"
facts_label.add_theme_font_size_override("font_size", 18)
facts_label.anchor_left = 0.24
facts_label.anchor_right = 0.96
facts_label.anchor_top = 0.16
facts_label.anchor_bottom = 0.50
facts_label.anchor_left = 0.21
facts_label.anchor_right = 0.97
facts_label.anchor_top = 0.08
facts_label.anchor_bottom = 0.34
facts_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
facts_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
facts_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
facts_label.clip_text = true
add_child(facts_label)
var divider := ColorRect.new()
divider.name = "ReadinessSummaryDivider"
divider.anchor_left = 0.03
divider.anchor_right = 0.97
divider.anchor_top = 0.365
divider.anchor_bottom = 0.378
divider.color = Color(PageChrome.GOLD, 0.55)
divider.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(divider)
for index in range(metrics.size()):
var item: Dictionary = Dictionary(metrics[index])
var label := UIFactory.label(String(item.get("text", "")), "label", 1.0, Color("#493a29"))
var left := float(item.get("left", 0.0))
var right := float(item.get("right", 0.2))
var column_width := right - left
var marker := PageChrome.new().configure("pip", {"shape": COLUMN_SHAPES[index % COLUMN_SHAPES.size()], "filled": true})
marker.name = String(item.get("name", "ReadinessMetric%d" % index)) + "Marker"
marker.anchor_left = left
marker.anchor_right = left + column_width * 0.16
marker.anchor_top = 0.44
marker.anchor_bottom = 0.64
marker.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(marker)
var label := AssetChrome.styled_label(String(item.get("text", "")), 16, PageChrome.INK, 0.6, 0.20)
label.name = String(item.get("name", "ReadinessMetric%d" % index))
label.anchor_left = float(item.get("left", 0.0))
label.anchor_top = 0.50
label.anchor_right = float(item.get("right", 0.2))
label.anchor_bottom = 0.78
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.anchor_left = left + column_width * 0.22
label.anchor_right = right
label.anchor_top = 0.42
label.anchor_bottom = 0.66
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.clip_text = true
label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
add_child(label)
var count := int(item.get("count", 0))
var pip_gap := column_width * 0.045
var pip_width := (column_width - pip_gap * 3.0) / 4.0
for pip_index in range(4):
var pip := PageChrome.new().configure("pip", {"shape": COLUMN_SHAPES[index % COLUMN_SHAPES.size()], "filled": pip_index < count})
pip.name = "ReadinessPip%d_%d" % [index, pip_index]
pip.anchor_left = left + float(pip_index) * (pip_width + pip_gap)
pip.anchor_right = pip.anchor_left + pip_width * 0.70
pip.anchor_top = 0.72
pip.anchor_bottom = 0.92
pip.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(pip)
+10 -11
View File
@@ -20,21 +20,20 @@ const HEADER_VALUE_SLOTS := [
Rect2(0.810, 0.036, 0.120, 0.078),
]
## Four equal runtime command slots on the bottom rail.
## Four independent command plates on the carved dock band. The primary
## (Accept) plate is wider than the three secondary plates.
const ACTION_HIT_SLOTS := [
Rect2(0.075, 0.855, 0.190, 0.100),
Rect2(0.285, 0.855, 0.190, 0.100),
Rect2(0.495, 0.855, 0.190, 0.100),
Rect2(0.705, 0.855, 0.190, 0.100),
Rect2(0.072, 0.852, 0.185, 0.120),
Rect2(0.274, 0.852, 0.185, 0.120),
Rect2(0.476, 0.852, 0.250, 0.120),
Rect2(0.743, 0.852, 0.185, 0.120),
]
## The cream plates are lower than the icon panels. Keep labels inside these
## slots so localization cannot collide with the icon or bottom matte.
const ACTION_LABEL_SLOTS := [
Rect2(0.075, 0.855, 0.190, 0.100),
Rect2(0.285, 0.855, 0.190, 0.100),
Rect2(0.495, 0.855, 0.190, 0.100),
Rect2(0.705, 0.855, 0.190, 0.100),
Rect2(0.072, 0.852, 0.185, 0.120),
Rect2(0.274, 0.852, 0.185, 0.120),
Rect2(0.476, 0.852, 0.250, 0.120),
Rect2(0.743, 0.852, 0.185, 0.120),
]
## Member portraits and the right warning card are runtime-owned content
+21
View File
@@ -96,6 +96,27 @@ static func place(control: Control, area: Rect2) -> void:
control.offset_bottom = 0.0
## Typography: one base family with explicit tracking and weight so titles,
## values and captions read as different ranks instead of one flat label size.
static func styled_font(tracking := 0.0, embolden := 0.0) -> FontVariation:
var variation := FontVariation.new()
variation.base_font = preload("res://assets/fonts/NotoSansSC.ttf")
variation.spacing_glyph = int(round(tracking))
variation.variation_embolden = embolden
return variation
static func styled_label(value: String, actual_size: int, color := INK, tracking := 0.0, embolden := 0.0) -> Label:
var label := Label.new()
label.text = value
label.add_theme_font_override("font", styled_font(tracking, embolden))
label.add_theme_font_size_override("font_size", actual_size)
label.add_theme_color_override("font_color", color)
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
return label
static func ink_label(value: String, color := INK, design_size := 14.0) -> Label:
var label := Label.new()
label.text = value
+182
View File
@@ -0,0 +1,182 @@
class_name ABGPageChrome
extends Control
## Per-region drawn chrome for first-play pages.
##
## One generic plaque was previously reused as the header, the command dock,
## the warning stack and the summary, which produced large empty frames with
## floating text. Each region now gets its own structure: a compartmented
## header band, a carved command dock with independent command plates, small
## warning cards and parchment cards. Everything scales cleanly at any size and
## text stays engine-rendered.
const PAPER := Color("#f0e0bd")
const PAPER_LIGHT := Color("#faf1d8")
const PAPER_SHADE := Color("#d9c096")
const WOOD := Color("#6f4a2e")
const WOOD_LIGHT := Color("#9a6b40")
const GOLD := Color("#c2a061")
const GOLD_LIGHT := Color("#eed9a4")
const INK := Color("#2f2417")
const INK_SOFT := Color("#6a5236")
const DOCK := Color("#1d3f4a")
const DOCK_LIGHT := Color("#2d5b67")
const DOCK_DARK := Color("#12272e")
const GREEN := Color("#57813f")
const GREEN_LIGHT := Color("#7aa85f")
const STONE := Color("#4a4f52")
const RED := Color("#a8503f")
const AMBER := Color("#b98a3c")
const TEAL := Color("#4f8a8f")
const SHADOW := Color(0.05, 0.06, 0.07, 0.34)
var variant := "card"
var params: Dictionary = {}
func configure(next_variant: String, next_params: Dictionary = {}) -> ABGPageChrome:
variant = next_variant
params = next_params
return self
func _ready() -> void:
mouse_filter = Control.MOUSE_FILTER_IGNORE
resized.connect(queue_redraw)
queue_redraw()
func _draw() -> void:
if size.x < 2.0 or size.y < 2.0:
return
match variant:
"header_band":
_draw_header_band()
"dock_band":
_draw_dock_band()
"command_plate":
_draw_command_plate()
"card":
_draw_card()
"warning_icon":
_draw_warning_icon()
"pip":
_draw_pip()
_:
_draw_card()
func _draw_header_band() -> void:
var rect := Rect2(2.0, 3.0, size.x - 4.0, size.y - 6.0)
_round(Rect2(rect.position + Vector2(0.0, 3.0), rect.size), 7.0, SHADOW)
_round(rect, 7.0, PAPER, GOLD, 2.0)
_round(rect.grow(-3.0), 5.0, Color(PAPER_LIGHT, 0.45), Color(PAPER_SHADE, 0.85), 1.0)
var dividers: Array = params.get("dividers", [])
for value in dividers:
var x := rect.position.x + rect.size.x * float(value)
draw_line(Vector2(x, rect.position.y + 8.0), Vector2(x, rect.end.y - 8.0), Color(GOLD, 0.75), 1.5, true)
draw_line(Vector2(rect.position.x + 10.0, rect.position.y + 1.0), Vector2(rect.end.x - 10.0, rect.position.y + 1.0), Color(PAPER_LIGHT, 0.9), 1.5, true)
func _draw_dock_band() -> void:
var rect := Rect2(0.0, 0.0, size.x, size.y)
draw_rect(Rect2(0.0, 0.0, size.x, size.y * 0.55), DOCK_LIGHT)
draw_rect(Rect2(0.0, size.y * 0.55, size.x, size.y * 0.45), DOCK)
draw_rect(Rect2(0.0, 0.0, size.x, 2.5), Color(GOLD, 0.85))
draw_rect(Rect2(0.0, 2.5, size.x, 2.0), Color(DOCK_DARK, 0.6))
draw_rect(Rect2(0.0, size.y - 3.0, size.x, 3.0), Color(DOCK_DARK, 0.7))
draw_rect(Rect2(0.0, 0.0, size.x, 1.0), Color(GOLD_LIGHT, 0.9))
func _draw_command_plate() -> void:
var state := String(params.get("state", "secondary"))
var face := PAPER
var edge := GOLD
var inner := PAPER_LIGHT
var label_line := PAPER_SHADE
match state:
"primary":
face = GREEN
edge = GOLD_LIGHT
inner = GREEN_LIGHT
label_line = Color(0.12, 0.20, 0.10, 0.35)
"disabled":
face = STONE
edge = Color("#6d7074")
inner = Color("#5c6064")
label_line = Color(0.0, 0.0, 0.0, 0.28)
var rect := Rect2(2.0, 3.0, size.x - 4.0, size.y - 7.0)
_round(Rect2(rect.position + Vector2(0.0, 4.0), rect.size), 9.0, SHADOW)
_round(rect, 9.0, WOOD, Color(0.10, 0.07, 0.05, 0.55), 1.0)
var face_rect := rect.grow(-5.0)
_round(face_rect, 6.0, face, edge, 2.0)
_round(face_rect.grow(-3.0), 4.0, Color(inner, 0.35), Color(edge, 0.55), 1.0)
draw_line(Vector2(face_rect.position.x + 8.0, face_rect.position.y + 3.0), Vector2(face_rect.end.x - 8.0, face_rect.position.y + 3.0), Color(PAPER_LIGHT, 0.55) if state != "disabled" else Color(1, 1, 1, 0.12), 1.2, true)
draw_line(Vector2(face_rect.position.x + 12.0, face_rect.end.y - 4.0), Vector2(face_rect.end.x - 12.0, face_rect.end.y - 4.0), label_line, 2.0, true)
func _draw_card() -> void:
var rect := Rect2(3.0, 3.0, size.x - 6.0, size.y - 6.0)
_round(Rect2(rect.position + Vector2(0.0, 3.0), rect.size), 7.0, SHADOW)
_round(rect, 7.0, PAPER, GOLD, 2.0)
_round(rect.grow(-3.0), 5.0, Color(PAPER_LIGHT, 0.50), Color(PAPER_SHADE, 0.85), 1.0)
draw_line(Vector2(rect.position.x + 8.0, rect.position.y + 2.0), Vector2(rect.end.x - 8.0, rect.position.y + 2.0), Color(PAPER_LIGHT, 0.85), 1.2, true)
func _draw_warning_icon() -> void:
var severity := String(params.get("severity", "warning"))
var fill := RED if severity == "warning" else AMBER if severity == "notice" else TEAL
var rect := Rect2(1.0, 1.0, size.x - 2.0, size.y - 2.0)
_round(rect, 5.0, Color(0.08, 0.06, 0.05, 0.4))
_round(rect.grow(-1.0), 4.0, fill, GOLD_LIGHT, 1.5)
var center := rect.get_center()
var bar_w := maxf(2.0, rect.size.x * 0.11)
draw_rect(Rect2(center.x - bar_w * 0.5, rect.position.y + rect.size.y * 0.26, bar_w, rect.size.y * 0.36), Color(PAPER_LIGHT, 0.95))
draw_circle(Vector2(center.x, rect.position.y + rect.size.y * 0.74), bar_w * 0.62, Color(PAPER_LIGHT, 0.95))
func _draw_pip() -> void:
var shape := String(params.get("shape", "circle"))
var filled := bool(params.get("filled", false))
var rect := Rect2(1.5, 1.5, size.x - 3.0, size.y - 3.0)
var line := Color(GOLD, 0.95) if filled else Color(GOLD, 0.55)
match shape:
"shield":
var points := PackedVector2Array([
Vector2(rect.position.x + rect.size.x * 0.5, rect.position.y),
Vector2(rect.end.x, rect.position.y + rect.size.y * 0.28),
Vector2(rect.end.x - rect.size.x * 0.16, rect.end.y),
Vector2(rect.position.x + rect.size.x * 0.16, rect.end.y),
Vector2(rect.position.x, rect.position.y + rect.size.y * 0.28)
])
if filled:
draw_colored_polygon(points, Color(GOLD, 0.35))
draw_polyline(points + PackedVector2Array([points[0]]), line, 1.6, true)
"diamond":
var center := rect.get_center()
var radius := minf(rect.size.x, rect.size.y) * 0.5
var diamond := PackedVector2Array([center + Vector2(0, -radius), center + Vector2(radius, 0), center + Vector2(0, radius), center + Vector2(-radius, 0)])
if filled:
draw_colored_polygon(diamond, Color(GOLD, 0.35))
draw_polyline(diamond + PackedVector2Array([diamond[0]]), line, 1.6, true)
_:
var center := rect.get_center()
var radius := minf(rect.size.x, rect.size.y) * 0.42
if filled:
draw_circle(center, radius * 0.8, Color(GOLD, 0.35))
draw_arc(center, radius, 0.0, TAU, 22, line, 1.6, true)
if filled:
draw_circle(rect.get_center(), 1.4, Color(GOLD, 0.95))
func _round(rect: Rect2, radius: float, fill: Color, border := Color(0, 0, 0, 0), width := 0.0) -> void:
var style := StyleBoxFlat.new()
style.bg_color = fill
style.corner_radius_top_left = int(radius)
style.corner_radius_top_right = int(radius)
style.corner_radius_bottom_left = int(radius)
style.corner_radius_bottom_right = int(radius)
if width > 0.0:
style.border_color = border
style.set_border_width_all(int(width))
draw_style_box(style, rect)