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