Enemies no longer attack; add a fixed bullet-ring emitter and make lasers permanent obstacles

walker.gd lost its firing code entirely: monsters now only patrol and hurt on contact, so the only way past is footwork or the key throw + W warp. New bullet_ring prefab fires a set number of bullets evenly spread over a fixed arc on a timer, with directions derived only from its own exports, so every volley is identical and the pattern can be memorised; the level places one at cell (12,12) aimed upward with the 12 bullets spread over 180 degrees. laser.gd gained always_on, defaulting to true, so the five copied lasers are now static hazards that are always lit and damage anything overlapping them rather than blinking on and off. Bullets now stop when they spawn inside terrain.
This commit is contained in:
2026-10-07 22:23:18 +08:00
parent 0d2bccc3a9
commit 1b3d61793d
8 changed files with 148 additions and 70 deletions
+30 -6
View File
@@ -19,6 +19,7 @@
[ext_resource type="PackedScene" path="res://scenes/prefabs/collectibles/diamond.tscn" id="17_icuov"]
[ext_resource type="PackedScene" path="res://scenes/prefabs/enemies/walker.tscn" id="18_walker"]
[ext_resource type="PackedScene" path="res://scenes/prefabs/enemies/laser.tscn" id="19_laser"]
[ext_resource type="PackedScene" path="res://scenes/prefabs/enemies/bullet_ring.tscn" id="20_ring"]
[node name="Main" type="Node2D" unique_id=2061129075]
@@ -162,7 +163,7 @@ script = ExtResource("8_ekhvm")
[node name="TerrainBase" type="TileMapLayer" parent="Level/TerrainTileMaps" unique_id=91115499]
position = Vector2(9, 9)
tile_map_data = PackedByteArray("AAD//wkAAAACAAEAAAD//woAAAAEAAAAAAABAA0AAAACAAYAAAACAA0AAAAEAAUAAAACAA4AAAAEAAUAAAADAA0AAAACAAYAAAABAAoAAAACAAYAAAAAAAoAAAAEAAUAAAAAAAkAAAACAAEAAAACAAsAAAAEAAUAAAAAAA0AAAAEAAUAAAD//w0AAAAEAAEAAAAFAA0AAAAEAAUAAAAEAA4AAAAEAAUAAAADAA4AAAACAAYAAAAAAA4AAAACAAYAAAABAA4AAAACAAYAAAAHAAwAAAACAAEAAAALAAwAAAACAAEAAAAKAA0AAAAEAAUAAAAIAA4AAAAEAAUAAAALAA4AAAAEAAUAAAAMAA4AAAACAAYAAAANAA0AAAACAAYAAAAOAA0AAAAEAAUAAAAPAA0AAAAEAAUAAAAQAA0AAAAEAAUAAAARAAwAAAACAAUAAAATAAwAAAACAAUAAAAVAAsAAAACAAYAAAAWAAsAAAAEAAUAAAAUAAwAAAACAAYAAAAQAAwAAAACAAMAAAAKAA4AAAAEAAUAAAAJAA4AAAAEAAUAAAAFAA4AAAACAAYAAAAGAA4AAAAEAAUAAAAHAA4AAAAEAAUAAAANAA4AAAACAAYAAAAOAA4AAAACAAYAAAAPAA4AAAACAAYAAAASAA0AAAACAAYAAAATAA0AAAAEAAUAAAAUAA0AAAACAAYAAAAVAA0AAAACAAYAAAAWAA0AAAACAAYAAAAXAA0AAAAEAAUAAAALAA0AAAAEAAUAAAAMAA0AAAACAAYAAAAPAAwAAAACAAMAAAAXAAsAAAAEAAUAAAAYAAsAAAAEAAUAAAAZAAsAAAACAAYAAAAZAAwAAAACAAYAAAAYAAwAAAAEAAUAAAAZAA4AAAAEAAUAAAAXAA4AAAAEAAUAAAAWAA4AAAAEAAUAAAAVAA4AAAACAAYAAAAUAA4AAAACAAYAAAATAA4AAAACAAYAAAASAA4AAAACAAYAAAAYAA4AAAACAAYAAAARAA4AAAACAAYAAAAEAA0AAAACAAYAAAD//w4AAAAEAAEAAAABAAkAAAACAAEAAAAIAAwAAAACAAEAAAAOAAwAAAACAAMAAAANAAwAAAACAAMAAAAMAAwAAAACAAMAAAAUAAkAAAABAAUAAAAUAAoAAAABAAYAAAAPAAgAAAASAAQAABAWAAoAAAACAAYAAAAXAAcAAAACAAUAAAAYAAkAAAACAAYAAAAYAAoAAAACAAYAAAAXAAoAAAACAAYAAAAXAAkAAAACAAYAAAAZAAkAAAACAAYAAAAZAAoAAAACAAYAAAAQAA4AAAAEAAUAAAARAA0AAAACAAYAAAASAAwAAAACAAUAAAAVAAoAAAACAAYAAAAWAAkAAAACAAUAAAAXAAgAAAAEAAEAAAAUAAsAAAAFAAEAAAAVAAwAAAAEAAUAAAAWAAwAAAACAAYAAAAXAAwAAAAEAAUAAAAYAA0AAAACAAYAAAAZAA0AAAAEAAUAAAAAAAwAAAACAAYAAAACAAwAAAACAAYAAAADAAwAAAAEAAUAAAAEAAwAAAACAAYAAAAFAAwAAAAEAAUAAAAGAAwAAAAEAAUAAAAGAA0AAAACAAYAAAAHAA0AAAACAAYAAAAIAA0AAAAEAAUAAAAJAA0AAAACAAYAAAAJAAwAAAACAAEAAAAKAAwAAAACAAEAAAD//wwAAAAFAAAAAAD//wsAAAAEAAAAAAAAAAsAAAAEAAUAAAABAAsAAAACAAYAAAABAAwAAAACAAYAAAAYAAcAAAACAAUAAAAZAAcAAAADAAUAAAAYAAgAAAACAAYAAAAZAAgAAAACAAYAAAAVAAkAAAACAAUAAAADAAsAAAACAAYAAAAEAAsAAAACAAEAAAAFAAsAAAACAAEAAAAGAAsAAAACAAEAAAACAAoAAAACAAEAAAADAAoAAAACAAEAAAARAAcAAAAOAAcAAAASAAcAAAAOAAcAAAAQAAcAAAAOAAcAAAAPAAcAAAANAAcAAAAIAAYAAAANAAcAAAATAAcAAAAPAAcAAAAMAAYAAAAPAAcAAAALAAYAAAAOAAcAAAAKAAYAAAAOAAcAAAAJAAYAAAAOAAcAAAABAAgAAAARAAYAAAABAAcAAAAQAAUAAAABAAYAAAARAAUAAAABAAUAAAAQAAUAAAAAAAQAAAASAAIAAAABAAQAAAASAAIAAAACAAQAAAASAAIAAAD//wQAAAARAAIAAAAEAAQAAAATAAIAAAADAAQAAAASAAIAAAAAAAMAAAASAAEAAAABAAMAAAASAAEAAAACAAMAAAASAAEAAAADAAMAAAASAAAAAAAEAAMAAAATAAAAAAD//wMAAAARAAEAAAD//wIAAAARAAEAAAD//wEAAAARAAEAAAAAAAEAAAASAAEAAAAAAAIAAAASAAEAAAABAAEAAAASAAEAAAABAAIAAAASAAEAAAACAAEAAAASAAEAAAACAAIAAAASAAEAAAACAAYAAAATAAQAAAA=")
tile_map_data = PackedByteArray("AAD//wkAAAACAAEAAAD//woAAAAEAAAAAAABAA0AAAACAAYAAAACAA0AAAAEAAUAAAACAA4AAAAEAAUAAAADAA0AAAACAAYAAAABAAoAAAACAAYAAAAAAAoAAAAEAAUAAAAAAAkAAAACAAEAAAACAAsAAAAEAAUAAAAAAA0AAAAEAAUAAAD//w0AAAAEAAEAAAAFAA0AAAAEAAUAAAAEAA4AAAAEAAUAAAADAA4AAAACAAYAAAAAAA4AAAACAAYAAAABAA4AAAACAAYAAAAHAAwAAAACAAEAAAALAAwAAAACAAEAAAAKAA0AAAAEAAUAAAAIAA4AAAAEAAUAAAALAA4AAAAEAAUAAAAMAA4AAAACAAYAAAANAA0AAAACAAYAAAAOAA0AAAAEAAUAAAAPAA0AAAAEAAUAAAAQAA0AAAAEAAUAAAARAAwAAAACAAUAAAATAAwAAAACAAUAAAAVAAsAAAACAAYAAAAWAAsAAAAEAAUAAAAUAAwAAAACAAYAAAAQAAwAAAACAAMAAAAKAA4AAAAEAAUAAAAJAA4AAAAEAAUAAAAFAA4AAAACAAYAAAAGAA4AAAAEAAUAAAAHAA4AAAAEAAUAAAANAA4AAAACAAYAAAAOAA4AAAACAAYAAAAPAA4AAAACAAYAAAASAA0AAAACAAYAAAATAA0AAAAEAAUAAAAUAA0AAAACAAYAAAAVAA0AAAACAAYAAAAWAA0AAAACAAYAAAAXAA0AAAAEAAUAAAALAA0AAAAEAAUAAAAMAA0AAAACAAYAAAAPAAwAAAACAAMAAAAXAAsAAAAEAAUAAAAYAAsAAAAEAAUAAAAZAAsAAAACAAYAAAAZAAwAAAACAAYAAAAYAAwAAAAEAAUAAAAZAA4AAAAEAAUAAAAXAA4AAAAEAAUAAAAWAA4AAAAEAAUAAAAVAA4AAAACAAYAAAAUAA4AAAACAAYAAAATAA4AAAACAAYAAAASAA4AAAACAAYAAAAYAA4AAAACAAYAAAARAA4AAAACAAYAAAAEAA0AAAACAAYAAAD//w4AAAAEAAEAAAABAAkAAAACAAEAAAAIAAwAAAACAAEAAAAOAAwAAAACAAMAAAANAAwAAAACAAMAAAAMAAwAAAACAAMAAAAUAAkAAAABAAUAAAAUAAoAAAABAAYAAAAPAAgAAAASAAQAABAWAAoAAAACAAYAAAAXAAcAAAACAAUAAAAYAAkAAAACAAYAAAAYAAoAAAACAAYAAAAXAAoAAAACAAYAAAAXAAkAAAACAAYAAAAZAAkAAAACAAYAAAAZAAoAAAACAAYAAAAQAA4AAAAEAAUAAAARAA0AAAACAAYAAAASAAwAAAACAAUAAAAVAAoAAAACAAYAAAAWAAkAAAACAAUAAAAXAAgAAAAEAAEAAAAUAAsAAAAFAAEAAAAVAAwAAAAEAAUAAAAWAAwAAAACAAYAAAAXAAwAAAAEAAUAAAAYAA0AAAACAAYAAAAZAA0AAAAEAAUAAAAAAAwAAAACAAYAAAACAAwAAAACAAYAAAADAAwAAAAEAAUAAAAEAAwAAAACAAYAAAAFAAwAAAAEAAUAAAAGAAwAAAAEAAUAAAAGAA0AAAACAAYAAAAHAA0AAAACAAYAAAAIAA0AAAAEAAUAAAAJAA0AAAACAAYAAAAJAAwAAAACAAEAAAAKAAwAAAACAAEAAAD//wwAAAAFAAAAAAD//wsAAAAEAAAAAAAAAAsAAAAEAAUAAAABAAsAAAACAAYAAAABAAwAAAACAAYAAAAYAAcAAAACAAUAAAAZAAcAAAADAAUAAAAYAAgAAAACAAYAAAAZAAgAAAACAAYAAAAVAAkAAAACAAUAAAADAAsAAAACAAYAAAAEAAsAAAACAAEAAAAFAAsAAAACAAEAAAAGAAsAAAACAAEAAAACAAoAAAACAAEAAAADAAoAAAACAAEAAAARAAcAAAAOAAcAAAASAAcAAAAOAAcAAAAQAAcAAAAOAAcAAAAPAAcAAAANAAcAAAAIAAYAAAANAAcAAAATAAcAAAAPAAcAAAAMAAYAAAAPAAcAAAALAAYAAAAOAAcAAAAKAAYAAAAOAAcAAAABAAgAAAARAAYAAAABAAcAAAAQAAUAAAABAAYAAAARAAUAAAABAAUAAAAQAAUAAAAAAAQAAAASAAIAAAABAAQAAAASAAIAAAACAAQAAAASAAIAAAD//wQAAAARAAIAAAAEAAQAAAATAAIAAAADAAQAAAASAAIAAAAAAAMAAAASAAEAAAABAAMAAAASAAEAAAACAAMAAAASAAEAAAADAAMAAAASAAAAAAAEAAMAAAATAAAAAAD//wMAAAARAAEAAAD//wIAAAARAAEAAAD//wEAAAARAAEAAAAAAAEAAAASAAEAAAAAAAIAAAASAAEAAAABAAEAAAASAAEAAAABAAIAAAASAAEAAAACAAEAAAASAAEAAAACAAIAAAASAAEAAAACAAYAAAATAAQAAAAJAAYAAAAOAAcAAAA=")
tile_set = ExtResource("9_nflru")
[node name="TerrainDetailsA" type="TileMapLayer" parent="Level/TerrainTileMaps" unique_id=1513560262]
@@ -220,15 +221,38 @@ start_direction = 1
position = Vector2(90, 198)
start_direction = 1
[node name="Laser_01" parent="Level/PrefabInstances/Enemies" instance=ExtResource("19_laser")]
position = Vector2(189, 216)
[node name="Laser_01" parent="Level/PrefabInstances/Enemies" unique_id=1298565970 instance=ExtResource("19_laser")]
position = Vector2(187, 223)
rotation = -1.5707964
[node name="Laser_02" parent="Level/PrefabInstances/Enemies" instance=ExtResource("19_laser")]
position = Vector2(297, 216)
[node name="Laser_02" parent="Level/PrefabInstances/Enemies" unique_id=1808104165 instance=ExtResource("19_laser")]
position = Vector2(298, 233)
rotation = -1.5707964
start_delay = 2.4
length = 108.0
start_delay = 2.4
[node name="Laser_03" parent="Level/PrefabInstances/Enemies" unique_id=1437324170 instance=ExtResource("19_laser")]
position = Vector2(387, 137)
rotation = -1.5707964
length = 108.0
start_delay = 2.4
[node name="Laser_04" parent="Level/PrefabInstances/Enemies" unique_id=1698684283 instance=ExtResource("19_laser")]
position = Vector2(125, 125)
rotation = -1.5707964
length = 108.0
start_delay = 2.4
[node name="Laser_05" parent="Level/PrefabInstances/Enemies" unique_id=231592857 instance=ExtResource("19_laser")]
position = Vector2(251, 78)
rotation = -1.5707964
length = 108.0
start_delay = 2.4
[node name="BulletRing_01" parent="Level/PrefabInstances/Enemies" instance=ExtResource("20_ring")]
position = Vector2(234, 224)
arc_deg = 180.0
base_angle_deg = 270.0
[node name="Interactables" type="Node2D" parent="Level/PrefabInstances" unique_id=769532488]
+1
View File
@@ -18,4 +18,5 @@ shape = SubResource("Shape_bullet")
[node name="Probe" type="RayCast2D" parent="."]
collision_mask = 1
hit_from_inside = true
target_position = Vector2(-12, 0)
+9
View File
@@ -0,0 +1,9 @@
[gd_scene load_steps=3 format=3]
[ext_resource type="Script" path="res://scripts/bullet_ring.gd" id="1_ring"]
[node name="BulletRing" type="Node2D" groups=["emitter"]]
script = ExtResource("1_ring")
[node name="Core" type="Sprite2D" parent="."]
texture_filter = 1
+1
View File
@@ -0,0 +1 @@
uid://c4v4jls42gsyk
+76
View File
@@ -0,0 +1,76 @@
extends Node2D
## 固定弹幕源:每隔一段时间以自己为中心,按**固定角度**围一圈发射子弹。
## 每一波的角度都算死(可选每波整体旋转),所以弹道可以背下来,
## 玩家靠走位找空隙,或者用「扔钥匙 + W 传送」直接穿过去。
const BULLET := preload("res://scenes/prefabs/enemies/bullet.tscn")
const BULLET_SCRIPT := preload("res://scripts/bullet.gd")
@export var enabled: bool = true
## 发射间隔(秒)
@export var interval: float = 2.0
## 开场后第一次发射前的延迟
@export var start_delay: float = 1.0
## 每波几颗
@export var count: int = 12
## 扇形张开角度:360 = 围一整圈
@export var arc_deg: float = 360.0
## 整圈的中心朝向(度):0=右 90=下 180=左 270=上
@export var base_angle_deg: float = 0.0
## 每发一波整体旋转多少度(0 = 弹道完全固定,可以背板)
@export var rotate_per_shot_deg: float = 0.0
@export var bullet_speed: float = 55.0
@export var bullet_color: Color = Color(1.0, 0.45, 0.85)
## 中心的圆点半径(只是给玩家一个视觉提示)
@export var core_radius: int = 4
var _timer: float = 0.0
var _angle: float = 0.0
func _ready() -> void:
add_to_group("emitter")
_timer = start_delay
_angle = base_angle_deg
var core := get_node_or_null("Core") as Sprite2D
if core != null:
core.texture = BULLET_SCRIPT.circle_texture(core_radius, bullet_color)
func _physics_process(delta: float) -> void:
if not enabled:
return
_timer -= delta
if _timer > 0.0:
return
_timer = maxf(interval, 0.05)
fire()
func fire() -> void:
var n: int = maxi(count, 1)
var base := deg_to_rad(_angle)
var full_circle := is_equal_approx(arc_deg, 360.0)
var step := deg_to_rad(arc_deg) / float(n) if full_circle else deg_to_rad(arc_deg)
for i in n:
var ang: float
if full_circle:
ang = base + step * float(i)
elif n <= 1:
ang = base
else:
ang = base + step * (float(i) / float(n - 1) - 0.5)
_spawn(ang)
if not is_zero_approx(rotate_per_shot_deg):
_angle = fmod(_angle + rotate_per_shot_deg, 360.0)
func _spawn(ang: float) -> void:
var bullet := BULLET.instantiate()
# 这两个属性必须在 add_child 之前设好,子弹的 _ready 会拿它们生成贴图
bullet.bullet_color = bullet_color
bullet.speed = bullet_speed
get_parent().add_child(bullet)
bullet.global_position = global_position
bullet.launch(Vector2(cos(ang), sin(ang)), bullet_speed)
+26 -10
View File
@@ -1,21 +1,24 @@
extends Area2D
## 固定激光:每隔一段时间循环「蓄力闪烁 -> 发射 -> 熄灭」。
## 蓄力阶段只闪不伤人(给玩家反应时间),发射阶段碰到就扣血。
## 激光。两种模式:
## - always_on = true(默认):**固定障碍** —— 一直亮着,碰到就扣血,只能绕开或用钥匙传送穿过去
## - always_on = false:循环「蓄力闪烁 -> 发射 -> 熄灭」,需要卡时间通过
## 光束从自己的原点沿 +X 方向伸出,想改朝向就转节点本身。
enum State { WAIT, WARMUP, FIRING }
## 固定障碍模式(一直亮着)
@export var always_on: bool = true
@export var length: float = 90.0
@export var thickness: float = 6.0
@export var damage: int = 1
## 蓄力(闪烁警告)时长
## 蓄力(闪烁警告)时长,只有循环模式用
@export var warmup: float = 0.8
## 发射持续时长
## 发射持续时长,只有循环模式用
@export var fire_time: float = 0.35
## 两次发射之间的间隔
## 两次发射之间的间隔,只有循环模式用
@export var cooldown: float = 1.8
## 开场后第一次发射前的延迟
## 开场后第一次发射前的延迟,只有循环模式用
@export var start_delay: float = 1.0
@export var laser_color: Color = Color(1.0, 0.25, 0.25)
@export var warn_color: Color = Color(1.0, 0.8, 0.4)
@@ -35,7 +38,7 @@ func _ready() -> void:
Vector2(0.0, -half), Vector2(length, -half),
Vector2(length, half), Vector2(0.0, half)])
_beam.color = laser_color
_beam.visible = false
_beam.visible = always_on
var hit := get_node_or_null("HitBox") as CollisionShape2D
if hit != null:
var rect := RectangleShape2D.new()
@@ -45,6 +48,13 @@ func _ready() -> void:
func _physics_process(delta: float) -> void:
if always_on:
if _beam != null:
_beam.visible = true
_beam.color = laser_color
_hurt_overlaps()
return
_timer += delta
match _state:
State.WAIT:
@@ -64,11 +74,17 @@ func _physics_process(delta: float) -> void:
_beam.color = laser_color
_beam.visible = true
State.FIRING:
for body in get_overlapping_bodies():
if body.has_method("hurt"):
body.hurt(damage)
_hurt_overlaps()
if _timer >= fire_time:
_state = State.WAIT
_timer = 0.0
if _beam != null:
_beam.visible = false
## 每帧检查压在光柱里的东西(不能只靠 body_entered,
## 因为玩家可能本来就在光柱里,那样不会触发 entered)
func _hurt_overlaps() -> void:
for body in get_overlapping_bodies():
if body.has_method("hurt"):
body.hurt(damage)
+1
View File
@@ -0,0 +1 @@
uid://de3i3dmyi8bjw
+4 -54
View File
@@ -1,11 +1,8 @@
extends CharacterBody2D
## 小怪:在平台上左右巡逻,并且按固定节奏发射弹幕。
## **打不死** —— 从上方踩上去也一样会被扣血。
## 弹道方向是写死的(不追着玩家走),所以每一波的弹道完全一样,
## 玩家可以背下空隙走位躲过去,或者用「扔钥匙 + W 传送」直接穿过去。
const BULLET := preload("res://scenes/prefabs/enemies/bullet.tscn")
## 小怪:只在平台上左右巡逻,**不发射任何东西**,也**打不死**。
## 作用纯粹是挡路 + 碰到扣血,逼玩家走位。
## 弹幕交给专门的「弹幕源」节点负责。
@export_category("巡逻")
@export var speed: float = 28.0
@@ -13,31 +10,12 @@ const BULLET := preload("res://scenes/prefabs/enemies/bullet.tscn")
@export var start_direction: int = -1
## 掉头后的冷却时间:防止贴着墙时每帧都掉头、结果原地抖动
@export var turn_cooldown: float = 0.25
## 接触玩家扣几颗心
## 速度设成 0 就变成待在原地不动的障碍
@export var damage: int = 1
## 速度设成 0 就变成固定炮台(弹幕就完全静止可背板了)
@export_category("弹幕")
@export var fires_bullets: bool = true
## 发射间隔(秒)
@export var fire_interval: float = 2.0
## 开场后第一次发射前的延迟
@export var fire_delay: float = 1.0
## 每次发射几颗
@export var bullet_count: int = 3
## 扇面张开角度
@export var spread_deg: float = 60.0
## 扇面中心朝向(度):0=右 90=下 180=左 270=上
@export var base_angle_deg: float = 0.0
## 开火方向跟随巡逻朝向(打开时 base_angle_deg 只当上下偏移用)
@export var aim_along_facing: bool = true
@export var bullet_speed: float = 70.0
@export var bullet_color: Color = Color(1.0, 0.35, 0.7)
var direction: int = -1
var _turn_timer: float = 0.0
var _fire_timer: float = 1.0
@onready var sprite: Sprite2D = $Sprite
@onready var floor_probe: RayCast2D = $FloorProbe
@@ -48,7 +26,6 @@ func _ready() -> void:
add_to_group("enemy")
direction = -1 if start_direction < 0 else 1
_apply_direction()
_fire_timer = fire_delay
touch_area.body_entered.connect(_on_touch)
@@ -69,33 +46,6 @@ func _physics_process(delta: float) -> void:
move_and_slide()
if fires_bullets:
_fire_timer -= delta
if _fire_timer <= 0.0:
_fire_timer = maxf(fire_interval, 0.05)
fire()
## 按固定扇面发射一批子弹。方向是算死的,不参考玩家位置,
## 所以每一波的弹道都一样,玩家能背下来找空隙。
func fire() -> void:
var n: int = maxi(bullet_count, 1)
var base_deg: float = base_angle_deg
if aim_along_facing:
base_deg = 0.0 if direction > 0 else 180.0
var base := deg_to_rad(base_deg)
var spread := deg_to_rad(spread_deg)
for i in n:
var t: float = 0.0 if n == 1 else (float(i) / float(n - 1) - 0.5)
var ang := base + spread * t
var bullet := BULLET.instantiate()
# 这两个属性必须在 add_child 之前设好,子弹的 _ready 会拿它们生成贴图
bullet.bullet_color = bullet_color
bullet.speed = bullet_speed
get_parent().add_child(bullet)
bullet.global_position = global_position + Vector2(0.0, -2.0)
bullet.launch(Vector2(cos(ang), sin(ang)), bullet_speed)
func _apply_direction() -> void:
if sprite != null: