Files
aetherbound-guild/runtime/ui/m52/candidate_ledger.gd
T

269 lines
11 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
class_name ABGM52CandidateLedger
extends Control
## Standalone candidate detail ledger. It owns only presentation and emits a
## single action intent; Market remains responsible for routing and services.
const MarketChrome = preload("res://ui/shared/market_chrome.gd")
const UIFactory = preload("res://ui/shared/ui_factory.gd")
const PortraitModule = preload("res://ui/m52/portrait_module.gd")
const IdentityModule = preload("res://ui/m52/identity_module.gd")
const TraitModule = preload("res://ui/m52/trait_module.gd")
const CostModule = preload("res://ui/m52/cost_module.gd")
const ProfessionModule = preload("res://ui/m52/profession_module.gd")
const DetailCopy = preload("res://ui/m52/detail_copy.gd")
const ActionDock = preload("res://ui/m52/action_dock.gd")
signal action_requested
var candidate_name := ""
var profession_name := ""
var trait_name := ""
var availability := ""
var cost_text := ""
var detail_text := ""
var portrait_texture: Texture2D
var header_text := "候选人详情"
var empty_copy := "从左侧选择一名候选人。"
var action_node_name := "ActionButton"
var action_text := "查看详情"
var action_disabled := true
var detail_rows := 1
var text_scale := 1.0
func configure(data: Dictionary) -> ABGM52CandidateLedger:
_candidate_from(data)
return self
func _candidate_from(data: Dictionary) -> void:
candidate_name = String(data.get("name", "选择候选人"))
profession_name = String(data.get("profession", ""))
trait_name = String(data.get("trait", ""))
availability = String(data.get("availability", ""))
cost_text = String(data.get("cost", ""))
detail_text = String(data.get("detail", ""))
portrait_texture = data.get("portrait") as Texture2D
header_text = String(data.get("header", "候选人详情"))
empty_copy = String(data.get("empty_copy", "从左侧选择一名候选人。"))
action_node_name = String(data.get("action_node_name", "ActionButton"))
action_text = String(data.get("action_text", "查看详情"))
action_disabled = bool(data.get("action_disabled", portrait_texture == null))
detail_rows = maxi(1, int(data.get("detail_rows", 1)))
text_scale = float(data.get("text_scale", 1.0))
func _ready() -> void:
clip_contents = true
mouse_filter = Control.MOUSE_FILTER_PASS
_build()
_apply_responsive_layout()
func _notification(what: int) -> void:
if what == NOTIFICATION_RESIZED and is_inside_tree():
_apply_responsive_layout()
func _build() -> void:
var shell := MarketChrome.new().configure("ledger")
shell.name = "LedgerShell"
shell.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
shell.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(shell)
var header := UIFactory.label(header_text, "label", 1.0, MarketChrome.PAPER_LIGHT)
header.name = "LedgerHeader"
header.add_theme_font_size_override("font_size", ABGUI_Tokens.font_size("body", text_scale))
_place(header, Rect2(0.08, 0.035, 0.84, 0.095))
header.z_index = 5
header.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
header.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
header.clip_text = true
header.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(header)
var portrait := PortraitModule.new().configure(portrait_texture)
portrait.name = "PortraitModule"
portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE
_place(portrait, Rect2(0.19, 0.21, 0.62, 0.28))
portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(portrait)
var identity := IdentityModule.new().configure(candidate_name)
identity.name = "IdentityModule"
identity.mouse_filter = Control.MOUSE_FILTER_IGNORE
_place(identity, Rect2(0.16, 0.515, 0.68, 0.105))
identity.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(identity)
var profession := ProfessionModule.new().configure(profession_name, text_scale)
profession.name = "ProfessionModule"
profession.mouse_filter = Control.MOUSE_FILTER_IGNORE
_place(profession, Rect2(0.17, 0.635, 0.66, 0.055))
profession.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(profession)
var detail_value := detail_text if not detail_text.is_empty() else (empty_copy if portrait_texture == null else "")
var detail := DetailCopy.new().configure(detail_value, text_scale)
detail.name = "DetailCopy"
_place(detail, Rect2(0.06, 0.692, 0.88, 0.125 if detail_rows > 1 else 0.062))
detail.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(detail)
if portrait_texture == null:
var empty_marker := Control.new()
empty_marker.name = "MarketNoSelection"
empty_marker.visible = false
empty_marker.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(empty_marker)
var trait_module := TraitModule.new().configure(trait_name, text_scale)
trait_module.name = "TraitModule"
trait_module.mouse_filter = Control.MOUSE_FILTER_IGNORE
# A two-row detail (equipment/route facts) owns the trait row; the trait
# module is empty for those kinds and must not overlap the taller slot.
trait_module.visible = detail_rows <= 1 or not trait_name.is_empty()
_place(trait_module, Rect2(0.18, 0.755, 0.64, 0.065))
trait_module.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(trait_module)
var cost := CostModule.new().configure(availability, cost_text, text_scale)
cost.name = "CostModule"
cost.mouse_filter = Control.MOUSE_FILTER_IGNORE
_place(cost, Rect2(0.12, 0.825, 0.76, 0.065))
cost.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(cost)
var action := ActionDock.new().configure(action_text, action_disabled)
action.name = action_node_name
action.pressed.connect(_on_action_pressed, CONNECT_DEFERRED)
_place(action, Rect2(0.15, 0.905, 0.70, 0.085))
action.mouse_filter = Control.MOUSE_FILTER_PASS
add_child(action)
func _apply_responsive_layout() -> void:
# The phone-landscape ledger is only ~240–280px wide and ~300–330px tall
# after the 16:9 canvas is fitted. Treat that whole range as compact so the
# 48px action dock is laid out before the shell's bottom edge.
# `size` is now authored 1280x720 units. Decide compactness from the
# transformed play-size so a phone-landscape ledger still gets its 48px
# action dock layout.
var play_size := get_global_rect().size
var compact := play_size.x < 300.0 or play_size.y < 380.0
# M73: 130% text needs taller text rows; the portrait yields the space.
var large_text := text_scale > 1.15
var portrait := get_node_or_null("PortraitModule") as Control
var identity := get_node_or_null("IdentityModule") as Control
var profession := get_node_or_null("ProfessionModule") as Control
var detail := get_node_or_null("DetailCopy") as Control
var trait_module := get_node_or_null("TraitModule") as Control
var cost := get_node_or_null("CostModule") as Control
var action := get_node_or_null("ActionButton") as Control
if action == null:
action = get_node_or_null(action_node_name) as Control
var header := get_node_or_null("LedgerHeader") as Label
if not compact and large_text:
if header != null:
_place(header, Rect2(0.04, 0.02, 0.92, 0.21))
header.add_theme_font_size_override("font_size", ABGUI_Tokens.font_size("body", text_scale))
# The EN section title is wider than the card face at 130%; wrap it
# to a second authored row instead of shrinking the token.
header.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
header.max_lines_visible = 2
if portrait != null:
_place(portrait, Rect2(0.24, 0.24, 0.52, 0.14))
if identity != null:
_place(identity, Rect2(0.06, 0.395, 0.88, 0.13))
if identity.has_method("set_compact"):
identity.set_compact(false)
if profession != null:
_place(profession, Rect2(0.10, 0.53, 0.80, 0.09))
if detail != null:
_place(detail, Rect2(0.06, 0.625, 0.88, 0.10 if detail_rows <= 1 else 0.16))
var large_detail_label := detail.get_node_or_null("DetailLabel") as Label
if large_detail_label != null:
large_detail_label.add_theme_font_size_override("font_size", ABGUI_Tokens.font_size("micro", text_scale))
large_detail_label.max_lines_visible = detail_rows
if trait_module != null:
_place(trait_module, Rect2(0.08, 0.735, 0.84, 0.10))
if trait_module.has_method("set_compact"):
trait_module.set_compact(false)
if cost != null:
# M73: the availability + amount pair needs the full card face at 130%.
_place(cost, Rect2(0.02, 0.845, 0.96, 0.10))
if cost.has_method("set_compact"):
cost.set_compact(false, true)
if action != null:
_place(action, Rect2(0.15, 0.90, 0.70, 0.09))
return
if not compact:
if header != null:
_place(header, Rect2(0.08, 0.035, 0.84, 0.095))
header.add_theme_font_size_override("font_size", ABGUI_Tokens.font_size("body", text_scale))
if portrait != null:
_place(portrait, Rect2(0.19, 0.21, 0.62, 0.28))
if identity != null:
_place(identity, Rect2(0.16, 0.515, 0.68, 0.105))
if identity.has_method("set_compact"):
identity.set_compact(false)
if profession != null:
_place(profession, Rect2(0.17, 0.635, 0.66, 0.055))
if detail != null:
# The context row is a caption row between the profession and the
# trait chip. At the canvas type scale the longest shipped string
# needs this width; 0.66 clipped it with an ellipsis. Equipment and
# route facts are longer, so those kinds author two rows.
_place(detail, Rect2(0.06, 0.692, 0.88, 0.125 if detail_rows > 1 else 0.062))
var wide_detail_label := detail.get_node_or_null("DetailLabel") as Label
if wide_detail_label != null:
wide_detail_label.add_theme_font_size_override("font_size", ABGUI_Tokens.font_size("micro", text_scale))
wide_detail_label.max_lines_visible = detail_rows
if trait_module != null:
_place(trait_module, Rect2(0.18, 0.755, 0.64, 0.065))
if trait_module.has_method("set_compact"):
trait_module.set_compact(false)
if cost != null:
_place(cost, Rect2(0.12, 0.825, 0.76, 0.065))
if cost.has_method("set_compact"):
# The ledger is the surface that carries the full availability and
# currency string; only the card row drops the availability label.
cost.set_compact(false, true)
if action != null:
_place(action, Rect2(0.15, 0.905, 0.70, 0.085))
return
if header != null:
_place(header, Rect2(0.10, 0.035, 0.80, 0.095))
header.add_theme_font_size_override("font_size", 14)
if portrait != null:
_place(portrait, Rect2(0.10, 0.16, 0.80, 0.25))
if identity != null:
_place(identity, Rect2(0.14, 0.42, 0.72, 0.11))
if identity.has_method("set_compact"):
identity.set_compact(true)
if profession != null:
_place(profession, Rect2(0.14, 0.53, 0.72, 0.05))
if detail != null:
_place(detail, Rect2(0.14, 0.585, 0.72, 0.115 if detail_rows > 1 else 0.055))
var detail_label := detail.get_node_or_null("DetailLabel") as Label
if detail_label != null:
detail_label.add_theme_font_size_override("font_size", 11)
detail_label.max_lines_visible = 2 if detail_rows > 1 else 2
if trait_module != null:
_place(trait_module, Rect2(0.16, 0.65, 0.68, 0.065))
if trait_module.has_method("set_compact"):
trait_module.set_compact(true)
if cost != null:
_place(cost, Rect2(0.10, 0.73, 0.80, 0.07))
if cost.has_method("set_compact"):
cost.set_compact(true, true)
if action != null:
_place(action, Rect2(0.10, 0.82, 0.80, 0.14))
func _on_action_pressed() -> void:
action_requested.emit()
func _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