diff --git a/runtime/entry/views/readiness_page_view.gd b/runtime/entry/views/readiness_page_view.gd index 6bf2f2a2..c2da59d2 100644 --- a/runtime/entry/views/readiness_page_view.gd +++ b/runtime/entry/views/readiness_page_view.gd @@ -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) diff --git a/runtime/ui/m55/readiness_formation_summary.gd b/runtime/ui/m55/readiness_formation_summary.gd index 030f88ee..e1dadd15 100644 --- a/runtime/ui/m55/readiness_formation_summary.gd +++ b/runtime/ui/m55/readiness_formation_summary.gd @@ -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) diff --git a/runtime/ui/m55/readiness_slots.gd b/runtime/ui/m55/readiness_slots.gd index 4412cdce..3d66bcd6 100644 --- a/runtime/ui/m55/readiness_slots.gd +++ b/runtime/ui/m55/readiness_slots.gd @@ -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 diff --git a/runtime/ui/shared/asset_chrome.gd b/runtime/ui/shared/asset_chrome.gd index 829ee428..1f680f33 100644 --- a/runtime/ui/shared/asset_chrome.gd +++ b/runtime/ui/shared/asset_chrome.gd @@ -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 diff --git a/runtime/ui/shared/page_chrome.gd b/runtime/ui/shared/page_chrome.gd new file mode 100644 index 00000000..8b84bd70 --- /dev/null +++ b/runtime/ui/shared/page_chrome.gd @@ -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)