feat: Implement enemy spawning and movement logic in the level manager

This commit is contained in:
2026-03-20 20:40:12 +11:00
parent 00af585cf4
commit 3f2cbcc6f2
4 changed files with 74 additions and 7 deletions
+16 -1
View File
@@ -3,6 +3,8 @@
# Responsible for generating, painting, and initializing the level, as well as providing pathfinding and coordinate helpers. # Responsible for generating, painting, and initializing the level, as well as providing pathfinding and coordinate helpers.
extends Node extends Node
const ENEMY_SCENE_PATH: String = "res://scenes/actors/enemy.tscn"
# The source of truth for the current level's layout (MapData resource). # The source of truth for the current level's layout (MapData resource).
var _data: MapData var _data: MapData
# The TileMapLayer node where the map is visually rendered. # The TileMapLayer node where the map is visually rendered.
@@ -19,7 +21,7 @@ var _pathfinding: PathfindingHandler = PathfindingHandler.new()
# 2. Paints the map visually. # 2. Paints the map visually.
# 3. Builds the navigation graph for AI/pathfinding. # 3. Builds the navigation graph for AI/pathfinding.
# 4. Spawns the player at the correct location. # 4. Spawns the player at the correct location.
func initialize(settings: MapSettings, layer: TileMapLayer, player: CharacterBody2D) -> void: func initialize(settings: MapSettings, layer: TileMapLayer, player: Player) -> void:
_layer = layer _layer = layer
# 1. Generate the abstract map data (rooms, corridors, etc.). # 1. Generate the abstract map data (rooms, corridors, etc.).
@@ -38,6 +40,19 @@ func initialize(settings: MapSettings, layer: TileMapLayer, player: CharacterBod
# Uses the TileMapLayer to convert grid coordinates to world pixel positions. # Uses the TileMapLayer to convert grid coordinates to world pixel positions.
player.position = _layer.map_to_local(_data.spawn_point) player.position = _layer.map_to_local(_data.spawn_point)
# 5. Spawn one enemy per room, skipping the player's spawn room (index 0).
var enemy_scene := load(ENEMY_SCENE_PATH) as PackedScene
var world_node := player.get_parent()
for i in range(1, _data.rooms.size()):
var enemy_instance = enemy_scene.instantiate()
var enemy = enemy_instance as Enemy
if enemy == null:
push_error("Failed to cast enemy instance to Enemy at room index %d" % i)
continue
enemy._player = player
world_node.add_child(enemy)
enemy.position = _layer.map_to_local(_data.rooms[i].get_center())
# Public API for other systems (like AI) to query the navigation grid. # Public API for other systems (like AI) to query the navigation grid.
# Encapsulates the PathfindingHandler so callers don't need to know about it directly. # Encapsulates the PathfindingHandler so callers don't need to know about it directly.
func find_path(from: Vector2i, to: Vector2i) -> Array[Vector2i]: func find_path(from: Vector2i, to: Vector2i) -> Array[Vector2i]:
+2 -2
View File
@@ -1,13 +1,13 @@
[gd_scene format=3 uid="uid://hwbs3q16spqx"] [gd_scene format=3 uid="uid://hwbs3q16spqx"]
[ext_resource type="Script" uid="uid://dh5hx2lospkug" path="res://scripts/actors/character.gd" id="1_4i4j6"] [ext_resource type="Script" uid="uid://lm82h1fj2mbp" path="res://scripts/actors/enemy.gd" id="1_qkj82"]
[ext_resource type="Texture2D" uid="uid://b7s06o6gm42rn" path="res://assets/sprites/icon.svg" id="2_qkj82"] [ext_resource type="Texture2D" uid="uid://b7s06o6gm42rn" path="res://assets/sprites/icon.svg" id="2_qkj82"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_35asm"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_35asm"]
size = Vector2(32, 32) size = Vector2(32, 32)
[node name="Enemy" type="CharacterBody2D" unique_id=1059490557] [node name="Enemy" type="CharacterBody2D" unique_id=1059490557]
script = ExtResource("1_4i4j6") script = ExtResource("1_qkj82")
metadata/_custom_type_script = "uid://dh5hx2lospkug" metadata/_custom_type_script = "uid://dh5hx2lospkug"
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=569576189] [node name="Sprite2D" type="Sprite2D" parent="." unique_id=569576189]
+53
View File
@@ -0,0 +1,53 @@
## enemy.gd
# Enemy character script. Each turn, chases the player one tile using pathfinding.
class_name Enemy
extends Character
# Movement speed in pixels per second.
const SPEED: float = 200.0
# Distance threshold for snapping to a tile (prevents jitter).
const ARRIVAL_THRESHOLD: float = 4.0
# Reference to the player — set by LevelManager after spawning.
var _player: Player
# True while animating toward _move_target.
var _is_moving: bool = false
# World position of the tile this enemy is stepping toward.
var _move_target: Vector2 = Vector2.ZERO
# Called by TurnManager. Pathfinds to the player and steps one tile closer.
func take_turn() -> void:
var my_tile := LevelManager.world_to_tile(position)
var player_tile := LevelManager.world_to_tile(_player.position)
var path := LevelManager.find_path(my_tile, player_tile)
# path[0] is own tile; path[-1] is the player's tile (don't step there).
if path.size() < 2:
turn_finished.emit()
return
# Stop one tile away from the player — don't overlap.
if path.size() == 2:
turn_finished.emit()
return
_move_target = LevelManager.tile_to_world(path[1])
_is_moving = true
# Glides toward _move_target each frame. Emits turn_finished on arrival.
func _physics_process(_delta: float) -> void:
if not _is_moving:
velocity = Vector2.ZERO
move_and_slide()
return
var to_target: Vector2 = _move_target - position
if to_target.length() < ARRIVAL_THRESHOLD:
position = _move_target
velocity = Vector2.ZERO
move_and_slide()
_is_moving = false
turn_finished.emit()
else:
velocity = to_target.normalized() * SPEED
move_and_slide()
+2 -3
View File
@@ -2,12 +2,11 @@
# Main world node script. Sets up the map, player, and level on scene ready. # Main world node script. Sets up the map, player, and level on scene ready.
extends Node extends Node
# Reference to the TileMapLayer node for rendering the map.
# Reference to the TileMapLayer node for rendering the map.
var map_layer: TileMapLayer
# Path to the Player scene to instance. # Path to the Player scene to instance.
const PLAYER_SCENE_PATH: String = "res://scenes/actors/player.tscn" const PLAYER_SCENE_PATH: String = "res://scenes/actors/player.tscn"
# Reference to the TileMapLayer node for rendering the map.
var map_layer: TileMapLayer
# Reference to the Player node. # Reference to the Player node.
var player: Player var player: Player
# Map generation settings (size, room count, etc.). # Map generation settings (size, room count, etc.).