Files
aetherbound-guild/runtime/entry/views/guild_page_view.gd
T

607 lines
28 KiB
GDScript

class_name ABGGuildPageView
extends "res://presentation/page_view.gd"
## UI-only HUB-001 Guild View.
##
## The View owns the live layered Guild desk, its responsive 16:9 canvas,
## engine-rendered values, focus and hit areas. The supplied snapshot is
## copied ViewData; application services, persistence and routing stay at the
## composition root and are reached only through explicit intents.
const InitialCompanyArt = preload("res://entry/initial_company_art.gd")
const GUILD_DESK_WORLD = preload("res://assets/entry/guild_desk_v001/world.png")
const M52_GUILD_BACKGROUND = preload("res://assets/backgrounds/m52_guild_background.png")
const M52_Q_GUILD_BACKGROUND = preload("res://assets/backgrounds/m52_q_guild_background.png")
const M52_GUILD_RESOURCE_STATUS = preload("res://ui/m52/guild_resource_status.gd")
const PageChrome = preload("res://ui/shared/page_chrome.gd")
const UITokens = preload("res://foundation/ui_tokens.gd")
const M52_GUILD_CONTRACT_BOARD = preload("res://ui/m52/guild_contract_board.gd")
const M52_GUILD_MEMBER_CARD = preload("res://ui/m52/guild_member_card.gd")
const M52_GUILD_HEADER = preload("res://ui/m52/guild_header.gd")
const GUILD_DESK_CONTRACT_BOARD = preload("res://assets/entry/guild_desk_v001/contract-board.png")
const GUILD_DESK_TOP_LEFT = preload("res://assets/entry/guild_desk_v001/top_rail/left.png")
const GUILD_DESK_TOP_CENTER = preload("res://assets/entry/guild_desk_v001/top_rail/center.png")
const GUILD_DESK_TOP_RIGHT = preload("res://assets/entry/guild_desk_v001/top_rail/right.png")
const GUILD_DESK_BOTTOM_LEFT = preload("res://assets/entry/guild_desk_v001/bottom_rail/left.png")
const GUILD_DESK_BOTTOM_CENTER = preload("res://assets/entry/guild_desk_v001/bottom_rail/center.png")
const GUILD_DESK_BOTTOM_RIGHT = preload("res://assets/entry/guild_desk_v001/bottom_rail/right.png")
var design_canvas: Control
func _notification(what: int) -> void:
if what == NOTIFICATION_RESIZED:
_layout_design_canvas()
func _render_view(data: Dictionary) -> void:
clear_view_children()
design_canvas = null
var world_extension := TextureRect.new()
world_extension.name = "GuildMasterWorldExtension"
world_extension.texture = M52_Q_GUILD_BACKGROUND
world_extension.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
world_extension.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
world_extension.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
world_extension.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
world_extension.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(world_extension)
design_canvas = Control.new()
design_canvas.name = "GuildMasterDesignCanvas"
design_canvas.clip_contents = true
design_canvas.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(design_canvas)
_layout_design_canvas()
var background := TextureRect.new()
background.name = "GuildMasterLayeredWorld"
background.texture = M52_Q_GUILD_BACKGROUND
background.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
background.stretch_mode = TextureRect.STRETCH_SCALE
background.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
background.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
background.mouse_filter = Control.MOUSE_FILTER_IGNORE
# The full-viewport layer owns the responsive crop; retain the named inner
# world node for the layered View contract without drawing a second crop.
background.modulate.a = 0.0
design_canvas.add_child(background)
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)
_refresh_typography()
_refresh_bottom_nav_geometry()
func _build_overlay(data: Dictionary) -> void:
var page_data: Dictionary = data.duplicate(true)
var overlay := Control.new()
overlay.name = "GuildAnchorRoot"
overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
overlay.mouse_filter = Control.MOUSE_FILTER_STOP
design_canvas.add_child(overlay)
_add_segmented_rail(
overlay,
"GuildTopRail",
Rect2(0.012, 0.010, 0.976, 0.115),
GUILD_DESK_TOP_LEFT,
GUILD_DESK_TOP_CENTER,
GUILD_DESK_TOP_RIGHT,
0.055,
0.046
)
_add_segmented_rail(
overlay,
"GuildBottomRail",
Rect2(0.010, 0.865, 0.980, 0.130),
GUILD_DESK_BOTTOM_LEFT,
GUILD_DESK_BOTTOM_CENTER,
GUILD_DESK_BOTTOM_RIGHT,
0.080,
0.090
)
var company_data: Dictionary = Dictionary(page_data.get("company", {}))
var contract_data: Dictionary = Dictionary(page_data.get("contract", {}))
var destinations: Dictionary = Dictionary(page_data.get("destinations", {}))
var active_contract := bool(contract_data.get("active", false))
var returned_contract := bool(contract_data.get("returned", false))
var has_contract := bool(contract_data.get("present", false))
var header := M52_GUILD_HEADER.new().configure(localize("公会大厅", "GUILD HALL", page_data), localize("持续远征团", "PERSISTENT COMPANY", page_data))
header.name = "GuildTitlePlate"
_set_normalized_rect(header, Rect2(0.070, 0.026, 0.255, 0.135))
overlay.add_child(header)
var pressure_copy := "0"
var coin_copy := "--"
if active_contract:
pressure_copy = _difficulty_name(String(contract_data.get("difficulty", "standard")), page_data)
coin_copy = "%d" % int(contract_data.get("coin", 0))
var pressure_status := _status_slot(overlay, "GuildProgressStatus", Rect2(0.595, 0.020, 0.110, 0.090), pressure_copy, "pressure", page_data)
pressure_status.tooltip_text = localize("当前委托压力", "Current Contract pressure", page_data)
var coin_status := _status_slot(overlay, "GuildCoinStatus", Rect2(0.735, 0.020, 0.100, 0.090), coin_copy, "coin", page_data)
coin_status.tooltip_text = localize("当前委托金币", "Current Contract Coin", page_data)
var company_status := _status_slot(overlay, "GuildAnchorCompanyStatus", Rect2(0.842, 0.020, 0.076, 0.090), "%d" % int(company_data.get("owned_count", 0)), "company", page_data)
company_status.tooltip_text = localize("远征团成员数", "Expedition Company members", page_data)
_add_icon_button(overlay, "GuildSettings", localize("设置", "Settings", page_data), "settings", "settings", Rect2(0.925, 0.010, 0.073, 0.082), page_data)
var title_button := _hit_button("GuildTitle", localize("返回标题", "Back to Title", page_data), "title", Rect2(0.000, 0.010, 0.075, 0.082))
title_button.set_meta("art_owned_control", "GuildTopRailLeft")
title_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
var title_command_label := make_label(localize("标题", "Title", page_data), _guild_text_size(8), Color.TRANSPARENT)
title_command_label.name = "GuildTitleCommandLabel"
title_command_label.visible = false
title_command_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
title_button.add_child(title_command_label)
overlay.add_child(title_button)
var roster := Control.new()
roster.name = "GuildRoster"
roster.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
roster.mouse_filter = Control.MOUSE_FILTER_IGNORE
overlay.add_child(roster)
var display_recruits: Array = Array(company_data.get("display_recruits", []))
for index in range(display_recruits.size()):
var recruit: Dictionary = Dictionary(display_recruits[index]).duplicate(true)
# Keep the visible feet on one shared floor line. The previous wide slots
# overlapped and made the old portrait stickers look like a single pile of
# characters on top of the painted hall.
var member_rect := Rect2(0.220 + index * 0.205, 0.300, 0.175, 0.515)
roster.add_child(_desk_member(index, recruit, member_rect))
var identity_copy := "%s · %s" % [_recruit_name(recruit, page_data), _recruit_profession_name(recruit, page_data)]
# M65-B: the member name tag is a shared parchment chip with ink copy;
# the authored plate node name stays for the view contract.
var identity_plate := Control.new()
identity_plate.name = "GuildMasterIdentityPlate%d" % index
_set_normalized_rect(identity_plate, Rect2(0.220 + index * 0.205, 0.808, 0.175, 0.048))
identity_plate.mouse_filter = Control.MOUSE_FILTER_IGNORE
overlay.add_child(identity_plate)
var identity_chrome := PageChrome.new().configure("card")
identity_chrome.name = "IdentityChrome"
identity_chrome.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
identity_chrome.mouse_filter = Control.MOUSE_FILTER_IGNORE
identity_plate.add_child(identity_chrome)
var identity := make_label(identity_copy, _guild_text_size(8), PageChrome.INK)
identity.name = "GuildMasterIdentity%d" % index
identity.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
identity.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
identity.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
identity.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
identity.mouse_filter = Control.MOUSE_FILTER_IGNORE
identity_plate.add_child(identity)
var kiosk := TextureRect.new()
kiosk.name = "GuildMasterKioskTexture"
kiosk.texture = GUILD_DESK_CONTRACT_BOARD
kiosk.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
kiosk.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
kiosk.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
_set_normalized_rect(kiosk, Rect2(0.615, 0.205, 0.370, 0.655))
kiosk.mouse_filter = Control.MOUSE_FILTER_IGNORE
# The approved M52 guild background already contains the authored notice
# board. Runtime copy and the hit area remain separate from that art.
kiosk.visible = false
overlay.add_child(kiosk)
var kiosk_copy_area := Control.new()
kiosk_copy_area.name = "GuildKioskCopyArea"
kiosk_copy_area.set_meta("ui_module", "GuildContractBoard")
# Keep all runtime copy inside the cream notice-board surface of the
# authored background; the previous lower slot landed on the counter.
_set_normalized_rect(kiosk_copy_area, Rect2(0.692, 0.145, 0.255, 0.395))
kiosk_copy_area.mouse_filter = Control.MOUSE_FILTER_IGNORE
overlay.add_child(kiosk_copy_area)
var kiosk_title := localize("远征市场", "EXPEDITION MARKET", page_data) if active_contract else localize("委托已返回", "CONTRACT RETURNED", page_data) if returned_contract else localize("委托板", "CONTRACT BOARD", page_data)
var kiosk_text := make_label(kiosk_title, _guild_text_size(13), PageChrome.INK)
kiosk_text.name = "GuildKioskText"
kiosk_text.set_meta("ui_module", "GuildContractBoardTitle")
_set_normalized_rect(kiosk_text, Rect2(0.03, 0.04, 0.94, 0.20))
kiosk_text.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
kiosk_text.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
kiosk_text.clip_text = true
kiosk_text.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
kiosk_text.mouse_filter = Control.MOUSE_FILTER_IGNORE
kiosk_copy_area.add_child(kiosk_text)
var contract_copy := localize("委托进行中", "CONTRACT ACTIVE", page_data) if active_contract else localize("委托已返回 · 成长保留", "CONTRACT RETURNED · GROWTH RETAINED", page_data) if returned_contract else localize("可用 · 选择难度", "AVAILABLE · CHOOSE DIFFICULTY", page_data)
var contract_reason := make_label(contract_copy, _guild_text_size(9), PageChrome.INK)
contract_reason.name = "GuildContractReason"
contract_reason.set_meta("ui_module", "GuildContractBoardStatus")
_set_normalized_rect(contract_reason, Rect2(0.04, 0.31, 0.92, 0.24))
contract_reason.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
contract_reason.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
contract_reason.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
contract_reason.clip_text = true
contract_reason.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
contract_reason.mouse_filter = Control.MOUSE_FILTER_IGNORE
kiosk_copy_area.add_child(contract_reason)
var market_copy := localize("市场已开放", "Market open", page_data) if active_contract else localize("市场锁定 · 委托已返回", "Market locked · Contract returned", page_data) if returned_contract else localize("市场锁定 · 先确认首个委托", "Market locked · Confirm first Contract", page_data)
var market_reason := make_label(market_copy, _guild_text_size(9), PageChrome.INK_SOFT)
market_reason.name = "GuildMarketReason"
market_reason.set_meta("ui_module", "GuildContractBoardMarketStatus")
_set_normalized_rect(market_reason, Rect2(0.04, 0.58, 0.92, 0.28))
market_reason.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
market_reason.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
market_reason.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
market_reason.clip_text = true
market_reason.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
market_reason.mouse_filter = Control.MOUSE_FILTER_IGNORE
kiosk_copy_area.add_child(market_reason)
var notice := make_label(String(page_data.get("notice", "")), _guild_text_size(8), UITokens.COLOR_DANGER)
notice.name = "GuildNotice"
notice.set_meta("ui_module", "GuildContractBoardNotice")
_set_normalized_rect(notice, Rect2(0.03, 0.86, 0.94, 0.13))
notice.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
notice.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
notice.clip_text = true
notice.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
notice.mouse_filter = Control.MOUSE_FILTER_IGNORE
kiosk_copy_area.add_child(notice)
for legacy_copy in [kiosk_text, contract_reason, market_reason, notice]:
legacy_copy.visible = false
var contract_board := M52_GUILD_CONTRACT_BOARD.new().configure(kiosk_title, contract_copy, market_copy, String(page_data.get("notice", "")))
contract_board.name = "GuildContractBoardContent"
contract_board.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
contract_board.mouse_filter = Control.MOUSE_FILTER_IGNORE
kiosk_copy_area.add_child(contract_board)
var equipment_state: Dictionary = Dictionary(destinations.get("equipment", {}))
var party_state: Dictionary = Dictionary(destinations.get("party", {}))
var risk_state: Dictionary = Dictionary(destinations.get("risk", {}))
_add_nav_button(overlay, "GuildRosterButton", "GuildRosterCommandLabel", localize("成员", "Roster", page_data), "roster", "roster", 0, false, page_data)
_add_nav_button(overlay, "GuildEquipment", "GuildEquipmentCommandLabel", localize("装备", "Gear", page_data), "equipment", "equipment", 1, not bool(equipment_state.get("enabled", false)), page_data)
_add_nav_button(overlay, "GuildParty", "GuildPartyCommandLabel", localize("队列", "Party", page_data), "party", "party", 2, not bool(party_state.get("enabled", false)), page_data)
_add_nav_button(overlay, "GuildRisk", "GuildRouteStatusLabel", localize("风险", "Risk", page_data), "risk", "risk", 3, not bool(risk_state.get("enabled", false)), page_data)
var contract_state: Dictionary = Dictionary(destinations.get("contract", {}))
var market_state: Dictionary = Dictionary(destinations.get("market", {}))
var contract_enabled := bool(contract_state.get("enabled", false))
var contract := _hit_button(
"GuildContractActive" if has_contract else "GuildContractBoard",
localize("查看委托板", "Open Contract Board", page_data),
"contract",
Rect2(0.615, 0.205, 0.370, 0.655)
)
contract.disabled = not contract_enabled
if not contract_enabled:
contract.mouse_filter = Control.MOUSE_FILTER_IGNORE
overlay.add_child(contract)
var market_enabled := bool(market_state.get("enabled", false))
var market := _hit_button(
"GuildMarket" if market_enabled else "GuildMarketUnavailable",
localize("进入市场", "Enter Market", page_data),
"market",
Rect2(0.850, 0.862, 0.140, 0.080)
)
market.disabled = not market_enabled
overlay.add_child(market)
for marker_name in ["GuildContractSticker", "GuildMarketSticker"]:
var marker := Control.new()
marker.name = marker_name
marker.visible = false
overlay.add_child(marker)
contract.call_deferred("grab_focus")
func _render_invalid(data: Dictionary) -> void:
var invalid := Control.new()
invalid.name = "GuildInvalidOverlay"
invalid.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
invalid.mouse_filter = Control.MOUSE_FILTER_IGNORE
design_canvas.add_child(invalid)
var title := make_label(localize("公会不可用", "GUILD UNAVAILABLE", data), _guild_text_size(16), Color("#f5ead0"))
_set_normalized_rect(title, Rect2(0.24, 0.40, 0.52, 0.08))
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
var detail := make_label(String(data.get("notice", data.get("code", ""))), _guild_text_size(9), Color("#ffd4d4"))
_set_normalized_rect(detail, Rect2(0.20, 0.49, 0.60, 0.12))
detail.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
invalid.add_child(title)
invalid.add_child(detail)
func _add_segmented_rail(parent: Control, node_name: String, rect: Rect2, left_texture: Texture2D, center_texture: Texture2D, right_texture: Texture2D, left_fraction: float, right_fraction: float) -> Control:
var rail := Control.new()
rail.name = node_name
_set_normalized_rect(rail, rect)
rail.mouse_filter = Control.MOUSE_FILTER_IGNORE
parent.add_child(rail)
# M65-A: the shared band owns the visible rail; the authored segment nodes
# stay as named compatibility shells (the M63 pattern).
var band_variant := "dock_band" if node_name == "GuildBottomRail" else "header_band"
var band := PageChrome.new().configure(band_variant)
band.name = "%sBand" % node_name
band.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
band.mouse_filter = Control.MOUSE_FILTER_IGNORE
rail.add_child(band)
var left := TextureRect.new()
left.name = "%sLeft" % node_name
left.texture = left_texture
left.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
left.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
left.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
left.anchor_left = 0.0
left.anchor_top = 0.0
left.anchor_right = left_fraction
left.anchor_bottom = 1.0
left.offset_left = 0
left.offset_top = 0
left.offset_right = 0
left.offset_bottom = 0
left.mouse_filter = Control.MOUSE_FILTER_IGNORE
left.modulate.a = 0.0
rail.add_child(left)
var center := TextureRect.new()
center.name = "%sCenter" % node_name
center.texture = center_texture
center.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
center.stretch_mode = TextureRect.STRETCH_SCALE
center.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
center.anchor_left = left_fraction
center.anchor_right = 1.0 - right_fraction
center.anchor_bottom = 1.0
center.offset_left = 0
center.offset_top = 0
center.offset_right = 0
center.offset_bottom = 0
center.mouse_filter = Control.MOUSE_FILTER_IGNORE
center.modulate.a = 0.0
rail.add_child(center)
var right := TextureRect.new()
right.name = "%sRight" % node_name
right.texture = right_texture
right.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
right.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
right.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
right.anchor_left = 1.0 - right_fraction
right.anchor_right = 1.0
right.anchor_bottom = 1.0
right.offset_left = 0
right.offset_top = 0
right.offset_right = 0
right.offset_bottom = 0
right.mouse_filter = Control.MOUSE_FILTER_IGNORE
right.modulate.a = 0.0
rail.add_child(right)
return rail
func _status_slot(parent: Control, node_name: String, rect: Rect2, value: String, icon_kind: String, data: Dictionary) -> Label:
var slot := Control.new()
slot.name = "%sSlot" % node_name
_set_normalized_rect(slot, rect)
slot.mouse_filter = Control.MOUSE_FILTER_IGNORE
parent.add_child(slot)
var pill := Panel.new()
pill.name = "%sPill" % node_name
pill.anchor_left = 0.25
pill.anchor_top = 0.24
pill.anchor_right = 0.98
pill.anchor_bottom = 0.76
pill.mouse_filter = Control.MOUSE_FILTER_IGNORE
slot.add_child(pill)
var pill_chrome := PageChrome.new().configure("card")
pill_chrome.name = "%sCard" % node_name
pill_chrome.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
pill_chrome.mouse_filter = Control.MOUSE_FILTER_IGNORE
pill.add_child(pill_chrome)
var badge := Control.new()
badge.name = "%sIcon" % node_name
badge.anchor_left = 0.00
badge.anchor_top = 0.02
badge.anchor_right = 0.36
badge.anchor_bottom = 0.98
badge.mouse_filter = Control.MOUSE_FILTER_IGNORE
badge.add_child(make_entry_chrome("Chrome", "guild_status_icon", icon_kind))
slot.add_child(badge)
var label := make_label(value, _guild_text_size(8), Color("#2f3130"))
label.name = node_name
label.anchor_left = 0.36
label.anchor_top = 0.24
label.anchor_right = 0.96
label.anchor_bottom = 0.76
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
slot.add_child(label)
# Keep the existing semantic slot/chrome for compatibility, but let the
# shared M52 resource module own the icon/value geometry.
badge.visible = false
# The accepted guild background already carries the compact nav captions;
# keep the hit-area label available for accessibility without painting a
# second caption over the authored medallion.
label.visible = false
label.z_index = 8
label.add_theme_font_size_override("font_size", _guild_text_size(9))
label.clip_text = true
label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
var resource_module := M52_GUILD_RESOURCE_STATUS.new().configure(value, icon_kind)
resource_module.name = "ResourceStatusModule"
resource_module.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
resource_module.mouse_filter = Control.MOUSE_FILTER_IGNORE
slot.add_child(resource_module)
return label
func _add_icon_button(parent: Control, node_name: String, tooltip: String, icon_kind: String, intent_id: String, rect: Rect2, data: Dictionary) -> Button:
var button := _hit_button(node_name, tooltip, intent_id, rect)
# M65-A: shared utility plate + shared icon; `Chrome` name kept for contract.
var chrome := PageChrome.new().configure("utility_plate")
chrome.name = "Chrome"
chrome.anchor_left = 0.20
chrome.anchor_right = 0.95
chrome.anchor_top = 0.10
chrome.anchor_bottom = 0.90
chrome.mouse_filter = Control.MOUSE_FILTER_IGNORE
button.add_child(chrome)
UIFactory.attach_icon(button, icon_kind, "Icon", "dark", Rect2(0.28, 0.16, 0.44, 0.68))
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
parent.add_child(button)
return button
func _add_nav_button(parent: Control, node_name: String, label_name: String, title: String, intent_id: String, icon_kind: String, index: int, disabled: bool, data: Dictionary) -> Button:
var button := _hit_button(node_name, title, intent_id, Rect2())
_set_bottom_nav_rect(button, index)
button.disabled = disabled
# M65-A: shared plate + shared icon; the authored `Chrome` node name stays.
var nav_plate := PageChrome.new().configure("command_plate", {"state": "disabled" if disabled else "secondary"})
nav_plate.name = "Chrome"
nav_plate.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
nav_plate.mouse_filter = Control.MOUSE_FILTER_IGNORE
button.add_child(nav_plate)
var nav_icon := UIFactory.attach_icon(button, icon_kind, "NavIcon", "dark", Rect2(0.30, 0.10, 0.40, 0.46))
nav_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
var label := make_label(title, _guild_text_size(10), UITokens.COLOR_MUTED if disabled else PageChrome.INK)
label.name = label_name
label.z_index = 12
label.visible = true
label.anchor_left = 0.18
label.anchor_top = 0.58
label.anchor_right = 0.94
label.anchor_bottom = 0.95
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.clip_text = true
label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
button.add_child(label)
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
parent.add_child(button)
return button
func _desk_member(index: int, recruit: Dictionary, rect: Rect2) -> Control:
var member := M52_GUILD_MEMBER_CARD.new().configure(index, recruit)
member.name = "GuildMember_%d" % index
_set_normalized_rect(member, rect)
return member
func _hit_button(node_name: String, tooltip: String, intent_id: String, rect: Rect2) -> Button:
var button := Button.new()
button.name = node_name
button.flat = true
button.tooltip_text = tooltip
button.focus_mode = Control.FOCUS_ALL
_set_normalized_rect(button, rect)
for style_name in ["normal", "hover", "pressed", "disabled", "focus", "hover_pressed"]:
button.add_theme_stylebox_override(style_name, StyleBoxEmpty.new())
button.pressed.connect(func(): emit_intent(intent_id), CONNECT_DEFERRED)
return button
func _overlay_plate(parent: Control, node_name: String, rect: Rect2, background: Color, border: Color, border_width: int, radius: int) -> Panel:
var plate := Panel.new()
plate.name = node_name
_set_normalized_rect(plate, rect)
plate.mouse_filter = Control.MOUSE_FILTER_IGNORE
plate.add_theme_stylebox_override("panel", _flat_style(background, border, border_width, radius))
parent.add_child(plate)
return plate
func _set_normalized_rect(control: Control, rect: Rect2) -> void:
control.anchor_left = rect.position.x
control.anchor_top = rect.position.y
control.anchor_right = rect.end.x
control.anchor_bottom = rect.end.y
control.offset_left = 0
control.offset_top = 0
control.offset_right = 0
control.offset_bottom = 0
func _set_bottom_nav_rect(control: Control, index: int) -> void:
var canvas_height := design_canvas.size.y if is_instance_valid(design_canvas) else size.y
var target_size := clampf(canvas_height * 0.145, 50.0, 76.0)
var gap := clampf(canvas_height * 0.012, 4.0, 8.0)
var start_x := clampf(canvas_height * 0.035, 10.0, 25.0)
var center_y := minf(canvas_height * 0.945, canvas_height - target_size * 0.5 - 2.0)
control.set_anchors_preset(Control.PRESET_TOP_LEFT)
control.position = Vector2(start_x + float(index) * (target_size + gap), center_y - target_size * 0.5)
control.size = Vector2(target_size, target_size)
func _refresh_typography() -> void:
var label_sizes := {
"GuildAnchorTitle": 15,
"GuildAnchorStatus": 8,
"GuildProgressStatus": 9,
"GuildCoinStatus": 9,
"GuildAnchorCompanyStatus": 9,
"GuildKioskText": 11,
"GuildContractReason": 8,
"GuildMarketReason": 8,
"GuildNotice": 8,
"GuildMasterIdentity0": 8,
"GuildMasterIdentity1": 8,
"GuildTitleCommandLabel": 8,
"GuildRosterCommandLabel": 8,
"GuildEquipmentCommandLabel": 8,
"GuildPartyCommandLabel": 8,
"GuildRouteStatusLabel": 8,
}
for node_name in label_sizes:
var label := find_child(String(node_name), true, false) as Label
if label != null:
label.add_theme_font_size_override("font_size", _guild_text_size(int(label_sizes[node_name])))
if label.has_theme_constant_override("outline_size"):
label.add_theme_constant_override("outline_size", maxi(1, _guild_text_size(1)))
func _refresh_bottom_nav_geometry() -> void:
for index in range(4):
var button_names := ["GuildRosterButton", "GuildEquipment", "GuildParty", "GuildRisk"]
var button := find_child(button_names[index], true, false) as Button
if button != null:
_set_bottom_nav_rect(button, index)
func _guild_text_size(design_size: int) -> int:
var canvas_height := design_canvas.size.y if is_instance_valid(design_canvas) else size.y
var height_ratio := clampf(canvas_height / 360.0, 1.0, 2.0)
var canvas_scale := lerpf(1.0, height_ratio, 0.35)
var text_scale := float(presentation.get("text_scale", 1.0))
return maxi(7, int(round(float(design_size) * canvas_scale * text_scale)))
func _difficulty_name(value: String, data: Dictionary) -> String:
match value:
"wayfinder": return localize("引路", "Wayfinder", data)
"stormbound": return localize("风暴", "Stormbound", data)
"iron_oath": return localize("铁誓", "Iron Oath", data)
return localize("标准", "Standard", data)
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 _recruit_profession_name(recruit: Dictionary, data: Dictionary) -> String:
var localized: Dictionary = Dictionary(recruit.get("profession_name", {}))
var locale := page_locale(data)
return String(localized.get(locale, localized.get("en", recruit.get("profession_id", "Unknown Profession"))))
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