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
+307
View File
@@ -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