feat: Implement patrol path markers and enhance enemy movement logic

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-19 18:48:11 +01:00
parent 4700e669ce
commit c62ea013ba
10 changed files with 393 additions and 85 deletions

View File

@@ -122,6 +122,9 @@ class WolfEngine {
/// The static level data source used for reloading or reference.
late WolfLevel activeLevel;
/// The static object layer (spawns, markers, triggers) for the active level.
late SpriteMap _objectLevel;
/// All dynamic entities currently in the level (Enemies, Pickups).
List<Entity> entities = [];
@@ -134,6 +137,9 @@ class WolfEngine {
/// Total number of known area indices in the active level.
int _areaCount = 0;
/// Last tile processed for patrol marker routing, keyed by enemy debug id.
final Map<int, ({int x, int y})> _lastPatrolTileByEnemy = {};
int _currentEpisodeIndex = 0;
bool _isPlayerMovingFast = false;
@@ -336,6 +342,7 @@ class WolfEngine {
/// Wipes the current world state and builds a new floor from map data.
void _loadLevel() {
entities.clear();
_lastPatrolTileByEnemy.clear();
final episode = data.episodes[_currentEpisodeIndex];
activeLevel = episode.levels[_currentLevelIndex];
@@ -343,16 +350,16 @@ class WolfEngine {
// Create a mutable copy of the wall grid so pushwalls can modify it
currentLevel = List.generate(64, (y) => List.from(activeLevel.wallGrid[y]));
_areaGrid = List.generate(64, (y) => List.from(activeLevel.areaGrid[y]));
final SpriteMap objectLevel = activeLevel.objectGrid;
_objectLevel = activeLevel.objectGrid;
doorManager.initDoors(currentLevel);
pushwallManager.initPushwalls(currentLevel, objectLevel);
pushwallManager.initPushwalls(currentLevel, _objectLevel);
audio.playLevelMusic(activeLevel);
// Spawn Player and Entities from the Object Grid
for (int y = 0; y < 64; y++) {
for (int x = 0; x < 64; x++) {
int objId = objectLevel[y][x];
int objId = _objectLevel[y][x];
// Map IDs 19-22 are Reserved for Player Starts
if (objId >= MapObject.playerNorth && objId <= MapObject.playerWest) {
@@ -552,6 +559,10 @@ class WolfEngine {
}
// Standard AI Update cycle
if (!entity.isAlerted && entity.state == EntityState.patrolling) {
_applyPatrolPathMarker(entity);
}
final intent = entity.update(
elapsedMs: _timeAliveMs,
elapsedDeltaMs: elapsed.inMilliseconds,
@@ -805,6 +816,40 @@ class WolfEngine {
}
}
void _applyPatrolPathMarker(Enemy enemy) {
final tileX = enemy.x.toInt();
final tileY = enemy.y.toInt();
if (tileX < 0 || tileX >= 64 || tileY < 0 || tileY >= 64) return;
final previousTile = _lastPatrolTileByEnemy[enemy.debugId];
if (previousTile != null &&
previousTile.x == tileX &&
previousTile.y == tileY) {
return;
}
_lastPatrolTileByEnemy[enemy.debugId] = (x: tileX, y: tileY);
final markerId = _objectLevel[tileY][tileX];
final markerAngle = MapObject.patrolAngleForMarker(markerId);
if (enemy is Dog) {
log(
'[DEBUG] Dog #${enemy.debugId} patrol tile entry '
'($tileX, $tileY) marker=$markerId',
);
}
if (markerAngle == null) return;
final normalizedDiff =
((enemy.angle - markerAngle + math.pi) % (2 * math.pi)) - math.pi;
if (normalizedDiff.abs() > 0.001) {
enemy.angle = markerAngle;
log(
'[DEBUG] Enemy #${enemy.debugId} (${enemy.type.name}) '
'patrol marker $markerId applied at ($tileX, $tileY)',
);
}
}
/// Returns true if a tile is empty or contains a door that is sufficiently open.
bool isWalkable(int x, int y) {
// 1. Boundary Guard: Prevent range errors by checking if coordinates are on the map