Fix wrong enemies spawning all over

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-15 17:10:54 +01:00
parent da00c5237f
commit 2892984e4e
7 changed files with 233 additions and 172 deletions

View File

@@ -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) {

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -16,12 +16,12 @@ typedef EntitySpawner =
abstract class EntityRegistry {
static final List<EntitySpawner> _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) {