Implement the bounded Godot opening Company and first-Market preparation flow with deterministic one-shot state, real touch verification, responsive layouts, prototype feedback, and current design regressions.\n\nCo-Authored-By: Codex <noreply@anthropic.com>
935 lines
40 KiB
GDScript
935 lines
40 KiB
GDScript
extends Control
|
|
|
|
const RunState = preload("res://domain/run_state.gd")
|
|
|
|
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 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()
|
|
var locale := "zh_CN"
|
|
var muted := false
|
|
var reduced_motion := false
|
|
var text_scale := 1.0
|
|
var notice := ""
|
|
var inspected_id := ""
|
|
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:
|
|
build_shell()
|
|
render()
|
|
|
|
|
|
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)
|
|
|
|
var wash := ColorRect.new()
|
|
wash.color = Color(0.025, 0.07, 0.085, 0.76)
|
|
wash.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
wash.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
add_child(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)
|
|
|
|
root_panel = VBoxContainer.new()
|
|
root_panel.name = "RuntimeShell"
|
|
root_panel.add_theme_constant_override("separation", 10)
|
|
safe.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.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:
|
|
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_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 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) -> 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)
|
|
return button
|
|
|
|
|
|
func make_button(node_name: String, label: String, callback: Callable, accent: bool) -> 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)
|
|
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))
|