Files
aetherbound-guild/runtime/main.gd
T

1187 lines
50 KiB
GDScript

extends Control
const RunState = preload("res://domain/run_state.gd")
const UI_FONT := preload("res://assets/fonts/NotoSansSC.ttf")
enum AppPage {
BOOT,
TITLE,
RUN,
}
const BG := preload("res://assets/backgrounds/windglass_verge.webp")
const ACTORS := {
"Hearthguard": preload("res://assets/actors/hearthguard.png"),
"Farstring": preload("res://assets/actors/farstring.png"),
"Wayrunner": preload("res://assets/actors/wayrunner.png"),
"Kindhand": preload("res://assets/actors/kindhand.png"),
}
const EQUIPMENT := preload("res://assets/equipment/eq_012_open_line_cord.png")
const BOOT_REQUIRED_PATHS: Array[String] = [
"res://main.tscn",
"res://domain/run_state.gd",
"res://assets/backgrounds/windglass_verge.webp",
"res://assets/actors/hearthguard.png",
"res://assets/actors/farstring.png",
"res://assets/actors/wayrunner.png",
"res://assets/actors/kindhand.png",
"res://assets/equipment/eq_012_open_line_cord.png",
"res://assets/fonts/NotoSansSC.ttf",
"res://default_bus_layout.tres",
]
const COLOR_INK := Color("#14242a")
const COLOR_PANEL := Color("#f4f6f2")
const COLOR_PANEL_SOFT := Color("#e2f0ec")
const COLOR_GOLD := Color("#e3b52f")
const COLOR_TEAL := Color("#197c78")
const COLOR_CORAL := Color("#c85156")
const COLOR_WHITE := Color("#ffffff")
const COLOR_MUTED := Color("#aebcb9")
var run := RunState.new()
@export var auto_boot := true
var page := AppPage.BOOT
var boot_index := 0
var boot_current_path := ""
var boot_failed_path := ""
var boot_verified := false
var boot_success_presented := false
var boot_running := false
var boot_paths_override: Array[String] = []
var locale := "zh_CN"
var muted := false
var reduced_motion := false
var text_scale := 1.0
var notice := ""
var inspected_id := ""
var background_wash: ColorRect
var entry_layer: Control
var root_panel: Control
var content: Control
var decision_scroll: ScrollContainer
var coin_label: Label
var company_label: Label
var step_label: Label
var status_label: Label
var primary_action: Button
var audio_player: AudioStreamPlayer
var sfx_playback: AudioStreamGeneratorPlayback
func _ready() -> void:
var runtime_theme := Theme.new()
runtime_theme.default_font = UI_FONT
theme = runtime_theme
build_shell()
render()
if auto_boot:
call_deferred("run_boot_verification")
func _exit_tree() -> void:
if audio_player != null:
audio_player.stop()
sfx_playback = null
audio_player = null
func build_shell() -> void:
var background := TextureRect.new()
background.name = "Background"
background.texture = BG
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)
add_child(background)
background_wash = ColorRect.new()
background_wash.name = "BackgroundWash"
background_wash.color = Color(0.025, 0.07, 0.085, 0.76)
background_wash.mouse_filter = Control.MOUSE_FILTER_IGNORE
background_wash.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
add_child(background_wash)
var safe := MarginContainer.new()
safe.name = "SafeArea"
var outer_margin := 12 if compact_layout() else 18 if stacked_chrome() else 24
safe.add_theme_constant_override("margin_left", outer_margin)
safe.add_theme_constant_override("margin_right", outer_margin)
safe.add_theme_constant_override("margin_top", 12 if compact_layout() else 18)
safe.add_theme_constant_override("margin_bottom", 12 if compact_layout() else 18)
safe.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
add_child(safe)
var page_stack := Control.new()
page_stack.name = "PageStack"
page_stack.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
page_stack.size_flags_horizontal = Control.SIZE_EXPAND_FILL
page_stack.size_flags_vertical = Control.SIZE_EXPAND_FILL
safe.add_child(page_stack)
entry_layer = Control.new()
entry_layer.name = "EntryLayer"
entry_layer.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
page_stack.add_child(entry_layer)
root_panel = VBoxContainer.new()
root_panel.name = "RuntimeShell"
root_panel.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
root_panel.add_theme_constant_override("separation", 10)
page_stack.add_child(root_panel)
root_panel.add_child(build_top_bar())
var decision_panel := PanelContainer.new()
decision_panel.name = "CurrentDecision"
decision_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
decision_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
decision_panel.add_theme_stylebox_override("panel", style_box(Color(0.035, 0.095, 0.11, 0.91), COLOR_MUTED, 1, 6))
root_panel.add_child(decision_panel)
decision_scroll = ScrollContainer.new()
decision_scroll.name = "DecisionScroll"
decision_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
decision_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
decision_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
decision_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO
decision_scroll.scroll_deadzone = 12
decision_panel.add_child(decision_scroll)
content = VBoxContainer.new()
content.name = "DecisionBody"
content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
decision_scroll.add_child(content)
root_panel.add_child(build_footer())
var generator := AudioStreamGenerator.new()
generator.mix_rate = 22050.0
generator.buffer_length = 0.2
audio_player = AudioStreamPlayer.new()
audio_player.name = "PrototypeSFX"
audio_player.bus = &"SFX"
audio_player.playback_type = AudioServer.PLAYBACK_TYPE_STREAM
audio_player.stream = generator
add_child(audio_player)
audio_player.play()
sfx_playback = audio_player.get_stream_playback()
func build_top_bar() -> Control:
var panel := PanelContainer.new()
panel.name = "TopBar"
panel.custom_minimum_size.y = 58
panel.add_theme_stylebox_override("panel", style_box(Color(0.045, 0.105, 0.12, 0.96), Color("#6e858a"), 1, 5))
var shell: BoxContainer
if stacked_chrome():
shell = VBoxContainer.new()
else:
shell = HBoxContainer.new()
shell.add_theme_constant_override("separation", 8)
panel.add_child(shell)
var identity_row: HBoxContainer
if stacked_chrome():
identity_row = HBoxContainer.new()
identity_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
identity_row.add_theme_constant_override("separation", 8)
shell.add_child(identity_row)
else:
identity_row = shell as HBoxContainer
var brand := Label.new()
brand.text = "AG"
brand.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
brand.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
brand.custom_minimum_size = Vector2(46, 38)
brand.add_theme_color_override("font_color", COLOR_INK)
brand.add_theme_font_size_override("font_size", 18)
brand.add_theme_stylebox_override("normal", style_box(COLOR_GOLD, Color("#f8da76"), 1, 4))
identity_row.add_child(brand)
var identity := VBoxContainer.new()
identity.size_flags_horizontal = Control.SIZE_EXPAND_FILL
identity.custom_minimum_size.x = 0
identity.clip_contents = true
var title := Label.new()
title.text = "AETHERBOUND GUILD"
title.custom_minimum_size.x = 0
title.clip_text = true
title.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
title.add_theme_color_override("font_color", COLOR_WHITE)
title.add_theme_font_size_override("font_size", 18)
identity.add_child(title)
step_label = Label.new()
step_label.custom_minimum_size.x = 0
step_label.clip_text = true
step_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
step_label.add_theme_color_override("font_color", Color("#9bd8ca"))
step_label.add_theme_font_size_override("font_size", 11)
identity.add_child(step_label)
identity_row.add_child(identity)
coin_label = resource_label("COIN 52")
coin_label.name = "CoinLabel"
company_label = resource_label("COMPANY 0 / 4")
company_label.name = "CompanyLabel"
var tools: Container
if compact_layout():
var grid := GridContainer.new()
grid.columns = 2
tools = grid
elif stacked_chrome():
tools = HBoxContainer.new()
else:
tools = shell
tools.size_flags_horizontal = Control.SIZE_EXPAND_FILL
tools.add_theme_constant_override("h_separation", 8)
tools.add_theme_constant_override("v_separation", 6)
if stacked_chrome():
shell.add_child(tools)
tools.add_child(coin_label)
tools.add_child(company_label)
tools.add_child(tool_button("LocaleButton", "简 / EN", toggle_locale))
tools.add_child(tool_button("TextScaleButton", "Aa", toggle_text_scale))
tools.add_child(tool_button("MotionButton", "~", toggle_motion))
tools.add_child(tool_button("MuteButton", "♪", toggle_mute))
return panel
func build_footer() -> Control:
var panel := PanelContainer.new()
panel.name = "Footer"
panel.custom_minimum_size.y = 60
panel.add_theme_stylebox_override("panel", style_box(Color(0.045, 0.105, 0.12, 0.96), Color("#6e858a"), 1, 5))
var row: BoxContainer
if compact_layout():
row = VBoxContainer.new()
else:
row = HBoxContainer.new()
row.add_theme_constant_override("separation", 10)
panel.add_child(row)
status_label = Label.new()
status_label.name = "StatusCaption"
status_label.text = tr_text("选择契约开始;无操作会自动提交。", "Choose a Contract to begin; nothing commits automatically.")
status_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
status_label.custom_minimum_size.x = 0
status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
status_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
status_label.add_theme_color_override("font_color", Color("#d4e5df"))
status_label.add_theme_font_size_override("font_size", 12)
row.add_child(status_label)
primary_action = Button.new()
primary_action.name = "PrimaryAction"
primary_action.custom_minimum_size = Vector2(190, 46)
if compact_layout():
primary_action.size_flags_horizontal = Control.SIZE_EXPAND_FILL
primary_action.add_theme_stylebox_override("normal", style_box(COLOR_GOLD, Color("#f8da76"), 1, 4))
primary_action.add_theme_stylebox_override("hover", style_box(Color("#f0c94d"), COLOR_WHITE, 1, 4))
primary_action.add_theme_stylebox_override("disabled", style_box(Color("#526267"), Color("#65777b"), 1, 4))
primary_action.add_theme_color_override("font_color", COLOR_INK)
primary_action.add_theme_color_override("font_disabled_color", Color("#aeb8b7"))
primary_action.add_theme_font_size_override("font_size", 13)
row.add_child(primary_action)
return panel
func render() -> void:
if page != AppPage.RUN:
render_entry_page()
return
entry_layer.visible = false
root_panel.visible = true
background_wash.color = Color(0.025, 0.07, 0.085, 0.76)
for child in content.get_children():
child.queue_free()
update_hud()
for connection in primary_action.pressed.get_connections():
primary_action.pressed.disconnect(connection.callable)
match run.step:
RunState.Step.CONTRACT:
render_contract()
RunState.Step.OFFERS:
render_offers()
RunState.Step.OPENING_REVIEW:
render_opening_review()
RunState.Step.OPENING_RECEIPT:
render_opening_receipt()
RunState.Step.MARKET_MAEL:
render_market_hire("MKT-R1-01")
RunState.Step.MARKET_ILYA:
render_market_hire("MKT-R1-02")
RunState.Step.EQUIPMENT_PURCHASE:
render_equipment_purchase()
RunState.Step.EQUIPMENT_EQUIP:
render_equipment_equip()
RunState.Step.ORDER:
render_order()
RunState.Step.READY:
render_ready()
update_footer()
func render_entry_page() -> void:
root_panel.visible = false
entry_layer.visible = true
background_wash.color = Color(0.01, 0.035, 0.045, 0.18 if page == AppPage.TITLE else 0.42)
for child in entry_layer.get_children():
child.free()
if page == AppPage.BOOT:
render_boot()
else:
render_title()
func render_boot() -> void:
var shell := CenterContainer.new()
shell.name = "BootScreen"
shell.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
entry_layer.add_child(shell)
var column := VBoxContainer.new()
column.custom_minimum_size = Vector2(320, 0)
column.add_theme_constant_override("separation", 12)
shell.add_child(column)
var mark := Label.new()
mark.text = "AG"
mark.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
mark.add_theme_color_override("font_color", COLOR_GOLD)
mark.add_theme_font_size_override("font_size", scaled(42))
column.add_child(mark)
column.add_child(entry_heading(tr_text("裂脉远征团", "AETHERBOUND GUILD"), 25))
var status_text := tr_text("本地内容验证完成", "Local content verified") if boot_verified else tr_text("正在验证本地内容", "Verifying local content")
var status := entry_body(status_text)
status.name = "BootStatus"
status.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
column.add_child(status)
var progress := ProgressBar.new()
progress.name = "BootProgress"
progress.show_percentage = false
progress.max_value = max(1, active_boot_paths().size())
progress.value = boot_index
progress.custom_minimum_size = Vector2(320, 8)
column.add_child(progress)
var detail_text := tr_text("准备开始本地校验", "Preparing local verification")
if not boot_failed_path.is_empty():
detail_text = tr_text("无法读取:%s" % boot_failed_path, "Could not load: %s" % boot_failed_path)
elif boot_verified:
detail_text = tr_text("%d / %d 项资源已就绪" % [boot_index, active_boot_paths().size()], "%d / %d resources ready" % [boot_index, active_boot_paths().size()])
elif not boot_current_path.is_empty():
detail_text = tr_text("正在检查:%s" % boot_current_path.get_file(), "Checking: %s" % boot_current_path.get_file())
var detail := entry_body(detail_text)
detail.name = "BootDetail"
detail.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
column.add_child(detail)
if not boot_failed_path.is_empty():
var retry := entry_button("BootRetry", tr_text("重试本地校验", "Retry verification"), retry_boot, true)
column.add_child(retry)
func render_title() -> void:
var margin := MarginContainer.new()
margin.name = "TitleScreen"
margin.add_theme_constant_override("margin_left", 18 if compact_layout() else 36)
margin.add_theme_constant_override("margin_right", 18 if compact_layout() else 36)
margin.add_theme_constant_override("margin_top", 18 if compact_layout() else 30)
margin.add_theme_constant_override("margin_bottom", 18 if compact_layout() else 30)
margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
entry_layer.add_child(margin)
var layout := VBoxContainer.new()
margin.add_child(layout)
var tools := HBoxContainer.new()
tools.name = "TitleTools"
tools.alignment = BoxContainer.ALIGNMENT_END
tools.add_theme_constant_override("separation", 8)
tools.add_child(title_tool_button("TitleLocaleButton", "简 / EN", toggle_locale))
tools.add_child(title_tool_button("TitleTextScaleButton", "Aa", toggle_text_scale))
tools.add_child(title_tool_button("TitleMotionButton", "~", toggle_motion))
tools.add_child(title_tool_button("TitleMuteButton", "♪", toggle_mute))
layout.add_child(tools)
var spacer := Control.new()
spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
layout.add_child(spacer)
var copy := VBoxContainer.new()
copy.name = "TitleCopy"
copy.custom_minimum_size.x = 330 if compact_layout() else 500
copy.add_theme_constant_override("separation", 8)
layout.add_child(copy)
copy.add_child(entry_kicker(tr_text("本地内容已验证", "LOCAL CONTENT VERIFIED")))
copy.add_child(entry_heading(tr_text("裂脉远征团", "AETHERBOUND GUILD"), 40 if compact_layout() else 54))
copy.add_child(entry_body(tr_text("从公会门槛出发,组建队伍,读懂风险,再让他们自动迎战。", "Build a Company at the guild threshold, read the risk, then watch them fight automatically.")))
var state := Label.new()
state.name = "NoRunStatus"
state.text = tr_text("尚无远征记录", "NO EXPEDITION YET")
state.add_theme_color_override("font_color", Color("#d6e6df"))
state.add_theme_font_size_override("font_size", scaled(14))
copy.add_child(state)
var action := entry_button("StartNewGame", tr_text("开始新游戏", "Start New Game"), start_new_game, true)
action.custom_minimum_size = Vector2(260, 54)
copy.add_child(action)
action.grab_focus()
var note := entry_body(tr_text("新游戏将先进入契约与难度选择,不会自动创建远征。", "New Game opens Contract and difficulty setup; it does not create an expedition automatically."))
note.add_theme_font_size_override("font_size", scaled(11))
copy.add_child(note)
func run_boot_verification() -> void:
if boot_running:
return
boot_running = true
boot_failed_path = ""
boot_verified = false
boot_success_presented = false
boot_index = 0
for path in active_boot_paths():
boot_current_path = path
render()
await get_tree().process_frame
if not ResourceLoader.exists(path):
boot_failed_path = path
boot_running = false
render()
return
var resource := ResourceLoader.load(path)
if resource == null:
boot_failed_path = path
boot_running = false
render()
return
boot_index += 1
boot_current_path = ""
boot_verified = true
boot_running = false
render()
boot_success_presented = true
await get_tree().create_timer(0.25).timeout
page = AppPage.TITLE
render()
func active_boot_paths() -> Array[String]:
return boot_paths_override if not boot_paths_override.is_empty() else BOOT_REQUIRED_PATHS
func retry_boot() -> void:
call_deferred("run_boot_verification")
func start_new_game() -> void:
if not boot_verified:
return
page = AppPage.RUN
notice = tr_text("新游戏设置已开启;尚未选择契约或创建远征。", "New Game setup opened; no Contract or expedition has been created.")
play_cue("confirm")
render()
func render_contract() -> void:
var layout := stage_layout()
layout.add_child(page_header("P9.1 · 01", tr_text("选择首次穿越契约", "Choose the First Crossing Contract"), tr_text("先确认已知压力,再选择难度;当前不会生成队员或消耗金币。", "Review the known pressure, then choose difficulty. No Recruit or Coin changes yet.")))
var columns: BoxContainer
if compact_layout():
columns = VBoxContainer.new()
else:
columns = HBoxContainer.new()
columns.size_flags_vertical = Control.SIZE_EXPAND_FILL
columns.add_theme_constant_override("separation", 12)
layout.add_child(columns)
var contract := selectable_panel(run.contract_selected)
contract.name = "ContractCard"
contract.size_flags_horizontal = Control.SIZE_EXPAND_FILL
contract.add_child(stack([
kicker("RG-01 · CTR-R1-01"),
heading(tr_text("首次穿越", "First Crossing"), 24),
body_text(tr_text("勘察悬浮晶板之间的安全通路", "Chart the suspended pane crossing")),
fact_row(tr_text("主要压力", "Pressure"), tr_text("击退 + 远程屏障", "Push + projectile screen")),
fact_row(tr_text("首领规则", "Boss rule"), tr_text("轮换伤害类型晶板", "Rotating damage-family panes")),
make_button("ContractSelect", tr_text("已选择契约", "Contract selected") if run.contract_selected else tr_text("选择此契约", "Select this Contract"), select_contract, true),
]))
columns.add_child(contract)
var difficulty := panel(COLOR_PANEL_SOFT)
difficulty.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var difficulty_stack := VBoxContainer.new()
difficulty_stack.add_theme_constant_override("separation", 8)
difficulty.add_child(difficulty_stack)
difficulty_stack.add_child(kicker(tr_text("选择难度", "CHOOSE DIFFICULTY")))
for entry in [
["wayfinder", tr_text("向导", "Wayfinder"), tr_text("敌人生命与伤害 90%", "Enemy HP and damage 90%")],
["standard", tr_text("标准", "Standard"), tr_text("完整系统 · 标准平衡", "Full systems · authored balance")],
["oathbound", tr_text("誓约", "Oathbound"), tr_text("伤害 112% · 奖励 115%", "Damage 112% · rewards 115%")],
]:
var button := make_button("Difficulty_%s" % entry[0], "%s\n%s" % [entry[1], entry[2]], func(): apply_result(run.select_difficulty(entry[0]), "difficulty"), false)
button.disabled = not run.contract_selected
button.button_pressed = run.difficulty == entry[0]
button.toggle_mode = true
difficulty_stack.add_child(button)
columns.add_child(difficulty)
set_primary(tr_text("载入四名固定候选", "Load four stable offers"), open_offers, not run.contract_selected or run.difficulty.is_empty())
func render_offers() -> void:
var layout := stage_layout()
layout.add_child(page_header("P9.1 · 02", tr_text("查看四名队员,恰好选择两人", "Inspect four Recruits, choose exactly two"), tr_text("候选组保持不变;查看不会替你选择。", "The offer set stays stable; inspection never selects for you.")))
var cards := GridContainer.new()
cards.columns = responsive_columns(4)
cards.size_flags_vertical = Control.SIZE_EXPAND_FILL
cards.add_theme_constant_override("h_separation", 8)
layout.add_child(cards)
for entry in RunState.OFFERS:
cards.add_child(recruit_offer_card(entry))
var inspect := panel(Color(0.88, 0.94, 0.92, 0.96))
inspect.custom_minimum_size.y = 72
if inspected_id.is_empty():
inspect.add_child(body_text(tr_text("先点击“查看”了解优势、代价和初始装备。", "Inspect a Recruit to read their benefit, cost and starting equipment.")))
else:
var entry := run.offer(inspected_id)
inspect.add_child(fact_row(tr_text("当前查看", "INSPECTED"), "%s · %s · %s" % [entry.name, localized_profession(entry.profession), entry.item]))
layout.add_child(inspect)
set_primary(tr_text("审核初始队伍 (%d/2)" % run.selected.size(), "Review Company (%d/2)" % run.selected.size()), review_opening, run.selected.size() != 2)
func render_opening_review() -> void:
var layout := stage_layout()
layout.add_child(page_header("P9.1 · 03", tr_text("创建远征前确认", "Review before creating the expedition"), tr_text("确认后未选候选离开;这一步不消耗金币。", "Commit discards unchosen offers and costs no Coin.")))
layout.add_child(company_line(run.selected, false, "OpeningReviewLine"))
var terms := GridContainer.new()
terms.columns = responsive_columns(4)
terms.add_theme_constant_override("separation", 8)
for pair in [[tr_text("契约", "Contract"), "CTR-R1-01"], [tr_text("难度", "Difficulty"), run.difficulty], [tr_text("金币", "Coin"), "52 → 52"], [tr_text("队伍", "Company"), "0 → 2 / 4"]]:
terms.add_child(fact_tile(pair[0], pair[1]))
layout.add_child(terms)
var revise := make_button("ReviseOpening", tr_text("返回修改", "Revise"), revise_opening, false)
layout.add_child(revise)
set_primary(tr_text("创建这次远征", "Create this expedition"), commit_opening, false)
func render_opening_receipt() -> void:
var layout := receipt_layout("RUN-P9-001", tr_text("远征已创建", "Expedition created"), tr_text("唯一回执已提交;重复确认不会创建第二次。", "One receipt was committed; repeated confirm cannot create another run."))
layout.add_child(company_line(run.company, false, "OpeningReceiptLine"))
layout.add_child(receipt_facts([[tr_text("金币", "Coin"), "52"], [tr_text("队伍", "Company"), "2 / 4"], [tr_text("容量", "Capacity"), "4"], [tr_text("下一步", "Next"), tr_text("第一集市", "First Market")]]))
set_primary(tr_text("进入第一集市", "Open First Market"), open_market, false)
func render_market_hire(id: String) -> void:
var entry := run.offer(id)
var layout := stage_layout()
layout.add_child(page_header("P9.1 · MARKET", tr_text("分开确认每次招募", "Commit each hire separately"), tr_text("查看不会扣费;当前队伍和金币始终可见。", "Inspection spends nothing; Company and Coin remain visible.")))
var columns: BoxContainer
if compact_layout():
columns = VBoxContainer.new()
else:
columns = HBoxContainer.new()
columns.size_flags_vertical = Control.SIZE_EXPAND_FILL
columns.add_theme_constant_override("separation", 12)
layout.add_child(columns)
columns.add_child(company_line(run.company, true, "MarketCompanyLine"))
var offer := panel(COLOR_PANEL)
offer.name = "MarketOffer_%s" % id
offer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
offer.add_child(stack([
actor_view(entry, 180), kicker(id), heading(entry.name, 20),
body_text("%s · %s" % [localized_profession(entry.profession), localized_trait(entry.trait)]),
fact_row(tr_text("价格", "Price"), "14 Coin"),
fact_row(tr_text("队伍变化", "Company delta"), "%d → %d / 4" % [run.company.size(), run.company.size() + 1]),
]))
columns.add_child(offer)
set_primary(tr_text("招募 %s · 14金币" % entry.name, "Hire %s · 14 Coin" % entry.name), func(): hire(id), false)
func render_equipment_purchase() -> void:
var layout := stage_layout()
layout.add_child(page_header("P9.1 · EQUIPMENT", tr_text("购买改变行为的装备", "Buy equipment that changes behavior"), tr_text("购买与装备是两次独立操作;购买后仍在库存。", "Purchase and equip are separate; the item enters inventory first.")))
var columns: BoxContainer
if compact_layout():
columns = VBoxContainer.new()
else:
columns = HBoxContainer.new()
columns.size_flags_vertical = Control.SIZE_EXPAND_FILL
layout.add_child(columns)
var image := TextureRect.new()
image.texture = EQUIPMENT
image.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
image.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
image.custom_minimum_size = Vector2(180 if compact_layout() else 260, 150 if compact_layout() else 250)
columns.add_child(image)
var details := panel(COLOR_PANEL)
details.size_flags_horizontal = Control.SIZE_EXPAND_FILL
details.add_child(stack([kicker("EQ-012"), heading(tr_text("开阵系带", "Open-Line Cord"), 24), body_text(tr_text("前方没有相邻队员时,普通攻击射程 +1。", "With no neighbor ahead, basic attacks gain reach +1.")), fact_row(tr_text("代价", "Tradeoff"), tr_text("前方出现新队员会取消预留远距目标", "A new front neighbor cancels a reserved long target")), fact_row(tr_text("价格", "Price"), "12 Coin"), fact_row(tr_text("购买后", "After purchase"), "24 → 12 Coin")]))
columns.add_child(details)
set_primary(tr_text("购买 EQ-012 · 12金币", "Buy EQ-012 · 12 Coin"), purchase_equipment, false)
func render_equipment_equip() -> void:
var layout := stage_layout()
layout.add_child(page_header("P9.1 · EQUIP", tr_text("选择一名当前队员装备", "Choose a current wearer"), tr_text("每人只有一个有效装备槽;固定体验路径选择 Tomas。", "Each Recruit has one active slot; the fixed review path chooses Tomas.")))
var row := GridContainer.new()
row.columns = responsive_columns(4)
row.size_flags_vertical = Control.SIZE_EXPAND_FILL
row.add_theme_constant_override("separation", 8)
layout.add_child(row)
for id in run.company:
var entry := run.offer(id)
var button := Button.new()
button.name = "EquipTarget_%s" % id
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
button.custom_minimum_size.y = 240 if compact_layout() else 260
button.add_theme_stylebox_override("normal", style_box(COLOR_PANEL, COLOR_MUTED, 1, 4))
button.add_theme_stylebox_override("hover", style_box(Color("#fff5cb"), COLOR_GOLD, 2, 4))
button.add_child(button_stack([actor_view(entry, 170), heading(entry.name, 15), body_text(localized_profession(entry.profession)), fact_row(tr_text("当前装备", "Current"), entry.item)]))
button.pressed.connect(func(): equip_member(id))
row.add_child(button)
set_primary(tr_text("先选择装备者", "Choose a wearer"), func(): pass, true)
func render_order() -> void:
var layout := stage_layout()
layout.add_child(page_header("P9.1 · ORDER", tr_text("先点来源,再点目标位置", "Tap a source, then a destination"), tr_text("受保护后排始终在左,暴露前排始终在右。至少交换一次后才能确认。", "Protected rear stays left and exposed front stays right. Make at least one swap.")))
var direction := HBoxContainer.new()
direction.add_child(kicker(tr_text("受保护后排 ←", "PROTECTED REAR ←")))
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
direction.add_child(spacer)
direction.add_child(kicker(tr_text("→ 暴露前排", "→ EXPOSED FRONT")))
layout.add_child(direction)
layout.add_child(order_line())
var state_text := tr_text("请选择来源队员", "Choose a source Recruit") if run.order_source.is_empty() else tr_text("已选择 %s;请选择目标位置" % run.offer(run.order_source).name, "%s selected; choose destination" % run.offer(run.order_source).name)
layout.add_child(body_text(state_text))
set_primary(tr_text("确认出战准备", "Commit readiness"), commit_order, not run.order_changed or not run.order_source.is_empty())
func render_ready() -> void:
var layout := receipt_layout("READY-P9-001", tr_text("第一集市整备完成", "First Market preparation complete"), tr_text("你的队伍、金币、装备与站位已冻结到同一回执。", "Company, Coin, equipment and order are frozen in one readiness receipt."))
layout.add_child(company_line(run.company, false, "ReadyCompanyLine"))
layout.add_child(receipt_facts([[tr_text("金币", "Coin"), "12"], [tr_text("队伍", "Company"), "4 / 4"], ["EQ-012", "→ %s" % run.offer(run.equipment_wearer).name], [tr_text("提交", "Commit"), "1 / 1"]]))
var handoff := panel(Color(0.95, 0.89, 0.68, 0.98))
handoff.add_child(fact_row(tr_text("P9.2 未开启", "P9.2 LOCKED"), tr_text("自动战斗将在你确认本运行时体验后开发。", "Automatic battle begins only after your P9.1 review.")))
layout.add_child(handoff)
set_primary(tr_text("P9.1 到此结束", "End of P9.1"), func(): pass, true)
func stage_layout() -> VBoxContainer:
var margin := MarginContainer.new()
var horizontal := 10 if compact_layout() else 16 if stacked_chrome() else 22
var vertical := 10 if compact_layout() else 12 if stacked_chrome() else 16
margin.add_theme_constant_override("margin_left", horizontal)
margin.add_theme_constant_override("margin_right", horizontal)
margin.add_theme_constant_override("margin_top", vertical)
margin.add_theme_constant_override("margin_bottom", vertical)
margin.size_flags_horizontal = Control.SIZE_EXPAND_FILL
content.add_child(margin)
var layout := VBoxContainer.new()
layout.add_theme_constant_override("separation", 10)
margin.add_child(layout)
return layout
func receipt_layout(id: String, title: String, description: String) -> VBoxContainer:
var layout := stage_layout()
var header := HBoxContainer.new()
var check := Label.new()
check.text = "✓"
check.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
check.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
check.custom_minimum_size = Vector2(54, 54)
check.add_theme_color_override("font_color", COLOR_WHITE)
check.add_theme_font_size_override("font_size", 28)
check.add_theme_stylebox_override("normal", style_box(COLOR_TEAL, Color("#8ee2c7"), 1, 4))
header.add_child(check)
var copy := VBoxContainer.new()
copy.size_flags_horizontal = Control.SIZE_EXPAND_FILL
copy.add_child(light_kicker(id))
copy.add_child(light_heading(title, 23))
copy.add_child(light_body(description))
header.add_child(copy)
layout.add_child(header)
return layout
func recruit_offer_card(entry: Dictionary) -> Control:
var selected: bool = entry.id in run.selected
var card := selectable_panel(selected)
card.name = "OfferCard_%s" % entry.id
card.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var box := VBoxContainer.new()
box.add_theme_constant_override("separation", 5)
card.add_child(box)
box.add_child(actor_view(entry, 105 if compact_layout() else 125 if stacked_chrome() else 150))
box.add_child(kicker(entry.id))
box.add_child(heading(entry.name, 15))
box.add_child(body_text("%s · LV 1" % localized_profession(entry.profession)))
box.add_child(fact_row(tr_text("特性", "Trait"), localized_trait(entry.trait)))
box.add_child(fact_row(tr_text("装备", "Item"), entry.item))
var actions := HBoxContainer.new()
actions.add_theme_constant_override("separation", 5)
var inspect := make_button("Inspect_%s" % entry.id, tr_text("已查看", "Inspected") if entry.id in run.inspected else tr_text("查看", "Inspect"), func(): inspect_offer(entry.id), false)
inspect.size_flags_horizontal = Control.SIZE_EXPAND_FILL
actions.add_child(inspect)
var choose := make_button("Select_%s" % entry.id, tr_text("✓ 已选择", "✓ Selected") if selected else tr_text("选择", "Select"), func(): toggle_offer(entry.id), true)
choose.size_flags_horizontal = Control.SIZE_EXPAND_FILL
actions.add_child(choose)
box.add_child(actions)
return card
func company_line(ids: Array, allow_slots: bool, node_name: String) -> Control:
var panel_node := PanelContainer.new()
panel_node.name = node_name
panel_node.size_flags_horizontal = Control.SIZE_EXPAND_FILL
panel_node.size_flags_vertical = Control.SIZE_EXPAND_FILL
panel_node.add_theme_stylebox_override("panel", style_box(Color(0.9, 0.94, 0.92, 0.96), COLOR_TEAL, 1, 4))
var line := GridContainer.new()
line.columns = responsive_columns(4)
line.add_theme_constant_override("separation", 6)
panel_node.add_child(line)
for id in ids:
var entry := run.offer(id)
var cell := panel(COLOR_PANEL)
cell.size_flags_horizontal = Control.SIZE_EXPAND_FILL
cell.add_child(stack([actor_view(entry, 105 if compact_layout() else 125 if stacked_chrome() else 145), heading(entry.name, 13), body_text(localized_profession(entry.profession)), body_text(RunState.EQUIPMENT_ID if run.equipment_wearer == id else entry.item)]))
line.add_child(cell)
if allow_slots:
for index in range(RunState.CAPACITY - ids.size()):
var empty := panel(Color(0.83, 0.87, 0.85, 0.8))
empty.size_flags_horizontal = Control.SIZE_EXPAND_FILL
empty.add_child(stack([heading("+", 30), body_text(tr_text("空席位", "Open slot"))]))
line.add_child(empty)
return panel_node
func order_line() -> Control:
var line := GridContainer.new()
line.columns = responsive_columns(4)
line.name = "OrderLine"
line.size_flags_vertical = Control.SIZE_EXPAND_FILL
line.add_theme_constant_override("separation", 8)
for index in range(run.company.size()):
var id := run.company[index]
var entry := run.offer(id)
var button := Button.new()
button.name = "OrderUnit_%s" % id
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
button.custom_minimum_size.y = 240 if compact_layout() else 260
button.add_theme_stylebox_override("normal", style_box(Color("#eef3ef"), COLOR_GOLD if run.order_source == id else COLOR_MUTED, 3 if run.order_source == id else 1, 4))
button.add_theme_stylebox_override("hover", style_box(Color("#fff5cb"), COLOR_GOLD, 2, 4))
button.add_child(button_stack([actor_view(entry, 110 if compact_layout() else 135 if stacked_chrome() else 155), kicker(str(run.company.size() - 1 - index)), heading(entry.name, 14), body_text(localized_profession(entry.profession)), body_text("EQ-012" if run.equipment_wearer == id else entry.item)]))
button.pressed.connect(func(): choose_order(id))
line.add_child(button)
return line
func page_header(code: String, title: String, description: String) -> Control:
var box := VBoxContainer.new()
box.add_theme_constant_override("separation", 3)
box.add_child(light_kicker(code))
box.add_child(light_heading(title, 24))
box.add_child(light_body(description))
return box
func receipt_facts(entries: Array) -> Control:
var row := GridContainer.new()
row.columns = responsive_columns(4)
row.add_theme_constant_override("separation", 8)
for entry in entries:
row.add_child(fact_tile(entry[0], entry[1]))
return row
func fact_tile(label: String, value: String) -> Control:
var tile := panel(COLOR_PANEL)
tile.size_flags_horizontal = Control.SIZE_EXPAND_FILL
tile.add_child(stack([kicker(label), heading(value, 17)]))
return tile
func fact_row(label: String, value: String) -> Control:
var row: BoxContainer
if compact_layout():
row = VBoxContainer.new()
else:
row = HBoxContainer.new()
row.add_theme_constant_override("separation", 8)
var left := Label.new()
left.text = label
left.size_flags_horizontal = Control.SIZE_EXPAND_FILL
left.custom_minimum_size.x = 0
left.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
left.add_theme_color_override("font_color", COLOR_TEAL)
left.add_theme_font_size_override("font_size", scaled(11))
row.add_child(left)
var right := Label.new()
right.text = value
right.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT if compact_layout() else HORIZONTAL_ALIGNMENT_RIGHT
right.size_flags_horizontal = Control.SIZE_EXPAND_FILL
right.custom_minimum_size.x = 0
right.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
right.add_theme_color_override("font_color", COLOR_INK)
right.add_theme_font_size_override("font_size", scaled(11))
row.add_child(right)
return row
func actor_view(entry: Dictionary, height: float) -> TextureRect:
var image := TextureRect.new()
image.texture = ACTORS.get(entry.profession, ACTORS.Hearthguard)
image.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
image.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
image.custom_minimum_size.y = height
image.mouse_filter = Control.MOUSE_FILTER_IGNORE
return image
func selectable_panel(selected: bool) -> PanelContainer:
var card := PanelContainer.new()
card.add_theme_stylebox_override("panel", style_box(Color("#fff7d8") if selected else COLOR_PANEL, COLOR_GOLD if selected else COLOR_MUTED, 3 if selected else 1, 5))
return card
func panel(color: Color) -> PanelContainer:
var node := PanelContainer.new()
node.add_theme_stylebox_override("panel", style_box(color, Color("#9caeaa"), 1, 4))
return node
func stack(nodes: Array) -> VBoxContainer:
var box := VBoxContainer.new()
box.add_theme_constant_override("separation", 5)
for node in nodes:
box.add_child(node)
return box
func entry_kicker(value: String) -> Label:
var label := Label.new()
label.text = value
label.add_theme_color_override("font_color", Color("#72d9cc"))
label.add_theme_font_size_override("font_size", scaled(11))
return label
func entry_heading(value: String, size: int) -> Label:
var label := Label.new()
label.text = value
label.add_theme_color_override("font_color", COLOR_WHITE)
label.add_theme_font_size_override("font_size", scaled(size))
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
return label
func entry_body(value: String) -> Label:
var label := Label.new()
label.text = value
label.add_theme_color_override("font_color", Color("#d6e2df"))
label.add_theme_font_size_override("font_size", scaled(13))
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
return label
func entry_button(node_name: String, label: String, callback: Callable, accent: bool) -> Button:
var button := make_button(node_name, label, callback, accent, true)
button.add_theme_font_size_override("font_size", scaled(15))
return button
func title_tool_button(node_name: String, label: String, callback: Callable) -> Button:
var button := tool_button(node_name, label, callback, true)
button.custom_minimum_size = Vector2(50, 48)
button.size_flags_horizontal = Control.SIZE_SHRINK_END
return button
func button_stack(nodes: Array) -> VBoxContainer:
var box := stack(nodes)
box.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
box.offset_left = 10
box.offset_top = 8
box.offset_right = -10
box.offset_bottom = -8
box.mouse_filter = Control.MOUSE_FILTER_IGNORE
return box
func kicker(value: String) -> Label:
var label := Label.new()
label.text = value
label.add_theme_color_override("font_color", COLOR_TEAL)
label.add_theme_font_size_override("font_size", scaled(10))
return label
func light_kicker(value: String) -> Label:
var label := kicker(value)
label.add_theme_color_override("font_color", Color("#63d5c8"))
return label
func heading(value: String, size: int) -> Label:
var label := Label.new()
label.text = value
label.add_theme_color_override("font_color", COLOR_INK)
label.add_theme_font_size_override("font_size", scaled(size))
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
return label
func light_heading(value: String, size: int) -> Label:
var label := heading(value, size)
label.add_theme_color_override("font_color", COLOR_WHITE)
return label
func body_text(value: String) -> Label:
var label := Label.new()
label.text = value
label.add_theme_color_override("font_color", Color("#33474d"))
label.add_theme_font_size_override("font_size", scaled(11))
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
return label
func light_body(value: String) -> Label:
var label := body_text(value)
label.add_theme_color_override("font_color", Color("#c8dad7"))
return label
func resource_label(value: String) -> Label:
var label := Label.new()
label.text = value
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.custom_minimum_size = Vector2(96, 46)
if stacked_chrome():
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.add_theme_color_override("font_color", COLOR_WHITE)
label.add_theme_font_size_override("font_size", 12)
label.add_theme_stylebox_override("normal", style_box(Color("#263a40"), Color("#6e858a"), 1, 4))
return label
func tool_button(node_name: String, label: String, callback: Callable, deferred_callback := false) -> Button:
var button := Button.new()
button.name = node_name
button.text = label
button.custom_minimum_size = Vector2(48, 46)
if stacked_chrome():
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
button.add_theme_stylebox_override("normal", style_box(Color("#263a40"), Color("#6e858a"), 1, 4))
button.add_theme_stylebox_override("hover", style_box(Color("#35525a"), COLOR_WHITE, 1, 4))
button.add_theme_color_override("font_color", COLOR_WHITE)
button.pressed.connect(callback, CONNECT_DEFERRED if deferred_callback else 0)
return button
func make_button(node_name: String, label: String, callback: Callable, accent: bool, deferred_callback := false) -> Button:
var button := Button.new()
button.name = node_name
button.text = label
button.custom_minimum_size.y = 46
button.add_theme_font_size_override("font_size", scaled(11))
button.add_theme_color_override("font_color", COLOR_INK if accent else COLOR_WHITE)
button.add_theme_stylebox_override("normal", style_box(COLOR_GOLD if accent else Color("#263a40"), Color("#f7dc84") if accent else Color("#6e858a"), 1, 4))
button.add_theme_stylebox_override("hover", style_box(Color("#f0c94d") if accent else Color("#35525a"), COLOR_WHITE, 1, 4))
button.pressed.connect(callback, CONNECT_DEFERRED if deferred_callback else 0)
return button
func style_box(fill: Color, border: Color, width: int, radius: int) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = fill
style.border_color = border
style.set_border_width_all(width)
style.set_corner_radius_all(radius)
style.content_margin_left = 10
style.content_margin_right = 10
style.content_margin_top = 8
style.content_margin_bottom = 8
return style
func set_primary(label: String, callback: Callable, disabled: bool) -> void:
primary_action.text = label
primary_action.disabled = disabled
primary_action.pressed.connect(callback)
func update_hud() -> void:
coin_label.text = "%s %d" % [tr_text("金币", "COIN"), run.coin]
company_label.text = "%s %d / 4" % [tr_text("队伍", "COMPANY"), run.company.size()]
step_label.text = step_name() if compact_layout() else "%s · %s" % [tr_text("P9.1 运行时垂直切片", "P9.1 RUNTIME VERTICAL SLICE"), step_name()]
func update_footer() -> void:
status_label.text = notice if not notice.is_empty() else step_guidance()
func step_name() -> String:
var names := {
RunState.Step.CONTRACT: tr_text("契约", "Contract"), RunState.Step.OFFERS: tr_text("初始队伍", "Opening Company"), RunState.Step.OPENING_REVIEW: tr_text("建队确认", "Company Review"), RunState.Step.OPENING_RECEIPT: tr_text("建队回执", "Creation Receipt"), RunState.Step.MARKET_MAEL: tr_text("招募 Mael", "Hire Mael"), RunState.Step.MARKET_ILYA: tr_text("招募 Ilya", "Hire Ilya"), RunState.Step.EQUIPMENT_PURCHASE: tr_text("购买装备", "Buy Equipment"), RunState.Step.EQUIPMENT_EQUIP: tr_text("选择装备者", "Choose Wearer"), RunState.Step.ORDER: tr_text("横队站位", "Party Order"), RunState.Step.READY: tr_text("整备回执", "Ready Receipt")
}
return names[run.step]
func step_guidance() -> String:
if run.step == RunState.Step.OFFERS:
return tr_text("已选择 %d/2 · 已查看 %d/4" % [run.selected.size(), run.inspected.size()], "Selected %d/2 · inspected %d/4" % [run.selected.size(), run.inspected.size()])
if run.step == RunState.Step.ORDER:
return tr_text("选择两名队员交换站位;确认前可以继续修改。", "Select two Recruits to swap; revise freely before commit.")
return tr_text("当前操作不会影响后续冻结阶段。", "The current action cannot enter a later frozen phase.")
func tr_text(zh: String, en: String) -> String:
return zh if locale == "zh_CN" else en
func localized_profession(value: String) -> String:
var translations := {"Hearthguard": "护心卫士", "Farstring": "远弦射手", "Wayrunner": "逐风行者", "Kindhand": "仁心医者"}
return translations.get(value, value) if locale == "zh_CN" else value
func localized_trait(value: String) -> String:
var translations := {"Narrow Guard": "窄阵护卫", "Patient Start": "耐心起手", "Open Stance": "开放步法", "Shared Cup": "共饮药杯", "Steady Brace": "稳固支撑", "Clear Measure": "清晰尺度"}
return translations.get(value, value) if locale == "zh_CN" else value
func scaled(value: int) -> int:
return int(round(value * text_scale))
func compact_layout() -> bool:
return get_viewport_rect().size.x < 700.0
func stacked_chrome() -> bool:
return get_viewport_rect().size.x < 1000.0
func responsive_columns(wide: int) -> int:
if compact_layout():
return 1
if stacked_chrome():
return min(2, wide)
return wide
func apply_result(result: Dictionary, cue := "confirm") -> void:
notice = message_for(result.code)
if result.ok:
play_cue(cue)
render()
func message_for(code: String) -> String:
var messages := {
"contract_selected": tr_text("已选择 CTR-R1-01;请继续选择难度。", "CTR-R1-01 selected; now choose difficulty."),
"difficulty_selected": tr_text("难度已选择;可以载入候选。", "Difficulty selected; offers can now load."),
"offers_opened": tr_text("四名固定候选已载入;查看不会改变选择。", "Four stable offers loaded; inspection does not select."),
"exactly_two_limit": tr_text("已经选择两人;请先取消一人。", "Two Recruits are selected; deselect one first."),
"exactly_two_required": tr_text("必须恰好选择两人。", "Choose exactly two Recruits."),
"offer_selected": tr_text("队员已选择。", "Recruit selected."),
"offer_removed": tr_text("队员已取消选择。", "Recruit deselected."),
"offer_inspected": tr_text("已查看队员取舍。", "Recruit tradeoff inspected."),
"opening_reviewed": tr_text("初始队伍已进入确认;尚未创建远征。", "Opening Company is under review; the expedition is not created yet."),
"opening_revised": tr_text("已返回候选;原选择仍保留。", "Returned to offers; the pair remains selected."),
"opening_committed": tr_text("远征已创建一次。", "Expedition created once."),
"market_opened": tr_text("第一集市已开启;招募尚未扣费。", "First Market opened; no hire has been charged yet."),
"hire_committed": tr_text("招募已提交,金币和队伍已更新。", "Hire committed; Coin and Company updated."),
"equipment_purchased": tr_text("EQ-012 已进入库存;尚未装备。", "EQ-012 entered inventory; it is not equipped yet."),
"equipment_equipped": tr_text("EQ-012 已装备。", "EQ-012 equipped."),
"order_source_selected": tr_text("来源已选择;请点击目标位置。", "Source selected; tap the destination."),
"order_source_cleared": tr_text("来源选择已取消;站位没有改变。", "Source selection cleared; order did not change."),
"order_swapped": tr_text("站位已交换;确认前仍可修改。", "Positions swapped; you can still revise."),
"readiness_committed": tr_text("第一集市整备回执已提交。", "First-Market readiness receipt committed."),
}
return messages.get(code, tr_text("当前操作不合法,状态没有改变。", "That action is not legal; state did not change."))
func select_contract() -> void:
apply_result(run.select_contract(), "select")
func open_offers() -> void:
apply_result(run.open_offers(), "confirm")
func inspect_offer(id: String) -> void:
inspected_id = id
apply_result(run.inspect_offer(id), "select")
func toggle_offer(id: String) -> void:
apply_result(run.toggle_offer(id), "select")
func review_opening() -> void:
apply_result(run.review_opening(), "confirm")
func revise_opening() -> void:
apply_result(run.revise_opening(), "select")
func commit_opening() -> void:
apply_result(run.commit_opening(), "receipt")
func open_market() -> void:
apply_result(run.open_market(), "confirm")
func hire(id: String) -> void:
apply_result(run.hire(id), "purchase")
func purchase_equipment() -> void:
apply_result(run.purchase_equipment(), "purchase")
func equip_member(id: String) -> void:
apply_result(run.equip_member(id), "equip")
func choose_order(id: String) -> void:
apply_result(run.choose_order_unit(id), "select")
func commit_order() -> void:
apply_result(run.commit_order(), "receipt")
func toggle_locale() -> void:
locale = "en" if locale == "zh_CN" else "zh_CN"
notice = tr_text("语言已切换;游戏状态没有改变。", "Language changed; gameplay state is unchanged.")
render()
func toggle_text_scale() -> void:
text_scale = 1.0 if text_scale > 1.0 else 1.3
notice = tr_text("文字缩放 %d%%。" % int(text_scale * 100), "Text scale %d%%." % int(text_scale * 100))
render()
func toggle_motion() -> void:
reduced_motion = not reduced_motion
notice = tr_text("减少动态已%s。" % ("开启" if reduced_motion else "关闭"), "Reduced motion %s." % ("on" if reduced_motion else "off"))
render()
func toggle_mute() -> void:
muted = not muted
AudioServer.set_bus_mute(AudioServer.get_bus_index(&"SFX"), muted)
notice = tr_text("声音提示已%s。" % ("关闭" if muted else "开启"), "Sound cues %s." % ("off" if muted else "on"))
if not muted:
play_cue("confirm")
render()
func play_cue(kind: String) -> void:
if muted or sfx_playback == null:
return
var frequency: float = {"select": 330.0, "confirm": 440.0, "purchase": 523.0, "equip": 587.0, "receipt": 659.0}.get(kind, 392.0)
var frames := int(22050.0 * 0.09)
for index in range(frames):
var envelope := sin(PI * float(index) / float(frames)) * 0.035
var value := sin(TAU * frequency * float(index) / 22050.0) * envelope
sfx_playback.push_frame(Vector2(value, value))