3c6f31b503
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
340 lines
9.3 KiB
GDScript
340 lines
9.3 KiB
GDScript
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)
|