From 3f2cbcc6f22210dc1bc223119596c3c43c43d004 Mon Sep 17 00:00:00 2001 From: Stefan Willoughby Date: Fri, 20 Mar 2026 20:40:12 +1100 Subject: [PATCH] feat: Implement enemy spawning and movement logic in the level manager --- managers/level_manager.gd | 19 ++++++++++++-- scenes/actors/enemy.tscn | 4 +-- scripts/actors/enemy.gd | 53 +++++++++++++++++++++++++++++++++++++++ scripts/world.gd | 5 ++-- 4 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 scripts/actors/enemy.gd diff --git a/managers/level_manager.gd b/managers/level_manager.gd index 095ab02..a8611d7 100644 --- a/managers/level_manager.gd +++ b/managers/level_manager.gd @@ -3,6 +3,8 @@ # Responsible for generating, painting, and initializing the level, as well as providing pathfinding and coordinate helpers. extends Node +const ENEMY_SCENE_PATH: String = "res://scenes/actors/enemy.tscn" + # The source of truth for the current level's layout (MapData resource). var _data: MapData # The TileMapLayer node where the map is visually rendered. @@ -18,8 +20,8 @@ var _pathfinding: PathfindingHandler = PathfindingHandler.new() # 1. Generates the map data. # 2. Paints the map visually. # 3. Builds the navigation graph for AI/pathfinding. -# 4. Spawns the player at the correct location. -func initialize(settings: MapSettings, layer: TileMapLayer, player: CharacterBody2D) -> void: + # 4. Spawns the player at the correct location. +func initialize(settings: MapSettings, layer: TileMapLayer, player: Player) -> void: _layer = layer # 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. 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. # Encapsulates the PathfindingHandler so callers don't need to know about it directly. func find_path(from: Vector2i, to: Vector2i) -> Array[Vector2i]: diff --git a/scenes/actors/enemy.tscn b/scenes/actors/enemy.tscn index 2876f29..3cd4216 100644 --- a/scenes/actors/enemy.tscn +++ b/scenes/actors/enemy.tscn @@ -1,13 +1,13 @@ [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"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_35asm"] size = Vector2(32, 32) [node name="Enemy" type="CharacterBody2D" unique_id=1059490557] -script = ExtResource("1_4i4j6") +script = ExtResource("1_qkj82") metadata/_custom_type_script = "uid://dh5hx2lospkug" [node name="Sprite2D" type="Sprite2D" parent="." unique_id=569576189] diff --git a/scripts/actors/enemy.gd b/scripts/actors/enemy.gd new file mode 100644 index 0000000..a17490a --- /dev/null +++ b/scripts/actors/enemy.gd @@ -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() diff --git a/scripts/world.gd b/scripts/world.gd index 2f16098..53577f4 100644 --- a/scripts/world.gd +++ b/scripts/world.gd @@ -2,12 +2,11 @@ # Main world node script. Sets up the map, player, and level on scene ready. 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. 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. var player: Player # Map generation settings (size, room count, etc.).