@@ -0,0 +1,791 @@
## 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 %d G " % [ 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 )