Files
hello-godot/scripts/player.gd
T

308 lines
9.4 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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