feat: 场景与核心玩法脚本(双机型/波次/Boss/道具/炸弹/地面单位/浮游炮激光)

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-08-31 00:31:29 +08:00
parent 024447fbda
commit 3c6f31b503
36 changed files with 1692 additions and 0 deletions
+339
View File
@@ -0,0 +1,339 @@
class_name Game
extends Node2D
## 游戏总装:世界构建、HUD、生命与游戏结束流程。
## 场景树全部由代码构建,入口场景只挂本脚本。
enum State { PLAYING, GAME_OVER }
const VIEW_SIZE := Vector2(540.0, 960.0)
const START_LIVES := 3
const BOMB_ICON := preload("res://assets/svg/bomb_icon.svg")
const BOSS_BAR_WIDTH := 260.0
const BOMB_DAMAGE := 30
var score := 0
var lives := START_LIVES
var state := State.PLAYING
var player: Player
var _hud_score: Label
var _lives_box: Node2D
var _overlay: CanvasLayer
var _final_score_label: Label
var _over_since_msec := 0
var _effects: Node2D
var _enemy_bullets: Node2D
var _enemies: Node2D
var _boss: Boss
var _boss_bar_bg: ColorRect
var _boss_bar_fill: ColorRect
var _warning: Label
var _shake_amp := 0.0
var _bombs_box: Node2D
var _flash_rect: ColorRect
var _flash_tween: Tween
func _ready() -> void:
add_to_group("game")
randomize()
_build_world()
_build_hud()
_build_overlay()
_update_lives()
_update_score_label()
func _process(delta: float) -> void:
if state == State.GAME_OVER:
var accept := Input.is_action_just_pressed("ui_accept") or Input.is_action_just_pressed("fire")
if accept and Time.get_ticks_msec() - _over_since_msec > 800:
get_tree().change_scene_to_file("res://scenes/title.tscn")
return
_update_boss_bar()
_update_shake(delta)
func _update_boss_bar() -> void:
# 注意:已释放实例 == null 也为真,判存活必须用 is_instance_valid
if not is_instance_valid(_boss):
if _boss_bar_bg.visible:
_boss_bar_bg.visible = false
_boss_bar_fill.visible = false
return
var ratio := clampf(float(_boss.hp) / float(_boss.max_hp), 0.0, 1.0)
_boss_bar_fill.size.x = BOSS_BAR_WIDTH * ratio
func _update_shake(delta: float) -> void:
if _shake_amp > 0.2:
_shake_amp = lerpf(_shake_amp, 0.0, 8.0 * delta)
position = Vector2(randf_range(-1.0, 1.0), randf_range(-1.0, 1.0)) * _shake_amp
elif _shake_amp != 0.0:
_shake_amp = 0.0
position = Vector2.ZERO
func _build_world() -> void:
add_child(OceanScroll.new())
var player_bullets := Node2D.new()
player_bullets.name = "PlayerBullets"
player_bullets.add_to_group("player_bullets")
add_child(player_bullets)
_enemy_bullets = Node2D.new()
_enemy_bullets.name = "EnemyBullets"
_enemy_bullets.add_to_group("enemy_bullets")
add_child(_enemy_bullets)
_enemies = Node2D.new()
_enemies.name = "Enemies"
_enemies.add_to_group("enemies")
add_child(_enemies)
var powerups := Node2D.new()
powerups.name = "Powerups"
powerups.add_to_group("powerups")
add_child(powerups)
player = Player.new()
player.variant = GameSession.plane_variant as Player.Variant
player.position = VIEW_SIZE * Vector2(0.5, 0.82)
player.hit.connect(_on_player_hit)
add_child(player)
_effects = Node2D.new()
_effects.name = "Effects"
add_child(_effects)
add_child(WaveSpawner.new())
func _build_hud() -> void:
var layer := CanvasLayer.new()
layer.layer = 10
add_child(layer)
_hud_score = Label.new()
_hud_score.position = Vector2(16.0, 10.0)
_hud_score.add_theme_font_size_override("font_size", 26)
_hud_score.add_theme_color_override("font_color", Color.WHITE)
_hud_score.add_theme_color_override("font_shadow_color", Color(0.0, 0.0, 0.0, 0.6))
_hud_score.add_theme_constant_override("shadow_offset_x", 2)
_hud_score.add_theme_constant_override("shadow_offset_y", 2)
layer.add_child(_hud_score)
_lives_box = Node2D.new()
_lives_box.position = Vector2(520.0, 38.0)
layer.add_child(_lives_box)
# 炸弹计数(生命图标下方)
_bombs_box = Node2D.new()
_bombs_box.position = Vector2(520.0, 72.0)
layer.add_child(_bombs_box)
_update_bombs()
# 炸弹白闪层(最顶层)
var flash_layer := CanvasLayer.new()
flash_layer.layer = 30
add_child(flash_layer)
_flash_rect = ColorRect.new()
_flash_rect.color = Color.WHITE
_flash_rect.set_anchors_preset(Control.PRESET_FULL_RECT)
_flash_rect.modulate.a = 0.0
flash_layer.add_child(_flash_rect)
# Boss 血条(顶栏中央)
_boss_bar_bg = ColorRect.new()
_boss_bar_bg.position = Vector2(140.0, 54.0)
_boss_bar_bg.size = Vector2(BOSS_BAR_WIDTH + 4.0, 14.0)
_boss_bar_bg.color = Color(0.0, 0.0, 0.0, 0.65)
_boss_bar_bg.visible = false
layer.add_child(_boss_bar_bg)
_boss_bar_fill = ColorRect.new()
_boss_bar_fill.position = Vector2(142.0, 56.0)
_boss_bar_fill.size = Vector2(BOSS_BAR_WIDTH, 10.0)
_boss_bar_fill.color = Color(0.85, 0.2, 0.15)
_boss_bar_fill.visible = false
layer.add_child(_boss_bar_fill)
# Boss 登场警告横幅
_warning = Label.new()
_warning.text = "WARNING!!"
_warning.position = Vector2(0.0, 330.0)
_warning.size = Vector2(540.0, 70.0)
_warning.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_warning.add_theme_font_size_override("font_size", 56)
_warning.add_theme_color_override("font_color", Color(1.0, 0.25, 0.2))
_warning.add_theme_color_override("font_shadow_color", Color(0.0, 0.0, 0.0, 0.7))
_warning.add_theme_constant_override("shadow_offset_x", 3)
_warning.add_theme_constant_override("shadow_offset_y", 3)
_warning.visible = false
layer.add_child(_warning)
func _build_overlay() -> void:
_overlay = CanvasLayer.new()
_overlay.layer = 20
var dim := ColorRect.new()
dim.color = Color(0.0, 0.0, 0.0, 0.55)
dim.set_anchors_preset(Control.PRESET_FULL_RECT)
_overlay.add_child(dim)
var center := CenterContainer.new()
center.set_anchors_preset(Control.PRESET_FULL_RECT)
_overlay.add_child(center)
var vbox := VBoxContainer.new()
vbox.add_theme_constant_override("separation", 18)
vbox.alignment = BoxContainer.ALIGNMENT_CENTER
center.add_child(vbox)
var title := Label.new()
title.text = "GAME OVER"
title.add_theme_font_size_override("font_size", 52)
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
vbox.add_child(title)
_final_score_label = Label.new()
_final_score_label.add_theme_font_size_override("font_size", 30)
_final_score_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
vbox.add_child(_final_score_label)
var hint := Label.new()
hint.text = "回车 / 空格 重新开始"
hint.add_theme_font_size_override("font_size", 22)
hint.modulate = Color(0.8, 0.8, 0.8)
hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
vbox.add_child(hint)
_overlay.visible = false
add_child(_overlay)
func add_score(value: int) -> void:
score += value
_update_score_label()
func spawn_explosion(pos: Vector2, explosion_scale: float = 1.0) -> void:
var explosion := Explosion.new()
explosion.size = explosion_scale
_effects.add_child(explosion)
explosion.global_position = pos
## 生成关底 Boss 并显示血条
func spawn_boss(p_difficulty: float) -> Boss:
var boss := Boss.new()
boss.setup(p_difficulty)
_enemies.add_child(boss)
_boss = boss
_boss_bar_bg.visible = true
_boss_bar_fill.visible = true
return boss
## Boss 登场前红色闪烁警告
func show_boss_warning() -> void:
_warning.visible = true
var tween := create_tween().set_loops(4)
tween.tween_property(_warning, "modulate:a", 0.1, 0.22)
tween.tween_property(_warning, "modulate:a", 1.0, 0.22)
tween.finished.connect(func() -> void: _warning.visible = false)
## 屏幕震动(强度衰减)
func shake(amp: float) -> void:
_shake_amp = maxf(_shake_amp, amp)
## 清除全场敌方弹幕
func clear_enemy_bullets() -> void:
for bullet in _enemy_bullets.get_children():
bullet.queue_free()
## 炸弹引爆:白闪 + 清弹 + 全场伤害 + 震屏(伤害数值由调用方计数)
func detonate_bomb() -> void:
_update_bombs()
if _flash_tween != null and _flash_tween.is_valid():
_flash_tween.kill()
_flash_rect.modulate.a = 0.85
_flash_tween = create_tween()
_flash_tween.tween_property(_flash_rect, "modulate:a", 0.0, 0.45) \
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
clear_enemy_bullets()
for enemy in _enemies.get_children():
if enemy is EnemyBase:
(enemy as EnemyBase).apply_damage(BOMB_DAMAGE)
shake(7.0)
## 炸弹 +1(上限 Player.BOMBS_MAX)
func add_bomb() -> void:
if player != null and is_instance_valid(player):
player.bombs = mini(player.bombs + 1, Player.BOMBS_MAX)
_update_bombs()
## 拾取 B 道具:+1 炸弹并刷新 HUD
func notify_bomb_pickup() -> void:
_update_bombs()
func _update_bombs() -> void:
if _bombs_box == null:
return
for icon in _bombs_box.get_children():
icon.queue_free()
var count := player.bombs if player != null else 0
for i in count:
var icon := Sprite2D.new()
icon.texture = BOMB_ICON
icon.scale = Vector2.ONE * 0.8
icon.position = Vector2(-22.0 * i, 0.0)
_bombs_box.add_child(icon)
func _on_player_hit() -> void:
if state != State.PLAYING or not player.active:
return
lives -= 1
_update_lives()
spawn_explosion(player.global_position, 1.6)
clear_enemy_bullets()
player.die()
if lives <= 0:
state = State.GAME_OVER
_show_game_over()
else:
await get_tree().create_timer(0.9).timeout
if state == State.PLAYING:
player.respawn(VIEW_SIZE * Vector2(0.5, 0.82))
func _show_game_over() -> void:
_final_score_label.text = "得分 " + str(score).pad_zeros(6)
_overlay.visible = true
_over_since_msec = Time.get_ticks_msec()
func _update_score_label() -> void:
_hud_score.text = "SCORE " + str(score).pad_zeros(6)
func _update_lives() -> void:
for icon in _lives_box.get_children():
icon.queue_free()
for i in maxi(lives - 1, 0):
var icon := Sprite2D.new()
icon.texture = Player.TEX
icon.scale = Vector2.ONE * 0.42
icon.position = Vector2(-36.0 * i, 0.0)
_lives_box.add_child(icon)