## CombatS2 — S2 核心循环场景 ## 目标:波次→商店→升级→下一波,循环可无限重复 ## 验收:P-S2-01 ~ P-S2-07 extends Node2D ## 玩家节点 var _player_node: Node2D = null ## 世界渲染(MultiMeshInstance2D,§4.4 渲染优化) var _bullet_mm: MultiMesh = null var _enemy_mm: MultiMesh = null ## UI 层引用 var _hud_layer: CanvasLayer = null var _shop_layer: CanvasLayer = null ## HUD 标签 var _lbl_wave: Label = null var _lbl_hp: Label = null var _lbl_gold: Label = null var _lbl_xp: Label = null var _lbl_kills: Label = null var _lbl_status: Label = null var _lbl_deck: Label = null var _lbl_dps: Label = null var _lbl_charge: Label = null var _lbl_statuses: Label = null var _dps_poll_timer: float = 0.0 ## 商店 var _shop_panel: Panel = null var _shop_btns: Array = [] # Array[Button] var _reroll_btn: Button = null var _close_btn: Button = null var _core_btn: Button = null var _inv_btn: Button = null var _lang_btn: Button = null ## 背包(插槽编辑) var _inv_layer: CanvasLayer = null var _inv_root: Control = null var _inv_sel_kind: String = "" # "" / "slot" / "bench" var _inv_sel_idx: int = -1 ## 结算 / 死亡屏 var _settle_layer: CanvasLayer = null var _settle_root: Control = null ## 暂停菜单 var _pause_layer: CanvasLayer = null var _is_paused: bool = false ## 设置面板 var _settings_layer: CanvasLayer = null var _settings_root: Control = null var _clear_confirm: bool = false # 清除数据二次确认 ## CombatManager var _cm: Node = null # CombatManager 实例 func _ready() -> void: # 暂停时仍需响应 ESC 与暂停菜单按钮(游戏世界由 autoload 的 PAUSABLE _physics_process 冻结) process_mode = Node.PROCESS_MODE_ALWAYS SceneManager.fade_in_first() # 单独测试时淡入;经 goto 进入时自动跳过 _setup_player() _setup_world_view() _setup_hud() _setup_shop_ui() _setup_inventory_ui() _setup_settlement_ui() _setup_settings_ui() _setup_pause_menu() _setup_combat_manager() _subscribe_events() func _input(event: InputEvent) -> void: if event.is_action_pressed("ui_cancel"): if _inv_layer and _inv_layer.visible: _close_inventory() elif _settings_layer and _settings_layer.visible: _close_settings() elif _settle_layer and _settle_layer.visible: pass # 结算屏:ESC 无效,必须按按钮 elif _shop_layer and _shop_layer.visible: pass # 商店中不暂停 else: _toggle_pause() func _process(delta: float) -> void: # 世界渲染同步(SoA → MultiMesh) BulletManager.sync_multimesh(_bullet_mm) EnemyManager.sync_multimesh(_enemy_mm) _refresh_hud() _dps_poll_timer -= delta if _dps_poll_timer <= 0.0: _dps_poll_timer = 0.5 if _lbl_dps: _lbl_dps.text = tr("UI_DPS") % DpsTracker.get_dps() # ── 初始化 ───────────────────────────────────────────────── func _setup_player() -> void: var p := Node2D.new() p.name = "Player" add_child(p) _player_node = p PlayerManager.set_player_node(p) ## 世界视图:竞技场网格背景 + 玩家标记 + 跟随相机 + 子弹/敌人 MultiMesh 渲染器 func _setup_world_view() -> void: # 竞技场网格地面(程序化 shader,世界静止,z 最底) var arena := Polygon2D.new() var ext: float = 5000.0 arena.polygon = PackedVector2Array([ Vector2(-ext, -ext), Vector2(ext, -ext), Vector2(ext, ext), Vector2(-ext, ext)]) arena.uv = PackedVector2Array([ Vector2(0, 0), Vector2(1, 0), Vector2(1, 1), Vector2(0, 1)]) var grid_mat := ShaderMaterial.new() grid_mat.shader = load("res://shaders/arena_grid.gdshader") arena.material = grid_mat arena.z_index = -100 add_child(arena) # 玩家可视标记(青色发光菱形 + 光晕环) var glow := Polygon2D.new() glow.polygon = PackedVector2Array([ Vector2(0, -22), Vector2(22, 0), Vector2(0, 22), Vector2(-22, 0)]) glow.color = Color(0.35, 0.9, 1.0, 0.22) _player_node.add_child(glow) var marker := Polygon2D.new() marker.polygon = PackedVector2Array([ Vector2(0, -13), Vector2(13, 0), Vector2(0, 13), Vector2(-13, 0)]) marker.color = Color(0.55, 0.95, 1.0) _player_node.add_child(marker) var core := Polygon2D.new() core.polygon = PackedVector2Array([ Vector2(0, -6), Vector2(6, 0), Vector2(0, 6), Vector2(-6, 0)]) core.color = Color(1.0, 1.0, 1.0) _player_node.add_child(core) # 跟随相机 var cam := Camera2D.new() cam.enabled = true _player_node.add_child(cam) var quad := QuadMesh.new() quad.size = Vector2(1.0, 1.0) # 单位四边形,transform 缩放即像素尺寸 # 敌人渲染器(per-instance 颜色) _enemy_mm = MultiMesh.new() _enemy_mm.transform_format = MultiMesh.TRANSFORM_2D _enemy_mm.use_colors = true _enemy_mm.mesh = quad _enemy_mm.instance_count = EnemyManager.MAX_ENEMIES _enemy_mm.visible_instance_count = 0 var enemy_mmi := MultiMeshInstance2D.new() enemy_mmi.multimesh = _enemy_mm # 程序化美术占位:方块 → 软边实体圆 + 高光 var enemy_mat := ShaderMaterial.new() enemy_mat.shader = load("res://shaders/enemy_circle.gdshader") enemy_mmi.material = enemy_mat add_child(enemy_mmi) # 子弹渲染器(统一淡黄色,modulate) _bullet_mm = MultiMesh.new() _bullet_mm.transform_format = MultiMesh.TRANSFORM_2D _bullet_mm.mesh = quad _bullet_mm.instance_count = BulletManager.MAX_BULLETS _bullet_mm.visible_instance_count = 0 var bullet_mmi := MultiMeshInstance2D.new() bullet_mmi.multimesh = _bullet_mm bullet_mmi.modulate = Color(1.0, 0.95, 0.45) # 程序化美术占位:方块 → 加法发光圆(弹幕质感) var bullet_mat := ShaderMaterial.new() bullet_mat.shader = load("res://shaders/bullet_glow.gdshader") bullet_mmi.material = bullet_mat add_child(bullet_mmi) func _setup_combat_manager() -> void: _cm = Node.new() _cm.set_script(load("res://scripts/domain/combat/combat_manager.gd")) add_child(_cm) _cm._on_shop_closed_cb = Callable(self, "_on_shop_ready") # 根据 SceneManager 传入的启动模式决定新游戏还是继续 if SceneManager.start_mode == "continue": call_deferred("_deferred_resume") else: call_deferred("_deferred_start") func _deferred_start() -> void: _cm.start_game() func _deferred_resume() -> void: _cm.resume_game() func _subscribe_events() -> void: EventBus.subscribe(EventID.GAME_STATE_CHANGED, _on_game_state_changed) EventBus.subscribe(EventID.GAME_OVER, _on_game_over) EventBus.subscribe(EventID.SPELL_DEPTH_EXCEEDED, _on_spell_depth_exceeded) EventBus.subscribe(EventID.CHARGE_STATE_CHANGED, _on_charge_state_changed) EventBus.subscribe(EventID.CHARGE_FIRED, _on_charge_fired) PlayerStats.stats_changed.connect(_on_stats_changed) func _setup_hud() -> void: _hud_layer = CanvasLayer.new() _hud_layer.name = "HUD" add_child(_hud_layer) var bg := ColorRect.new() bg.color = Color(0, 0, 0, 0.5) bg.size = Vector2(380, 190) bg.position = Vector2(10, 10) _hud_layer.add_child(bg) var make_label := func(pos: Vector2, font_size: int = 16) -> Label: var lbl := Label.new() lbl.position = pos lbl.add_theme_font_size_override("font_size", font_size) _hud_layer.add_child(lbl) return lbl _lbl_wave = make_label.call(Vector2(20, 18), 18) _lbl_hp = make_label.call(Vector2(20, 42)) _lbl_gold = make_label.call(Vector2(20, 62)) _lbl_xp = make_label.call(Vector2(20, 82)) _lbl_kills = make_label.call(Vector2(20, 102)) _lbl_dps = make_label.call(Vector2(200, 42)) _lbl_charge = make_label.call(Vector2(200, 62)) _lbl_deck = make_label.call(Vector2(20, 122), 13) _lbl_statuses = make_label.call(Vector2(20, 142), 13) _lbl_status = make_label.call(Vector2(20, 620), 20) _lbl_status.modulate = Color.YELLOW func _setup_shop_ui() -> void: _shop_layer = CanvasLayer.new() _shop_layer.name = "ShopLayer" _shop_layer.visible = false add_child(_shop_layer) var bg := ColorRect.new() bg.color = Color(0.08, 0.08, 0.2, 0.92) bg.size = Vector2(600, 340) bg.position = Vector2(200, 200) _shop_layer.add_child(bg) var title := Label.new() title.text = tr("SHOP_TITLE") title.position = Vector2(220, 215) title.add_theme_font_size_override("font_size", 20) title.modulate = Color.GOLD _shop_layer.add_child(title) # 3 张法术卡按鈕 for i in 3: var btn := Button.new() btn.position = Vector2(220 + i * 195, 255) btn.size = Vector2(180, 130) btn.connect("pressed", _on_shop_slot_pressed.bind(i)) _shop_layer.add_child(btn) _shop_btns.append(btn) _reroll_btn = Button.new() _reroll_btn.text = tr("SHOP_REROLL") % ShopManager.REROLL_BASE_COST _reroll_btn.position = Vector2(220, 400) _reroll_btn.size = Vector2(130, 38) _reroll_btn.connect("pressed", _on_reroll_pressed) _shop_layer.add_child(_reroll_btn) # 法杖(Core)切换按钮:循环 wand_basic→fast→memory→matrix→circuit _core_btn = Button.new() _core_btn.position = Vector2(360, 400) _core_btn.size = Vector2(180, 38) _core_btn.connect("pressed", _on_switch_core_pressed) _shop_layer.add_child(_core_btn) # 背包(插槽编辑)按钮 _inv_btn = Button.new() _inv_btn.text = tr("SHOP_INV_BTN") _inv_btn.position = Vector2(220, 445) _inv_btn.size = Vector2(130, 34) _inv_btn.connect("pressed", _open_inventory) _shop_layer.add_child(_inv_btn) # 语言切换(循环 zh_CN→zh_TW→en→ja) _lang_btn = Button.new() _lang_btn.position = Vector2(360, 445) _lang_btn.size = Vector2(120, 34) _lang_btn.connect("pressed", _on_lang_pressed) _shop_layer.add_child(_lang_btn) # 设置面板入口 var settings_btn := Button.new() settings_btn.text = tr("SHOP_SETTINGS_BTN") settings_btn.position = Vector2(490, 445) settings_btn.size = Vector2(120, 34) settings_btn.connect("pressed", _open_settings) _shop_layer.add_child(settings_btn) _close_btn = Button.new() _close_btn.text = tr("SHOP_NEXT_WAVE") _close_btn.position = Vector2(550, 400) _close_btn.size = Vector2(140, 38) _close_btn.connect("pressed", _on_close_shop_pressed) _shop_layer.add_child(_close_btn) # ── HUD 刷新 ──────────────────────────────────────────────── func _refresh_hud() -> void: if _lbl_wave == null: return _lbl_wave.text = tr("UI_WAVE") % WaveManager.current_wave _lbl_hp.text = tr("UI_HP") % [PlayerStats.hp, PlayerStats.hp_max] _lbl_gold.text = tr("UI_GOLD") % PlayerStats.gold _lbl_xp.text = tr("UI_XP") % [PlayerStats.xp, PlayerStats.xp_to_next, PlayerStats.level] _lbl_kills.text = tr("UI_KILLS") % [PlayerManager.kill_count, EnemyManager.get_active_count()] if _cm: _lbl_deck.text = "[%s] %s" % [_cm.get_core_display(), str(_cm.get_deck_spell_ids())] _lbl_statuses.text = tr("UI_STATUSES") % [StatusManager.get_active_count(), VFXManager.get_active_count()] func _refresh_shop_ui() -> void: for i in _shop_btns.size(): var btn: Button = _shop_btns[i] var spell: SpellNode = ShopManager.current_slots[i] if i < ShopManager.current_slots.size() else null if spell: var cost: int = int(spell.meta.get("shop_cost", 20)) btn.text = "%s\n%s\n%dG" % [spell.display_name, spell.description, cost] btn.disabled = PlayerStats.gold < cost else: btn.text = tr("SHOP_SOLD") btn.disabled = true var reroll_cost: int = ShopManager.get_reroll_cost() _reroll_btn.text = tr("SHOP_REROLL") % reroll_cost _reroll_btn.disabled = PlayerStats.gold < reroll_cost if _core_btn and _cm: _core_btn.text = tr("SHOP_CORE_BTN") % _cm.get_core_display() if _lang_btn: _lang_btn.text = "🌐 " + Locale.get_locale() # ── 事件 ───────────────────────────────────────────────── func _on_game_state_changed(payload: Dictionary) -> void: var to_state: String = str(payload.get("to", "")) _lbl_status.text = "[状态: %s]" % to_state func _on_stats_changed() -> void: if _shop_layer and _shop_layer.visible: _refresh_shop_ui() func _on_shop_ready(_wave: int) -> void: _shop_layer.visible = true _refresh_shop_ui() func _on_shop_slot_pressed(slot_idx: int) -> void: var spell: SpellNode = ShopManager.buy_spell(slot_idx) if spell: _cm.install_spell(spell) _lbl_status.text = tr("STATUS_PURCHASED") % spell.display_name _refresh_shop_ui() func _on_reroll_pressed() -> void: if ShopManager.reroll(): _refresh_shop_ui() func _on_switch_core_pressed() -> void: _cm.switch_core() _lbl_status.text = tr("STATUS_WAND_SWITCH") % _cm.get_core_display() _refresh_shop_ui() func _on_lang_pressed() -> void: SettingsManager.set_locale(Locale.cycle_locale()) # 切换并持久化 _refresh_shop_ui() # 立即刷新商店文本(HUD 由 _process 持续刷新) # ── 暂停菜单 ──────────────────────────────────────────────── func _setup_pause_menu() -> void: _pause_layer = CanvasLayer.new() _pause_layer.name = "PauseLayer" _pause_layer.layer = 20 _pause_layer.visible = false add_child(_pause_layer) # 半透明遮罩 var dim := ColorRect.new() dim.color = Color(0.0, 0.0, 0.0, 0.65) dim.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) dim.mouse_filter = Control.MOUSE_FILTER_STOP _pause_layer.add_child(dim) # 面板 var panel := ColorRect.new() panel.color = Color(0.06, 0.06, 0.14, 0.98) panel.size = Vector2(380, 340) panel.position = Vector2(386, 154) _pause_layer.add_child(panel) # 标题 var title := Label.new() title.text = tr("PAUSE_TITLE") title.position = Vector2(480, 174) title.add_theme_font_size_override("font_size", 30) title.modulate = Color.GOLD _pause_layer.add_child(title) # 按钮 var btns = [ [tr("PAUSE_RESUME"), func(): _toggle_pause()], [tr("PAUSE_SETTINGS"), func(): _toggle_pause(); _open_settings()], [tr("PAUSE_QUIT_MENU"), func(): get_tree().paused = false; SceneManager.goto_main_menu()], [tr("PAUSE_QUIT_GAME"), func(): get_tree().quit()], ] var y: float = 240.0 for bd in btns: var b := Button.new() b.text = bd[0] b.position = Vector2(436, y) b.size = Vector2(280, 46) b.add_theme_font_size_override("font_size", 18) b.connect("pressed", bd[1]) _pause_layer.add_child(b) y += 58.0 func _toggle_pause() -> void: if _settle_layer and _settle_layer.visible: return _is_paused = not _is_paused _pause_layer.visible = _is_paused get_tree().paused = _is_paused # ── 设置面板(难度 / 语言 / 音量 / 清除数据 ST-03)────────────── func _setup_settings_ui() -> void: _settings_layer = CanvasLayer.new() _settings_layer.name = "SettingsLayer" _settings_layer.visible = false add_child(_settings_layer) var bg := ColorRect.new() bg.color = Color(0.06, 0.06, 0.12, 0.97) bg.size = Vector2(560, 440) bg.position = Vector2(300, 110) _settings_layer.add_child(bg) _settings_root = Control.new() _settings_layer.add_child(_settings_root) func _open_settings() -> void: _clear_confirm = false _settings_layer.visible = true _rebuild_settings() func _close_settings() -> void: _settings_layer.visible = false func _rebuild_settings() -> void: for c in _settings_root.get_children(): c.queue_free() var ox: float = 300.0 var oy: float = 110.0 var title := Label.new() title.text = tr("SETTINGS_TITLE") title.position = Vector2(ox + 24, oy + 18) title.add_theme_font_size_override("font_size", 26) title.modulate = Color.GOLD _settings_root.add_child(title) # 难度循环 var diff_btn := Button.new() diff_btn.text = tr("SETTINGS_DIFFICULTY") % tr(SettingsManager.difficulty_name()) diff_btn.position = Vector2(ox + 24, oy + 70) diff_btn.size = Vector2(360, 40) diff_btn.connect("pressed", _on_diff_cycle) _settings_root.add_child(diff_btn) # 语言循环 var lang_btn := Button.new() lang_btn.text = tr("SETTINGS_LANGUAGE") % Locale.get_locale() lang_btn.position = Vector2(ox + 24, oy + 120) lang_btn.size = Vector2(360, 40) lang_btn.connect("pressed", _on_settings_lang) _settings_root.add_child(lang_btn) # 音量 var vol_lbl := Label.new() vol_lbl.text = tr("SETTINGS_VOLUME") % int(SettingsManager.master_volume * 100.0) vol_lbl.position = Vector2(ox + 24, oy + 175) vol_lbl.add_theme_font_size_override("font_size", 16) _settings_root.add_child(vol_lbl) var slider := HSlider.new() slider.min_value = 0.0 slider.max_value = 1.0 slider.step = 0.05 slider.value = SettingsManager.master_volume slider.position = Vector2(ox + 24, oy + 200) slider.size = Vector2(360, 24) slider.connect("value_changed", _on_volume_changed) _settings_root.add_child(slider) # 清除所有本地数据(ST-03,二次确认) var clear_btn := Button.new() clear_btn.text = (tr("SETTINGS_CLEAR_CONFIRM") if _clear_confirm else tr("SETTINGS_CLEAR_DATA")) clear_btn.position = Vector2(ox + 24, oy + 250) clear_btn.size = Vector2(360, 40) clear_btn.modulate = (Color(1.0, 0.4, 0.4) if _clear_confirm else Color.WHITE) clear_btn.connect("pressed", _on_clear_data) _settings_root.add_child(clear_btn) var close := Button.new() close.text = tr("SETTINGS_CLOSE") close.position = Vector2(ox + 410, oy + 360) close.size = Vector2(120, 40) close.connect("pressed", _close_settings) _settings_root.add_child(close) func _on_diff_cycle() -> void: SettingsManager.cycle_difficulty() _rebuild_settings() func _on_settings_lang() -> void: SettingsManager.set_locale(Locale.cycle_locale()) _rebuild_settings() _refresh_shop_ui() func _on_volume_changed(v: float) -> void: SettingsManager.set_master_volume(v) _rebuild_settings() func _on_clear_data() -> void: if not _clear_confirm: _clear_confirm = true # 首次点击进入确认态 _rebuild_settings() return SettingsManager.clear_all_local_data() _clear_confirm = false _close_settings() _lbl_status.text = tr("SETTINGS_CLEARED") _refresh_shop_ui() # ── 背包 / 插槽编辑 UI ──────────────────────────────────────── func _setup_inventory_ui() -> void: _inv_layer = CanvasLayer.new() _inv_layer.name = "InventoryLayer" _inv_layer.visible = false add_child(_inv_layer) var bg := ColorRect.new() bg.color = Color(0.05, 0.06, 0.12, 0.96) bg.size = Vector2(720, 520) bg.position = Vector2(150, 90) _inv_layer.add_child(bg) _inv_root = Control.new() # 动态内容容器(每次刷新清空重建) _inv_layer.add_child(_inv_root) func _open_inventory() -> void: _inv_sel_kind = "" _inv_sel_idx = -1 _inv_layer.visible = true _rebuild_inventory() func _close_inventory() -> void: _inv_layer.visible = false func _spell_short_name(sid: String) -> String: if sid == "": return tr("INV_EMPTY") var sp: SpellNode = SpellRegistry.get_spell(sid) return sp.display_name if sp else sid func _rebuild_inventory() -> void: for c in _inv_root.get_children(): c.queue_free() var ox: float = 150.0 var oy: float = 90.0 # 标题 var title := Label.new() title.text = tr("INV_TITLE") % [ _cm.get_core_display(), _cm.get_slot_count(), _topology_name()] title.position = Vector2(ox + 20, oy + 12) title.add_theme_font_size_override("font_size", 19) title.modulate = Color.GOLD _inv_root.add_child(title) var tip := Label.new() tip.text = tr("INV_TIP") tip.position = Vector2(ox + 20, oy + 42) tip.add_theme_font_size_override("font_size", 12) tip.modulate = Color(0.8, 0.85, 1.0) _inv_root.add_child(tip) # 插槽网格(拓扑布局) var deck: Array = _cm.get_padded_deck() var rows: int = 1 var cols: int = deck.size() if _cm._equipped_core and _cm._equipped_core.topology == CoreDefinition.Topology.MATRIX: rows = max(1, _cm._equipped_core.grid_rows) cols = max(1, _cm._equipped_core.grid_cols) var bw: float = 150.0 var bh: float = 56.0 var gx: float = ox + 24 var gy: float = oy + 80 for idx in deck.size(): var r: int = idx / cols var col: int = idx % cols var btn := Button.new() btn.position = Vector2(gx + col * (bw + 8), gy + r * (bh + 8)) btn.size = Vector2(bw, bh) var sid: String = String(deck[idx]) var rowtag: String = "" if rows > 1: rowtag = (tr("INV_ROW_A") if r == 0 else tr("INV_ROW_B")) btn.text = "[%d] %s%s" % [idx, rowtag, _spell_short_name(sid)] if sid == "": btn.modulate = Color(0.6, 0.6, 0.6) if _inv_sel_kind == "slot" and _inv_sel_idx == idx: btn.modulate = Color(0.4, 1.0, 0.5) # 选中高亮 btn.connect("pressed", _on_inv_slot_pressed.bind(idx)) _inv_root.add_child(btn) # 备牌区 var bench: Array = _cm.get_bench() var bench_lbl := Label.new() bench_lbl.text = tr("INV_BENCH") % bench.size() bench_lbl.position = Vector2(gx, gy + rows * (bh + 8) + 16) bench_lbl.add_theme_font_size_override("font_size", 14) _inv_root.add_child(bench_lbl) for bi in bench.size(): var bbtn := Button.new() bbtn.position = Vector2(gx + bi * (bw + 8), gy + rows * (bh + 8) + 42) bbtn.size = Vector2(bw, bh) bbtn.text = _spell_short_name(String(bench[bi])) bbtn.modulate = Color(0.85, 0.8, 1.0) if _inv_sel_kind == "bench" and _inv_sel_idx == bi: bbtn.modulate = Color(0.4, 1.0, 0.5) bbtn.connect("pressed", _on_inv_bench_pressed.bind(bi)) _inv_root.add_child(bbtn) # 关闭按钮 var close := Button.new() close.text = tr("INV_CLOSE") close.position = Vector2(ox + 560, oy + 470) close.size = Vector2(130, 36) close.connect("pressed", _close_inventory) _inv_root.add_child(close) func _topology_name() -> String: if _cm._equipped_core == null: return "LINEAR" match _cm._equipped_core.topology: CoreDefinition.Topology.MATRIX: return "MATRIX" CoreDefinition.Topology.CIRCUIT: return "CIRCUIT" _: return "LINEAR" func _on_inv_slot_pressed(idx: int) -> void: if _inv_sel_kind == "": _inv_sel_kind = "slot"; _inv_sel_idx = idx elif _inv_sel_kind == "slot": if _inv_sel_idx == idx: _cm.slot_to_bench(idx) # 点自身 → 移入备牌 else: _cm.swap_slots(_inv_sel_idx, idx) # 交换 _inv_sel_kind = ""; _inv_sel_idx = -1 elif _inv_sel_kind == "bench": _cm.bench_to_slot(_inv_sel_idx, idx) # 备牌 → 插槽 _inv_sel_kind = ""; _inv_sel_idx = -1 _rebuild_inventory() func _on_inv_bench_pressed(bi: int) -> void: if _inv_sel_kind == "bench" and _inv_sel_idx == bi: _inv_sel_kind = ""; _inv_sel_idx = -1 # 取消选择 else: _inv_sel_kind = "bench"; _inv_sel_idx = bi _rebuild_inventory() func _on_close_shop_pressed() -> void: _shop_layer.visible = false _cm.close_shop_and_next_wave() func _on_charge_state_changed(payload: Dictionary) -> void: var prog: float = float(payload.get("progress", 0.0)) if _lbl_charge: _lbl_charge.text = tr("UI_CHARGE") % int(prog * 100.0) func _on_charge_fired(_payload: Dictionary) -> void: if _lbl_charge: _lbl_charge.text = tr("UI_CHARGE_DONE") _lbl_status.text = tr("STATUS_CHARGE_FIRED") func _on_spell_depth_exceeded(_payload: Dictionary) -> void: _lbl_status.text = tr("STATUS_DEPTH") % SpellEvaluator.MAX_TRIGGER_DEPTH _lbl_status.modulate = Color.YELLOW # 3 秒后自动清除 var t := Timer.new() add_child(t) t.wait_time = 3.0 t.one_shot = true t.timeout.connect(func(): _lbl_status.text = ""; _lbl_status.modulate = Color.YELLOW) t.start() func _on_game_over(payload: Dictionary) -> void: _shop_layer.visible = false _inv_layer.visible = false if _pause_layer: _pause_layer.visible = false _is_paused = false get_tree().paused = false _lbl_status.text = "" _rebuild_settlement(payload) _settle_layer.visible = true # P-S6-04:保存本局结算截图到 user://runs/(死亡屏,非黑屏) call_deferred("_save_run_screenshot") func _restart_game() -> void: _settle_layer.visible = false _lbl_status.text = "" PlayerManager.kill_count = 0 _cm.start_game() func _return_to_menu() -> void: SceneManager.goto_main_menu() # ── 结算 / 死亡屏 + 本地排行榜 ──────────────────────────────── func _setup_settlement_ui() -> void: _settle_layer = CanvasLayer.new() _settle_layer.name = "SettlementLayer" _settle_layer.visible = false add_child(_settle_layer) var bg := ColorRect.new() bg.color = Color(0.04, 0.04, 0.09, 0.97) bg.size = Vector2(1152, 648) _settle_layer.add_child(bg) _settle_root = Control.new() _settle_layer.add_child(_settle_root) func _fmt_time(sec: int) -> String: return "%d:%02d" % [sec / 60, sec % 60] func _rebuild_settlement(payload: Dictionary) -> void: for c in _settle_root.get_children(): c.queue_free() var wave: int = int(payload.get("wave", 1)) var kills: int = int(payload.get("kills", 0)) var elapsed: int = int(payload.get("elapsed_sec", 0)) var score: int = int(payload.get("score", 0)) var title := Label.new() title.text = tr("SETTLE_TITLE") title.position = Vector2(440, 60) title.add_theme_font_size_override("font_size", 40) title.modulate = Color(1.0, 0.35, 0.35) _settle_root.add_child(title) var stats := Label.new() stats.text = tr("SETTLE_STATS") % [wave, kills, _fmt_time(elapsed), score] stats.position = Vector2(400, 140) stats.add_theme_font_size_override("font_size", 22) _settle_root.add_child(stats) # 本地排行榜(前 10) var board_title := Label.new() board_title.text = tr("SETTLE_BOARD") board_title.position = Vector2(400, 230) board_title.add_theme_font_size_override("font_size", 18) board_title.modulate = Color.GOLD _settle_root.add_child(board_title) var top: Array = EndlessRecords.get_top(10) var my_score: int = score for i in top.size(): var rec: Dictionary = top[i] var row := Label.new() var is_me: bool = int(rec.get("score", -1)) == my_score row.text = tr("SETTLE_BOARD_ROW") % [ i + 1, int(rec.get("wave", 0)), _fmt_time(int(rec.get("elapsed", 0))), int(rec.get("score", 0)), (tr("SETTLE_THIS_RUN") if is_me else "")] row.position = Vector2(420, 265 + i * 26) row.add_theme_font_size_override("font_size", 15) if is_me: row.modulate = Color(0.4, 1.0, 0.5) _settle_root.add_child(row) var restart := Button.new() restart.text = tr("SETTLE_RESTART") restart.position = Vector2(380, 560) restart.size = Vector2(180, 46) restart.connect("pressed", _restart_game) _settle_root.add_child(restart) var menu_btn := Button.new() menu_btn.text = tr("SETTLE_RETURN_MENU") menu_btn.position = Vector2(580, 560) menu_btn.size = Vector2(180, 46) menu_btn.connect("pressed", _return_to_menu) _settle_root.add_child(menu_btn) func _save_run_screenshot() -> void: var img := get_viewport().get_texture().get_image() if img == null: return DirAccess.make_dir_recursive_absolute("user://runs") var path: String = "user://runs/run_%d.png" % Time.get_ticks_msec() img.save_png(path)