Fix wrong enemies spawning all over
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,15 +53,23 @@ 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 (state == EntityState.patrolling) {
|
||||
if (distance > 0.8) {
|
||||
double moveX = math.cos(angleToPlayer) * speed;
|
||||
double moveY = math.sin(angleToPlayer) * speed;
|
||||
@@ -71,11 +79,6 @@ class Officer extends Enemy {
|
||||
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;
|
||||
@@ -83,51 +86,23 @@ class Officer extends Enemy {
|
||||
_hasFiredThisCycle = false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case EntityState.attacking:
|
||||
if (state == 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) {
|
||||
if (timeShooting >= 150 && timeShooting < 300 && !_hasFiredThisCycle) {
|
||||
onDamagePlayer(damage);
|
||||
_hasFiredThisCycle = true;
|
||||
}
|
||||
} else if (timeShooting < 450) {
|
||||
spriteIndex = EnemyType.officer.spriteBaseIdx + 40; // Recoil
|
||||
} else {
|
||||
} else if (timeShooting >= 450) {
|
||||
state = EntityState.patrolling;
|
||||
lastActionTime = elapsedMs;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case EntityState.pain:
|
||||
spriteIndex = EnemyType.officer.spriteBaseIdx + 42;
|
||||
if (elapsedMs - lastActionTime > 250) {
|
||||
if (state == EntityState.pain && elapsedMs - lastActionTime > 250) {
|
||||
state = EntityState.patrolling;
|
||||
lastActionTime = elapsedMs;
|
||||
}
|
||||
break;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return (movement: movement, newAngle: newAngle);
|
||||
}
|
||||
|
||||
@@ -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,15 +53,23 @@ 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 (state == EntityState.patrolling) {
|
||||
if (distance > 0.8) {
|
||||
double moveX = math.cos(angleToPlayer) * speed;
|
||||
double moveY = math.sin(angleToPlayer) * speed;
|
||||
@@ -69,10 +80,6 @@ class SS extends Enemy {
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -80,52 +87,24 @@ class SS extends Enemy {
|
||||
_hasFiredThisCycle = false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case EntityState.attacking:
|
||||
// SS machine gun fires much faster than a standard pistol!
|
||||
if (state == EntityState.attacking) {
|
||||
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) {
|
||||
// SS-Specific firing logic
|
||||
if (timeShooting >= 100 && timeShooting < 200 && !_hasFiredThisCycle) {
|
||||
onDamagePlayer(damage);
|
||||
_hasFiredThisCycle = true;
|
||||
}
|
||||
} else if (timeShooting < 300) {
|
||||
spriteIndex = EnemyType.ss.spriteBaseIdx + 48; // Recoil
|
||||
} else {
|
||||
} else if (timeShooting >= 300) {
|
||||
state = EntityState.patrolling;
|
||||
lastActionTime = elapsedMs;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case EntityState.pain:
|
||||
spriteIndex = EnemyType.ss.spriteBaseIdx + 44;
|
||||
if (elapsedMs - lastActionTime > 250) {
|
||||
if (state == EntityState.pain && elapsedMs - lastActionTime > 250) {
|
||||
state = EntityState.patrolling;
|
||||
lastActionTime = elapsedMs;
|
||||
}
|
||||
break;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return (movement: movement, newAngle: newAngle);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user