Files
aetherbound-guild/runtime/entry/views/readiness_page_view.gd
T
wushenghua 67b5e3fda0 fix(m57-c): keep header values inside the band face and repaint settings
The first header value started at the authored slot edge, which now sits on the
parchment band's corner bracket, so the title overlapped the trim. Header
labels are clamped to the band face (edge fraction plus a small pad) in both
locales. The header settings control was still a flat angular utility plate;
it now uses a compact member of the same hand-drawn plate family with the
engine-rendered settings glyph, so the top row carries no vector chrome.
2026-09-13 16:21:02 +08:00

622 lines
29 KiB
GDScript

class_name ABGReadinessPageView
extends "res://presentation/page_view.gd"
## UI-only PTY-002 Readiness Review View.
##
## 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_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
func _notification(what: int) -> void:
if what == NOTIFICATION_RESIZED:
_layout_design_canvas()
func _exit_tree() -> void:
# Release cached textures so a strict run reports no leaked RIDs.
AssetChrome.clear_cache()
_shadow_texture_cache = null
func _render_view(data: Dictionary) -> void:
clear_view_children()
design_canvas = null
var matte := ColorRect.new()
matte.name = "ReadinessMasterMatte"
matte.color = Color("#17252c")
matte.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
matte.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(matte)
design_canvas = Control.new()
design_canvas.name = "ReadinessMasterCanvas"
design_canvas.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(design_canvas)
_layout_design_canvas()
var master := TextureRect.new()
master.name = "ReadinessStaticMaster"
var source := Image.load_from_file(ProjectSettings.globalize_path(READINESS_STATIC_MASTER_PATH))
master.texture = ImageTexture.create_from_image(source)
master.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
master.stretch_mode = TextureRect.STRETCH_SCALE
master.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
master.mouse_filter = Control.MOUSE_FILTER_IGNORE
design_canvas.add_child(master)
if not bool(data.get("ok", false)):
_render_invalid(data)
return
_build_overlay(data)
func _layout_design_canvas() -> void:
layout_fixed_design_canvas(design_canvas)
func _build_overlay(data: Dictionary) -> void:
var overlay := Control.new()
overlay.name = "ReadinessMasterOverlay"
overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
overlay.mouse_filter = Control.MOUSE_FILTER_PASS
design_canvas.add_child(overlay)
var view_formation: Dictionary = Dictionary(data.get("formation", {}))
var view_order: Dictionary = Dictionary(data.get("order", {}))
var view_contract: Dictionary = Dictionary(data.get("contract", {}))
var view_company: Dictionary = Dictionary(data.get("company", {}))
var command_data: Dictionary = Dictionary(data.get("commands", {}))
var view_coverage: Dictionary = Dictionary(view_formation.get("coverage", {}))
var front_recruit: Dictionary = Dictionary(view_order.get("front_recruit", {}))
var ordered_recruits: Array = Array(view_order.get("ordered_recruits", []))
var warning_rows: Array = Array(Dictionary(data.get("warnings", {})).get("rows", []))
var locale := page_locale(data)
var contract_name := String(view_contract.get("contract_id", view_contract.get("name", "")))
if contract_name.is_empty():
contract_name = localize("委托", "CONTRACT", 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()
cord_track.name = "ReadinessFormationCord"
cord_track.render_layer = PartyLineTrack.RenderLayer.CORD
cord_track.z_index = 25
cord_track.set_meta("render_layer", "cord")
_readiness_place(cord_track, Rect2(0.06, 0.28, 0.60, 0.30))
cord_track.mouse_filter = Control.MOUSE_FILTER_IGNORE
overlay.add_child(cord_track)
var member_areas := M55_READINESS_SLOTS.MEMBER_HIT_SLOTS.duplicate()
var member_nodes: Array[Control] = []
for visual_index in range(ordered_recruits.size() - 1, -1, -1):
var ordered: Dictionary = Dictionary(ordered_recruits[visual_index])
var recruit := Dictionary(ordered.get("recruit", {})).duplicate(true)
var recruit_id := String(ordered.get("recruit_id", recruit.get("recruit_id", "")))
var member_slot: Rect2 = member_areas[visual_index]
var button := _readiness_member_button(visual_index, recruit, member_slot, recruit_id, data)
button.z_index = 30
overlay.add_child(button)
member_nodes.append(button)
_add_member_shadow(overlay, member_slot)
_add_member_name_plate(overlay, visual_index, member_slot, _recruit_name(recruit, data))
var effects_track := PartyLineTrack.new()
effects_track.name = "ReadinessFormationEffects"
effects_track.render_layer = PartyLineTrack.RenderLayer.EFFECTS
effects_track.z_index = 35
effects_track.set_meta("render_layer", "effects")
_readiness_place(effects_track, Rect2(0.06, 0.28, 0.60, 0.30))
effects_track.mouse_filter = Control.MOUSE_FILTER_IGNORE
effects_track.bind(member_nodes, "", locale)
overlay.add_child(effects_track)
cord_track.bind(member_nodes, "", locale)
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("保护", "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.040, 0.672, 0.620, 0.156))
summary.z_index = 20
overlay.add_child(summary)
var warning_button := _readiness_warning_button(warning_rows, data)
warning_button.z_index = 45
overlay.add_child(warning_button)
_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 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)
for command in [revise, back, accept, risk]:
command.z_index = 50
action_dock.add_child(command)
var focus_nodes: Array[Control] = [settings_button]
focus_nodes.append_array(member_nodes)
focus_nodes.append(warning_button)
focus_nodes.append(revise)
focus_nodes.append(back)
focus_nodes.append(accept)
focus_nodes.append(risk)
for index in range(focus_nodes.size()):
var current := focus_nodes[index]
var next := focus_nodes[(index + 1) % focus_nodes.size()]
var previous := focus_nodes[(index - 1 + focus_nodes.size()) % focus_nodes.size()]
current.focus_next = current.get_path_to(next)
current.focus_previous = current.get_path_to(previous)
var initial_focus: Control = accept if not accept.disabled else revise
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)
var face_inset := PageChrome.PANEL_EDGE_X * HEADER_BAND.size.x + 0.007
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 = maxf(x0 + width * text_left, slots_left + face_inset)
label.anchor_right = minf(x0 + width * 0.94, slots_left + HEADER_BAND.size.x - face_inset)
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"
overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
design_canvas.add_child(overlay)
var title := _readiness_label(overlay, "ReadinessInvalidTitle", Rect2(0.28, 0.40, 0.44, 0.08), localize("准备不可用", "READINESS UNAVAILABLE", data), 16, Color("#f5ead0"))
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
var detail := _readiness_label(overlay, "ReadinessInvalidDetail", Rect2(0.22, 0.49, 0.56, 0.12), String(data.get("notice", data.get("code", ""))), 9, Color("#ffd4d4"))
detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
detail.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
func _add_member_shadow(parent: Control, slot: Rect2) -> void:
var shadow := TextureRect.new()
shadow.name = "ReadinessGroundShadow"
shadow.texture = _ground_shadow_texture()
shadow.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
shadow.stretch_mode = TextureRect.STRETCH_SCALE
shadow.modulate = Color(0.04, 0.05, 0.06, 0.55)
shadow.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
var width := slot.size.x * 0.74
var cx := slot.position.x + slot.size.x * 0.5
var baseline := slot.position.y + slot.size.y * 0.965
_readiness_place(shadow, Rect2(cx - width * 0.5, baseline - 0.016, width, 0.032))
shadow.z_index = 28
parent.add_child(shadow)
func _add_member_name_plate(parent: Control, index: int, slot: Rect2, name_text: String) -> void:
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 := PageChrome.new().configure("card")
plate.name = "ReadinessMemberPlate%d" % index
_readiness_place(plate, area)
plate.z_index = 31
parent.add_child(plate)
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:
if _shadow_texture_cache != null:
return _shadow_texture_cache
var image := Image.create(64, 24, false, Image.FORMAT_RGBA8)
for y in range(24):
for x in range(64):
var nx := (float(x) - 31.5) / 31.5
var ny := (float(y) - 11.5) / 11.5
var distance := sqrt(nx * nx + ny * ny)
var alpha := clampf(1.0 - distance, 0.0, 1.0)
image.set_pixel(x, y, Color(1.0, 1.0, 1.0, alpha * alpha))
_shadow_texture_cache = ImageTexture.create_from_image(image)
return _shadow_texture_cache
func _readiness_place(control: Control, area: Rect2) -> void:
control.anchor_left = area.position.x
control.anchor_top = area.position.y
control.anchor_right = area.end.x
control.anchor_bottom = area.end.y
control.offset_left = 0.0
control.offset_top = 0.0
control.offset_right = 0.0
control.offset_bottom = 0.0
func _readiness_label(parent: Control, node_name: String, area: Rect2, text_value: String, font_size: int, color: Color) -> Label:
var label := make_label(text_value, _readiness_text_size(font_size), color)
label.name = node_name
label.set_meta("design_font_size", font_size)
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.text_overrun_behavior = TextServer.OVERRUN_NO_TRIMMING
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
_readiness_place(label, area)
parent.add_child(label)
return label
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(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:
if intent_id == "settings":
var utility := Button.new()
utility.name = node_name
utility.flat = true
utility.focus_mode = Control.FOCUS_ALL
utility.tooltip_text = tooltip
utility.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
utility.set_meta("ui_factory_control", true)
utility.set_meta("ui_semantic_variant", "utility")
utility.set_meta("ui_shape", "dock_plate")
utility.set_meta("ui_family", "company")
utility.set_meta("ui_icon_kind", "settings")
for style_name in ["normal", "hover", "pressed", "focus", "disabled", "hover_pressed"]:
utility.add_theme_stylebox_override(style_name, StyleBoxEmpty.new())
_readiness_place(utility, area)
var plate := PageChrome.new().configure("utility_plate")
plate.name = node_name + "Plate"
plate.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
plate.mouse_filter = Control.MOUSE_FILTER_IGNORE
utility.add_child(plate)
var icon := UIIcon.new().configure("settings", "light")
icon.name = "UtilityIcon"
icon.anchor_left = 0.22
icon.anchor_right = 0.78
icon.anchor_top = 0.22
icon.anchor_bottom = 0.78
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
utility.add_child(icon)
utility.pressed.connect(func(): emit_intent(intent_id, payload), CONNECT_DEFERRED)
return utility
var button := Button.new()
button.name = node_name
button.flat = true
button.focus_mode = Control.FOCUS_ALL
button.tooltip_text = tooltip
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
button.set_meta("ui_factory_control", true)
button.add_theme_stylebox_override("normal", Tokens.surface_style("overlay"))
button.add_theme_stylebox_override("hover", Tokens.flat_command_style("utility", "hover"))
button.add_theme_stylebox_override("pressed", Tokens.flat_command_style("utility", "pressed"))
button.add_theme_stylebox_override("focus", Tokens.flat_command_style("utility", "focus"))
button.add_theme_stylebox_override("disabled", Tokens.surface_style("overlay"))
_readiness_place(button, area)
button.pressed.connect(func(): emit_intent(intent_id, payload), CONNECT_DEFERRED)
return button
func _readiness_member_button(position: int, recruit: Dictionary, area: Rect2, recruit_id: String, data: Dictionary) -> Button:
var button := Button.new()
button.name = "ReadinessPosition%d" % position
button.flat = true
button.focus_mode = Control.FOCUS_ALL
button.tooltip_text = localize("查看 %s 的准备事实", "Review %s readiness facts", data) % _recruit_name(recruit, data)
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
button.set_meta("ui_factory_control", true)
button.set_meta("recruit_id", recruit_id)
button.set_meta("position", position)
button.clip_contents = false
for style_name in ["normal", "hover", "pressed", "disabled"]:
button.add_theme_stylebox_override(style_name, StyleBoxEmpty.new())
button.add_theme_stylebox_override("focus", _flat_style(Color(0.95, 0.82, 0.34, 0.11), Color("#d6ad5d"), 2, 8))
_readiness_place(button, area)
var portrait := TextureRect.new()
portrait.name = "ReadinessPortrait"
portrait.texture = InitialCompanyArt.texture_for(recruit)
portrait.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
portrait.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
portrait.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
portrait.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE
button.add_child(portrait)
button.pressed.connect(func(): emit_intent("review_member", {"recruit_id": recruit_id}), CONNECT_DEFERRED)
return button
func _readiness_warning_button(warning_rows: Array, data: Dictionary) -> Button:
var button := Button.new()
button.name = "ReadinessWarning"
button.flat = true
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
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 index in range(warning_rows.size()):
var row: Variant = warning_rows[index]
var fallback_severity: String = ["warning", "notice", "info"][mini(index, 2)]
if row is Dictionary:
lines.append(String(row.get("text", "")))
severities.append(String(Dictionary(row).get("severity", fallback_severity)))
else:
lines.append(String(row))
severities.append(fallback_severity)
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.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 label := AssetChrome.styled_label(warning_text, _readiness_text_size(8), PageChrome.INK, 0.4, 0.0)
label.name = "WarningLabel"
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
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
label.text_overrun_behavior = TextServer.OVERRUN_NO_TRIMMING
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
row.add_child(label)
return button
func _readiness_warning_text(value: String, data: Dictionary) -> String:
if page_locale(data) != "zh_CN":
return value
match value:
"NO GUARD": return "无保护"
"Outcome not guaranteed": return "结果不保证"
"Reserve: none": return "预备席:无"
"No explicit protection contributor": return "没有明确保护贡献者"
"No full-line reach": return "没有整线射程"
"No sustain responsibility": return "没有续航职责"
return value
func _readiness_command_button(node_name: String, text_value: String, intent_id: String, area: Rect2, disabled: bool, tooltip: String, primary: bool) -> Button:
var button := Button.new()
button.name = node_name
button.flat = true
button.focus_mode = Control.FOCUS_ALL
button.disabled = disabled
button.tooltip_text = tooltip
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", "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 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.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
plate.mouse_filter = Control.MOUSE_FILTER_IGNORE
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"
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.255
icon.anchor_bottom = 0.575
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.anchor_left = 0.06
label.anchor_right = 0.94
label.anchor_top = 0.565
label.anchor_bottom = 0.775
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)
return button
func _button_label(node_name: String, text_value: String, font_size: int, color: Color) -> Label:
var label := make_label(text_value, font_size, color)
label.name = node_name
label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
return label
func _flat_style(background: Color, border: Color, width: int, radius: int) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = background
style.border_color = border
style.set_border_width_all(width)
style.set_corner_radius_all(radius)
return style
func _recruit_name(recruit: Dictionary, data: Dictionary) -> String:
var localized: Dictionary = Dictionary(recruit.get("rendered_name_snapshot", {}))
var locale := page_locale(data)
return String(localized.get(locale, localized.get("en", recruit.get("recruit_id", "Unknown"))))
func _party_pressure_short(difficulty: String, data: Dictionary) -> String:
match difficulty:
"wayfinder": return localize("引路", "WAY", data)
"stormbound": return localize("风暴", "STORM", data)
"iron_oath": return localize("铁誓", "OATH", data)
return localize("标准", "STD", data)