317 lines
15 KiB
GDScript
317 lines
15 KiB
GDScript
class_name ABGContractChoicePageView
|
|
extends "res://presentation/page_view.gd"
|
|
|
|
## UI-only Contract Choice View.
|
|
##
|
|
## The View owns the framed composition, Company portraits, local difficulty
|
|
## selection controls, focus and explicit intents. The supplied snapshot is
|
|
## already copied and presentation-ready; application decisions stay with
|
|
## the composition root.
|
|
|
|
const CONTENT_BACKGROUND = preload("res://assets/backgrounds/first_expedition_field.png")
|
|
const M52_CONTRACT_BACKGROUND = preload("res://assets/backgrounds/m52_contract_background.png")
|
|
# M69: character-free world master (the painted duo was removed so the real
|
|
# Company pair can stand in the authored spot). Old master kept for provenance.
|
|
const M52_Q_CONTRACT_BACKGROUND = preload("res://assets/backgrounds/m69_contract_world.png")
|
|
const InitialCompanyArt = preload("res://entry/initial_company_art.gd")
|
|
const GuildDestinationSticker = preload("res://entry/guild_destination_sticker.gd")
|
|
const M54_CONTRACT_HEADER = preload("res://ui/m54/contract_header.gd")
|
|
const M54_CONTRACT_BOARD = preload("res://ui/m54/contract_board.gd")
|
|
const M54_CONTRACT_PARTY_SUMMARY = preload("res://ui/m54/contract_party_summary.gd")
|
|
const M54_CONTRACT_ACTION_DOCK = preload("res://ui/m54/contract_action_dock.gd")
|
|
const M54_CONTRACT_NAVIGATION = preload("res://ui/m54/contract_navigation.gd")
|
|
const PageChrome = preload("res://ui/shared/page_chrome.gd")
|
|
const UITokens = preload("res://foundation/ui_tokens.gd")
|
|
|
|
|
|
func _render_view(data: Dictionary) -> void:
|
|
clear_view_children()
|
|
_build_world_base()
|
|
if not bool(data.get("ok", false)):
|
|
_render_invalid(data)
|
|
return
|
|
|
|
var frame_safe := make_framed_page_safe("ContractChoice")
|
|
# M65-C: the shared parchment card owns the frame; the authored node name is
|
|
# kept for the page contract.
|
|
# The Q route illustration owns the scene, so the shared card only acts as a
|
|
# dimmed vignette; the header band and review column carry the copy.
|
|
var frame_chrome := PageChrome.new().configure("card")
|
|
frame_chrome.name = "ContractChoiceFrame"
|
|
frame_chrome.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
frame_chrome.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
# The illustration is the page identity; keep the named frame as an
|
|
# invisible compatibility shell (the M63 pattern) instead of covering it.
|
|
frame_chrome.modulate.a = 0.0
|
|
frame_safe.add_child(frame_chrome)
|
|
var safe := make_content_page_safe("ContractChoice")
|
|
var layout := VBoxContainer.new()
|
|
layout.name = "ContractChoiceLayout"
|
|
layout.add_theme_constant_override("separation", 6)
|
|
safe.add_child(layout)
|
|
|
|
var company_data: Dictionary = Dictionary(data.get("company", {}))
|
|
var contract_data: Dictionary = Dictionary(data.get("contract", {}))
|
|
var selected_difficulty := String(data.get("selected_difficulty", "standard"))
|
|
var difficulties: Array = Array(data.get("difficulties", []))
|
|
|
|
var header_host := Control.new()
|
|
header_host.name = "ContractHeaderHost"
|
|
header_host.custom_minimum_size = Vector2(0, 68)
|
|
layout.add_child(header_host)
|
|
var header_band := PageChrome.new().configure("header_band")
|
|
header_band.name = "ContractHeaderBand"
|
|
header_band.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
header_band.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
header_host.add_child(header_band)
|
|
var header := M54_CONTRACT_HEADER.new().configure(
|
|
localize("委托板", "CONTRACT BOARD", data),
|
|
localize("风镜原野 · 首次远征", "WINDGLASS VERGE · FIRST EXPEDITION", data),
|
|
localize("远征团 %d · 持续保留", "COMPANY %d · PERSISTENT", data) % int(company_data.get("owned_count", 0))
|
|
)
|
|
header.name = "ContractHeader"
|
|
header.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
# Seat the copy on the band face (measured rolled edges).
|
|
header.offset_left = 1280.0 * PageChrome.PANEL_EDGE_X * 0.5
|
|
header.offset_right = -1280.0 * PageChrome.PANEL_EDGE_X * 0.5
|
|
header.offset_top = 68.0 * PageChrome.PANEL_EDGE_Y * 0.5
|
|
header.offset_bottom = 0.0
|
|
header_host.add_child(header)
|
|
|
|
var body := HBoxContainer.new()
|
|
body.name = "ContractChoiceBody"
|
|
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
body.add_theme_constant_override("separation", 18)
|
|
layout.add_child(body)
|
|
|
|
var scene := VBoxContainer.new()
|
|
scene.name = "ContractBoardScene"
|
|
scene.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
scene.size_flags_stretch_ratio = 1.25
|
|
scene.add_theme_constant_override("separation", 2)
|
|
body.add_child(scene)
|
|
var board := M54_CONTRACT_BOARD.new().configure(
|
|
String(contract_data.get("route_title", localize("风镜初渡", "FIRST WINDGLASS CROSSING", data))),
|
|
String(contract_data.get("route_copy", localize("穿越悬浮风镜,建立公会的第一条安全通路。", "Cross the suspended windglass and establish the Guild's first safe route.", data))),
|
|
Array(company_data.get("recruits", []))
|
|
)
|
|
board.name = "ContractBoard"
|
|
board.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
scene.add_child(board)
|
|
# Preserve stable legacy nodes for the View contract while the new board owns
|
|
# the visible party composition.
|
|
var legacy_scene := _contract_company_scene(Array(company_data.get("recruits", [])))
|
|
legacy_scene.name = "ContractCompanyScene"
|
|
# M69: the stage is a page-level overlay so the real Company pair stands in
|
|
# the authored lower-left spot of the (now character-free) route master.
|
|
legacy_scene.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
legacy_scene.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
legacy_scene.z_index = 5
|
|
safe.add_child(legacy_scene)
|
|
|
|
var review_host := Control.new()
|
|
review_host.name = "ContractReviewHost"
|
|
review_host.custom_minimum_size = Vector2(minf(360.0, size.x * 0.42), 300.0)
|
|
review_host.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
|
body.add_child(review_host)
|
|
var review_card := PageChrome.new().configure("card")
|
|
review_card.name = "ContractReviewCard"
|
|
review_card.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
review_card.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
# The authored route board already is the parchment surface; keep the named
|
|
# card node for the page contract without stacking a second sheet.
|
|
review_card.modulate.a = 0.0
|
|
review_host.add_child(review_card)
|
|
var review := VBoxContainer.new()
|
|
review.name = "ContractReview"
|
|
review.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
review.offset_left = 12.0
|
|
review.offset_right = -12.0
|
|
review.offset_top = 12.0
|
|
review.offset_bottom = -12.0
|
|
review.add_theme_constant_override("separation", 5)
|
|
review_host.add_child(review)
|
|
review.add_child(make_label(localize("选择难度", "CHOOSE DIFFICULTY", data), scaled(11), PageChrome.INK_SOFT))
|
|
var segments := HBoxContainer.new()
|
|
segments.name = "ContractDifficultySegments"
|
|
segments.custom_minimum_size = Vector2(0, 48)
|
|
segments.add_theme_constant_override("separation", 3)
|
|
review.add_child(segments)
|
|
for option_value in difficulties:
|
|
if option_value is Dictionary:
|
|
segments.add_child(_contract_segment_button(Dictionary(option_value), selected_difficulty, data))
|
|
var selected_option := _difficulty_option(difficulties, selected_difficulty, data)
|
|
var difficulty_copy := make_label(String(selected_option.get("copy", "")), scaled(11), PageChrome.INK)
|
|
difficulty_copy.name = "ContractDifficultyCopy"
|
|
difficulty_copy.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
difficulty_copy.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
review.add_child(difficulty_copy)
|
|
|
|
var market_counts: Dictionary = Dictionary(contract_data.get("market_counts", {}))
|
|
var facts_text := localize(
|
|
"建立后:%d 金币 · 队列容量 %d\n市场:%d 名招募 · %d 件装备 · %d 条战斗路线",
|
|
"On creation: %d Coin · Line capacity %d\nMarket: %d Recruits · %d equipment · %d battle routes",
|
|
data
|
|
) % [
|
|
int(contract_data.get("starting_coin", 0)),
|
|
int(contract_data.get("line_capacity", 0)),
|
|
int(market_counts.get("recruit", 0)),
|
|
int(market_counts.get("equipment", 0)),
|
|
int(market_counts.get("battle", 0)),
|
|
]
|
|
var party_summary := M54_CONTRACT_PARTY_SUMMARY.new().configure(facts_text)
|
|
party_summary.name = "ContractPartySummary"
|
|
# M62-I04: the shipped two-line fact block needs 56px at label size; the old
|
|
# 48px slot silently trimmed it.
|
|
party_summary.custom_minimum_size = Vector2(0, 56)
|
|
review.add_child(party_summary)
|
|
var notice := make_label(String(data.get("notice", "")), scaled(10), UITokens.COLOR_DANGER)
|
|
notice.name = "ContractNotice"
|
|
notice.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
review.add_child(notice)
|
|
|
|
var footer := M54_CONTRACT_ACTION_DOCK.new().configure(
|
|
localize("返回公会", "Back to Guild", data),
|
|
localize("建立委托", "Create Contract", data),
|
|
bool(Dictionary(data.get("confirm", {})).get("eligible", false))
|
|
)
|
|
footer.name = "ContractChoiceFooter"
|
|
layout.add_child(footer)
|
|
footer.action_pressed.connect(func(intent_id: String): emit_intent(intent_id))
|
|
var navigation := M54_CONTRACT_NAVIGATION.new().configure(localize("返回公会", "Back to Guild", data))
|
|
navigation.name = "ContractNavigation"
|
|
navigation.visible = false
|
|
layout.add_child(navigation)
|
|
|
|
var selected_index := _difficulty_index(difficulties, selected_difficulty)
|
|
if selected_index >= 0 and selected_index < segments.get_child_count():
|
|
segments.get_child(selected_index).call_deferred("grab_focus")
|
|
|
|
|
|
func _build_world_base() -> void:
|
|
var background := TextureRect.new()
|
|
background.name = "WorldBackground"
|
|
background.texture = M52_Q_CONTRACT_BACKGROUND
|
|
background.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
|
background.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
|
|
background.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
background.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
add_child(background)
|
|
var shade := ColorRect.new()
|
|
shade.color = Color(0.03, 0.06, 0.08, 0.12)
|
|
shade.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
shade.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
add_child(shade)
|
|
|
|
|
|
func _contract_company_scene(recruits: Array) -> Control:
|
|
var stage := Control.new()
|
|
stage.name = "ContractCompanyScene"
|
|
stage.clip_contents = false
|
|
for index in range(mini(2, recruits.size())):
|
|
var recruit: Dictionary = Dictionary(recruits[index])
|
|
var portrait := TextureRect.new()
|
|
portrait.name = "ContractCompanyMember_%d" % index
|
|
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
|
|
# The generated route already owns the scenic composition. Keep the
|
|
# runtime Company portraits in compact, non-overlapping slots with a
|
|
# common baseline so they do not cover the route board or each other.
|
|
# Page-relative spots where the authored duo used to stand.
|
|
portrait.anchor_left = 0.035 + float(index) * 0.135
|
|
portrait.anchor_top = 0.42
|
|
portrait.anchor_right = 0.185 + float(index) * 0.135
|
|
portrait.anchor_bottom = 0.95
|
|
# M69: the world master is character-free and the identity-locked soft
|
|
# Q sprite set has shipped, so the stable runtime member nodes own the
|
|
# two authored expedition spots.
|
|
portrait.visible = true
|
|
portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
stage.add_child(portrait)
|
|
var board := GuildDestinationSticker.new().configure("contract")
|
|
board.name = "ContractBoardSticker"
|
|
board.anchor_left = 0.39
|
|
board.anchor_top = 0.20
|
|
board.anchor_right = 0.61
|
|
board.anchor_bottom = 0.78
|
|
# The Q route background already contains the authored parchment destination
|
|
# board; retain this node for the contract shape but avoid drawing a second
|
|
# opaque sticker over the illustration.
|
|
board.visible = false
|
|
board.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
stage.add_child(board)
|
|
return stage
|
|
|
|
|
|
func _contract_segment_button(option: Dictionary, selected_difficulty: String, data: Dictionary) -> Button:
|
|
var difficulty := String(option.get("id", "standard"))
|
|
var selected := bool(option.get("selected", difficulty == selected_difficulty))
|
|
var node_name := "ContractDifficulty%s" % difficulty.to_pascal_case().replace("_", "")
|
|
var button := Button.new()
|
|
button.name = node_name
|
|
button.flat = true
|
|
button.custom_minimum_size = Vector2(0, 48)
|
|
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
button.tooltip_text = String(option.get("copy", ""))
|
|
button.focus_mode = Control.FOCUS_ALL
|
|
for style_name in ["normal", "hover", "pressed", "disabled", "focus", "hover_pressed"]:
|
|
button.add_theme_stylebox_override(style_name, StyleBoxEmpty.new())
|
|
var chrome := PageChrome.new().configure("command_plate", {"state": "primary" if selected else "secondary"})
|
|
chrome.name = "Chrome"
|
|
chrome.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
chrome.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
button.add_child(chrome)
|
|
var label := make_label(String(option.get("name", difficulty)), scaled(10), PageChrome.INK)
|
|
label.name = "ButtonLabel"
|
|
label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
label.offset_left = 5.0
|
|
label.offset_right = -5.0
|
|
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
|
|
button.add_child(label)
|
|
button.set_meta("selected", selected)
|
|
button.set_meta("difficulty", difficulty)
|
|
button.pressed.connect(func(): emit_intent("select_difficulty", {"difficulty": difficulty}), CONNECT_DEFERRED)
|
|
return button
|
|
|
|
|
|
func _difficulty_option(options: Array, selected_difficulty: String, data: Dictionary) -> Dictionary:
|
|
for option_value in options:
|
|
if option_value is Dictionary and String(option_value.get("id", "")) == selected_difficulty:
|
|
return Dictionary(option_value)
|
|
return {
|
|
"id": selected_difficulty,
|
|
"name": selected_difficulty,
|
|
"copy": localize("请选择一个有效难度。", "Choose a valid difficulty.", data),
|
|
}
|
|
|
|
|
|
func _difficulty_index(options: Array, selected_difficulty: String) -> int:
|
|
for index in range(options.size()):
|
|
if options[index] is Dictionary and String(options[index].get("id", "")) == selected_difficulty:
|
|
return index
|
|
return -1
|
|
|
|
|
|
func _render_invalid(data: Dictionary) -> void:
|
|
var safe := make_content_page_safe("ContractChoice")
|
|
var invalid := VBoxContainer.new()
|
|
invalid.name = "ContractChoiceInvalidOverlay"
|
|
invalid.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
|
|
invalid.custom_minimum_size = Vector2(420, 120)
|
|
invalid.add_theme_constant_override("separation", 8)
|
|
safe.add_child(invalid)
|
|
var title := make_label(localize("委托不可用", "CONTRACT CHOICE UNAVAILABLE", data), scaled(20), PageChrome.INK)
|
|
title.name = "ContractChoiceInvalidTitle"
|
|
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
invalid.add_child(title)
|
|
var detail := make_label(String(data.get("notice", data.get("code", ""))), scaled(11), UITokens.COLOR_DANGER)
|
|
detail.name = "ContractChoiceInvalidDetail"
|
|
detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
detail.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
invalid.add_child(detail)
|