## 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
var _enemy_bullet_mm: MultiMesh = null

## UI 层引用
var _hud_layer:    CanvasLayer = null
var _shop_layer:   CanvasLayer = null
var _shop_warn:    Label       = null   # 防呆警告：Deck 无 ACTION 时提示

## 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 _mana_bar: ProgressBar = null
var _mana_lbl: 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)
	EnemyBulletManager.sync_multimesh(_enemy_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)

	# 敌方子弹渲染器（红粉色 modulate，区分玩家黄弹）
	_enemy_bullet_mm = MultiMesh.new()
	_enemy_bullet_mm.transform_format = MultiMesh.TRANSFORM_2D
	_enemy_bullet_mm.mesh = quad
	_enemy_bullet_mm.instance_count = EnemyBulletManager.MAX_ENEMY_BULLETS
	_enemy_bullet_mm.visible_instance_count = 0
	var eb_mmi := MultiMeshInstance2D.new()
	eb_mmi.multimesh = _enemy_bullet_mm
	eb_mmi.modulate = Color(1.0, 0.35, 0.5)
	var eb_mat := ShaderMaterial.new()
	eb_mat.shader = load("res://shaders/bullet_glow.gdshader")
	eb_mmi.material = eb_mat
	add_child(eb_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)
	EventBus.subscribe(EventID.BOSS_PHASE_CHANGED, _on_boss_phase_changed)
	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)
	_mana_bar = ProgressBar.new()
	_mana_bar.position = Vector2(200, 86)
	_mana_bar.custom_minimum_size = Vector2(170, 16)
	_mana_bar.size = Vector2(170, 16)
	_mana_bar.min_value = 0.0
	_mana_bar.show_percentage = false
	var mana_fill := StyleBoxFlat.new()
	mana_fill.bg_color = Color(0.3, 0.55, 1.0)
	_mana_bar.add_theme_stylebox_override("fill", mana_fill)
	var mana_bg := StyleBoxFlat.new()
	mana_bg.bg_color = Color(0.12, 0.12, 0.22)
	_mana_bar.add_theme_stylebox_override("background", mana_bg)
	_hud_layer.add_child(_mana_bar)
	_mana_lbl = Label.new()
	_mana_lbl.position = Vector2(200, 86)
	_mana_lbl.size = Vector2(170, 16)
	_mana_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	_mana_lbl.add_theme_font_size_override("font_size", 11)
	_hud_layer.add_child(_mana_lbl)

	_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)

	_shop_warn = Label.new()
	_shop_warn.position = Vector2(250, 502)   # 商店按钮行下方空白区，避免遮挡背包/语言/设置按钮
	_shop_warn.add_theme_color_override("font_color", Color(1.0, 0.45, 0.35))
	_shop_warn.visible = false
	_shop_layer.add_child(_shop_warn)

# ── 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()]
	if _mana_bar:
		_mana_bar.max_value = PlayerStats.mana_max
		_mana_bar.value = PlayerStats.mana
		var frac: float = PlayerStats.mana / maxf(1.0, PlayerStats.mana_max)
		var fill := _mana_bar.get_theme_stylebox("fill") as StyleBoxFlat
		if fill:
			if frac < 0.2:
				var a: float = 0.6 + 0.4 * absf(sin(float(Time.get_ticks_msec()) / 150.0))
				fill.bg_color = Color(0.8, 0.2, 0.2, a)
			else:
				fill.bg_color = Color(0.3, 0.55, 1.0)
		var h: float = clampf(PlayerStats.mana_heat, 0.0, 0.7)
		_mana_bar.modulate = Color(1.0, 1.0 - h, 1.0 - h)
	if _mana_lbl:
		_mana_lbl.text = tr("UI_MANA") % [int(PlayerStats.mana), int(PlayerStats.mana_max)]

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:
	if _shop_warn:
		_shop_warn.visible = false
	_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:
	# 防呆：Deck 无 ACTION 法术则法术塔无法开火，阻止进入战斗并提示
	if not _cm.has_castable_action():
		_shop_warn.text = tr("SHOP_NEED_ACTION")
		_shop_warn.visible = true
		return
	_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_boss_phase_changed(payload: Dictionary) -> void:
	if _lbl_status:
		_lbl_status.text = tr("STATUS_BOSS_PHASE") % int(payload.get("phase", 0))

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)
