diff --git a/scenes/level2.tscn b/scenes/level2.tscn index 56e409d..93d5107 100644 --- a/scenes/level2.tscn +++ b/scenes/level2.tscn @@ -18,6 +18,7 @@ [ext_resource type="PackedScene" path="res://scenes/prefabs/collectibles/key_gold.tscn" id="16_3xffc"] [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"] [node name="Main" type="Node2D" unique_id=2061129075] @@ -167,7 +168,7 @@ tile_set = ExtResource("9_nflru") [node name="TerrainDetailsA" type="TileMapLayer" parent="Level/TerrainTileMaps" unique_id=1513560262] z_index = 1 position = Vector2(9, 9) -tile_map_data = PackedByteArray("AAADAA0AAAADAAYAAAABAAwAAAABAAYAAAABAA0AAAABAAYAAAACAAoAAAAIAAYAAAACAAsAAAACAAEAAAADAAsAAAADAAEAAAACAAwAAAACAAYAAAACAA4AAAACAAYAAAAXAAsAAAADAAUAAAAXAAwAAAADAAYAAAAXAA0AAAADAAYAAAAXAA4AAAADAAYAAAAWAAoAAAAEAAcAAAAWAAsAAAACAAUAAAAWAAwAAAAEAAUAAAAWAA0AAAACAAYAAAAWAA4AAAAEAAUAAAAVAAsAAAABAAUAAAAVAAwAAAABAAYAAAAVAA0AAAABAAYAAAAVAA4AAAABAAYAAAAQAA4AAAADAAYAAAAOAA4AAAAEAAUAAAAPAA4AAAACAAYAAAAQAA0AAAADAAMAAAANAA4AAAABAAYAAAAOAAwAAAAEAAYAAAAPAA0AAAACAAMAAAAOAA0AAAACAAMAAAANAA0AAAABAAMAAAAQAAQAAAATAAIAAAAQAAMAAAATAAEAAAAPAAQAAAASAAIAAAAKAAMAAAARAAAAAAAKAAQAAAARAAIAAAALAAMAAAASAAAAAAAMAAMAAAATAAAAAAAOAAIAAAARAAAAAAAPAAIAAAASAAAAAAAOAAMAAAARAAEAAAAMAAQAAAATAAIAAAALAAQAAAASAAIAAAADAAwAAAADAAYAAAAOAAQAAAARAAIAAAAPAAMAAAASAAEAAAAQAAIAAAATAAAAAAACAA0AAAAEAAUAAAABAA4AAAABAAYAAAADAA4AAAADAAYAAAABAAsAAAABAAEAAAAOAAsAAAANAAUAAAAOAAoAAAAPAAUAAAAPAAsAAAANAAYAAAAPAAkAAAANAAYAAAAOAAkAAAANAAQAAAAQAAkAAAAOAAQAAAAQAAoAAAAPAAUAAAAQAAsAAAAOAAUAAAAPAAoAAAASAAcAAAAPAAgAAAAIAAMAAAAUAPz/AAASAAcAAAA=") +tile_map_data = PackedByteArray("AAADAA0AAAADAAYAAAABAAwAAAABAAYAAAABAA0AAAABAAYAAAACAAsAAAACAAEAAAADAAsAAAADAAEAAAACAAwAAAACAAYAAAACAA4AAAACAAYAAAAXAAsAAAADAAUAAAAXAAwAAAADAAYAAAAXAA0AAAADAAYAAAAXAA4AAAADAAYAAAAWAAoAAAAEAAcAAAAWAAsAAAACAAUAAAAWAAwAAAAEAAUAAAAWAA0AAAACAAYAAAAWAA4AAAAEAAUAAAAVAAsAAAABAAUAAAAVAAwAAAABAAYAAAAVAA0AAAABAAYAAAAVAA4AAAABAAYAAAAQAA4AAAADAAYAAAAOAA4AAAAEAAUAAAAPAA4AAAACAAYAAAAQAA0AAAADAAMAAAANAA4AAAABAAYAAAAOAAwAAAAEAAYAAAAPAA0AAAACAAMAAAAOAA0AAAACAAMAAAANAA0AAAABAAMAAAAQAAQAAAATAAIAAAAQAAMAAAATAAEAAAAPAAQAAAASAAIAAAAKAAMAAAARAAAAAAAKAAQAAAARAAIAAAALAAMAAAASAAAAAAAMAAMAAAATAAAAAAAOAAIAAAARAAAAAAAPAAIAAAASAAAAAAAOAAMAAAARAAEAAAAMAAQAAAATAAIAAAALAAQAAAASAAIAAAADAAwAAAADAAYAAAAOAAQAAAARAAIAAAAPAAMAAAASAAEAAAAQAAIAAAATAAAAAAACAA0AAAAEAAUAAAABAA4AAAABAAYAAAADAA4AAAADAAYAAAABAAsAAAABAAEAAAAUAPz/AAASAAcAAAA=") tile_set = ExtResource("9_nflru") [node name="TerrainDetailsB" type="TileMapLayer" parent="Level/TerrainTileMaps" unique_id=478648603] @@ -206,19 +207,29 @@ position = Vector2(109, 199) z_index = 5 position = Vector2(331, 216) -[node name="Enemies" type="Node2D" parent="Level/PrefabInstances"] +[node name="Enemies" type="Node2D" parent="Level/PrefabInstances" unique_id=93211253] -[node name="Walker_01" parent="Level/PrefabInstances/Enemies" instance=ExtResource("18_walker")] +[node name="Walker_01" parent="Level/PrefabInstances/Enemies" unique_id=2100888628 instance=ExtResource("18_walker")] position = Vector2(216, 216) -[node name="Walker_02" parent="Level/PrefabInstances/Enemies" instance=ExtResource("18_walker")] +[node name="Walker_02" parent="Level/PrefabInstances/Enemies" unique_id=1806208529 instance=ExtResource("18_walker")] position = Vector2(306, 216) start_direction = 1 -[node name="Walker_03" parent="Level/PrefabInstances/Enemies" instance=ExtResource("18_walker")] +[node name="Walker_03" parent="Level/PrefabInstances/Enemies" unique_id=1711052130 instance=ExtResource("18_walker")] position = Vector2(90, 198) start_direction = 1 +[node name="Laser_01" parent="Level/PrefabInstances/Enemies" instance=ExtResource("19_laser")] +position = Vector2(189, 216) +rotation = -1.5707964 + +[node name="Laser_02" parent="Level/PrefabInstances/Enemies" instance=ExtResource("19_laser")] +position = Vector2(297, 216) +rotation = -1.5707964 +start_delay = 2.4 +length = 108.0 + [node name="Interactables" type="Node2D" parent="Level/PrefabInstances" unique_id=769532488] [node name="SpringRed_01" parent="Level/PrefabInstances/Interactables" unique_id=34192139 instance=ExtResource("13_ac7c3")] @@ -246,7 +257,7 @@ position = Vector2(219, 54) [node name="KeyGold_01" parent="Level/PrefabInstances/Collectibles" unique_id=2060959680 instance=ExtResource("16_3xffc")] z_index = 5 -position = Vector2(126, 144) +position = Vector2(71, 182) [node name="Diamond_01" parent="Level/PrefabInstances/Collectibles" unique_id=909237898 instance=ExtResource("17_icuov")] z_index = 5 @@ -258,6 +269,6 @@ position = Vector2(204, 217) [node name="Diamond_03" parent="Level/PrefabInstances/Collectibles" unique_id=1261326529 instance=ExtResource("17_icuov")] z_index = 5 -position = Vector2(270, 144) +position = Vector2(288, 125) [node name="Signs" type="Node2D" parent="Level/PrefabInstances" unique_id=113197076] diff --git a/scenes/prefabs/enemies/bullet.tscn b/scenes/prefabs/enemies/bullet.tscn new file mode 100644 index 0000000..b4b55d2 --- /dev/null +++ b/scenes/prefabs/enemies/bullet.tscn @@ -0,0 +1,21 @@ +[gd_scene load_steps=4 format=3] + +[ext_resource type="Script" path="res://scripts/bullet.gd" id="1_bullet"] + +[sub_resource type="CircleShape2D" id="Shape_bullet"] +radius = 3.0 + +[node name="Bullet" type="Area2D" groups=["bullet"]] +collision_layer = 4 +collision_mask = 2 +script = ExtResource("1_bullet") + +[node name="Sprite" type="Sprite2D" parent="."] +texture_filter = 1 + +[node name="HitBox" type="CollisionShape2D" parent="."] +shape = SubResource("Shape_bullet") + +[node name="Probe" type="RayCast2D" parent="."] +collision_mask = 1 +target_position = Vector2(-12, 0) diff --git a/scenes/prefabs/enemies/laser.tscn b/scenes/prefabs/enemies/laser.tscn new file mode 100644 index 0000000..b93acd2 --- /dev/null +++ b/scenes/prefabs/enemies/laser.tscn @@ -0,0 +1,19 @@ +[gd_scene load_steps=3 format=3] + +[ext_resource type="Script" path="res://scripts/laser.gd" id="1_laser"] + +[sub_resource type="RectangleShape2D" id="Shape_laser"] +size = Vector2(90, 6) + +[node name="Laser" type="Area2D" groups=["hazard"]] +collision_layer = 4 +collision_mask = 2 +script = ExtResource("1_laser") + +[node name="Beam" type="Polygon2D" parent="."] +color = Color(1, 0.25, 0.25, 1) +polygon = PackedVector2Array(0, -3, 90, -3, 90, 3, 0, 3) + +[node name="HitBox" type="CollisionShape2D" parent="."] +position = Vector2(45, 0) +shape = SubResource("Shape_laser") diff --git a/scripts/bullet.gd b/scripts/bullet.gd new file mode 100644 index 0000000..8a604b5 --- /dev/null +++ b/scripts/bullet.gd @@ -0,0 +1,79 @@ +extends Area2D + +## 弹幕子弹:直线飞行,碰到玩家扣血,撞到地形或超时消失。 +## 贴图是代码生成的实心圆,不需要额外美术素材。 + +@export var speed: float = 70.0 +@export var damage: int = 1 +@export var lifetime: float = 8.0 +@export var radius: int = 3 +@export var bullet_color: Color = Color(1.0, 0.35, 0.7) + +var direction: Vector2 = Vector2.LEFT + +var _age: float = 0.0 +var _probe: RayCast2D = null + +static var _cache: Dictionary = {} + + +## 生成(并缓存)一个实心圆贴图 +static func circle_texture(r: int, color: Color) -> ImageTexture: + var key := "%d|%s" % [r, color.to_html()] + if _cache.has(key): + return _cache[key] + var size: int = r * 2 + 1 + var img := Image.create_empty(size, size, false, Image.FORMAT_RGBA8) + img.fill(Color(0.0, 0.0, 0.0, 0.0)) + var limit := float(r * r) + float(r) * 0.6 + for y in size: + for x in size: + var dx := float(x - r) + var dy := float(y - r) + if dx * dx + dy * dy <= limit: + img.set_pixel(x, y, color) + var tex := ImageTexture.create_from_image(img) + _cache[key] = tex + return tex + + +func _ready() -> void: + add_to_group("bullet") + var sprite := get_node_or_null("Sprite") as Sprite2D + if sprite != null: + sprite.texture = circle_texture(radius, bullet_color) + var hit := get_node_or_null("HitBox") as CollisionShape2D + if hit != null and hit.shape is CircleShape2D: + var circle := (hit.shape as CircleShape2D).duplicate() as CircleShape2D + circle.radius = float(radius) + hit.shape = circle + _probe = get_node_or_null("Probe") as RayCast2D + body_entered.connect(_on_body_entered) + + +func launch(dir: Vector2, spd: float = -1.0) -> void: + direction = dir.normalized() + if spd > 0.0: + speed = spd + + +func _physics_process(delta: float) -> void: + _age += delta + if _age >= lifetime: + queue_free() + return + + var step := direction * speed * delta + if _probe != null: + _probe.target_position = step * 1.5 + _probe.force_raycast_update() + if _probe.is_colliding(): + queue_free() + return + position += step + + +func _on_body_entered(body: Node2D) -> void: + if body.has_method("hurt"): + body.hurt(damage) + queue_free() diff --git a/scripts/laser.gd b/scripts/laser.gd new file mode 100644 index 0000000..6067504 --- /dev/null +++ b/scripts/laser.gd @@ -0,0 +1,74 @@ +extends Area2D + +## 固定激光:每隔一段时间循环「蓄力闪烁 -> 发射 -> 熄灭」。 +## 蓄力阶段只闪不伤人(给玩家反应时间),发射阶段碰到就扣血。 +## 光束从自己的原点沿 +X 方向伸出,想改朝向就转节点本身。 + +enum State { WAIT, WARMUP, FIRING } + +@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) + +var _state: int = State.WAIT +var _timer: float = 0.0 +var _first: bool = true +var _beam: Polygon2D = null + + +func _ready() -> void: + add_to_group("hazard") + _beam = get_node_or_null("Beam") as Polygon2D + var half := thickness * 0.5 + if _beam != null: + _beam.polygon = PackedVector2Array([ + Vector2(0.0, -half), Vector2(length, -half), + Vector2(length, half), Vector2(0.0, half)]) + _beam.color = laser_color + _beam.visible = false + var hit := get_node_or_null("HitBox") as CollisionShape2D + if hit != null: + var rect := RectangleShape2D.new() + rect.size = Vector2(length, thickness) + hit.shape = rect + hit.position = Vector2(length * 0.5, 0.0) + + +func _physics_process(delta: float) -> void: + _timer += delta + match _state: + State.WAIT: + var wait: float = start_delay if _first else cooldown + if _timer >= wait: + _first = false + _state = State.WARMUP + _timer = 0.0 + State.WARMUP: + if _beam != null: + _beam.color = warn_color + _beam.visible = fmod(_timer, 0.16) < 0.08 + if _timer >= warmup: + _state = State.FIRING + _timer = 0.0 + if _beam != null: + _beam.color = laser_color + _beam.visible = true + State.FIRING: + for body in get_overlapping_bodies(): + if body.has_method("hurt"): + body.hurt(damage) + if _timer >= fire_time: + _state = State.WAIT + _timer = 0.0 + if _beam != null: + _beam.visible = false diff --git a/scripts/walker.gd b/scripts/walker.gd index 3eb44fa..65d3503 100644 --- a/scripts/walker.gd +++ b/scripts/walker.gd @@ -1,22 +1,43 @@ extends CharacterBody2D -## 小怪:在平台上左右巡逻,碰到玩家扣一颗心; -## 被玩家从上方踩到就死,并且把玩家弹起来。 -## 走到平台边缘会自动掉头,所以不会掉下去。 +## 小怪:在平台上左右巡逻,并且按固定节奏发射弹幕。 +## **打不死** —— 从上方踩上去也一样会被扣血。 +## 弹道方向是写死的(不追着玩家走),所以每一波的弹道完全一样, +## 玩家可以背下空隙走位躲过去,或者用「扔钥匙 + W 传送」直接穿过去。 +const BULLET := preload("res://scenes/prefabs/enemies/bullet.tscn") + +@export_category("巡逻") @export var speed: float = 28.0 -@export var damage: int = 1 -## 被踩死时把玩家往上弹的速度 -@export var stomp_bounce: float = -150.0 ## 巡逻方向:-1 向左,1 向右 @export var start_direction: int = -1 ## 掉头后的冷却时间:防止贴着墙时每帧都掉头、结果原地抖动 @export var turn_cooldown: float = 0.25 +## 接触玩家扣几颗心 +@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 dead: bool = false var _turn_timer: float = 0.0 +var _fire_timer: float = 1.0 @onready var sprite: Sprite2D = $Sprite @onready var floor_probe: RayCast2D = $FloorProbe @@ -27,13 +48,11 @@ 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) func _physics_process(delta: float) -> void: - if dead: - return - if _turn_timer > 0.0: _turn_timer -= delta @@ -50,6 +69,33 @@ 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: @@ -59,29 +105,6 @@ func _apply_direction() -> void: func _on_touch(body: Node2D) -> void: - if dead or not body.has_method("hurt"): - return - - # 玩家从上往下踩到 -> 小怪死,玩家弹一下 - var p := body as CharacterBody2D - if p != null and p.velocity.y > 0.0 and p.global_position.y < global_position.y - 3.0: - p.velocity.y = stomp_bounce - _die() - return - - body.hurt(damage) - - -func _die() -> void: - dead = true - velocity = Vector2.ZERO - touch_area.set_deferred("monitoring", false) - set_deferred("collision_layer", 0) - set_deferred("collision_mask", 0) - - # 被踩扁 + 淡出 - var tw := create_tween() - tw.set_parallel(true) - tw.tween_property(sprite, "scale", Vector2(1.35, 0.2), 0.12) - tw.tween_property(sprite, "modulate:a", 0.0, 0.3) - tw.chain().tween_callback(queue_free) + # 打不死:踩上去也一样只是扣血 + if body.has_method("hurt"): + body.hurt(damage) diff --git a/scripts/walker.gd.uid b/scripts/walker.gd.uid new file mode 100644 index 0000000..701a473 --- /dev/null +++ b/scripts/walker.gd.uid @@ -0,0 +1 @@ +uid://b0lnhlnkjhxug