Files
aetherbound-guild/runtime/entry/initial_company_page.gd
T

862 lines
39 KiB
GDScript

class_name ABGInitialCompanyPage
extends Control
const PageChrome = preload("res://ui/shared/page_chrome.gd")
const UITokens = preload("res://foundation/ui_tokens.gd")
const InitialCompanyArt = preload("res://entry/initial_company_art.gd")
const GUILD_BACKGROUND = preload("res://assets/backgrounds/p9_3_guild_hall.png")
const UI_FONT = preload("res://assets/fonts/NotoSansSC.ttf")
signal back_requested
signal company_committed(company: Dictionary)
const ATTRIBUTE_ORDER := ["might", "finesse", "insight", "vigor", "resolve"]
const PROFESSION_JOB_ZH := {
"PF-B01": "前排守护", "PF-B02": "单体压制", "PF-B03": "中排控距",
"PF-B04": "换位机动", "PF-B05": "后排射击", "PF-B06": "后排范围法术",
"PF-B07": "治疗净化", "PF-B08": "邻位支援", "PF-B09": "装置构筑",
"PF-B10": "妨害延续", "PF-B11": "节奏调度", "PF-B12": "物资支援",
}
const TRAIT_ZH := {
"TR-001": ["成为最前位后的首次行动距离 +1。", "该行动不能以当前最前敌人为目标。"],
"TR-002": ["两名队友行动后,首次普攻获得 1 点职业资源。", "首次普攻延后一档节奏。"],
"TR-003": ["仅有一名邻位时,免费拦截一次非范围攻击。", "有两名邻位时不能接收转移的护卫。"],
"TR-004": ["重复合法目标会让状态延长 1 tick。", "改变目标会让下次行动延后 1 tick。"],
"TR-005": ["换位时可携带一个装置或召唤单位。", "被携带对象失去下一次自动行动。"],
"TR-006": ["被推拉后,若可达则立即反击施力者。", "反击完成前自身进入暴露。"],
"TR-007": ["首个消耗型装备触发可作用于第二个合法目标。", "两次触发的冷却都延长一档。"],
"TR-008": ["身前有两名队友时,不会被声音类敌方行动选中。", "自身声音类行动不产生职业资源。"],
"TR-009": ["抵抗首次拉拽,并把位移转给前方邻位。", "邻位无法移动时,两人都陷入迟滞。"],
"TR-010": ["受到有效治疗时,也小幅治疗最低生命邻位。", "直接自疗效果降低。"],
"TR-011": ["同等距离下,敌人更倾向攻击自己。", "每次承受攻击获得专注,移动后清空。"],
"TR-012": ["前方邻位被击倒时,当前蓄力完成后再补位。", "随后一次队列收拢 tick 无法行动。"],
"TR-013": ["每三次不耗资源的普攻缩短一次装备冷却。", "消耗资源会重置计数。"],
"TR-014": ["首次打断改为延迟而非取消。", "延迟后的蓄力再慢一档。"],
"TR-015": ["可从更远一格接收支援行动。", "满生命时不能接收紧邻支援。"],
"TR-016": ["击败迟滞敌人时恢复 1 点职业资源。", "击败未迟滞敌人时失去 1 点资源。"],
"TR-017": ["首次行动对预定目标判定距离 +1,并追踪仍合法的移动。", "目标失效时强制落空,且不返还资源或冷却。"],
"TR-018": ["受到范围伤害时,下次行动提前 1 tick。", "单体直接治疗效果降低。"],
"TR-019": ["首名进入我方后排的敌人立即被标记。", "后排无入侵者时,向前距离 -1。"],
"TR-020": ["装备首次触发的优先级高于职业行动。", "装备触发或过期前,首次技巧等待。"],
"TR-021": ["与不同职业职责者换位后获得 1 点资源。", "重复同一换位对象会使自己迟滞。"],
"TR-022": ["同一敌人的第三次命中会给予护卫并清除其标记。", "攻击者变化会重置计数。"],
"TR-023": ["受到行动提前后,下一次普攻复制提供者的节奏。", "复制较慢节奏时,完成前不能再次提前。"],
"TR-024": ["首次净化也移除自身最早的正面状态并获得资源。", "没有正面状态时,净化多耗 1 tick。"],
"TR-025": ["在暴露状态下存活会累积下次个人装备折扣。", "战斗结束时未暴露会清空折扣。"],
"TR-026": ["普攻可穿过召唤单位。", "普攻对召唤单位无效。"],
"TR-027": ["未移动时,声音与指令行动不会被打断。", "移动后沉默至下次普攻完成。"],
"TR-028": ["首次自施负面状态改为较短的迟滞。", "第二次自施负面状态延长 1 tick。"],
"TR-029": ["资源分享可穿过一名队友。", "远距离分享时,紧邻队友本档得不到分享。"],
"TR-030": ["被推出最后位后,立即移步到中排。", "移动时失去一个装备正面标签。"],
"TR-031": ["敌方每完成一次大型蓄力便获得专注。", "敌方大型行动被打断时清空专注。"],
"TR-032": ["过量击杀伤害转化为最低生命队友的治疗资源。", "不能获得斩杀阈值效果。"],
"TR-033": ["可同时维持两个装备施加的标记。", "职业施加的标记少持续 1 tick。"],
"TR-034": ["每场首次区域危害无效。", "第二个不同标签的危害持续更久。"],
"TR-035": ["未装备武器时,普攻获得职业专属替代目标规则。", "装备武器会停用特性,并让该购买贵一档。"],
"TR-036": ["成为一个阵线区段唯一成员时,预示敌方目标且距离 +1。", "不能获得隐形或不可选中状态。"],
}
var draft_service: RefCounted
var settings: Dictionary = {}
var draft: Dictionary = {}
var mode := "choice"
var inspected_index := 0
var compare_a_index := -1
var compare_b_index := -1
var notice := ""
func open(service: RefCounted, value: Dictionary) -> void:
draft_service = service
settings = value.duplicate(true)
_load_draft()
func _load_draft() -> void:
var loaded: Dictionary = draft_service.begin_or_load()
if not loaded.ok:
_build_error(String(loaded.code))
return
draft = loaded.draft.duplicate(true)
inspected_index = clampi(inspected_index, 0, 3)
mode = "choice"
_build_choice()
func request_back() -> void:
if mode in ["inspect", "compare", "review"]:
mode = "choice"
notice = ""
_build_choice()
else:
back_requested.emit()
func inspect_candidate(index: int) -> void:
inspected_index = clampi(index, 0, 3)
mode = "inspect"
_build_inspect()
func toggle_candidate(index: int) -> void:
var offer_id := String(draft.offers[index].offer_id)
var selected: Array = Array(draft.selected_offer_ids).duplicate()
if offer_id in selected:
selected.erase(offer_id)
elif selected.size() >= 2:
notice = _tr("已选满两人,请先取消一人。", "Two are already selected. Clear one before choosing another.")
_build_current_mode()
return
else:
selected.append(offer_id)
var saved: Dictionary = draft_service.set_selection(selected)
if not saved.ok:
notice = "%s [%s]" % [_tr("选择未保存,请重试。", "Selection was not saved. Retry."), String(saved.code)]
else:
draft = saved.draft.duplicate(true)
notice = ""
_build_current_mode()
func set_compare_a() -> void:
compare_a_index = inspected_index
notice = ""
_build_choice()
func set_compare_b() -> void:
compare_b_index = inspected_index
notice = ""
_build_choice()
func show_compare() -> void:
if not _compare_ready():
notice = _tr("请先检查两名不同候选,并分别设为 A 与 B。", "Inspect two different candidates and assign them to A and B first.")
_build_choice()
return
mode = "compare"
_build_compare()
func request_review() -> void:
if draft.selected_offer_ids.size() != 2:
notice = _tr("必须恰好选择两人。", "Exactly two candidates are required.")
_build_choice()
return
notice = ""
mode = "review"
_build_review()
func revise_selection() -> void:
mode = "choice"
notice = ""
_build_choice()
func create_company() -> void:
var committed: Dictionary = draft_service.commit_company()
if not committed.ok:
notice = "%s [%s]" % [_tr("远征团未创建,请重试。", "Company was not created. Retry."), String(committed.code)]
_build_review()
return
company_committed.emit(committed.company)
func _build_current_mode() -> void:
match mode:
"inspect": _build_inspect()
"compare": _build_compare()
"review": _build_review()
_: _build_choice()
func _build_choice() -> void:
mode = "choice"
_build_base("InitialCompanySafeArea", true)
var layout := VBoxContainer.new()
layout.name = "InitialCompanyLayout"
layout.add_theme_constant_override("separation", 6)
_get_safe().add_child(layout)
layout.add_child(_choice_header())
var candidates := HBoxContainer.new()
candidates.name = "CandidateRow"
candidates.size_flags_vertical = Control.SIZE_SHRINK_CENTER
candidates.custom_minimum_size = Vector2(0, clampf(size.y * 0.58, 180.0, 420.0))
candidates.add_theme_constant_override("separation", 6)
layout.add_child(candidates)
for index in range(4):
candidates.add_child(_candidate_column(index))
layout.add_child(_choice_footer())
func _choice_header() -> Control:
var header := HBoxContainer.new()
header.name = "ChoiceHeader"
header.custom_minimum_size = Vector2(0, 50)
header.add_theme_constant_override("separation", 8)
var copy := VBoxContainer.new()
copy.size_flags_horizontal = Control.SIZE_EXPAND_FILL
copy.add_theme_constant_override("separation", 0)
header.add_child(copy)
var heading := _label(_tr("初始远征团", "INITIAL EXPEDITION COMPANY"), _text("heading"), PageChrome.INK)
heading.add_theme_color_override("font_shadow_color", Color(1, 1, 1, 0.65))
heading.add_theme_constant_override("shadow_offset_x", 1)
heading.add_theme_constant_override("shadow_offset_y", 1)
copy.add_child(heading)
var subheading := _label(_tr("四名候选 · 恰好选择两人", "FOUR CANDIDATES · CHOOSE EXACTLY TWO"), _text("micro"), PageChrome.INK_SOFT)
copy.add_child(subheading)
var count := _label(_selected_count_text(), _text("heading"), PageChrome.INK)
count.name = "SelectedCount"
count.custom_minimum_size = Vector2(118, 0)
count.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
count.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
header.add_child(count)
return header
func _candidate_column(index: int) -> Control:
var offer: Dictionary = draft.offers[index]
var selected: bool = String(offer.offer_id) in draft.selected_offer_ids
var column := VBoxContainer.new()
column.name = "Candidate_%d" % index
column.size_flags_horizontal = Control.SIZE_EXPAND_FILL
column.size_flags_stretch_ratio = 1.0
column.add_theme_constant_override("separation", 2)
var inspect := Button.new()
inspect.name = "CandidateInspect_%d" % index
inspect.flat = true
inspect.size_flags_vertical = Control.SIZE_EXPAND_FILL
inspect.tooltip_text = _tr("检查 %s" % _name(offer), "Inspect %s" % _name(offer))
_clear_button_styles(inspect)
inspect.custom_minimum_size = Vector2(0, 150)
inspect.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
var portrait := _portrait(offer, "CandidatePortrait_%d" % index)
portrait.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
portrait.anchor_left = 0.12
portrait.anchor_right = 0.88
portrait.anchor_top = 0.02
portrait.anchor_bottom = 0.92
portrait.offset_left = 0.0
portrait.offset_right = 0.0
portrait.offset_top = 0.0
portrait.offset_bottom = 0.0
inspect.add_child(portrait)
var scrim := ColorRect.new()
scrim.color = Color(0.035, 0.075, 0.10, 0.70)
scrim.anchor_top = 1.0
scrim.anchor_right = 1.0
scrim.anchor_bottom = 1.0
scrim.offset_top = -40.0
scrim.mouse_filter = Control.MOUSE_FILTER_IGNORE
inspect.add_child(scrim)
var info := VBoxContainer.new()
info.name = "CandidateInfo"
info.anchor_top = 1.0
info.anchor_right = 1.0
info.anchor_bottom = 1.0
info.offset_left = 6.0
info.offset_right = -6.0
info.offset_top = -39.0
info.offset_bottom = -3.0
info.add_theme_constant_override("separation", 0)
info.mouse_filter = Control.MOUSE_FILTER_IGNORE
inspect.add_child(info)
var name := _label(_name(offer), _text("label"), Color("#f5ead0"))
name.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
info.add_child(name)
var profession := _label(_profession_name(offer), _text("micro"), Color("#73b9e7"))
profession.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
profession.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
info.add_child(profession)
inspect.pressed.connect(inspect_candidate.bind(index), CONNECT_DEFERRED)
column.add_child(inspect)
var select := _selection_marker_button(index, selected)
select.tooltip_text = _tr("取消选择" if selected else "加入初始远征团", "Clear selection" if selected else "Add to the initial Company")
select.set_meta("command_selected", selected)
column.add_child(select)
return column
func _selection_marker_button(index: int, selected: bool) -> Button:
var button := Button.new()
button.name = "CandidateSelect_%d" % index
button.flat = true
button.focus_mode = Control.FOCUS_ALL
button.custom_minimum_size = Vector2(0, 48)
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())
# M64-A: shared plate + diamond pip carries the selected state.
var plate := PageChrome.new().configure("command_plate", {"state": "primary" if selected else "secondary"})
plate.name = "Chrome"
plate.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
plate.mouse_filter = Control.MOUSE_FILTER_IGNORE
button.add_child(plate)
var state_pip := PageChrome.new().configure("pip", {"shape": "diamond", "filled": selected})
state_pip.name = "SelectPip"
state_pip.anchor_left = 0.5
state_pip.anchor_right = 0.5
state_pip.anchor_top = 0.5
state_pip.anchor_bottom = 0.5
state_pip.offset_left = -14.0
state_pip.offset_right = 14.0
state_pip.offset_top = -14.0
state_pip.offset_bottom = 14.0
state_pip.mouse_filter = Control.MOUSE_FILTER_IGNORE
button.add_child(state_pip)
var state_label := _label(_tr("已选", "Selected") if selected else _tr("选择", "Select"), _text("micro"), PageChrome.INK)
state_label.name = "SelectStateLabel"
state_label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
state_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
state_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
state_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
button.add_child(state_label)
button.pressed.connect(toggle_candidate.bind(index), CONNECT_DEFERRED)
return button
func _choice_footer() -> Control:
var footer := HBoxContainer.new()
footer.name = "ChoiceFooter"
footer.custom_minimum_size = Vector2(0, 52)
footer.add_theme_constant_override("separation", 5)
footer.add_child(_button("ChoiceBack", _tr("返回", "Back"), request_back, "utility", 84))
var status := _label(_compare_status(), _text("micro"), PageChrome.INK_SOFT)
status.name = "ChoiceNotice"
status.size_flags_horizontal = Control.SIZE_EXPAND_FILL
status.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
status.text = notice if not notice.is_empty() else status.text
footer.add_child(status)
footer.add_child(_button("SetCompareA", "A", set_compare_a, "tool", 52))
footer.add_child(_button("SetCompareB", "B", set_compare_b, "tool", 52))
var compare := _button("OpenCompare", _tr("对比", "Compare"), show_compare, "action_secondary", 92)
compare.disabled = not _compare_ready()
compare.tooltip_text = _tr("需要两名不同候选", "Two different candidates are required") if compare.disabled else _tr("对比 A 与 B", "Compare A and B")
_set_button_disabled(compare, compare.disabled)
footer.add_child(compare)
var review := _button("ReviewCompany", _tr("复核 2/2", "Review 2/2"), request_review, "action_primary", 126)
review.disabled = draft.selected_offer_ids.size() != 2
review.tooltip_text = _tr("必须恰好选择两人", "Exactly two candidates are required") if review.disabled else _tr("复核所选远征团", "Review selected Company")
_set_button_disabled(review, review.disabled)
footer.add_child(review)
return footer
func _build_inspect() -> void:
mode = "inspect"
_build_base("InitialCompanyInspectSafeArea")
var layout := VBoxContainer.new()
layout.name = "InspectLayout"
layout.add_theme_constant_override("separation", 6)
_get_safe().add_child(layout)
layout.add_child(_detail_header(_tr("候选详情", "CANDIDATE INSPECT"), _name(draft.offers[inspected_index])))
var body := HBoxContainer.new()
body.name = "InspectBody"
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
body.add_theme_constant_override("separation", 16)
layout.add_child(body)
var offer: Dictionary = draft.offers[inspected_index]
var portrait_stage := _portrait_stage(offer, "InspectPortraitStage", Vector2(150, 120))
portrait_stage.size_flags_horizontal = Control.SIZE_EXPAND_FILL
portrait_stage.size_flags_vertical = Control.SIZE_SHRINK_CENTER
portrait_stage.size_flags_stretch_ratio = 0.90
body.add_child(portrait_stage)
var identity := VBoxContainer.new()
identity.size_flags_horizontal = Control.SIZE_EXPAND_FILL
identity.size_flags_vertical = Control.SIZE_SHRINK_CENTER
identity.size_flags_stretch_ratio = 0.72
identity.add_theme_constant_override("separation", 5)
body.add_child(identity)
identity.add_child(_section(_tr("职业", "PROFESSION"), "%s · %s" % [_profession_name(offer), _profession_job(offer)]))
identity.add_child(_section(_tr("五项属性", "FIVE ATTRIBUTES"), _attribute_full(offer)))
identity.add_child(_section(_tr("初始装备", "STARTING ITEM"), "%s · %s" % [_item_name(offer), _tr("已装备", "Equipped")]))
var trait_box := VBoxContainer.new()
trait_box.size_flags_horizontal = Control.SIZE_EXPAND_FILL
trait_box.size_flags_vertical = Control.SIZE_SHRINK_CENTER
trait_box.add_theme_constant_override("separation", 5)
body.add_child(trait_box)
trait_box.add_child(_section(_tr("特性", "TRAIT"), _trait_name(offer)))
trait_box.add_child(_section(_tr("收益", "BENEFIT"), _trait_benefit(offer), PageChrome.INK))
trait_box.add_child(_section(_tr("代价", "COST"), _trait_cost(offer), UITokens.COLOR_DANGER))
var footer := HBoxContainer.new()
footer.name = "InspectFooter"
footer.custom_minimum_size = Vector2(0, 52)
footer.add_theme_constant_override("separation", 8)
layout.add_child(footer)
footer.add_child(_button("InspectBack", _tr("返回候选", "Back to candidates"), request_back, "utility", 158))
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
footer.add_child(spacer)
footer.add_child(_button("InspectSetA", _tr("设为 A", "Set A"), set_compare_a, "tool", 92))
footer.add_child(_button("InspectSetB", _tr("设为 B", "Set B"), set_compare_b, "tool", 92))
var selected: bool = String(offer.offer_id) in draft.selected_offer_ids
var select := _button("InspectSelect", _tr("取消选择" if selected else "选择", "Clear" if selected else "Select"), toggle_candidate.bind(inspected_index), "action_primary" if selected else "action_secondary", 142)
select.set_meta("command_selected", selected)
footer.add_child(select)
func _build_compare() -> void:
mode = "compare"
_build_base("InitialCompanyCompareSafeArea")
var layout := VBoxContainer.new()
layout.name = "CompareLayout"
layout.add_theme_constant_override("separation", 6)
_get_safe().add_child(layout)
layout.add_child(_detail_header(_tr("候选对比", "CANDIDATE COMPARISON"), "A / B"))
var body := HBoxContainer.new()
body.name = "CompareBody"
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
body.add_theme_constant_override("separation", 14)
layout.add_child(body)
body.add_child(_comparison_column("A", compare_a_index))
body.add_child(_comparison_column("B", compare_b_index))
var footer := HBoxContainer.new()
footer.name = "CompareFooter"
footer.custom_minimum_size = Vector2(0, 52)
footer.add_theme_constant_override("separation", 8)
layout.add_child(footer)
footer.add_child(_button("CompareBack", _tr("返回候选", "Back to candidates"), request_back, "utility", 158))
var note := _label(_tr("对比不会改变选择。", "Comparison does not change selection."), _text("micro"), PageChrome.INK_SOFT)
note.size_flags_horizontal = Control.SIZE_EXPAND_FILL
note.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
footer.add_child(note)
footer.add_child(_button("CompareSelectA", _tr("选择 A", "Select A"), toggle_candidate.bind(compare_a_index), "action_secondary", 116))
footer.add_child(_button("CompareSelectB", _tr("选择 B", "Select B"), toggle_candidate.bind(compare_b_index), "action_secondary", 116))
func _comparison_column(mark: String, index: int) -> Control:
var offer: Dictionary = draft.offers[index]
var box := HBoxContainer.new()
box.name = "Compare%s" % mark
box.size_flags_horizontal = Control.SIZE_EXPAND_FILL
box.size_flags_stretch_ratio = 1.0
box.add_theme_constant_override("separation", 8)
var portrait_stage := _portrait_stage(offer, "ComparePortrait%s" % mark, Vector2(104, 120))
portrait_stage.size_flags_horizontal = Control.SIZE_EXPAND_FILL
portrait_stage.size_flags_vertical = Control.SIZE_SHRINK_CENTER
portrait_stage.size_flags_stretch_ratio = 0.85
box.add_child(portrait_stage)
var details := VBoxContainer.new()
details.size_flags_horizontal = Control.SIZE_EXPAND_FILL
details.size_flags_vertical = Control.SIZE_SHRINK_CENTER
details.size_flags_stretch_ratio = 1.0
details.add_theme_constant_override("separation", 1)
box.add_child(details)
var heading := _label("%s %s" % [mark, _name(offer)], _text("heading"), PageChrome.INK)
heading.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
details.add_child(heading)
details.add_child(_label("%s · %s" % [_profession_name(offer), _profession_job(offer)], _text("micro"), PageChrome.INK_SOFT))
details.add_child(_label(_attribute_full(offer), _text("micro"), PageChrome.INK))
details.add_child(_label(_trait_name(offer), _text("micro"), PageChrome.INK))
var benefit := _label("+ %s" % _trait_benefit(offer), _text("micro"), PageChrome.INK)
benefit.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
details.add_child(benefit)
var cost := _label("- %s" % _trait_cost(offer), _text("micro"), UITokens.COLOR_DANGER)
cost.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
cost.size_flags_vertical = Control.SIZE_EXPAND_FILL
details.add_child(cost)
details.add_child(_label("%s · %s" % [_item_name(offer), _tr("已装备", "Equipped")], _text("micro"), PageChrome.INK))
return box
func _build_review() -> void:
mode = "review"
_build_base("InitialCompanyReviewSafeArea")
var layout := VBoxContainer.new()
layout.name = "ReviewLayout"
layout.add_theme_constant_override("separation", 6)
_get_safe().add_child(layout)
layout.add_child(_detail_header(_tr("远征团复核", "EXPEDITION COMPANY REVIEW"), _tr("确认初始两人", "Confirm the initial pair")))
var body := HBoxContainer.new()
body.name = "ReviewBody"
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
body.add_theme_constant_override("separation", 14)
layout.add_child(body)
var selected_box := HBoxContainer.new()
selected_box.name = "ReviewSelected"
selected_box.size_flags_horizontal = Control.SIZE_EXPAND_FILL
selected_box.size_flags_stretch_ratio = 1.7
selected_box.add_theme_constant_override("separation", 12)
body.add_child(selected_box)
var selected_offers: Array[Dictionary] = []
var unchosen_offers: Array[Dictionary] = []
for offer in draft.offers:
if String(offer.offer_id) in draft.selected_offer_ids:
selected_offers.append(offer)
else:
unchosen_offers.append(offer)
for index in range(selected_offers.size()):
selected_box.add_child(_review_selected_column(index, selected_offers[index]))
var unchosen := VBoxContainer.new()
unchosen.name = "ReviewUnchosen"
unchosen.size_flags_horizontal = Control.SIZE_EXPAND_FILL
unchosen.size_flags_stretch_ratio = 0.8
unchosen.add_theme_constant_override("separation", 5)
body.add_child(unchosen)
unchosen.add_child(_label(_tr("未选择的临时候选", "UNCHOSEN PROVISIONAL OFFERS"), _text("micro"), PageChrome.INK_SOFT))
for index in range(unchosen_offers.size()):
unchosen.add_child(_review_unchosen_row(index, unchosen_offers[index]))
var consequence := _label(_tr("这两份 offer 不会写入远征团。", "These two offers will not be stored in the Company."), _text("micro"), UITokens.COLOR_DANGER)
consequence.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
unchosen.add_child(consequence)
if not notice.is_empty():
var error := _label(notice, _text("micro"), UITokens.COLOR_DANGER)
error.name = "ReviewNotice"
error.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
unchosen.add_child(error)
var footer := HBoxContainer.new()
footer.name = "ReviewFooter"
footer.custom_minimum_size = Vector2(0, 52)
footer.add_theme_constant_override("separation", 8)
layout.add_child(footer)
footer.add_child(_button("ReviewRevise", _tr("重新选择", "Revise selection"), revise_selection, "utility", 160))
var promise := _label(_tr("创建后,两名 Recruit 会持续成长并保留。", "Both Recruits persist and continue growing after creation."), _text("micro"), PageChrome.INK_SOFT)
promise.size_flags_horizontal = Control.SIZE_EXPAND_FILL
promise.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
promise.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
footer.add_child(promise)
footer.add_child(_button("CreateCompany", _tr("创建远征团", "Create Company"), create_company, "action_primary", 178))
func _review_selected_column(index: int, offer: Dictionary) -> Control:
var box := VBoxContainer.new()
box.name = "ReviewSelected_%d" % index
box.size_flags_horizontal = Control.SIZE_EXPAND_FILL
box.size_flags_stretch_ratio = 1.0
box.add_theme_constant_override("separation", 3)
box.add_child(_label(_tr("成员 %d" % (index + 1), "MEMBER %d" % (index + 1)), _text("micro"), PageChrome.INK_SOFT))
var portrait_stage := _portrait_stage(offer, "ReviewSelectedPortrait_%d" % index, Vector2(100, 90))
portrait_stage.size_flags_vertical = Control.SIZE_EXPAND_FILL
box.add_child(portrait_stage)
var name := _label(_name(offer), _text("heading"), PageChrome.INK)
name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
box.add_child(name)
box.add_child(_label("%s · %s" % [_profession_name(offer), _profession_job(offer)], _text("micro"), PageChrome.INK_SOFT))
box.add_child(_label(_attribute_full(offer), _text("micro"), PageChrome.INK))
box.add_child(_label(_trait_name(offer), _text("micro"), PageChrome.INK))
box.add_child(_label("%s · %s" % [_item_name(offer), _tr("已装备", "Equipped")], _text("micro"), PageChrome.INK))
return box
func _review_unchosen_row(index: int, offer: Dictionary) -> Control:
var row := HBoxContainer.new()
row.name = "ReviewUnchosen_%d" % index
row.custom_minimum_size = Vector2(0, 52)
row.add_theme_constant_override("separation", 6)
var portrait := _portrait(offer, "ReviewUnchosenPortrait_%d" % index)
portrait.custom_minimum_size = Vector2(48, 48)
portrait.modulate = Color(0.72, 0.75, 0.76, 0.78)
row.add_child(portrait)
var copy := VBoxContainer.new()
copy.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(copy)
var name := _label(_name(offer), _text("micro"), PageChrome.INK)
name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
copy.add_child(name)
copy.add_child(_label(_profession_name(offer), _text("micro"), PageChrome.INK_SOFT))
return row
func _detail_header(overline: String, title: String) -> Control:
var header := HBoxContainer.new()
header.custom_minimum_size = Vector2(0, 50)
var copy := VBoxContainer.new()
copy.size_flags_horizontal = Control.SIZE_EXPAND_FILL
header.add_child(copy)
copy.add_child(_label(overline, _text("micro"), PageChrome.INK_SOFT))
var heading := _label(title, _text("heading"), PageChrome.INK)
heading.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
copy.add_child(heading)
var count := _label(_selected_count_text(), _text("label"), PageChrome.INK_SOFT)
count.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
header.add_child(count)
return header
func _section(title: String, value: String, color := PageChrome.INK) -> Control:
var box := VBoxContainer.new()
box.add_theme_constant_override("separation", 0)
box.add_child(_label(title, _text("micro"), PageChrome.INK_SOFT))
var body := _label(value, _text("micro"), color)
body.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
box.add_child(body)
return box
func _portrait_stage(offer: Dictionary, node_name: String, minimum: Vector2) -> Control:
var stage := Control.new()
stage.name = node_name
stage.custom_minimum_size = minimum
stage.mouse_filter = Control.MOUSE_FILTER_IGNORE
var tint := ColorRect.new()
tint.color = Color(InitialCompanyArt.accent_for(offer), 0.11)
tint.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
tint.mouse_filter = Control.MOUSE_FILTER_IGNORE
stage.add_child(tint)
var portrait := _portrait(offer, "%sPortrait" % node_name)
portrait.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
portrait.offset_left = 4.0
portrait.offset_right = -4.0
portrait.offset_top = 2.0
portrait.offset_bottom = -2.0
stage.add_child(portrait)
return stage
func _portrait(offer: Dictionary, node_name: String) -> TextureRect:
var portrait := TextureRect.new()
portrait.name = node_name
portrait.texture = InitialCompanyArt.texture_for(offer)
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.mouse_filter = Control.MOUSE_FILTER_IGNORE
return portrait
func _build_error(code: String) -> void:
mode = "error"
_build_base("InitialCompanyErrorSafeArea")
var box := VBoxContainer.new()
box.name = "DraftErrorLayout"
box.alignment = BoxContainer.ALIGNMENT_CENTER
box.add_theme_constant_override("separation", 10)
_get_safe().add_child(box)
var heading := _label(_tr("初始远征团数据需要处理", "INITIAL COMPANY DATA NEEDS ATTENTION"), _text("heading"), Color("#f5ead0"))
heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
box.add_child(heading)
var detail := _label(_tr("没有重新生成候选。请重试读取,或返回标题首页。", "No candidates were rerolled. Retry the same data or return to Title."), _text("label"), Color("#aebbc2"))
detail.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
box.add_child(detail)
var diagnostic := _label("%s: %s" % [_tr("诊断", "Diagnostic"), code], _text("micro"), Color("#f2a89c"))
diagnostic.name = "DraftErrorCode"
diagnostic.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
box.add_child(diagnostic)
var actions := HBoxContainer.new()
actions.alignment = BoxContainer.ALIGNMENT_CENTER
actions.add_theme_constant_override("separation", 8)
box.add_child(actions)
actions.add_child(_button("DraftErrorBack", _tr("返回标题", "Back to Title"), func(): back_requested.emit(), "utility", 148))
actions.add_child(_button("DraftErrorRetry", _tr("重试读取", "Retry"), _load_draft, "action_primary", 148))
func _build_base(safe_name: String, scene_forward := false) -> void:
for child in get_children():
child.free()
var background := TextureRect.new()
background.name = "InitialCompanyBackground"
background.texture = GUILD_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.025, 0.05, 0.07, 0.08 if scene_forward else 0.52)
shade.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
shade.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(shade)
var frame_safe := MarginContainer.new()
frame_safe.name = "%sFrame" % safe_name
frame_safe.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
frame_safe.mouse_filter = Control.MOUSE_FILTER_IGNORE
var margin := maxi(10, int(settings.get("safe_area", 24)))
var vertical_margin := 10 if size.y < 500.0 else margin
frame_safe.add_theme_constant_override("margin_left", margin)
frame_safe.add_theme_constant_override("margin_right", margin)
frame_safe.add_theme_constant_override("margin_top", vertical_margin)
frame_safe.add_theme_constant_override("margin_bottom", vertical_margin)
add_child(frame_safe)
# M64-A: the opening page uses the shared parchment card; every mode lives
# on its face between the measured rolled edges.
var page_card := PageChrome.new().configure("card")
page_card.name = "InitialCompanyFrame"
page_card.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
page_card.mouse_filter = Control.MOUSE_FILTER_IGNORE
frame_safe.add_child(page_card)
var card_width := maxf(0.0, size.x - float(margin) * 2.0)
var card_height := maxf(0.0, size.y - float(vertical_margin) * 2.0)
var face_inset_x := card_width * PageChrome.PANEL_EDGE_X + 8.0
var face_inset_y := card_height * PageChrome.PANEL_EDGE_Y + 8.0
var safe := MarginContainer.new()
safe.name = safe_name
safe.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
safe.set_meta("initial_company_content", true)
safe.add_theme_constant_override("margin_left", int(face_inset_x))
safe.add_theme_constant_override("margin_right", int(face_inset_x))
safe.add_theme_constant_override("margin_top", int(face_inset_y))
safe.add_theme_constant_override("margin_bottom", int(face_inset_y))
add_child(safe)
func _get_safe() -> MarginContainer:
for child in get_children():
if child is MarginContainer and bool(child.get_meta("initial_company_content", false)):
return child
return null
func _button(node_name: String, text_value: String, callback: Callable, variant: String, width: float) -> Button:
var button := Button.new()
button.name = node_name
button.flat = true
button.custom_minimum_size = Vector2(width, 48)
button.tooltip_text = text_value
_clear_button_styles(button)
# M64-A: every action surface is the shared hand-drawn plate; the authored
# `Chrome`/`ButtonLabel` names stay for the focused contract.
var plate_state := "primary" if variant == "action_primary" else "secondary"
var chrome := PageChrome.new().configure("command_plate", {"state": plate_state})
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 icon_kind := _button_icon(node_name)
if not icon_kind.is_empty():
var badge := PageChrome.new().configure("pip", {"shape": "diamond", "filled": variant == "action_primary"})
badge.name = "ChromeIcon"
badge.anchor_top = 0.5
badge.anchor_bottom = 0.5
badge.offset_left = 8.0
badge.offset_right = 34.0
badge.offset_top = -13.0
badge.offset_bottom = 13.0
badge.mouse_filter = Control.MOUSE_FILTER_IGNORE
button.add_child(badge)
button.set_meta("entry_icon_kind", icon_kind)
var label := _label(text_value, _text("micro"), PageChrome.INK)
label.name = "ButtonLabel"
label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
label.offset_left = 8.0
label.offset_right = -8.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.pressed.connect(callback, CONNECT_DEFERRED)
return button
func _button_icon(node_name: String) -> String:
if node_name.contains("Back") or node_name.contains("Revise"):
return "back"
if node_name.contains("SetA") or node_name == "SetCompareA":
return "mark_a"
if node_name.contains("SetB") or node_name == "SetCompareB":
return "mark_b"
if node_name.contains("Compare"):
return "compare"
if node_name.contains("Review") or node_name.contains("Create"):
return "confirm"
if node_name.contains("Retry"):
return "retry"
if node_name.contains("Select"):
return "select"
return ""
func _set_button_disabled(button: Button, disabled: bool) -> void:
button.disabled = disabled
var label := button.get_node("ButtonLabel") as Label
if disabled:
label.add_theme_color_override("font_color", UITokens.COLOR_MUTED)
func _clear_button_styles(button: Button) -> void:
for style_name in ["normal", "hover", "pressed", "disabled", "focus", "hover_pressed"]:
button.add_theme_stylebox_override(style_name, StyleBoxEmpty.new())
func _label(value: String, font_size: int, color: Color) -> Label:
var label := Label.new()
label.text = value
label.add_theme_font_override("font", UI_FONT)
label.add_theme_font_size_override("font_size", font_size)
label.add_theme_color_override("font_color", color)
return label
func _text(role: String) -> int:
return UITokens.font_size(role, float(settings.get("text_scale", 1.0)))
func _name(offer: Dictionary) -> String:
return String(offer.rendered_name_snapshot.get(_locale(), offer.rendered_name_snapshot.en))
func _profession_name(offer: Dictionary) -> String:
return String(offer.profession_name.get(_locale(), offer.profession_name.en))
func _profession_job(offer: Dictionary) -> String:
return String(PROFESSION_JOB_ZH.get(String(offer.profession_id), offer.profession_job)) if _locale() == "zh_CN" else String(offer.profession_job)
func _trait_name(offer: Dictionary) -> String:
return String(offer.trait_name.get(_locale(), offer.trait_name.en))
func _trait_benefit(offer: Dictionary) -> String:
if _locale() == "zh_CN" and TRAIT_ZH.has(String(offer.trait_id)):
return String(TRAIT_ZH[String(offer.trait_id)][0])
return String(offer.trait_benefit)
func _trait_cost(offer: Dictionary) -> String:
if _locale() == "zh_CN" and TRAIT_ZH.has(String(offer.trait_id)):
return String(TRAIT_ZH[String(offer.trait_id)][1])
return String(offer.trait_cost)
func _item_name(offer: Dictionary) -> String:
return String(offer.starting_item_name.get(_locale(), offer.starting_item_name.en))
func _attribute_compact(offer: Dictionary) -> String:
var labels := ["力", "巧", "智", "体", "志"] if _locale() == "zh_CN" else ["M", "F", "I", "V", "R"]
var parts: Array[String] = []
for index in range(ATTRIBUTE_ORDER.size()):
parts.append("%s%d" % [labels[index], int(offer.attributes[ATTRIBUTE_ORDER[index]])])
return " ".join(parts)
func _attribute_full(offer: Dictionary) -> String:
var labels := ["力量", "技巧", "洞察", "体魄", "意志"] if _locale() == "zh_CN" else ["Might", "Finesse", "Insight", "Vigor", "Resolve"]
var parts: Array[String] = []
for index in range(ATTRIBUTE_ORDER.size()):
parts.append("%s %d" % [labels[index], int(offer.attributes[ATTRIBUTE_ORDER[index]])])
return " ".join(parts)
func _selected_count_text() -> String:
return _tr("已选择 %d/2" % draft.selected_offer_ids.size(), "SELECTED %d/2" % draft.selected_offer_ids.size())
func _compare_status() -> String:
var a := _name(draft.offers[compare_a_index]) if compare_a_index >= 0 else "—"
var b := _name(draft.offers[compare_b_index]) if compare_b_index >= 0 else "—"
return "A %s · B %s" % [a, b]
func _compare_ready() -> bool:
return compare_a_index >= 0 and compare_b_index >= 0 and compare_a_index != compare_b_index
func _locale() -> String:
return String(settings.get("locale", "zh_CN"))
func _tr(zh: String, en: String) -> String:
return zh if _locale() == "zh_CN" else en
func _scaled(value: int) -> int:
return int(round(float(value) * float(settings.get("text_scale", 1.0))))