From 2892984e4ed77c4a56540384e67f549eb1fb00e9 Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Sun, 15 Mar 2026 17:10:54 +0100 Subject: [PATCH] Fix wrong enemies spawning all over Signed-off-by: Hans Kokx --- .../lib/src/map_objects.dart | 5 +- .../lib/src/entities/enemies/enemy.dart | 90 ++++++++++---- .../lib/src/entities/enemies/guard.dart | 37 +++++- .../lib/src/entities/enemies/mutant.dart | 33 ++++- .../lib/src/entities/enemies/officer.dart | 111 +++++++---------- .../lib/src/entities/enemies/ss.dart | 115 +++++++----------- .../lib/src/entity_registry.dart | 14 ++- 7 files changed, 233 insertions(+), 172 deletions(-) diff --git a/packages/wolf_3d_data_types/lib/src/map_objects.dart b/packages/wolf_3d_data_types/lib/src/map_objects.dart index beb3f4e..0ea6c18 100644 --- a/packages/wolf_3d_data_types/lib/src/map_objects.dart +++ b/packages/wolf_3d_data_types/lib/src/map_objects.dart @@ -136,7 +136,7 @@ abstract class MapObject { // Normalize patrolling enemies back to the standing block, THEN get the // 4-way angle - int directionIndex = ((id - type.mapBaseId) % 18) % 4; + int directionIndex = ((id - type.patrolId) % 18) % 4; return CardinalDirection.fromEnemyIndex(directionIndex).radians; } @@ -146,7 +146,8 @@ abstract class MapObject { // If it's not a standard enemy (it's a decoration, boss, or player), spawn it if (type == null) return true; - int offset = id - type.mapBaseId; + bool isStaticId = id >= (type.staticId - 2) && id <= type.staticId; + int offset = isStaticId ? (id - type.staticId) : (id - type.patrolId); int normalizedOffset = offset >= 18 ? offset - 18 : offset; return switch (normalizedOffset) { diff --git a/packages/wolf_3d_entities/lib/src/entities/enemies/enemy.dart b/packages/wolf_3d_entities/lib/src/entities/enemies/enemy.dart index ee41ea2..ffc5cd6 100644 --- a/packages/wolf_3d_entities/lib/src/entities/enemies/enemy.dart +++ b/packages/wolf_3d_entities/lib/src/entities/enemies/enemy.dart @@ -11,24 +11,55 @@ import 'package:wolf_3d_entities/src/entity.dart'; enum EnemyAnimation { idle, walking, attacking, pain, dying, dead } enum EnemyType { - guard(mapBaseId: 108, spriteBaseIdx: 50), - dog(mapBaseId: 216, spriteBaseIdx: 99), - ss(mapBaseId: 180, spriteBaseIdx: 138), - mutant(mapBaseId: 252, spriteBaseIdx: 187), - officer(mapBaseId: 144, spriteBaseIdx: 238); + guard( + staticId: 108, + patrolId: 124, + spriteBaseIdx: 50, + ), // Update spriteBaseIdx to your actual value + dog( + staticId: 144, + patrolId: 160, + spriteBaseIdx: 99, + ), // Update spriteBaseIdx to your actual value + ss( + staticId: 176, + patrolId: 192, + spriteBaseIdx: 138, + ), // Retained from your snippet + mutant( + staticId: 238, + patrolId: 254, + spriteBaseIdx: 185, + ), // Update spriteBaseIdx to your actual value + officer( + staticId: 270, + patrolId: 286, + spriteBaseIdx: 226, + ); // Update spriteBaseIdx to your actual value - final int mapBaseId; + final int staticId; + final int patrolId; final int spriteBaseIdx; - const EnemyType({required this.mapBaseId, required this.spriteBaseIdx}); + const EnemyType({ + required this.staticId, + required this.patrolId, + required this.spriteBaseIdx, + }); - /// Helper to check if a specific TED5 Map ID belongs to this enemy - bool claimsMapId(int id) => id >= mapBaseId && id <= mapBaseId + 35; + /// Wolfenstein 3D allocates blocks of 16 IDs per enemy type for standing and patrolling + /// (4 directions x 4 difficulty levels = 16 IDs) + bool claimsMapId(int id) { + bool isStatic = id >= staticId && id < staticId + 16; + bool isPatrol = id >= patrolId && id < patrolId + 16; + return isStatic || isPatrol; + } - /// Helper to find which EnemyType a given Map ID belongs to static EnemyType? fromMapId(int id) { for (final type in EnemyType.values) { - if (type.claimsMapId(id)) return type; + if (type.claimsMapId(id)) { + return type; + } } return null; } @@ -92,7 +123,7 @@ enum EnemyType { required int elapsedMs, required int lastActionTime, double angleDiff = 0, - int? walkFrameOverride, // Optional for custom timing + int? walkFrameOverride, }) { // 1. Calculate Octant for directional sprites (Idle/Walk) int octant = ((angleDiff + (math.pi / 8)) / (math.pi / 4)).floor() % 8; @@ -110,28 +141,46 @@ enum EnemyType { EnemyAnimation.attacking => () { int time = elapsedMs - lastActionTime; return switch (this) { - EnemyType.guard || EnemyType.ss || EnemyType.dog => + // Offset 40-42 is the Shooting sequence (Aim, Fire, Recoil) + EnemyType.guard || EnemyType.ss => spriteBaseIdx + (time < 150 ? 40 : time < 300 ? 41 - : 40), + : 42), EnemyType.officer || EnemyType.mutant => spriteBaseIdx + (time < 200 ? 40 : 41), + EnemyType.dog => + spriteBaseIdx + (time < 150 ? 32 : 33), // Dog Leap/Bite }; }(), - EnemyAnimation.pain => spriteBaseIdx + (this == EnemyType.dog ? 32 : 42), + EnemyAnimation.pain => + spriteBaseIdx + + switch (this) { + EnemyType.dog => 32, + EnemyType.officer || EnemyType.mutant => 42, + _ => 43, // guard, ss + }, EnemyAnimation.dying => () { int frame = (elapsedMs - lastActionTime) ~/ 150; - int maxFrames = this == EnemyType.dog ? 2 : 3; - int offset = this == EnemyType.dog ? 35 : 43; - return spriteBaseIdx + offset + (frame.clamp(0, maxFrames)); + int dyingStart = switch (this) { + EnemyType.dog => 35, + EnemyType.officer || EnemyType.mutant => 43, + _ => 44, // guard, ss + }; + return spriteBaseIdx + dyingStart + (frame.clamp(0, 2)); }(), - EnemyAnimation.dead => spriteBaseIdx + (this == EnemyType.dog ? 37 : 45), + EnemyAnimation.dead => + spriteBaseIdx + + switch (this) { + EnemyType.dog => 38, + EnemyType.officer || EnemyType.mutant => 46, + _ => 48, // guard, ss + }, }; } } @@ -298,7 +347,6 @@ abstract class Enemy extends Entity { return intendedMovement; } - // Updated Signature ({Coordinate2D movement, double newAngle}) update({ required int elapsedMs, required Coordinate2D playerPosition, @@ -328,7 +376,7 @@ abstract class Enemy extends Entity { final type = EnemyType.fromMapId(objId); if (type == null) return null; - bool isPatrolling = objId >= type.mapBaseId + 18; + bool isPatrolling = objId >= type.patrolId; double spawnAngle = MapObject.getAngle(objId); // 2. Return the specific instance diff --git a/packages/wolf_3d_entities/lib/src/entities/enemies/guard.dart b/packages/wolf_3d_entities/lib/src/entities/enemies/guard.dart index 3cf5ed7..37c46df 100644 --- a/packages/wolf_3d_entities/lib/src/entities/enemies/guard.dart +++ b/packages/wolf_3d_entities/lib/src/entities/enemies/guard.dart @@ -50,7 +50,7 @@ class Guard extends Enemy { diff -= 2 * math.pi; } - // Helper to get sprite based on current state + // Use the centralized animation logic to avoid manual offset errors EnemyAnimation currentAnim = switch (state) { EntityState.patrolling => EnemyAnimation.walking, EntityState.attacking => EnemyAnimation.attacking, @@ -66,18 +66,43 @@ class Guard extends Enemy { angleDiff: diff, ); - // Logic triggers (Damage, State transitions) + if (state == EntityState.patrolling) { + if (distance > 0.8) { + double moveX = math.cos(angleToPlayer) * speed; + double moveY = math.sin(angleToPlayer) * speed; + movement = getValidMovement( + Coordinate2D(moveX, moveY), + isWalkable, + tryOpenDoor, + ); + } + + if (distance < 6.0 && elapsedMs - lastActionTime > 1500) { + if (hasLineOfSight(playerPosition, isWalkable)) { + state = EntityState.attacking; + lastActionTime = elapsedMs; + _hasFiredThisCycle = false; + } + } + } + if (state == EntityState.attacking) { - int time = elapsedMs - lastActionTime; - if (time >= 150 && time < 300 && !_hasFiredThisCycle) { - onDamagePlayer(10); + int timeShooting = elapsedMs - lastActionTime; + // SS-Specific firing logic + if (timeShooting >= 100 && timeShooting < 200 && !_hasFiredThisCycle) { + onDamagePlayer(damage); _hasFiredThisCycle = true; - } else if (time >= 450) { + } else if (timeShooting >= 300) { state = EntityState.patrolling; lastActionTime = elapsedMs; } } + if (state == EntityState.pain && elapsedMs - lastActionTime > 250) { + state = EntityState.patrolling; + lastActionTime = elapsedMs; + } + return (movement: movement, newAngle: newAngle); } } diff --git a/packages/wolf_3d_entities/lib/src/entities/enemies/mutant.dart b/packages/wolf_3d_entities/lib/src/entities/enemies/mutant.dart index 8ef6b73..e72c2be 100644 --- a/packages/wolf_3d_entities/lib/src/entities/enemies/mutant.dart +++ b/packages/wolf_3d_entities/lib/src/entities/enemies/mutant.dart @@ -53,6 +53,7 @@ class Mutant extends Enemy { diff -= 2 * math.pi; } + // Use the centralized animation logic to avoid manual offset errors EnemyAnimation currentAnim = switch (state) { EntityState.patrolling => EnemyAnimation.walking, EntityState.attacking => EnemyAnimation.attacking, @@ -68,17 +69,43 @@ class Mutant extends Enemy { angleDiff: diff, ); + if (state == EntityState.patrolling) { + if (distance > 0.8) { + double moveX = math.cos(angleToPlayer) * speed; + double moveY = math.sin(angleToPlayer) * speed; + movement = getValidMovement( + Coordinate2D(moveX, moveY), + isWalkable, + tryOpenDoor, + ); + } + + if (distance < 6.0 && elapsedMs - lastActionTime > 1500) { + if (hasLineOfSight(playerPosition, isWalkable)) { + state = EntityState.attacking; + lastActionTime = elapsedMs; + _hasFiredThisCycle = false; + } + } + } + if (state == EntityState.attacking) { - int time = elapsedMs - lastActionTime; - if (time >= 150 && !_hasFiredThisCycle) { + int timeShooting = elapsedMs - lastActionTime; + // SS-Specific firing logic + if (timeShooting >= 100 && timeShooting < 200 && !_hasFiredThisCycle) { onDamagePlayer(damage); _hasFiredThisCycle = true; - } else if (time >= 300) { + } else if (timeShooting >= 300) { state = EntityState.patrolling; lastActionTime = elapsedMs; } } + if (state == EntityState.pain && elapsedMs - lastActionTime > 250) { + state = EntityState.patrolling; + lastActionTime = elapsedMs; + } + return (movement: movement, newAngle: newAngle); } } diff --git a/packages/wolf_3d_entities/lib/src/entities/enemies/officer.dart b/packages/wolf_3d_entities/lib/src/entities/enemies/officer.dart index bf037ec..35aa063 100644 --- a/packages/wolf_3d_entities/lib/src/entities/enemies/officer.dart +++ b/packages/wolf_3d_entities/lib/src/entities/enemies/officer.dart @@ -53,80 +53,55 @@ class Officer extends Enemy { diff -= 2 * math.pi; } - int octant = ((diff + (math.pi / 8)) / (math.pi / 4)).floor() % 8; - if (octant < 0) octant += 8; + // Use centralized animation logic + EnemyAnimation currentAnim = switch (state) { + EntityState.patrolling => EnemyAnimation.walking, + EntityState.attacking => EnemyAnimation.attacking, + EntityState.pain => EnemyAnimation.pain, + EntityState.dead => isDying ? EnemyAnimation.dying : EnemyAnimation.dead, + _ => EnemyAnimation.idle, + }; - switch (state) { - case EntityState.idle: - spriteIndex = EnemyType.officer.spriteBaseIdx + octant; - break; + spriteIndex = EnemyType.officer.getSpriteFromAnimation( + animation: currentAnim, + elapsedMs: elapsedMs, + lastActionTime: lastActionTime, + angleDiff: diff, + ); - case EntityState.patrolling: - if (distance > 0.8) { - double moveX = math.cos(angleToPlayer) * speed; - double moveY = math.sin(angleToPlayer) * speed; - movement = getValidMovement( - Coordinate2D(moveX, moveY), - isWalkable, - tryOpenDoor, - ); - } - - int walkFrame = (elapsedMs ~/ 150) % 4; - spriteIndex = - (EnemyType.officer.spriteBaseIdx + 8) + (walkFrame * 8) + octant; - - if (distance < 6.0 && elapsedMs - lastActionTime > 1000) { - if (hasLineOfSight(playerPosition, isWalkable)) { - state = EntityState.attacking; - lastActionTime = elapsedMs; - _hasFiredThisCycle = false; - } - } - break; - - case EntityState.attacking: - int timeShooting = elapsedMs - lastActionTime; - if (timeShooting < 150) { - spriteIndex = EnemyType.officer.spriteBaseIdx + 40; // Aiming - } else if (timeShooting < 300) { - spriteIndex = EnemyType.officer.spriteBaseIdx + 41; // Firing - if (!_hasFiredThisCycle) { - onDamagePlayer(damage); - _hasFiredThisCycle = true; - } - } else if (timeShooting < 450) { - spriteIndex = EnemyType.officer.spriteBaseIdx + 40; // Recoil - } else { - state = EntityState.patrolling; + if (state == EntityState.patrolling) { + if (distance > 0.8) { + double moveX = math.cos(angleToPlayer) * speed; + double moveY = math.sin(angleToPlayer) * speed; + movement = getValidMovement( + Coordinate2D(moveX, moveY), + isWalkable, + tryOpenDoor, + ); + } + if (distance < 6.0 && elapsedMs - lastActionTime > 1000) { + if (hasLineOfSight(playerPosition, isWalkable)) { + state = EntityState.attacking; lastActionTime = elapsedMs; + _hasFiredThisCycle = false; } - break; + } + } - case EntityState.pain: - spriteIndex = EnemyType.officer.spriteBaseIdx + 42; - if (elapsedMs - lastActionTime > 250) { - state = EntityState.patrolling; - lastActionTime = elapsedMs; - } - break; + if (state == EntityState.attacking) { + int timeShooting = elapsedMs - lastActionTime; + if (timeShooting >= 150 && timeShooting < 300 && !_hasFiredThisCycle) { + onDamagePlayer(damage); + _hasFiredThisCycle = true; + } else if (timeShooting >= 450) { + state = EntityState.patrolling; + lastActionTime = elapsedMs; + } + } - case EntityState.dead: - if (isDying) { - int deathFrame = (elapsedMs - lastActionTime) ~/ 150; - if (deathFrame < 3) { - spriteIndex = (EnemyType.officer.spriteBaseIdx + 43) + deathFrame; - } else { - spriteIndex = EnemyType.officer.spriteBaseIdx + 45; - isDying = false; - } - } else { - spriteIndex = EnemyType.officer.spriteBaseIdx + 45; - } - break; - - default: - break; + if (state == EntityState.pain && elapsedMs - lastActionTime > 250) { + state = EntityState.patrolling; + lastActionTime = elapsedMs; } return (movement: movement, newAngle: newAngle); diff --git a/packages/wolf_3d_entities/lib/src/entities/enemies/ss.dart b/packages/wolf_3d_entities/lib/src/entities/enemies/ss.dart index 830b43a..cdd2daf 100644 --- a/packages/wolf_3d_entities/lib/src/entities/enemies/ss.dart +++ b/packages/wolf_3d_entities/lib/src/entities/enemies/ss.dart @@ -8,6 +8,8 @@ class SS extends Enemy { static const double speed = 0.04; bool _hasFiredThisCycle = false; + static EnemyType get type => EnemyType.ss; + SS({ required super.x, required super.y, @@ -42,6 +44,7 @@ class SS extends Enemy { newAngle = angleToPlayer; } + // Calculate angle diff for the octant logic double diff = angleToPlayer - newAngle; while (diff <= -math.pi) { diff += 2 * math.pi; @@ -50,81 +53,57 @@ class SS extends Enemy { diff -= 2 * math.pi; } - int octant = ((diff + (math.pi / 8)) / (math.pi / 4)).floor() % 8; - if (octant < 0) octant += 8; + // Use the centralized animation logic to avoid manual offset errors + EnemyAnimation currentAnim = switch (state) { + EntityState.patrolling => EnemyAnimation.walking, + EntityState.attacking => EnemyAnimation.attacking, + EntityState.pain => EnemyAnimation.pain, + EntityState.dead => isDying ? EnemyAnimation.dying : EnemyAnimation.dead, + _ => EnemyAnimation.idle, + }; - switch (state) { - case EntityState.idle: - spriteIndex = EnemyType.ss.spriteBaseIdx + octant; - break; + spriteIndex = type.getSpriteFromAnimation( + animation: currentAnim, + elapsedMs: elapsedMs, + lastActionTime: lastActionTime, + angleDiff: diff, + ); - case EntityState.patrolling: - if (distance > 0.8) { - double moveX = math.cos(angleToPlayer) * speed; - double moveY = math.sin(angleToPlayer) * speed; - movement = getValidMovement( - Coordinate2D(moveX, moveY), - isWalkable, - tryOpenDoor, - ); - } + if (state == EntityState.patrolling) { + if (distance > 0.8) { + double moveX = math.cos(angleToPlayer) * speed; + double moveY = math.sin(angleToPlayer) * speed; + movement = getValidMovement( + Coordinate2D(moveX, moveY), + isWalkable, + tryOpenDoor, + ); + } - int walkFrame = (elapsedMs ~/ 150) % 4; - spriteIndex = - (EnemyType.ss.spriteBaseIdx + 8) + (walkFrame * 8) + octant; - - if (distance < 6.0 && elapsedMs - lastActionTime > 1500) { - if (hasLineOfSight(playerPosition, isWalkable)) { - state = EntityState.attacking; - lastActionTime = elapsedMs; - _hasFiredThisCycle = false; - } - } - break; - - case EntityState.attacking: - // SS machine gun fires much faster than a standard pistol! - int timeShooting = elapsedMs - lastActionTime; - if (timeShooting < 100) { - spriteIndex = EnemyType.ss.spriteBaseIdx + 46; // Aiming - } else if (timeShooting < 200) { - spriteIndex = EnemyType.ss.spriteBaseIdx + 47; // Firing - if (!_hasFiredThisCycle) { - onDamagePlayer(damage); - _hasFiredThisCycle = true; - } - } else if (timeShooting < 300) { - spriteIndex = EnemyType.ss.spriteBaseIdx + 48; // Recoil - } else { - state = EntityState.patrolling; + if (distance < 6.0 && elapsedMs - lastActionTime > 1500) { + if (hasLineOfSight(playerPosition, isWalkable)) { + state = EntityState.attacking; lastActionTime = elapsedMs; + _hasFiredThisCycle = false; } - break; + } + } - case EntityState.pain: - spriteIndex = EnemyType.ss.spriteBaseIdx + 44; - if (elapsedMs - lastActionTime > 250) { - state = EntityState.patrolling; - lastActionTime = elapsedMs; - } - break; + if (state == EntityState.attacking) { + int timeShooting = elapsedMs - lastActionTime; + // SS-Specific firing logic + if (timeShooting >= 100 && timeShooting < 200 && !_hasFiredThisCycle) { + onDamagePlayer(damage); + _hasFiredThisCycle = true; + } else if (timeShooting >= 300) { + state = EntityState.patrolling; + lastActionTime = elapsedMs; + } + } - case EntityState.dead: - if (isDying) { - int deathFrame = (elapsedMs - lastActionTime) ~/ 150; - if (deathFrame < 4) { - spriteIndex = (EnemyType.ss.spriteBaseIdx + 40) + deathFrame; - } else { - spriteIndex = EnemyType.ss.spriteBaseIdx + 45; - isDying = false; - } - } else { - spriteIndex = EnemyType.ss.spriteBaseIdx + 45; - } - break; - - default: - break; + if (state == EntityState.pain && elapsedMs - lastActionTime > 250) { + state = EntityState.patrolling; + lastActionTime = elapsedMs; } return (movement: movement, newAngle: newAngle); diff --git a/packages/wolf_3d_entities/lib/src/entity_registry.dart b/packages/wolf_3d_entities/lib/src/entity_registry.dart index ae09327..68e4ce4 100644 --- a/packages/wolf_3d_entities/lib/src/entity_registry.dart +++ b/packages/wolf_3d_entities/lib/src/entity_registry.dart @@ -16,12 +16,12 @@ typedef EntitySpawner = abstract class EntityRegistry { static final List _spawners = [ - // Enemies need to try to spawn first - Enemy.spawn, - // Bosses HansGrosse.trySpawn, + // Enemies need to try to spawn first + Enemy.spawn, + // Everything else Collectible.trySpawn, Decorative.trySpawn, @@ -44,7 +44,13 @@ abstract class EntityRegistry { if (objId == 0) return null; for (final spawner in _spawners) { - Entity? entity = spawner(objId, x, y, difficulty); + Entity? entity = spawner( + objId, + x, + y, + difficulty, + isSharewareMode: isSharewareMode, + ); final EnemyType? type = EnemyType.fromMapId(objId); if (type != null) {