初次提交

This commit is contained in:
2026-07-20 10:56:52 +08:00
commit 7bcc0026e0
462 changed files with 50191 additions and 0 deletions
+791
View File
@@ -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%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)
+1
View File
@@ -0,0 +1 @@
uid://bhjufa2ng137y
+6
View File
@@ -0,0 +1,6 @@
[gd_scene format=3 uid="uid://hv4b6soy60l"]
[ext_resource type="Script" uid="uid://bhjufa2ng137y" path="res://scenes/main/combat_s2.gd" id="1_yyrjn"]
[node name="CombatS2" type="Node2D" unique_id=1360302971]
script = ExtResource("1_yyrjn")
+62
View File
@@ -0,0 +1,62 @@
## CombatTest — S1 最小战斗调试场景
## 目标:玩家自动发射 spark_bolt,子弹命中敌人,敌人死亡,屏幕显示击杀数
## 验收:P-S1-01 ~ P-S1-05
extends Node2D
const SPAWN_ENEMIES: int = 50 # S1 验收:50 只散兵(充分验证伤害)
var _player_node: Node2D = null
var _perf_label: Label = null
var _spawned: bool = false
func _ready() -> void:
_setup_debug_ui()
PlayerManager.set_player_node(_player_node)
# 装备法杯:wand_basic + spark_bolt
var loadout: Dictionary = WandPreset.make_default_loadout()
PlayerManager.equip_wand(loadout["core"], loadout["compiled"])
_run_spawn()
func _process(_delta: float) -> void:
_update_perf_ui()
func _run_spawn() -> void:
var arena: float = 600.0
for _i in SPAWN_ENEMIES:
EnemyManager.spawn_enemy(
Vector2(randf_range(-arena, arena), randf_range(-arena, arena)),
50.0 # P-S1-01: HP = 50spark_bolt 卷 3 伤
)
_spawned = true
print("[CombatTest S1] 生成完成: %d 敌人, wand_basic+spark_bolt 装备" % SPAWN_ENEMIES)
func _update_perf_ui() -> void:
if not _spawned or not _perf_label:
return
_perf_label.text = (
"FPS: %.1f\n敌人: %d\n击杀: %d\n子弹: %d\nTick: %d\n池: %s" % [
Engine.get_frames_per_second(),
EnemyManager.get_active_count(),
PlayerManager.kill_count,
BulletManager.get_active_count(),
TimeManager.game_tick,
DamageContextPool.get_pool_usage()
]
)
func _setup_debug_ui() -> void:
var canvas := CanvasLayer.new()
canvas.name = "DebugUI"
add_child(canvas)
var perf := Label.new()
perf.name = "PerfLabel"
perf.position = Vector2(16, 16)
perf.add_theme_font_size_override("font_size", 18)
canvas.add_child(perf)
_perf_label = perf
var p := Node2D.new()
p.name = "Player"
add_child(p)
_player_node = p
+1
View File
@@ -0,0 +1 @@
uid://2ruoy0wv4dnt
+6
View File
@@ -0,0 +1,6 @@
[gd_scene format=3 uid="uid://pjspwe2drl6d"]
[ext_resource type="Script" uid="uid://2ruoy0wv4dnt" path="res://scenes/main/combat_test.gd" id="1_hyeb1"]
[node name="CombatTest" type="Node2D" unique_id=2032556871]
script = ExtResource("1_hyeb1")
+353
View File
@@ -0,0 +1,353 @@
## 主菜单:新游戏 / 继续 / 排行榜 / 设置 / 退出
extends Control
var _settings_layer: CanvasLayer = null
var _settings_root: Control = null
var _leaderboard_layer: CanvasLayer = null
var _ftue_layer: CanvasLayer = null
var _continue_btn: Button = null
var _quit_confirm: bool = false
func _ready() -> void:
SceneManager.fade_in_first()
_build_bg()
_build_menu()
_build_settings_panel()
_build_leaderboard_panel()
_build_ftue_panel()
func _build_bg() -> void:
# 深色渐变背景
var bg := ColorRect.new()
bg.color = Color(0.04, 0.04, 0.09)
bg.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
add_child(bg)
# 左侧装饰色条
var stripe := ColorRect.new()
stripe.color = Color(0.18, 0.12, 0.40, 0.6)
stripe.size = Vector2(320, 648)
add_child(stripe)
func _build_menu() -> void:
# 游戏大标题
var title := Label.new()
title.text = "魔法工匠"
title.position = Vector2(40, 80)
title.add_theme_font_size_override("font_size", 56)
title.modulate = Color(1.0, 0.85, 0.3)
add_child(title)
var sub := Label.new()
sub.text = "Arcane Artificer"
sub.position = Vector2(44, 148)
sub.add_theme_font_size_override("font_size", 18)
sub.modulate = Color(0.7, 0.75, 1.0)
add_child(sub)
# 按钮列
var items: Array = [
["MENU_NEW_GAME", _on_new_game],
["MENU_CONTINUE", _on_continue],
["MENU_LEADERBOARD", _on_leaderboard],
["MENU_SETTINGS", _on_settings],
["MENU_QUIT", _on_quit],
]
var y: float = 240.0
for item in items:
var btn := _make_btn(tr(item[0]), item[1])
btn.position = Vector2(52, y)
btn.size = Vector2(220, 48)
add_child(btn)
if item[0] == "MENU_CONTINUE":
_continue_btn = btn
_continue_btn.disabled = not SceneManager.has_save()
_continue_btn.modulate = Color(1, 1, 1, 0.45) if _continue_btn.disabled else Color.WHITE
y += 62.0
# 版本号
var ver := Label.new()
ver.text = "v1.0.0"
ver.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
ver.position = Vector2(-80, -20)
ver.add_theme_font_size_override("font_size", 12)
ver.modulate = Color(0.4, 0.4, 0.4)
add_child(ver)
# 语言切换
var lang := Button.new()
lang.text = "🌐 %s" % Locale.get_locale()
lang.set_anchors_preset(Control.PRESET_BOTTOM_LEFT)
lang.position = Vector2(16, -52)
lang.size = Vector2(120, 36)
lang.flat = true
lang.connect("pressed", _on_lang)
add_child(lang)
func _make_btn(label: String, cb: Callable) -> Button:
var btn := Button.new()
btn.text = label
btn.flat = false
btn.alignment = HORIZONTAL_ALIGNMENT_LEFT
btn.add_theme_font_size_override("font_size", 20)
btn.connect("pressed", cb)
return btn
func _on_new_game() -> void:
AudioManager.play("shop")
# 首次启动:先引导玩家选难度(FTUE),否则直接开始
if SettingsManager.needs_ftue():
_ftue_layer.visible = true
_rebuild_ftue()
else:
SceneManager.start_new_game()
func _on_continue() -> void:
if SceneManager.has_save():
AudioManager.play("shop")
SceneManager.continue_game()
func _on_leaderboard() -> void:
_leaderboard_layer.visible = true
_rebuild_leaderboard()
func _on_settings() -> void:
_settings_layer.visible = true
_rebuild_settings()
func _on_lang() -> void:
SettingsManager.set_locale(Locale.cycle_locale())
# 重建菜单文本(语言已切换)
for c in get_children():
c.queue_free()
_settings_layer = null
_leaderboard_layer = null
_settings_root = null
_build_bg()
_build_menu()
_build_settings_panel()
_build_leaderboard_panel()
func _on_quit() -> void:
if not _quit_confirm:
_quit_confirm = true
# 把退出按钮文字改为确认提示
for c in get_children():
if c is Button and tr("MENU_QUIT") in c.text:
c.text = tr("MENU_QUIT_CONFIRM")
c.modulate = Color(1.0, 0.4, 0.4)
# 3 秒后自动取消确认态
var t := Timer.new()
add_child(t)
t.wait_time = 3.0
t.one_shot = true
t.timeout.connect(func():
c.text = tr("MENU_QUIT")
c.modulate = Color.WHITE
_quit_confirm = false)
t.start()
return
get_tree().quit()
func _input(event: InputEvent) -> void:
if event.is_action_pressed("ui_cancel"):
if _settings_layer and _settings_layer.visible:
_settings_layer.visible = false
elif _leaderboard_layer and _leaderboard_layer.visible:
_leaderboard_layer.visible = false
# ── 设置面板 ─────────────────────────────────────────────────
func _build_settings_panel() -> void:
_settings_layer = CanvasLayer.new()
_settings_layer.layer = 10
_settings_layer.visible = false
add_child(_settings_layer)
var bg := ColorRect.new()
bg.color = Color(0.0, 0.0, 0.0, 0.72)
bg.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_settings_layer.add_child(bg)
_settings_root = Control.new()
_settings_layer.add_child(_settings_root)
func _rebuild_settings() -> void:
for c in _settings_root.get_children():
c.queue_free()
var panel := ColorRect.new()
panel.color = Color(0.06, 0.06, 0.14, 0.98)
panel.size = Vector2(560, 440)
panel.position = Vector2(300, 100)
_settings_root.add_child(panel)
var title := Label.new()
title.text = tr("SETTINGS_TITLE")
title.position = Vector2(324, 120)
title.add_theme_font_size_override("font_size", 28)
title.modulate = Color.GOLD
_settings_root.add_child(title)
_settings_row(tr("SETTINGS_DIFFICULTY") % tr(SettingsManager.difficulty_name()), 176, func():
SettingsManager.cycle_difficulty(); _rebuild_settings())
_settings_row(tr("SETTINGS_LANGUAGE") % Locale.get_locale(), 232, func():
SettingsManager.set_locale(Locale.cycle_locale()); _rebuild_settings())
var vol_lbl := Label.new()
vol_lbl.text = tr("SETTINGS_VOLUME") % int(SettingsManager.master_volume * 100.0)
vol_lbl.position = Vector2(324, 292)
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(324, 318); slider.size = Vector2(360, 24)
slider.connect("value_changed", func(v): SettingsManager.set_master_volume(v); _rebuild_settings())
_settings_root.add_child(slider)
var close := Button.new()
close.text = tr("SETTINGS_CLOSE")
close.position = Vector2(710, 504)
close.size = Vector2(130, 40)
close.connect("pressed", func(): _settings_layer.visible = false)
_settings_root.add_child(close)
func _settings_row(text: String, y: float, cb: Callable) -> void:
var btn := Button.new()
btn.text = text
btn.position = Vector2(324, y)
btn.size = Vector2(360, 40)
btn.connect("pressed", cb)
_settings_root.add_child(btn)
# ── 排行榜面板 ───────────────────────────────────────────────
func _build_leaderboard_panel() -> void:
_leaderboard_layer = CanvasLayer.new()
_leaderboard_layer.layer = 10
_leaderboard_layer.visible = false
add_child(_leaderboard_layer)
func _rebuild_leaderboard() -> void:
for c in _leaderboard_layer.get_children():
if c is ColorRect or c is Control:
c.queue_free()
var bg := ColorRect.new()
bg.color = Color(0.0, 0.0, 0.0, 0.72)
bg.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_leaderboard_layer.add_child(bg)
var panel := ColorRect.new()
panel.color = Color(0.05, 0.05, 0.13, 0.98)
panel.size = Vector2(640, 500)
panel.position = Vector2(256, 74)
_leaderboard_layer.add_child(panel)
var title := Label.new()
title.text = tr("SETTLE_BOARD")
title.position = Vector2(280, 92)
title.add_theme_font_size_override("font_size", 22)
title.modulate = Color.GOLD
_leaderboard_layer.add_child(title)
var top: Array = EndlessRecords.get_top(10)
if top.is_empty():
var empty := Label.new()
empty.text = tr("LEADERBOARD_EMPTY")
empty.position = Vector2(440, 300)
empty.add_theme_font_size_override("font_size", 18)
empty.modulate = Color(0.6, 0.6, 0.6)
_leaderboard_layer.add_child(empty)
else:
for i in top.size():
var rec: Dictionary = top[i]
var row := Label.new()
var elapsed: int = int(rec.get("elapsed", 0))
row.text = "%2d. Wave %d %d:%02d Score %d" % [
i + 1, int(rec.get("wave", 0)),
int(elapsed / 60.0), elapsed % 60,
int(rec.get("score", 0))]
row.position = Vector2(296, 140 + i*30)
row.add_theme_font_size_override("font_size", 15)
_leaderboard_layer.add_child(row)
var close := Button.new()
close.text = tr("SETTINGS_CLOSE")
close.position = Vector2(762, 538)
close.size = Vector2(120, 40)
close.connect("pressed", func(): _leaderboard_layer.visible = false)
_leaderboard_layer.add_child(close)
# ── FTUE:首启难度引导 ───────────────────────────────────────
func _build_ftue_panel() -> void:
_ftue_layer = CanvasLayer.new()
_ftue_layer.layer = 12
_ftue_layer.visible = false
add_child(_ftue_layer)
func _rebuild_ftue() -> void:
for c in _ftue_layer.get_children():
c.queue_free()
var dim := ColorRect.new()
dim.color = Color(0.02, 0.02, 0.06, 0.97)
dim.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_ftue_layer.add_child(dim)
var title := Label.new()
title.text = tr("FTUE_TITLE")
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title.set_anchors_preset(Control.PRESET_TOP_WIDE)
title.position.y = 70
title.add_theme_font_size_override("font_size", 38)
title.modulate = Color(1.0, 0.85, 0.3)
_ftue_layer.add_child(title)
# 三张难度卡片(横排)
var cards: Array = [
[0, "DIFF_BEGINNER", "FTUE_BEGINNER_DESC", Color(0.3, 0.85, 0.5)],
[1, "DIFF_STANDARD", "FTUE_STANDARD_DESC", Color(0.4, 0.7, 1.0)],
[2, "DIFF_CHALLENGE", "FTUE_CHALLENGE_DESC", Color(1.0, 0.45, 0.4)],
]
var card_w: float = 300.0
var gap: float = 30.0
var total_w: float = card_w * 3 + gap * 2
var start_x: float = (1152.0 - total_w) / 2.0
for i in cards.size():
var cd: Array = cards[i]
var x: float = start_x + i * (card_w + gap)
var card := Button.new()
card.position = Vector2(x, 200)
card.size = Vector2(card_w, 240)
card.connect("pressed", _on_ftue_pick.bind(int(cd[0])))
_ftue_layer.add_child(card)
# 难度名
var name_lbl := Label.new()
name_lbl.text = tr(cd[1])
name_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
name_lbl.size = Vector2(card_w, 40)
name_lbl.position = Vector2(x, 230)
name_lbl.add_theme_font_size_override("font_size", 30)
name_lbl.modulate = cd[3]
name_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
_ftue_layer.add_child(name_lbl)
# 说明
var desc := Label.new()
desc.text = tr(cd[2])
desc.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
desc.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
desc.size = Vector2(card_w - 40, 120)
desc.position = Vector2(x + 20, 300)
desc.add_theme_font_size_override("font_size", 16)
desc.modulate = Color(0.85, 0.85, 0.9)
desc.mouse_filter = Control.MOUSE_FILTER_IGNORE
_ftue_layer.add_child(desc)
var hint := Label.new()
hint.text = tr("FTUE_HINT")
hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
hint.set_anchors_preset(Control.PRESET_BOTTOM_WIDE)
hint.position.y = -60
hint.add_theme_font_size_override("font_size", 14)
hint.modulate = Color(0.55, 0.55, 0.6)
_ftue_layer.add_child(hint)
func _on_ftue_pick(difficulty_idx: int) -> void:
SettingsManager.complete_ftue(difficulty_idx) # 设难度 + 标记 FTUE 完成 + 存档
AudioManager.play("buy")
_ftue_layer.visible = false
SceneManager.start_new_game()
+1
View File
@@ -0,0 +1 @@
uid://o7kn0dw126em
+10
View File
@@ -0,0 +1,10 @@
[gd_scene format=3 uid="uid://cu6pbsyyjldos"]
[ext_resource type="Script" uid="uid://o7kn0dw126em" path="res://scenes/ui/main_menu.gd" id="1"]
[node name="MainMenu" type="Control" unique_id=369072083]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("1")
+91
View File
@@ -0,0 +1,91 @@
## 启动画面:公司/游戏商标 + 引擎版权 → 自动跳转主菜单
extends Control
const HOLD_SEC: float = 1.8 # 完全显示后停留时长
var _timer: float = 0.0
var _skip_ready: bool = false # 防止首帧立即跳过
func _ready() -> void:
SceneManager.fade_in_first()
_build_ui()
# 延迟一帧后才允许跳过(防止启动时按键残留)
call_deferred("_allow_skip")
func _allow_skip() -> void:
_skip_ready = true
func _build_ui() -> void:
# 全屏渐变背景
var bg := ColorRect.new()
bg.color = Color(0.04, 0.04, 0.09)
bg.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
add_child(bg)
# 工作室名
var studio := Label.new()
studio.text = "LANTERN GAMES"
studio.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
studio.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
studio.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
studio.position.y = -80
studio.add_theme_font_size_override("font_size", 18)
studio.modulate = Color(0.7, 0.7, 0.7)
add_child(studio)
# 游戏标题
var title := Label.new()
title.text = "魔法工匠"
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
title.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
title.add_theme_font_size_override("font_size", 64)
title.modulate = Color(1.0, 0.85, 0.3)
add_child(title)
# 英文副标题
var sub := Label.new()
sub.text = "Arcane Artificer"
sub.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
sub.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
sub.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
sub.position.y = 64
sub.add_theme_font_size_override("font_size", 22)
sub.modulate = Color(0.7, 0.75, 1.0)
add_child(sub)
# 提示
var hint := Label.new()
hint.text = tr("SPLASH_SKIP_HINT")
hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
hint.set_anchors_preset(Control.PRESET_BOTTOM_WIDE)
hint.position.y = -40
hint.add_theme_font_size_override("font_size", 13)
hint.modulate = Color(0.5, 0.5, 0.5)
add_child(hint)
# 版权
var copy := Label.new()
copy.text = "© 2026 Lantern Games | Powered by Godot 4.6"
copy.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
copy.set_anchors_preset(Control.PRESET_BOTTOM_WIDE)
copy.position.y = -16
copy.add_theme_font_size_override("font_size", 11)
copy.modulate = Color(0.4, 0.4, 0.4)
add_child(copy)
func _process(delta: float) -> void:
_timer += delta
if _timer >= HOLD_SEC:
_go_to_menu()
func _input(event: InputEvent) -> void:
if not _skip_ready:
return
if event.is_action_pressed("ui_accept") or event.is_action_pressed("ui_cancel") \
or (event is InputEventMouseButton and event.pressed):
_go_to_menu()
func _go_to_menu() -> void:
set_process(false)
set_process_input(false)
SceneManager.goto_main_menu()
+1
View File
@@ -0,0 +1 @@
uid://uch25wxg7638
+8
View File
@@ -0,0 +1,8 @@
[gd_scene load_steps=2 format=3 uid="uid://splash_scene"]
[ext_resource type="Script" path="res://scenes/ui/splash.gd" id="1"]
[node name="Splash" type="Control"]
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("1")