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:
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/game.gd" id="1_game"]
|
||||
|
||||
[node name="Game" type="Node2D"]
|
||||
script = ExtResource("1_game")
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/title.gd" id="1_title"]
|
||||
|
||||
[node name="Title" type="Node2D"]
|
||||
script = ExtResource("1_title")
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
class_name Boss
|
||||
extends EnemyBase
|
||||
## 关底 Boss:四发重型轰炸机。
|
||||
## 进场悬停后左右巡逻;弹幕双模式交替(瞄准连发 / 扇形散布),
|
||||
## 半血进入狂暴阶段(弹速、频率、弹幕密度提升)。
|
||||
## 死亡时爆炸链 + 震屏 + 清除全场敌弹。
|
||||
|
||||
const TEX := preload("res://assets/svg/boss_plane.svg")
|
||||
const CENTER_X := 270.0
|
||||
const ENTER_Y := 150.0
|
||||
const ENTER_SPEED := 100.0
|
||||
const PATROL_AMP := 145.0
|
||||
const MUZZLE := Vector2(0.0, 55.0)
|
||||
|
||||
var difficulty := 1.0
|
||||
var max_hp := 120
|
||||
|
||||
var _t := 0.0
|
||||
var _entered := false
|
||||
var _pattern_index := 0
|
||||
var _pattern_timer := 1.4
|
||||
var _burst_left := 0
|
||||
var _burst_tick := 0.0
|
||||
|
||||
|
||||
func setup(p_difficulty: float) -> void:
|
||||
difficulty = p_difficulty
|
||||
hp = int(60.0 + 60.0 * difficulty)
|
||||
max_hp = hp
|
||||
score_value = 5000
|
||||
drops_powerup = true
|
||||
hit_radius = 62.0
|
||||
position = Vector2(CENTER_X, -120.0)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
collision_layer = 4
|
||||
collision_mask = 0
|
||||
var sprite := Sprite2D.new()
|
||||
sprite.texture = TEX
|
||||
add_child(sprite)
|
||||
var shape := CollisionShape2D.new()
|
||||
var rect := RectangleShape2D.new()
|
||||
rect.size = Vector2(190.0, 84.0)
|
||||
shape.shape = rect
|
||||
add_child(shape)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if dead:
|
||||
return
|
||||
if not _entered:
|
||||
position.y += ENTER_SPEED * delta
|
||||
if position.y >= ENTER_Y:
|
||||
position.y = ENTER_Y
|
||||
_entered = true
|
||||
return
|
||||
|
||||
_t += delta * (1.25 if _in_phase2() else 1.0)
|
||||
position.x = CENTER_X + sin(_t * 0.55) * PATROL_AMP
|
||||
position.y = ENTER_Y + sin(_t * 1.6) * 16.0
|
||||
|
||||
if _burst_left > 0:
|
||||
_burst_tick -= delta
|
||||
if _burst_tick <= 0.0:
|
||||
_burst_tick = 0.14
|
||||
_burst_left -= 1
|
||||
_fire_bullet(_aim_direction() * _bullet_speed(), MUZZLE)
|
||||
else:
|
||||
_pattern_timer -= delta
|
||||
if _pattern_timer <= 0.0:
|
||||
_fire_pattern()
|
||||
|
||||
|
||||
func _in_phase2() -> bool:
|
||||
return hp <= max_hp / 2.0
|
||||
|
||||
|
||||
func _bullet_speed() -> float:
|
||||
return (235.0 + 40.0 * difficulty) * (1.12 if _in_phase2() else 1.0)
|
||||
|
||||
|
||||
func _fire_pattern() -> void:
|
||||
var phase2 := _in_phase2()
|
||||
if _pattern_index % 2 == 0:
|
||||
# 瞄准连发
|
||||
_burst_left = 5 if phase2 else 3
|
||||
_burst_tick = 0.0
|
||||
_pattern_timer = 1.5 if phase2 else 2.0
|
||||
else:
|
||||
# 扇形散布
|
||||
var count := 8 if phase2 else 5
|
||||
var spread := 50.0 if phase2 else 36.0
|
||||
var speed := _bullet_speed()
|
||||
for i in count:
|
||||
var angle := deg_to_rad(-spread + spread * 2.0 * float(i) / float(count - 1))
|
||||
_fire_bullet(Vector2.DOWN.rotated(angle) * speed, MUZZLE)
|
||||
_pattern_timer = 1.7 if phase2 else 2.2
|
||||
_pattern_index += 1
|
||||
|
||||
|
||||
func _die() -> void:
|
||||
_spawn_drop(Powerup.Kind.POWER)
|
||||
var game := get_tree().get_first_node_in_group("game") as Game
|
||||
if game != null and is_instance_valid(game):
|
||||
game.add_score(score_value)
|
||||
game.add_bomb()
|
||||
game.clear_enemy_bullets()
|
||||
game.shake(9.0)
|
||||
for i in 8:
|
||||
var offset := Vector2(randf_range(-92.0, 92.0), randf_range(-56.0, 56.0))
|
||||
if game != null and is_instance_valid(game):
|
||||
game.spawn_explosion(global_position + offset, randf_range(1.3, 2.0))
|
||||
await get_tree().create_timer(0.12).timeout
|
||||
if game != null and is_instance_valid(game):
|
||||
game.spawn_explosion(global_position, 3.0)
|
||||
await get_tree().create_timer(0.3).timeout
|
||||
queue_free()
|
||||
@@ -0,0 +1 @@
|
||||
uid://bu1qx3d8xsx2
|
||||
@@ -0,0 +1,73 @@
|
||||
class_name EnemyBase
|
||||
extends Area2D
|
||||
## 敌机基类:血量、受击闪白、死亡结算与爆炸。
|
||||
|
||||
var hp := 1
|
||||
var score_value := 100
|
||||
var dead := false
|
||||
var drops_powerup := false
|
||||
## 激光命中判定的近似半径(水平覆盖 + 垂直截断)
|
||||
var hit_radius := 16.0
|
||||
|
||||
|
||||
func apply_damage(dmg: int) -> void:
|
||||
if dead:
|
||||
return
|
||||
hp -= dmg
|
||||
if hp <= 0:
|
||||
dead = true
|
||||
_die()
|
||||
else:
|
||||
_flash()
|
||||
|
||||
|
||||
func _die() -> void:
|
||||
_spawn_drop()
|
||||
var game := get_tree().get_first_node_in_group("game") as Game
|
||||
if game != null:
|
||||
game.add_score(score_value)
|
||||
game.spawn_explosion(global_position, 1.0)
|
||||
queue_free()
|
||||
|
||||
|
||||
## 死亡时按标记掉落道具(可能在物理信号回调中调用,需延迟入树)。
|
||||
## forced 为 -1 时随机 P/B(各 50%)。
|
||||
func _spawn_drop(forced := -1) -> void:
|
||||
if not drops_powerup:
|
||||
return
|
||||
var container := get_tree().get_first_node_in_group("powerups")
|
||||
if container == null:
|
||||
return
|
||||
var pickup := Powerup.new()
|
||||
pickup.position = global_position
|
||||
if forced >= 0:
|
||||
pickup.kind = forced
|
||||
else:
|
||||
pickup.kind = Powerup.Kind.BOMB if randf() < 0.5 else Powerup.Kind.POWER
|
||||
container.call_deferred("add_child", pickup)
|
||||
|
||||
|
||||
func _flash() -> void:
|
||||
modulate = Color(3.0, 3.0, 3.0)
|
||||
create_tween().tween_property(self, "modulate", Color.WHITE, 0.12)
|
||||
|
||||
|
||||
## 朝玩家方向的单位向量;玩家不存在或不活跃时朝正下方
|
||||
func _aim_direction() -> Vector2:
|
||||
var game := get_tree().get_first_node_in_group("game") as Game
|
||||
if game != null and is_instance_valid(game.player) and game.player.active:
|
||||
var to_player: Vector2 = game.player.global_position - global_position
|
||||
if to_player.length() > 1.0:
|
||||
return to_player.normalized()
|
||||
return Vector2.DOWN
|
||||
|
||||
|
||||
## 向敌方弹幕容器发射一发子弹
|
||||
func _fire_bullet(velocity: Vector2, muzzle_offset := Vector2(0.0, 14.0)) -> void:
|
||||
var container := get_tree().get_first_node_in_group("enemy_bullets")
|
||||
if container == null:
|
||||
return
|
||||
var bullet := EnemyBullet.new()
|
||||
bullet.velocity = velocity
|
||||
container.add_child(bullet)
|
||||
bullet.global_position = global_position + muzzle_offset
|
||||
@@ -0,0 +1 @@
|
||||
uid://tj1k463onld4
|
||||
@@ -0,0 +1,26 @@
|
||||
class_name EnemyBullet
|
||||
extends Area2D
|
||||
## 敌方弹丸:直线飞行,出屏即回收。
|
||||
|
||||
const TEX := preload("res://assets/svg/bullet_enemy.svg")
|
||||
|
||||
var velocity := Vector2(0.0, 260.0)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
collision_layer = 8
|
||||
collision_mask = 0
|
||||
var sprite := Sprite2D.new()
|
||||
sprite.texture = TEX
|
||||
add_child(sprite)
|
||||
var shape := CollisionShape2D.new()
|
||||
var circle := CircleShape2D.new()
|
||||
circle.radius = 5.0
|
||||
shape.shape = circle
|
||||
add_child(shape)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
position += velocity * delta
|
||||
if position.y < -40.0 or position.y > 1000.0 or position.x < -40.0 or position.x > 580.0:
|
||||
queue_free()
|
||||
@@ -0,0 +1 @@
|
||||
uid://dd3gtsnqjt3i3
|
||||
@@ -0,0 +1,58 @@
|
||||
class_name EnemyFortress
|
||||
extends EnemyBase
|
||||
## 地面固定堡垒:随地面同速下滚,炮塔持续追踪玩家并瞄准射击。
|
||||
|
||||
const TEX := preload("res://assets/svg/fortress.svg")
|
||||
const TURRET_TEX := preload("res://assets/svg/fortress_turret.svg")
|
||||
const SCROLL_SPEED := 120.0
|
||||
const MUZZLE_LEN := 26.0
|
||||
|
||||
var difficulty := 1.0
|
||||
var fire_interval := 2.6
|
||||
var bullet_speed := 220.0
|
||||
|
||||
var _turret: Sprite2D
|
||||
var _fire_timer := 0.0
|
||||
|
||||
|
||||
func setup(p_x: float, p_y: float, p_difficulty := 1.0) -> void:
|
||||
position = Vector2(p_x, p_y)
|
||||
difficulty = p_difficulty
|
||||
fire_interval = maxf(1.5, 2.6 / p_difficulty)
|
||||
bullet_speed = 220.0 * p_difficulty
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
# 层 32:地面单位——玩家子弹可命中,但玩家机体不与其碰撞(飞越上空)
|
||||
collision_layer = 32
|
||||
collision_mask = 0
|
||||
hp = 4
|
||||
score_value = 300
|
||||
hit_radius = 26.0
|
||||
var base := Sprite2D.new()
|
||||
base.texture = TEX
|
||||
add_child(base)
|
||||
_turret = Sprite2D.new()
|
||||
_turret.texture = TURRET_TEX
|
||||
add_child(_turret)
|
||||
var shape := CollisionShape2D.new()
|
||||
var circle := CircleShape2D.new()
|
||||
circle.radius = 24.0
|
||||
shape.shape = circle
|
||||
add_child(shape)
|
||||
_fire_timer = randf_range(0.8, 1.8)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
position.y += SCROLL_SPEED * delta
|
||||
if position.y > 1020.0:
|
||||
queue_free()
|
||||
return
|
||||
var dir := _aim_direction()
|
||||
# 炮塔美术默认朝下(+Y):rotate 对齐瞄准方向
|
||||
_turret.rotation = dir.angle() - PI / 2.0
|
||||
if position.y > 60.0 and position.y < 720.0:
|
||||
_fire_timer -= delta
|
||||
if _fire_timer <= 0.0:
|
||||
_fire_timer = fire_interval
|
||||
_fire_bullet(dir * bullet_speed, dir * MUZZLE_LEN)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cbxqadj716vyc
|
||||
@@ -0,0 +1,53 @@
|
||||
class_name EnemyMedium
|
||||
extends EnemyBase
|
||||
## 中型轰炸机:从屏幕一侧横掠,正弦起伏,朝玩家方向瞄准射击。
|
||||
|
||||
const TEX := preload("res://assets/svg/enemy_medium.svg")
|
||||
|
||||
var side := 1 ## 1 = 从左向右,-1 = 从右向左
|
||||
var base_y := 220.0
|
||||
var vx := 110.0
|
||||
var fire_interval := 1.6
|
||||
var bullet_speed := 235.0
|
||||
|
||||
var _t := 0.0
|
||||
var _fire_timer := 1.0
|
||||
|
||||
|
||||
func setup(p_side: int, p_y: float, p_vx := 110.0, p_bullet_speed := 235.0, p_fire_interval := 1.6) -> void:
|
||||
side = p_side
|
||||
base_y = p_y
|
||||
vx = p_vx
|
||||
bullet_speed = p_bullet_speed
|
||||
fire_interval = p_fire_interval
|
||||
drops_powerup = true
|
||||
position = Vector2(-70.0 if side == 1 else 610.0, base_y)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
collision_layer = 4
|
||||
collision_mask = 0
|
||||
hp = 6
|
||||
score_value = 400
|
||||
hit_radius = 30.0
|
||||
var sprite := Sprite2D.new()
|
||||
sprite.texture = TEX
|
||||
add_child(sprite)
|
||||
var shape := CollisionShape2D.new()
|
||||
var rect := RectangleShape2D.new()
|
||||
rect.size = Vector2(54.0, 30.0)
|
||||
shape.shape = rect
|
||||
add_child(shape)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_t += delta
|
||||
position.x += vx * float(side) * delta
|
||||
position.y = base_y + sin(_t * 1.1) * 46.0
|
||||
if (side == 1 and position.x > 610.0) or (side == -1 and position.x < -70.0):
|
||||
queue_free()
|
||||
return
|
||||
_fire_timer -= delta
|
||||
if _fire_timer <= 0.0:
|
||||
_fire_timer = fire_interval
|
||||
_fire_bullet(_aim_direction() * bullet_speed)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dgt7piqw0kwrx
|
||||
@@ -0,0 +1,51 @@
|
||||
class_name EnemyShip
|
||||
extends EnemyBase
|
||||
## 驱逐舰:随海面航行(比地面稍快),舰桥瞄准玩家射击。
|
||||
|
||||
const TEX := preload("res://assets/svg/ship.svg")
|
||||
const MUZZLE := Vector2(0.0, 0.0)
|
||||
|
||||
var difficulty := 1.0
|
||||
var scroll_speed := 160.0
|
||||
var fire_interval := 2.4
|
||||
var bullet_speed := 230.0
|
||||
|
||||
var _fire_timer := 0.0
|
||||
|
||||
|
||||
func setup(p_x: float, p_y: float, p_difficulty := 1.0) -> void:
|
||||
position = Vector2(p_x, p_y)
|
||||
difficulty = p_difficulty
|
||||
scroll_speed = 150.0 + 20.0 * p_difficulty
|
||||
fire_interval = maxf(1.6, 2.4 / p_difficulty)
|
||||
bullet_speed = 230.0 * p_difficulty
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
# 层 32:海面单位——玩家子弹可命中,但玩家机体不与其碰撞(飞越上空)
|
||||
collision_layer = 32
|
||||
collision_mask = 0
|
||||
hp = 8
|
||||
score_value = 800
|
||||
hit_radius = 46.0
|
||||
var sprite := Sprite2D.new()
|
||||
sprite.texture = TEX
|
||||
add_child(sprite)
|
||||
var shape := CollisionShape2D.new()
|
||||
var rect := RectangleShape2D.new()
|
||||
rect.size = Vector2(38.0, 112.0)
|
||||
shape.shape = rect
|
||||
add_child(shape)
|
||||
_fire_timer = randf_range(1.0, 2.2)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
position.y += scroll_speed * delta
|
||||
if position.y > 1100.0:
|
||||
queue_free()
|
||||
return
|
||||
if position.y > 60.0 and position.y < 760.0:
|
||||
_fire_timer -= delta
|
||||
if _fire_timer <= 0.0:
|
||||
_fire_timer = fire_interval
|
||||
_fire_bullet(_aim_direction() * bullet_speed, MUZZLE)
|
||||
@@ -0,0 +1 @@
|
||||
uid://h8rqsoqx57oo
|
||||
@@ -0,0 +1,71 @@
|
||||
class_name EnemySmall
|
||||
extends EnemyBase
|
||||
## 小型敌机:直线下压 + 可选横向摆动,部分会朝正下方开火。
|
||||
|
||||
const TEX := preload("res://assets/svg/enemy_small.svg")
|
||||
|
||||
var base_x := 270.0
|
||||
var vy := 150.0
|
||||
var sway_amp := 0.0
|
||||
var sway_freq := 2.0
|
||||
var phase := 0.0
|
||||
var shoot := false
|
||||
var bullet_speed := 250.0
|
||||
|
||||
var _t := 0.0
|
||||
var _shoot_timer := 0.0
|
||||
|
||||
|
||||
func setup(
|
||||
p_base_x: float,
|
||||
p_y: float,
|
||||
p_vy: float,
|
||||
p_sway_amp := 0.0,
|
||||
p_sway_freq := 2.0,
|
||||
p_phase := 0.0,
|
||||
p_shoot := false,
|
||||
p_bullet_speed := 250.0
|
||||
) -> void:
|
||||
base_x = p_base_x
|
||||
position = Vector2(p_base_x, p_y)
|
||||
vy = p_vy
|
||||
sway_amp = p_sway_amp
|
||||
sway_freq = p_sway_freq
|
||||
phase = p_phase
|
||||
shoot = p_shoot
|
||||
bullet_speed = p_bullet_speed
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
collision_layer = 4
|
||||
collision_mask = 0
|
||||
hp = 1
|
||||
score_value = 100
|
||||
hit_radius = 14.0
|
||||
var sprite := Sprite2D.new()
|
||||
sprite.texture = TEX
|
||||
# 掉落道具的敌机染成红色涂装(1942 惯例:红机掉宝)
|
||||
if drops_powerup:
|
||||
sprite.modulate = Color(1.0, 0.5, 0.4)
|
||||
add_child(sprite)
|
||||
var shape := CollisionShape2D.new()
|
||||
var capsule := CapsuleShape2D.new()
|
||||
capsule.radius = 12.0
|
||||
capsule.height = 34.0
|
||||
shape.shape = capsule
|
||||
add_child(shape)
|
||||
_shoot_timer = randf_range(1.2, 2.4)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_t += delta
|
||||
position.y += vy * delta
|
||||
position.x = base_x + sin(_t * sway_freq + phase) * sway_amp
|
||||
if position.y > 1010.0 or position.x < -50.0 or position.x > 590.0:
|
||||
queue_free()
|
||||
return
|
||||
if shoot and position.y > 90.0 and position.y < 640.0:
|
||||
_shoot_timer -= delta
|
||||
if _shoot_timer <= 0.0:
|
||||
_shoot_timer = randf_range(1.8, 3.0)
|
||||
_fire_bullet(Vector2(0.0, bullet_speed))
|
||||
@@ -0,0 +1 @@
|
||||
uid://b1eyvldxsrtcg
|
||||
@@ -0,0 +1,33 @@
|
||||
class_name Explosion
|
||||
extends Node2D
|
||||
## 爆炸特效:双火球缩放淡出后自动回收。
|
||||
|
||||
const TEX := preload("res://assets/svg/explosion.svg")
|
||||
|
||||
var size := 1.0
|
||||
|
||||
var _core: Sprite2D
|
||||
var _ring: Sprite2D
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_core = Sprite2D.new()
|
||||
_core.texture = TEX
|
||||
_core.scale = Vector2(0.2, 0.2) * size
|
||||
add_child(_core)
|
||||
|
||||
_ring = Sprite2D.new()
|
||||
_ring.texture = TEX
|
||||
_ring.scale = Vector2(0.55, 0.55) * size
|
||||
_ring.modulate = Color(1.0, 0.55, 0.25, 0.9)
|
||||
add_child(_ring)
|
||||
|
||||
var tween := create_tween()
|
||||
tween.set_parallel(true)
|
||||
tween.tween_property(_core, "scale", Vector2(1.5, 1.5) * size, 0.3) \
|
||||
.set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT)
|
||||
tween.tween_property(_core, "modulate:a", 0.0, 0.38)
|
||||
tween.tween_property(_ring, "scale", Vector2(2.6, 2.6) * size, 0.42) \
|
||||
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
tween.tween_property(_ring, "modulate:a", 0.0, 0.42)
|
||||
tween.chain().tween_callback(queue_free)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dml5cxody73k0
|
||||
+339
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cer2qb8xqv72q
|
||||
@@ -0,0 +1,7 @@
|
||||
class_name GameSession
|
||||
extends RefCounted
|
||||
## 跨场景会话数据(静态变量,进程内共享):标题界面 → 游戏场景传递选择。
|
||||
## 0 = Fighter(战斗机) 1 = Funnel(浮游炮型)
|
||||
|
||||
|
||||
static var plane_variant := 0
|
||||
@@ -0,0 +1 @@
|
||||
uid://ciff1ryon7nvd
|
||||
@@ -0,0 +1,78 @@
|
||||
class_name LaserBeam
|
||||
extends Node2D
|
||||
## 直线激光束:从所在位置垂直向上延伸,被第一个命中的敌人截断,
|
||||
## 对其造成持续伤害(dps)。视觉为核心线 + 外发光双层 Line2D。
|
||||
|
||||
var dps := 10.0
|
||||
var width := 5.0
|
||||
var color := Color(0.55, 0.9, 1.0)
|
||||
var active := false
|
||||
|
||||
var _core: Line2D
|
||||
var _glow: Line2D
|
||||
var _acc := 0.0
|
||||
var _t := 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
z_index = 5
|
||||
_glow = Line2D.new()
|
||||
_glow.width = width * 2.8
|
||||
_glow.default_color = Color(color, 0.28)
|
||||
_glow.points = [Vector2.ZERO, Vector2(0.0, -10.0)]
|
||||
add_child(_glow)
|
||||
_core = Line2D.new()
|
||||
_core.width = width
|
||||
_core.default_color = color
|
||||
_core.points = [Vector2.ZERO, Vector2(0.0, -10.0)]
|
||||
add_child(_core)
|
||||
visible = false
|
||||
|
||||
|
||||
## 从自身祖先中解析所属游戏的敌容器(避免多实例时全局组查找拿错容器)
|
||||
func _get_enemies_container() -> Node2D:
|
||||
var node := get_parent()
|
||||
while node != null:
|
||||
var candidate := node.get_node_or_null("Enemies")
|
||||
if candidate != null:
|
||||
return candidate as Node2D
|
||||
node = node.get_parent()
|
||||
return null
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not active:
|
||||
if visible:
|
||||
visible = false
|
||||
return
|
||||
visible = true
|
||||
_t += delta
|
||||
|
||||
var muzzle := global_position
|
||||
var hit_y := -40.0
|
||||
var target: EnemyBase = null
|
||||
var container := _get_enemies_container()
|
||||
if container != null:
|
||||
for enemy in container.get_children():
|
||||
if enemy is EnemyBase and not (enemy as EnemyBase).dead:
|
||||
var e := enemy as EnemyBase
|
||||
if absf(e.global_position.x - muzzle.x) < e.hit_radius + width * 0.5:
|
||||
# 光束向上:命中敌人底缘;若炮口已进入敌人范围则从炮口起算
|
||||
var hy := minf(muzzle.y, e.global_position.y + e.hit_radius)
|
||||
if hy >= hit_y:
|
||||
hit_y = hy
|
||||
target = e
|
||||
|
||||
var length := maxf(muzzle.y - hit_y, 0.0)
|
||||
var pulse := 1.0 + 0.1 * sin(_t * 42.0)
|
||||
_core.width = width * pulse
|
||||
_glow.width = width * 2.8 * pulse
|
||||
_core.points = [Vector2.ZERO, Vector2(0.0, -length)]
|
||||
_glow.points = [Vector2.ZERO, Vector2(0.0, -length)]
|
||||
|
||||
if target != null and is_instance_valid(target):
|
||||
_acc += dps * delta
|
||||
if _acc >= 1.0:
|
||||
var whole := int(_acc)
|
||||
_acc -= float(whole)
|
||||
target.apply_damage(whole)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b8663segsokte
|
||||
@@ -0,0 +1,59 @@
|
||||
class_name OceanScroll
|
||||
extends Node2D
|
||||
## 滚动海面:三贴图循环 + 岛屿装饰随机飘过。
|
||||
## 视口 540x960,贴图世界尺寸 540:3 块连续贴图在任意滚动相位下
|
||||
## 都能覆盖 [0,960],2 块会在屏幕底部留缺口。
|
||||
|
||||
const TILE := preload("res://assets/svg/ocean_tile.svg")
|
||||
const ISLAND := preload("res://assets/svg/island.svg")
|
||||
const SPEED := 120.0
|
||||
const TILE_WORLD := 540.0
|
||||
const CENTER_X := 270.0
|
||||
const TILE_COUNT := 3
|
||||
|
||||
var _offset := 0.0
|
||||
var _tiles: Array[Sprite2D] = []
|
||||
var _islands: Array[Sprite2D] = []
|
||||
var _island_timer := 4.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var tile_scale := TILE_WORLD / 256.0
|
||||
for i in TILE_COUNT:
|
||||
var tile := Sprite2D.new()
|
||||
tile.texture = TILE
|
||||
tile.scale = Vector2(tile_scale, tile_scale)
|
||||
tile.position.x = CENTER_X
|
||||
add_child(tile)
|
||||
_tiles.append(tile)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_offset = fmod(_offset + SPEED * delta, TILE_WORLD)
|
||||
for i in TILE_COUNT:
|
||||
_tiles[i].position.y = _offset - TILE_WORLD * 0.5 + TILE_WORLD * float(i)
|
||||
|
||||
_update_islands(delta)
|
||||
|
||||
|
||||
func _update_islands(delta: float) -> void:
|
||||
_island_timer -= delta
|
||||
if _island_timer <= 0.0:
|
||||
_island_timer = randf_range(5.0, 9.0)
|
||||
if _islands.size() < 3:
|
||||
_spawn_island()
|
||||
for island in _islands:
|
||||
island.position.y += SPEED * delta
|
||||
for island in _islands.duplicate():
|
||||
if island.position.y > 1060.0:
|
||||
_islands.erase(island)
|
||||
island.queue_free()
|
||||
|
||||
|
||||
func _spawn_island() -> void:
|
||||
var island := Sprite2D.new()
|
||||
island.texture = ISLAND
|
||||
island.position = Vector2(randf_range(90.0, 450.0), -90.0)
|
||||
island.scale = Vector2.ONE * randf_range(0.7, 1.15)
|
||||
add_child(island)
|
||||
_islands.append(island)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bfw6u5ejmhxn1
|
||||
@@ -0,0 +1,307 @@
|
||||
class_name Player
|
||||
extends Area2D
|
||||
## 玩家战机:移动钳制、无敌闪烁与受击。
|
||||
## Variant.FIGHTER:机枪弹幕(PlayerBullet)。
|
||||
## Variant.FUNNEL:机体+浮游炮持续激光;按 focus(I) 浮游炮聚拢合体粗激光。
|
||||
|
||||
signal hit
|
||||
|
||||
enum Variant { FIGHTER, FUNNEL }
|
||||
|
||||
const SPEED := 340.0
|
||||
const FIRE_INTERVAL := 0.13
|
||||
const INVULN_ON_RESPAWN := 2.5
|
||||
const WEAPON_LEVEL_MAX := 4
|
||||
const BOMBS_MAX := 5
|
||||
const BOMB_INVULN := 1.0
|
||||
const TEX := preload("res://assets/art/P-38.png")
|
||||
const TEX_FUNNEL := preload("res://assets/svg/plane_funnel.svg")
|
||||
const FUNNEL_TEX := preload("res://assets/svg/funnel.svg")
|
||||
|
||||
const PLANE_LASER_DPS := 18.0
|
||||
const FUNNEL_LASER_DPS := 7.0
|
||||
const FOCUS_MULT := 1.5
|
||||
const FOCUS_SPEED_MULT := 0.5
|
||||
const PLANE_MUZZLE := Vector2(0.0, -30.0)
|
||||
## 侧身帧切换:三帧贴图(平飞/左倾/右倾),机头始终垂直
|
||||
const BANK_LERP := 12.0
|
||||
const BANK_ENTER := 0.45
|
||||
const BANK_EXIT := 0.25
|
||||
## 展开阵列(左侧三槽,右侧镜像):错开的三角编队
|
||||
const FUNNEL_SPREAD_LEFT: Array[Vector2] = [
|
||||
Vector2(-36.0, -4.0),
|
||||
Vector2(-62.0, -22.0),
|
||||
Vector2(-40.0, -46.0),
|
||||
]
|
||||
const FUNNEL_FOCUS_X := 12.0
|
||||
|
||||
var variant: Variant = Variant.FIGHTER
|
||||
var weapon_level := 1
|
||||
var bombs := 2
|
||||
var fire_timer := 0.0
|
||||
var invuln := 0.0
|
||||
var active := true
|
||||
var sprite: Sprite2D
|
||||
|
||||
var _bank := 0.0
|
||||
var _bank_state := 0
|
||||
var _tex_neutral: Texture2D
|
||||
var _tex_bank_left: Texture2D
|
||||
var _tex_bank_right: Texture2D
|
||||
|
||||
var _funnels: Array[Node2D] = []
|
||||
var _funnel_beams: Array[LaserBeam] = []
|
||||
var _plane_beam: LaserBeam
|
||||
var _focus_beam: LaserBeam
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
collision_layer = 1
|
||||
collision_mask = 4 | 8 | 16
|
||||
sprite = Sprite2D.new()
|
||||
if variant == Variant.FUNNEL:
|
||||
_tex_neutral = _load_optional_texture("res://assets/art/plane_funnel.png")
|
||||
if _tex_neutral == null:
|
||||
_tex_neutral = TEX_FUNNEL
|
||||
_tex_bank_left = _load_optional_texture("res://assets/art/plane_funnel-bank-left.png")
|
||||
_tex_bank_right = _load_optional_texture("res://assets/art/plane_funnel-bank-right.png")
|
||||
else:
|
||||
_tex_neutral = TEX
|
||||
_tex_bank_left = _load_optional_texture("res://assets/art/P-38-bank-left.png")
|
||||
_tex_bank_right = _load_optional_texture("res://assets/art/P-38-bank-right.png")
|
||||
sprite.texture = _tex_neutral
|
||||
add_child(sprite)
|
||||
var shape := CollisionShape2D.new()
|
||||
var capsule := CapsuleShape2D.new()
|
||||
capsule.radius = 13.0
|
||||
capsule.height = 44.0
|
||||
shape.shape = capsule
|
||||
add_child(shape)
|
||||
area_entered.connect(_on_area_entered)
|
||||
if variant == Variant.FUNNEL:
|
||||
_plane_beam = _make_beam(PLANE_LASER_DPS, 5.0, Color(0.6, 0.85, 1.0), PLANE_MUZZLE)
|
||||
_focus_beam = _make_beam(0.0, 16.0, Color(0.92, 0.98, 1.0), PLANE_MUZZLE)
|
||||
_sync_funnels()
|
||||
|
||||
|
||||
func _make_beam(p_dps: float, p_width: float, p_color: Color, offset: Vector2) -> LaserBeam:
|
||||
var beam := LaserBeam.new()
|
||||
beam.dps = p_dps
|
||||
beam.width = p_width
|
||||
beam.color = p_color
|
||||
beam.position = offset
|
||||
add_child(beam)
|
||||
return beam
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not active:
|
||||
return
|
||||
var direction := Input.get_vector("move_left", "move_right", "move_up", "move_down")
|
||||
# 聚焦合体期间:威力换机动,移动速度减半(仍可移动)
|
||||
var speed := SPEED
|
||||
if variant == Variant.FUNNEL and Input.is_action_pressed("fire") and Input.is_action_pressed("focus"):
|
||||
speed *= FOCUS_SPEED_MULT
|
||||
position += direction * speed * delta
|
||||
|
||||
# 侧身帧:带迟滞的三态切换(左倾/平飞/右倾),纯贴图切换、机头不歪
|
||||
var dir_x := clampf(direction.x, -1.0, 1.0)
|
||||
_bank = lerpf(_bank, dir_x, minf(BANK_LERP * delta, 1.0))
|
||||
if _bank_state == 0:
|
||||
if _bank > BANK_ENTER:
|
||||
_bank_state = 1
|
||||
elif _bank < -BANK_ENTER:
|
||||
_bank_state = -1
|
||||
elif absf(_bank) < BANK_EXIT:
|
||||
_bank_state = 0
|
||||
match _bank_state:
|
||||
-1:
|
||||
if _tex_bank_left != null:
|
||||
sprite.texture = _tex_bank_left
|
||||
1:
|
||||
if _tex_bank_right != null:
|
||||
sprite.texture = _tex_bank_right
|
||||
_:
|
||||
sprite.texture = _tex_neutral
|
||||
position.x = clampf(position.x, 22.0, 518.0)
|
||||
position.y = clampf(position.y, 90.0, 930.0)
|
||||
|
||||
if variant == Variant.FIGHTER:
|
||||
fire_timer -= delta
|
||||
if Input.is_action_pressed("fire") and fire_timer <= 0.0:
|
||||
fire_timer = FIRE_INTERVAL
|
||||
_fire()
|
||||
else:
|
||||
_update_funnel_weapons(delta)
|
||||
|
||||
if Input.is_action_just_pressed("bomb") and active and bombs > 0:
|
||||
use_bomb()
|
||||
|
||||
if invuln > 0.0:
|
||||
invuln -= delta
|
||||
sprite.modulate.a = 1.0 if fmod(invuln * 8.0, 2.0) < 1.0 else 0.3
|
||||
else:
|
||||
sprite.modulate.a = 1.0
|
||||
|
||||
|
||||
## 浮游炮机体武器:常态机体激光 + 各浮游炮激光;按 I 聚焦合体粗激光。
|
||||
## 合体总威力 = 散开总威力 × FOCUS_MULT。
|
||||
func _update_funnel_weapons(delta: float) -> void:
|
||||
var firing := Input.is_action_pressed("fire")
|
||||
var focus := firing and Input.is_action_pressed("focus")
|
||||
_plane_beam.active = firing and not focus
|
||||
_focus_beam.active = focus
|
||||
if focus:
|
||||
_focus_beam.dps = (PLANE_LASER_DPS + float(_funnels.size()) * FUNNEL_LASER_DPS) * FOCUS_MULT
|
||||
for beam in _funnel_beams:
|
||||
beam.active = firing and not focus
|
||||
for i in _funnels.size():
|
||||
var left := i % 2 == 0
|
||||
var rank := i / 2
|
||||
var target: Vector2
|
||||
if focus:
|
||||
target = Vector2(-FUNNEL_FOCUS_X if left else FUNNEL_FOCUS_X, -28.0 - 20.0 * float(rank))
|
||||
else:
|
||||
var slot: Vector2 = FUNNEL_SPREAD_LEFT[rank]
|
||||
target = slot if left else Vector2(-slot.x, slot.y)
|
||||
var funnel: Node2D = _funnels[i]
|
||||
funnel.position = funnel.position.lerp(target, minf(10.0 * delta, 1.0))
|
||||
|
||||
|
||||
## 按当前火力等级同步浮游炮数量(每侧 level-1 台)
|
||||
func _sync_funnels() -> void:
|
||||
if variant != Variant.FUNNEL:
|
||||
return
|
||||
var wanted := (weapon_level - 1) * 2
|
||||
while _funnels.size() > wanted:
|
||||
var removed: Node2D = _funnels.pop_back()
|
||||
_funnel_beams.pop_back()
|
||||
removed.queue_free()
|
||||
while _funnels.size() < wanted:
|
||||
var funnel := Node2D.new()
|
||||
var icon := Sprite2D.new()
|
||||
icon.texture = FUNNEL_TEX
|
||||
funnel.add_child(icon)
|
||||
var beam := LaserBeam.new()
|
||||
beam.dps = FUNNEL_LASER_DPS
|
||||
beam.width = 4.0
|
||||
beam.color = Color(0.45, 0.9, 1.0)
|
||||
beam.position = Vector2(0.0, -8.0)
|
||||
funnel.add_child(beam)
|
||||
add_child(funnel)
|
||||
funnel.position = Vector2(0.0, 20.0)
|
||||
_funnels.append(funnel)
|
||||
_funnel_beams.append(beam)
|
||||
|
||||
|
||||
func _fire() -> void:
|
||||
var container := get_tree().get_first_node_in_group("player_bullets")
|
||||
if container == null:
|
||||
return
|
||||
match weapon_level:
|
||||
1:
|
||||
_spawn_volley(container, [-11.0, 11.0], Vector2.UP)
|
||||
2:
|
||||
_spawn_volley(container, [-17.0, -6.0, 6.0, 17.0], Vector2.UP)
|
||||
3:
|
||||
_spawn_volley(container, [-17.0, -6.0, 6.0, 17.0], Vector2.UP)
|
||||
_spawn_bullet(container, Vector2(-22.0, -4.0), Vector2.UP.rotated(deg_to_rad(-12.0)))
|
||||
_spawn_bullet(container, Vector2(22.0, -4.0), Vector2.UP.rotated(deg_to_rad(12.0)))
|
||||
_:
|
||||
# L4:6 直射 + 双道 22° 斜射 + 30° 斜射
|
||||
_spawn_volley(container, [-25.0, -15.0, -5.0, 5.0, 15.0, 25.0], Vector2.UP)
|
||||
_spawn_bullet(container, Vector2(-32.0, -4.0), Vector2.UP.rotated(deg_to_rad(-22.0)))
|
||||
_spawn_bullet(container, Vector2(-32.0, 6.0), Vector2.UP.rotated(deg_to_rad(-22.0)))
|
||||
_spawn_bullet(container, Vector2(32.0, -4.0), Vector2.UP.rotated(deg_to_rad(22.0)))
|
||||
_spawn_bullet(container, Vector2(32.0, 6.0), Vector2.UP.rotated(deg_to_rad(22.0)))
|
||||
_spawn_bullet(container, Vector2(-20.0, 10.0), Vector2.UP.rotated(deg_to_rad(-30.0)))
|
||||
_spawn_bullet(container, Vector2(20.0, 10.0), Vector2.UP.rotated(deg_to_rad(30.0)))
|
||||
|
||||
|
||||
func _spawn_volley(container: Node, x_offsets: Array, dir: Vector2) -> void:
|
||||
for x in x_offsets:
|
||||
_spawn_bullet(container, Vector2(x, -8.0), dir)
|
||||
|
||||
|
||||
func _spawn_bullet(container: Node, offset: Vector2, dir: Vector2) -> void:
|
||||
var bullet := PlayerBullet.new()
|
||||
bullet.direction = dir
|
||||
container.add_child(bullet)
|
||||
bullet.global_position = global_position + offset
|
||||
|
||||
|
||||
func _collect_powerup(pickup: Powerup) -> void:
|
||||
pickup.queue_free()
|
||||
match pickup.kind:
|
||||
Powerup.Kind.BOMB:
|
||||
var game := get_tree().get_first_node_in_group("game") as Game
|
||||
bombs = mini(bombs + 1, BOMBS_MAX)
|
||||
if game != null:
|
||||
game.notify_bomb_pickup()
|
||||
_:
|
||||
if weapon_level < WEAPON_LEVEL_MAX:
|
||||
weapon_level += 1
|
||||
_sync_funnels()
|
||||
else:
|
||||
var game := get_tree().get_first_node_in_group("game") as Game
|
||||
if game != null:
|
||||
game.add_score(500)
|
||||
|
||||
|
||||
## 投放炸弹:清屏伤害 + 清除敌弹 + 白闪光效 + 短暂无敌
|
||||
func use_bomb() -> void:
|
||||
if not active or bombs <= 0:
|
||||
return
|
||||
bombs -= 1
|
||||
invuln = maxf(invuln, BOMB_INVULN)
|
||||
var game := get_tree().get_first_node_in_group("game") as Game
|
||||
if game != null:
|
||||
game.detonate_bomb()
|
||||
|
||||
|
||||
func _on_area_entered(area: Area2D) -> void:
|
||||
if area is Powerup:
|
||||
_collect_powerup(area)
|
||||
return
|
||||
if not active or invuln > 0.0:
|
||||
return
|
||||
if area is EnemyBullet:
|
||||
area.queue_free()
|
||||
emit_signal("hit")
|
||||
elif area is EnemyBase:
|
||||
emit_signal("hit")
|
||||
|
||||
|
||||
## 被击中后的消隐(禁用碰撞需延迟,避免在物理回调中直接修改);火力降一级
|
||||
func die() -> void:
|
||||
active = false
|
||||
weapon_level = maxi(1, weapon_level - 1)
|
||||
_sync_funnels()
|
||||
if _plane_beam != null:
|
||||
_plane_beam.active = false
|
||||
if _focus_beam != null:
|
||||
_focus_beam.active = false
|
||||
for beam in _funnel_beams:
|
||||
beam.active = false
|
||||
visible = false
|
||||
set_deferred("monitoring", false)
|
||||
|
||||
|
||||
## 复活并给予无敌时间
|
||||
func respawn(pos: Vector2) -> void:
|
||||
position = pos
|
||||
active = true
|
||||
visible = true
|
||||
invuln = INVULN_ON_RESPAWN
|
||||
fire_timer = 0.0
|
||||
_bank = 0.0
|
||||
_bank_state = 0
|
||||
sprite.texture = _tex_neutral
|
||||
set_deferred("monitoring", true)
|
||||
|
||||
|
||||
## 可选素材:文件存在才加载(侧身帧未生成时回退平飞帧)
|
||||
func _load_optional_texture(path: String) -> Texture2D:
|
||||
if not ResourceLoader.exists(path):
|
||||
return null
|
||||
return load(path) as Texture2D
|
||||
@@ -0,0 +1 @@
|
||||
uid://mox72d37bu25
|
||||
@@ -0,0 +1,37 @@
|
||||
class_name PlayerBullet
|
||||
extends Area2D
|
||||
## 玩家子弹:沿自身方向直线飞行(斜射弹共享此类),命中敌机造成伤害。
|
||||
|
||||
const SPEED := 760.0
|
||||
const TEX := preload("res://assets/svg/bullet_player.svg")
|
||||
|
||||
var direction := Vector2.UP
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
collision_layer = 2
|
||||
# 4 空中敌机 + 32 地面/海面单位(只被打,不与玩家机体碰撞)
|
||||
collision_mask = 4 | 32
|
||||
var sprite := Sprite2D.new()
|
||||
sprite.texture = TEX
|
||||
add_child(sprite)
|
||||
var shape := CollisionShape2D.new()
|
||||
var rect := RectangleShape2D.new()
|
||||
rect.size = Vector2(6.0, 16.0)
|
||||
shape.shape = rect
|
||||
add_child(shape)
|
||||
# 旋转节点本身:贴图与碰撞矩形一起对齐弹道方向
|
||||
rotation = Vector2.UP.angle_to(direction)
|
||||
area_entered.connect(_on_area_entered)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
position += direction * SPEED * delta
|
||||
if global_position.y < -30.0 or global_position.x < -30.0 or global_position.x > 570.0:
|
||||
queue_free()
|
||||
|
||||
|
||||
func _on_area_entered(area: Area2D) -> void:
|
||||
if area is EnemyBase:
|
||||
(area as EnemyBase).apply_damage(1)
|
||||
queue_free()
|
||||
@@ -0,0 +1 @@
|
||||
uid://cun0cmlwov27j
|
||||
@@ -0,0 +1,40 @@
|
||||
class_name Powerup
|
||||
extends Area2D
|
||||
## 掉落道具:缓慢下落 + 左右摇摆 + 缓慢自转,玩家接触即收集。
|
||||
## kind: Kind.POWER 火力升级 / Kind.BOMB 炸弹+1
|
||||
|
||||
enum Kind { POWER, BOMB }
|
||||
|
||||
const FALL_SPEED := 105.0
|
||||
const SWAY_AMP := 26.0
|
||||
const TEX_POWER := preload("res://assets/svg/powerup.svg")
|
||||
const TEX_BOMB := preload("res://assets/svg/bomb_powerup.svg")
|
||||
|
||||
var kind := Kind.POWER
|
||||
|
||||
var _t := 0.0
|
||||
var _base_x := 0.0
|
||||
var _sprite: Sprite2D
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
collision_layer = 16
|
||||
collision_mask = 0
|
||||
_base_x = position.x
|
||||
_sprite = Sprite2D.new()
|
||||
_sprite.texture = TEX_BOMB if kind == Kind.BOMB else TEX_POWER
|
||||
add_child(_sprite)
|
||||
var shape := CollisionShape2D.new()
|
||||
var circle := CircleShape2D.new()
|
||||
circle.radius = 15.0
|
||||
shape.shape = circle
|
||||
add_child(shape)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_t += delta
|
||||
position.y += FALL_SPEED * delta
|
||||
position.x = _base_x + sin(_t * 1.9) * SWAY_AMP
|
||||
_sprite.rotation += delta * 2.4
|
||||
if position.y > 1010.0:
|
||||
queue_free()
|
||||
@@ -0,0 +1 @@
|
||||
uid://bu4bn1v74e4xc
|
||||
@@ -0,0 +1,150 @@
|
||||
class_name Title
|
||||
extends Node2D
|
||||
## 启动界面:滚动海面打底,左右选择战机,回车/J 出击。
|
||||
|
||||
const PLANE_A_TEX := Player.TEX
|
||||
const PLANE_B_TEX := preload("res://assets/svg/plane_funnel.svg") # 兜底:美术 PNG 存在时优先
|
||||
const FUNNEL_TEX := preload("res://assets/svg/funnel.svg")
|
||||
const PLANE_B_ART := "res://assets/art/plane_funnel.png"
|
||||
|
||||
|
||||
static func _plane_b_texture() -> Texture2D:
|
||||
if ResourceLoader.exists(PLANE_B_ART):
|
||||
return load(PLANE_B_ART)
|
||||
return PLANE_B_TEX
|
||||
|
||||
var _sel := 0
|
||||
var _started := false
|
||||
var _borders: Array[ColorRect] = []
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
add_child(OceanScroll.new())
|
||||
|
||||
var dim := ColorRect.new()
|
||||
dim.color = Color(0.02, 0.05, 0.1, 0.62)
|
||||
dim.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
add_child(dim)
|
||||
|
||||
var layer := CanvasLayer.new()
|
||||
layer.layer = 10
|
||||
add_child(layer)
|
||||
|
||||
var title := Label.new()
|
||||
title.text = "AERO STRIKE"
|
||||
title.position = Vector2(0.0, 130.0)
|
||||
title.size = Vector2(540.0, 80.0)
|
||||
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
title.add_theme_font_size_override("font_size", 64)
|
||||
title.add_theme_color_override("font_color", Color(1.0, 0.85, 0.35))
|
||||
title.add_theme_color_override("font_shadow_color", Color(0.0, 0.0, 0.0, 0.8))
|
||||
title.add_theme_constant_override("shadow_offset_x", 3)
|
||||
title.add_theme_constant_override("shadow_offset_y", 3)
|
||||
layer.add_child(title)
|
||||
|
||||
var subtitle := Label.new()
|
||||
subtitle.text = "1 9 4 2"
|
||||
subtitle.position = Vector2(0.0, 205.0)
|
||||
subtitle.size = Vector2(540.0, 40.0)
|
||||
subtitle.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
subtitle.add_theme_font_size_override("font_size", 28)
|
||||
subtitle.add_theme_color_override("font_color", Color(0.85, 0.9, 1.0))
|
||||
layer.add_child(subtitle)
|
||||
|
||||
var choose := Label.new()
|
||||
choose.text = "— 选择战机 —"
|
||||
choose.position = Vector2(0.0, 300.0)
|
||||
choose.size = Vector2(540.0, 34.0)
|
||||
choose.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
choose.add_theme_font_size_override("font_size", 24)
|
||||
choose.add_theme_color_override("font_color", Color.WHITE)
|
||||
layer.add_child(choose)
|
||||
|
||||
_build_card(layer, 0, 40.0, PLANE_A_TEX, "P-38 战斗机", "宽域机枪弹幕", "P 升级火力 · K 炸弹")
|
||||
_build_card(layer, 1, 280.0, _plane_b_texture(), "雷光 浮游炮型", "贯穿激光 + 浮游炮", "I 聚焦合体激光 · K 炸弹", true)
|
||||
|
||||
var hint := Label.new()
|
||||
hint.text = "←→ 选择 回车 / J 出击"
|
||||
hint.position = Vector2(0.0, 880.0)
|
||||
hint.size = Vector2(540.0, 34.0)
|
||||
hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
hint.add_theme_font_size_override("font_size", 22)
|
||||
hint.add_theme_color_override("font_color", Color(0.75, 0.8, 0.85))
|
||||
layer.add_child(hint)
|
||||
|
||||
_update_selection()
|
||||
|
||||
|
||||
func _build_card(parent: Node, index: int, x: float, tex: Texture2D, plane_name: String, line1: String, line2: String, with_funnels := false) -> void:
|
||||
var bg := ColorRect.new()
|
||||
bg.position = Vector2(x, 350.0)
|
||||
bg.size = Vector2(220.0, 330.0)
|
||||
bg.color = Color(0.08, 0.12, 0.18, 0.92)
|
||||
parent.add_child(bg)
|
||||
|
||||
var border := ColorRect.new()
|
||||
border.position = bg.position - Vector2(4.0, 4.0)
|
||||
border.size = bg.size + Vector2(8.0, 8.0)
|
||||
border.color = Color(1.0, 0.82, 0.25)
|
||||
parent.add_child(border)
|
||||
_borders.append(border)
|
||||
|
||||
var icon := Sprite2D.new()
|
||||
icon.texture = tex
|
||||
icon.scale = Vector2.ONE * 1.7
|
||||
icon.position = Vector2(x + 110.0, 455.0)
|
||||
parent.add_child(icon)
|
||||
|
||||
if with_funnels:
|
||||
for i in 2:
|
||||
var funnel := Sprite2D.new()
|
||||
funnel.texture = FUNNEL_TEX
|
||||
funnel.position = Vector2(x + 110.0 + (-60.0 if i == 0 else 60.0), 425.0 + 30.0 * i)
|
||||
parent.add_child(funnel)
|
||||
|
||||
var name_label := Label.new()
|
||||
name_label.text = plane_name
|
||||
name_label.position = Vector2(x, 545.0)
|
||||
name_label.size = Vector2(220.0, 34.0)
|
||||
name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
name_label.add_theme_font_size_override("font_size", 24)
|
||||
name_label.add_theme_color_override("font_color", Color.WHITE)
|
||||
parent.add_child(name_label)
|
||||
|
||||
var desc1 := Label.new()
|
||||
desc1.text = line1
|
||||
desc1.position = Vector2(x, 600.0)
|
||||
desc1.size = Vector2(220.0, 28.0)
|
||||
desc1.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
desc1.add_theme_font_size_override("font_size", 19)
|
||||
desc1.add_theme_color_override("font_color", Color(0.8, 0.88, 1.0))
|
||||
parent.add_child(desc1)
|
||||
|
||||
var desc2 := Label.new()
|
||||
desc2.text = line2
|
||||
desc2.position = Vector2(x, 640.0)
|
||||
desc2.size = Vector2(220.0, 26.0)
|
||||
desc2.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
desc2.add_theme_font_size_override("font_size", 17)
|
||||
desc2.add_theme_color_override("font_color", Color(0.7, 0.78, 0.88))
|
||||
parent.add_child(desc2)
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if _started:
|
||||
return
|
||||
if Input.is_action_just_pressed("move_left") and _sel != 0:
|
||||
_sel = 0
|
||||
_update_selection()
|
||||
elif Input.is_action_just_pressed("move_right") and _sel != 1:
|
||||
_sel = 1
|
||||
_update_selection()
|
||||
if Input.is_action_just_pressed("ui_accept") or Input.is_action_just_pressed("fire"):
|
||||
_started = true
|
||||
GameSession.plane_variant = _sel
|
||||
get_tree().change_scene_to_file("res://scenes/game.tscn")
|
||||
|
||||
|
||||
func _update_selection() -> void:
|
||||
for i in _borders.size():
|
||||
_borders[i].visible = i == _sel
|
||||
@@ -0,0 +1 @@
|
||||
uid://bteck1h6rqrlb
|
||||
@@ -0,0 +1,163 @@
|
||||
class_name WaveSpawner
|
||||
extends Node
|
||||
## 波次生成器:时间线循环刷敌,每轮提升难度。
|
||||
|
||||
var difficulty := 1.0
|
||||
var game: Game
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
game = get_tree().get_first_node_in_group("game") as Game
|
||||
await get_tree().create_timer(0.8).timeout
|
||||
var round_num := 1
|
||||
while true:
|
||||
difficulty = minf(1.0 + (round_num - 1) * 0.12, 1.8)
|
||||
await _play_round()
|
||||
await _wait_playing()
|
||||
# 关底 Boss 战
|
||||
if game != null and is_instance_valid(game):
|
||||
game.show_boss_warning()
|
||||
await _timer(2.0)
|
||||
await _wait_playing()
|
||||
var boss: Boss = null
|
||||
if game != null and is_instance_valid(game):
|
||||
boss = game.spawn_boss(difficulty)
|
||||
while boss != null and is_instance_valid(boss):
|
||||
await _timer(0.3)
|
||||
await _wait_playing()
|
||||
await _timer(2.5)
|
||||
round_num += 1
|
||||
await get_tree().create_timer(2.5).timeout
|
||||
|
||||
|
||||
func _play_round() -> void:
|
||||
await _wait_playing()
|
||||
await _column(140.0)
|
||||
await _timer(3.2)
|
||||
|
||||
await _wait_playing()
|
||||
_v_formation(270.0)
|
||||
await _timer(3.8)
|
||||
|
||||
await _wait_playing()
|
||||
_sine_squad(170.0)
|
||||
_sine_squad(370.0)
|
||||
await _timer(3.8)
|
||||
|
||||
await _wait_playing()
|
||||
_fortress(120.0)
|
||||
_fortress(420.0)
|
||||
await _timer(2.6)
|
||||
|
||||
await _wait_playing()
|
||||
await _column(400.0)
|
||||
await _timer(0.8)
|
||||
await _column(120.0)
|
||||
await _timer(3.0)
|
||||
|
||||
await _wait_playing()
|
||||
_medium(1, 210.0)
|
||||
await _timer(4.0)
|
||||
|
||||
await _wait_playing()
|
||||
_ship(190.0)
|
||||
await _timer(4.0)
|
||||
|
||||
await _wait_playing()
|
||||
_v_formation(160.0)
|
||||
await _timer(1.4)
|
||||
_v_formation(380.0)
|
||||
await _timer(3.6)
|
||||
|
||||
await _wait_playing()
|
||||
_sine_squad(270.0)
|
||||
_fortress(270.0)
|
||||
await _timer(3.4)
|
||||
|
||||
await _wait_playing()
|
||||
_medium(-1, 260.0)
|
||||
await _column(270.0)
|
||||
_ship(380.0)
|
||||
await _timer(4.2)
|
||||
|
||||
|
||||
## 游戏结束时挂起等待,避免继续刷敌
|
||||
func _wait_playing() -> void:
|
||||
while game == null or game.state != Game.State.PLAYING:
|
||||
await get_tree().create_timer(0.5).timeout
|
||||
|
||||
|
||||
func _timer(duration: float) -> void:
|
||||
await get_tree().create_timer(duration).timeout
|
||||
|
||||
|
||||
## 纵队:同一直线依次下压,头机掉宝
|
||||
func _column(x: float) -> void:
|
||||
for i in 5:
|
||||
_spawn_small(x, -30.0 - 46.0, 190.0 * difficulty, 0.0, 2.0, 0.0, true, 250.0 * difficulty, i == 0)
|
||||
await _timer(0.32)
|
||||
|
||||
|
||||
## V 字编队:5 机整体压境,仅领队开火并掉宝
|
||||
func _v_formation(center_x: float) -> void:
|
||||
var offsets := [Vector2(0.0, -30.0), Vector2(-44.0, -66.0), Vector2(44.0, -66.0), Vector2(-88.0, -102.0), Vector2(88.0, -102.0)]
|
||||
for i in offsets.size():
|
||||
var offset: Vector2 = offsets[i]
|
||||
var is_leader := i == 0
|
||||
_spawn_small(center_x + offset.x, offset.y, 150.0 * difficulty, 16.0, 1.6, 0.0, is_leader, 240.0 * difficulty, is_leader)
|
||||
|
||||
|
||||
## 摆动机队:同路径依次进场、正弦横摆,头机掉宝
|
||||
func _sine_squad(x: float) -> void:
|
||||
for i in 4:
|
||||
_spawn_small(x, -30.0, 130.0 * difficulty, 72.0, 1.8, 0.0, randf() < 0.5, 230.0 * difficulty, i == 0)
|
||||
await _timer(0.4)
|
||||
|
||||
|
||||
## 中型轰炸机横掠
|
||||
func _medium(p_side: int, y: float) -> void:
|
||||
var enemy := EnemyMedium.new()
|
||||
enemy.setup(p_side, y, 112.0 * difficulty, 235.0 * difficulty, maxf(0.9, 1.6 / difficulty))
|
||||
_add_enemy(enemy)
|
||||
|
||||
|
||||
## 地面固定堡垒
|
||||
func _fortress(x: float) -> void:
|
||||
var enemy := EnemyFortress.new()
|
||||
enemy.setup(x, -60.0, difficulty)
|
||||
_add_enemy(enemy)
|
||||
|
||||
|
||||
## 驱逐舰
|
||||
func _ship(x: float) -> void:
|
||||
var enemy := EnemyShip.new()
|
||||
enemy.setup(x, -140.0, difficulty)
|
||||
_add_enemy(enemy)
|
||||
|
||||
|
||||
func _spawn_small(
|
||||
x: float,
|
||||
y: float,
|
||||
vy: float,
|
||||
sway_amp: float,
|
||||
sway_freq: float,
|
||||
phase: float,
|
||||
shoot: bool,
|
||||
bullet_speed: float,
|
||||
drop := false
|
||||
) -> void:
|
||||
var enemy := EnemySmall.new()
|
||||
enemy.setup(x, y, vy, sway_amp, sway_freq, phase, shoot, bullet_speed)
|
||||
enemy.drops_powerup = drop
|
||||
_add_enemy(enemy)
|
||||
|
||||
|
||||
func _add_enemy(enemy: EnemyBase) -> void:
|
||||
var container := get_tree().get_first_node_in_group("enemies")
|
||||
if container == null:
|
||||
return
|
||||
container.add_child(enemy)
|
||||
@@ -0,0 +1 @@
|
||||
uid://fwblkorrxm2i
|
||||
Reference in New Issue
Block a user