From 673f82108d232701580ef9112354f7238174f73b Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Tue, 17 Mar 2026 18:46:44 +0100 Subject: [PATCH] Fix enemy difficulty spawn logic Signed-off-by: Hans Kokx --- .../lib/src/data_types/enemy_map_data.dart | 65 ++++++++++++------- .../lib/src/engine/wolf_3d_engine_base.dart | 2 +- .../entities/enemies/bosses/hans_grosse.dart | 2 +- .../src/entities/entities/enemies/dog.dart | 12 ++-- .../src/entities/entities/enemies/enemy.dart | 36 ++++++---- .../src/entities/entities/enemies/guard.dart | 2 +- .../src/entities/entities/enemies/mutant.dart | 2 +- .../entities/entities/enemies/officer.dart | 2 +- .../lib/src/entities/entities/enemies/ss.dart | 2 +- .../wolf_3d_dart/lib/src/entities/entity.dart | 2 +- 10 files changed, 78 insertions(+), 49 deletions(-) diff --git a/packages/wolf_3d_dart/lib/src/data_types/enemy_map_data.dart b/packages/wolf_3d_dart/lib/src/data_types/enemy_map_data.dart index 59b9018..bd07d69 100644 --- a/packages/wolf_3d_dart/lib/src/data_types/enemy_map_data.dart +++ b/packages/wolf_3d_dart/lib/src/data_types/enemy_map_data.dart @@ -2,36 +2,57 @@ import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; /// Handles identity and difficulty-based logic for enemies in map data. /// -/// In Wolf3D, each enemy type (Guard, SS, etc.) occupies a block of 48 IDs -/// in the map object layer to account for behavior, direction, and difficulty. +/// Enemies in Wolf3D use a base 8-ID block (4 static, 4 patrol) and jump by +/// a tier offset (36 for most, 18 for Mutants) for higher difficulties. class EnemyMapData { - /// The starting ID for this specific enemy type (e.g., 108 for Guards). + /// The starting ID for the "Easy" tier of this enemy (e.g., 108 for Guards). final int baseId; - const EnemyMapData(this.baseId); + /// The ID jump between difficulty tiers (36 by default, 18 for Mutants). + final int tierOffset; - /// Returns true if the provided [id] belongs to this enemy type's 48-ID block. - bool claimsId(int id) => id >= baseId && id < baseId + 48; + const EnemyMapData(this.baseId, {this.tierOffset = 36}); - /// Returns true if the ID represents a patrolling version for the [difficulty]. - bool isPatrolForDifficulty(int id, Difficulty difficulty) { - // Patrolling enemies occupy the first 16 IDs (4 per difficulty level) - int start = baseId + (difficulty.level * 4); - return id >= start && id < start + 4; + /// Evaluates an [id] to see if it belongs to this enemy type and is valid + /// for the current [difficulty]. + /// + /// Returns the normalized "Easy" base ID (e.g., 108-115 for a Guard) if + /// the enemy should spawn, or `null` if it should be skipped. + int? getNormalizedId(int id, Difficulty difficulty) { + // 1. Check Easy Tier (Base IDs) + // The original C logic always spawns these regardless of difficulty. + if (id >= baseId && id < baseId + 8) { + return id; + } + + // 2. Check Medium Tier (+1 tierOffset) + int mediumBase = baseId + tierOffset; + if (id >= mediumBase && id < mediumBase + 8) { + // In Dart enum, Medium (Bring 'em on!) is level 1. + if (difficulty.level < 1) return null; + return id - tierOffset; // Shift down to base Easy IDs + } + + // 3. Check Hard Tier (+2 tierOffsets) + int hardBase = baseId + (tierOffset * 2); + if (id >= hardBase && id < hardBase + 8) { + // In Dart enum, Hard (Death incarnate!) is level 2. + if (difficulty.level < 2) return null; + return id - (tierOffset * 2); // Shift down to base Easy IDs + } + + return null; } - /// Returns true if the ID represents a standing (static) version for the [difficulty]. - bool isStaticForDifficulty(int id, Difficulty difficulty) { - // Standing enemies occupy the next 16 IDs - int start = baseId + 16 + (difficulty.level * 4); - return id >= start && id < start + 4; + /// Returns true if the normalized ID represents a standing (static) version. + bool isStatic(int normalizedId) { + // The first 4 IDs are always Stand (matching the C switch statements) + return normalizedId >= baseId && normalizedId < baseId + 4; } - /// Returns true if the ID represents an "Ambush" version for the [difficulty]. - /// Ambush enemies wait for the player to enter their line of sight before moving. - bool isAmbushForDifficulty(int id, Difficulty difficulty) { - // Ambush enemies occupy the final 16 IDs of the block - int start = baseId + 32 + (difficulty.level * 4); - return id >= start && id < start + 4; + /// Returns true if the normalized ID represents a patrolling version. + bool isPatrol(int normalizedId) { + // The next 4 IDs are always Patrol + return normalizedId >= baseId + 4 && normalizedId < baseId + 8; } } diff --git a/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart b/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart index 3423af8..6da0ccb 100644 --- a/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart +++ b/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart @@ -416,7 +416,7 @@ class WolfEngine { // Wake them up! if (entity.state == EntityState.idle || - entity.state == EntityState.chasing) { + entity.state == EntityState.ambushing) { entity.state = EntityState.patrolling; entity.lastActionTime = _timeAliveMs; } diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/bosses/hans_grosse.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/bosses/hans_grosse.dart index eb2f85f..c686338 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/bosses/hans_grosse.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/bosses/hans_grosse.dart @@ -88,7 +88,7 @@ class HansGrosse extends Enemy { switch (state) { case EntityState.idle: - case EntityState.chasing: + case EntityState.ambushing: spriteIndex = _baseSprite; break; diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/dog.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/dog.dart index 1dbcbb8..7e876dc 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/dog.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/dog.dart @@ -72,7 +72,7 @@ class Dog extends Enemy { } if (currentFrame >= 5) { - state = EntityState.chasing; + state = EntityState.ambushing; currentFrame = 0; } setTics(10); @@ -84,8 +84,8 @@ class Dog extends Enemy { double ticsThisFrame = elapsedDeltaMs / 14.28; double currentMoveSpeed = speedPerTic * ticsThisFrame; - if (isAlerted || state == EntityState.chasing) { - state = EntityState.chasing; + if (isAlerted || state == EntityState.ambushing) { + state = EntityState.ambushing; double dx = (position.x - playerPosition.x).abs() - currentMoveSpeed; double dy = (position.y - playerPosition.y).abs() - currentMoveSpeed; @@ -118,7 +118,7 @@ class Dog extends Enemy { ), playerPosition: playerPosition, isWalkable: isWalkable, - // FIX: Alerted dogs cannot open doors! Pass an empty closure to treat doors as walls. + // Alerted dogs cannot open doors! Pass an empty closure to treat doors as walls. tryOpenDoor: (x, y) {}, ); @@ -162,7 +162,7 @@ class Dog extends Enemy { } EnemyAnimation currentAnim = switch (state) { - EntityState.patrolling || EntityState.chasing => EnemyAnimation.walking, + EntityState.patrolling || EntityState.ambushing => EnemyAnimation.walking, EntityState.attacking => EnemyAnimation.attacking, EntityState.pain => EnemyAnimation.pain, EntityState.dead => isDying ? EnemyAnimation.dying : EnemyAnimation.dead, @@ -176,7 +176,7 @@ class Dog extends Enemy { angleDiff: diff, walkFrameOverride: (state == EntityState.patrolling || - state == EntityState.chasing || + state == EntityState.ambushing || isAlerted) ? currentFrame : null, diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy.dart index be8cf24..6d8f851 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy.dart @@ -165,7 +165,7 @@ abstract class Enemy extends Entity { // Reaction delay has passed isAlerted = true; - if (state == EntityState.idle || state == EntityState.chasing) { + if (state == EntityState.idle || state == EntityState.ambushing) { state = EntityState.patrolling; setTics(10); } @@ -339,31 +339,39 @@ abstract class Enemy extends Entity { return null; } - final type = EnemyType.fromMapId(objId); - if (type == null) return null; + EnemyType? matchedType; + int? normalizedId; - // Check version compatibility - if (isSharewareMode && !type.existsInShareware) return null; + // Find which enemy type claims this ID for the current difficulty + for (final type in EnemyType.values) { + if (isSharewareMode && !type.existsInShareware) continue; - final mapData = type.mapData; + normalizedId = type.mapData.getNormalizedId(objId, difficulty); + if (normalizedId != null) { + matchedType = type; + break; + } + } + + // If no type claimed it, or the difficulty was too low, abort spawn + if (matchedType == null || normalizedId == null) return null; + + // Resolve spawn orientation using the NORMALIZED ID (0-7 offset from base) + // This prevents offset math bugs (like the Mutant's 18-ID shift) from breaking facing directions. + double spawnAngle = CardinalDirection.fromEnemyIndex(normalizedId).radians; - // Resolve spawn orientation and initial AI state - double spawnAngle = CardinalDirection.fromEnemyIndex(objId).radians; EntityState spawnState; - if (mapData.isPatrolForDifficulty(objId, difficulty)) { + if (matchedType.mapData.isPatrol(normalizedId)) { spawnState = EntityState.patrolling; - } else if (mapData.isStaticForDifficulty(objId, difficulty)) { + } else if (matchedType.mapData.isStatic(normalizedId)) { spawnState = EntityState.idle; - } else if (mapData.isAmbushForDifficulty(objId, difficulty)) { - spawnState = EntityState.chasing; } else { - // The ID belongs to this enemy type, but not for this specific difficulty level return null; } // Return the specific instance - return switch (type) { + return switch (matchedType) { EnemyType.guard => Guard(x: x, y: y, angle: spawnAngle, mapId: objId), EnemyType.dog => Dog(x: x, y: y, angle: spawnAngle, mapId: objId), EnemyType.ss => SS(x: x, y: y, angle: spawnAngle, mapId: objId), diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/guard.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/guard.dart index 32551df..3cace7f 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/guard.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/guard.dart @@ -132,7 +132,7 @@ class Guard extends Enemy { } EnemyAnimation currentAnim = switch (state) { - EntityState.patrolling || EntityState.chasing => EnemyAnimation.walking, + EntityState.patrolling || EntityState.ambushing => EnemyAnimation.walking, EntityState.attacking => EnemyAnimation.attacking, EntityState.pain => EnemyAnimation.pain, EntityState.dead => isDying ? EnemyAnimation.dying : EnemyAnimation.dead, diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/mutant.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/mutant.dart index 616b957..c01edd0 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/mutant.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/mutant.dart @@ -57,7 +57,7 @@ class Mutant extends Enemy { } EnemyAnimation currentAnim = switch (state) { - EntityState.patrolling || EntityState.chasing => EnemyAnimation.walking, + EntityState.patrolling || EntityState.ambushing => EnemyAnimation.walking, EntityState.attacking => EnemyAnimation.attacking, EntityState.pain => EnemyAnimation.pain, EntityState.dead => isDying ? EnemyAnimation.dying : EnemyAnimation.dead, diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/officer.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/officer.dart index 42f65b9..7b148b6 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/officer.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/officer.dart @@ -57,7 +57,7 @@ class Officer extends Enemy { } EnemyAnimation currentAnim = switch (state) { - EntityState.patrolling || EntityState.chasing => EnemyAnimation.walking, + EntityState.patrolling || EntityState.ambushing => EnemyAnimation.walking, EntityState.attacking => EnemyAnimation.attacking, EntityState.pain => EnemyAnimation.pain, EntityState.dead => isDying ? EnemyAnimation.dying : EnemyAnimation.dead, diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/ss.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/ss.dart index e36a836..0f95b0e 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/ss.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/ss.dart @@ -56,7 +56,7 @@ class SS extends Enemy { } EnemyAnimation currentAnim = switch (state) { - EntityState.patrolling || EntityState.chasing => EnemyAnimation.walking, + EntityState.patrolling || EntityState.ambushing => EnemyAnimation.walking, EntityState.attacking => EnemyAnimation.attacking, EntityState.pain => EnemyAnimation.pain, EntityState.dead => isDying ? EnemyAnimation.dying : EnemyAnimation.dead, diff --git a/packages/wolf_3d_dart/lib/src/entities/entity.dart b/packages/wolf_3d_dart/lib/src/entities/entity.dart index 4952407..ebb3f8a 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entity.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entity.dart @@ -6,7 +6,7 @@ enum EntityState { staticObj, /// Enemies waiting for the player to enter their field of view. - chasing, + ambushing, /// Enemies standing still but capable of hearing noise. idle,