862 lines
39 KiB
GDScript
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))))
|