Fix enemy spawning

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-17 13:56:49 +01:00
parent 99cca5cc10
commit 6927c902a7
3 changed files with 139 additions and 133 deletions

View File

@@ -2,7 +2,7 @@ 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 36 IDs
/// 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.
class EnemyMapData {
/// The starting ID for this specific enemy type (e.g., 108 for Guards).
@@ -10,28 +10,28 @@ class EnemyMapData {
const EnemyMapData(this.baseId);
/// Returns true if the provided [id] belongs to this enemy type's 36-ID block.
bool claimsId(int id) => id >= baseId && id < baseId + 36;
/// Returns true if the provided [id] belongs to this enemy type's 48-ID block.
bool claimsId(int id) => id >= baseId && id < baseId + 48;
/// Returns true if the ID represents a standing (static) version for the [difficulty].
bool isStaticForDifficulty(int id, Difficulty difficulty) {
// Standing enemies occupy the first 12 IDs (4 per difficulty level)
/// 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;
}
/// Returns true if the ID represents a patrolling version for the [difficulty].
bool isPatrolForDifficulty(int id, Difficulty difficulty) {
// Patrolling enemies occupy the next 12 IDs
int start = baseId + 12 + (difficulty.level * 4);
/// 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 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 12 IDs of the block
int start = baseId + 24 + (difficulty.level * 4);
// Ambush enemies occupy the final 16 IDs of the block
int start = baseId + 32 + (difficulty.level * 4);
return id >= start && id < start + 4;
}
}

View File

@@ -1 +1,122 @@
import 'package:wolf_3d_dart/src/data_types/sprite_frame_range.dart';
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_type.dart';
enum EnemyAnimation { idle, walking, attacking, pain, dying, dead }
/// A mapping container that defines the sprite index ranges for an enemy's life cycle.
///
/// Wolfenstein 3D enemies use specific ranges within the global sprite list
/// to handle state transitions like walking, attacking, or dying.
class EnemyAnimationMap {
final SpriteFrameRange idle;
final SpriteFrameRange walking;
final SpriteFrameRange attacking;
final SpriteFrameRange pain;
final SpriteFrameRange dying;
final SpriteFrameRange dead;
const EnemyAnimationMap({
required this.idle,
required this.walking,
required this.attacking,
required this.pain,
required this.dying,
required this.dead,
});
/// Identifies which [EnemyAnimation] state a specific [spriteIndex] belongs to.
///
/// Returns `null` if the index is outside this enemy's defined animation ranges.
EnemyAnimation? getAnimation(int spriteIndex) {
if (idle.contains(spriteIndex)) return EnemyAnimation.idle;
if (walking.contains(spriteIndex)) return EnemyAnimation.walking;
if (attacking.contains(spriteIndex)) return EnemyAnimation.attacking;
if (pain.contains(spriteIndex)) return EnemyAnimation.pain;
if (dying.contains(spriteIndex)) return EnemyAnimation.dying;
if (dead.contains(spriteIndex)) return EnemyAnimation.dead;
return null;
}
/// Returns the [SpriteFrameRange] associated with a specific [animation] state.
SpriteFrameRange getRange(EnemyAnimation animation) {
return switch (animation) {
EnemyAnimation.idle => idle,
EnemyAnimation.walking => walking,
EnemyAnimation.attacking => attacking,
EnemyAnimation.pain => pain,
EnemyAnimation.dying => dying,
EnemyAnimation.dead => dead,
};
}
/// Checks if any animation range in this map overlaps with any range in [other].
bool overlapsWith(EnemyAnimationMap other) {
final myRanges = [idle, walking, attacking, pain, dying, dead];
final otherRanges = [
other.idle,
other.walking,
other.attacking,
other.pain,
other.dying,
other.dead,
];
for (final myRange in myRanges) {
for (final otherRange in otherRanges) {
if (myRange.overlaps(otherRange)) {
return true;
}
}
}
return false;
}
/// Checks if any animation ranges within this specific enemy overlap with each other.
///
/// Useful for validating that an enemy isn't trying to use the same sprite
/// for two different states (e.g., walking and attacking).
bool hasInternalOverlaps() {
final ranges = [idle, walking, attacking, pain, dying, dead];
// Compare every unique pair of ranges
for (int i = 0; i < ranges.length; i++) {
for (int j = i + 1; j < ranges.length; j++) {
if (ranges[i].overlaps(ranges[j])) {
return true;
}
}
}
return false;
}
void validateEnemyAnimations() {
bool hasErrors = false;
for (final enemy in EnemyType.values) {
// 1. Check for internal overlaps (e.g., Guard walking overlaps Guard attacking)
if (enemy.animations.hasInternalOverlaps()) {
print(
'❌ ERROR: ${enemy.name} has overlapping internal animation states!',
);
hasErrors = true;
}
// 2. Check for external overlaps (e.g., Guard sprites overlap SS sprites)
for (final otherEnemy in EnemyType.values) {
if (enemy != otherEnemy && enemy.sharesFramesWith(otherEnemy)) {
print(
'❌ ERROR: ${enemy.name} shares sprite frames with ${otherEnemy.name}!',
);
hasErrors = true;
}
}
}
if (!hasErrors) {
print(
'✅ All enemy animations validated successfully! No overlaps found.',
);
}
}
}

View File

@@ -3,124 +3,6 @@ import 'dart:math' as math;
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_animation.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
/// A mapping container that defines the sprite index ranges for an enemy's life cycle.
///
/// Wolfenstein 3D enemies use specific ranges within the global sprite list
/// to handle state transitions like walking, attacking, or dying.
class EnemyAnimationMap {
final SpriteFrameRange idle;
final SpriteFrameRange walking;
final SpriteFrameRange attacking;
final SpriteFrameRange pain;
final SpriteFrameRange dying;
final SpriteFrameRange dead;
const EnemyAnimationMap({
required this.idle,
required this.walking,
required this.attacking,
required this.pain,
required this.dying,
required this.dead,
});
/// Identifies which [EnemyAnimation] state a specific [spriteIndex] belongs to.
///
/// Returns `null` if the index is outside this enemy's defined animation ranges.
EnemyAnimation? getAnimation(int spriteIndex) {
if (idle.contains(spriteIndex)) return EnemyAnimation.idle;
if (walking.contains(spriteIndex)) return EnemyAnimation.walking;
if (attacking.contains(spriteIndex)) return EnemyAnimation.attacking;
if (pain.contains(spriteIndex)) return EnemyAnimation.pain;
if (dying.contains(spriteIndex)) return EnemyAnimation.dying;
if (dead.contains(spriteIndex)) return EnemyAnimation.dead;
return null;
}
/// Returns the [SpriteFrameRange] associated with a specific [animation] state.
SpriteFrameRange getRange(EnemyAnimation animation) {
return switch (animation) {
EnemyAnimation.idle => idle,
EnemyAnimation.walking => walking,
EnemyAnimation.attacking => attacking,
EnemyAnimation.pain => pain,
EnemyAnimation.dying => dying,
EnemyAnimation.dead => dead,
};
}
/// Checks if any animation range in this map overlaps with any range in [other].
bool overlapsWith(EnemyAnimationMap other) {
final myRanges = [idle, walking, attacking, pain, dying, dead];
final otherRanges = [
other.idle,
other.walking,
other.attacking,
other.pain,
other.dying,
other.dead,
];
for (final myRange in myRanges) {
for (final otherRange in otherRanges) {
if (myRange.overlaps(otherRange)) {
return true;
}
}
}
return false;
}
/// Checks if any animation ranges within this specific enemy overlap with each other.
///
/// Useful for validating that an enemy isn't trying to use the same sprite
/// for two different states (e.g., walking and attacking).
bool hasInternalOverlaps() {
final ranges = [idle, walking, attacking, pain, dying, dead];
// Compare every unique pair of ranges
for (int i = 0; i < ranges.length; i++) {
for (int j = i + 1; j < ranges.length; j++) {
if (ranges[i].overlaps(ranges[j])) {
return true;
}
}
}
return false;
}
void validateEnemyAnimations() {
bool hasErrors = false;
for (final enemy in EnemyType.values) {
// 1. Check for internal overlaps (e.g., Guard walking overlaps Guard attacking)
if (enemy.animations.hasInternalOverlaps()) {
print(
'❌ ERROR: ${enemy.name} has overlapping internal animation states!',
);
hasErrors = true;
}
// 2. Check for external overlaps (e.g., Guard sprites overlap SS sprites)
for (final otherEnemy in EnemyType.values) {
if (enemy != otherEnemy && enemy.sharesFramesWith(otherEnemy)) {
print(
'❌ ERROR: ${enemy.name} shares sprite frames with ${otherEnemy.name}!',
);
hasErrors = true;
}
}
}
if (!hasErrors) {
print(
'✅ All enemy animations validated successfully! No overlaps found.',
);
}
}
}
/// Defines the primary enemy types and their behavioral metadata.
///
/// This enum maps level object IDs to their corresponding sprite ranges and
@@ -211,9 +93,12 @@ enum EnemyType {
/// Identifies an [EnemyType] based on a tile ID from the map's object layer.
static EnemyType? fromMapId(int id) {
for (final type in EnemyType.values) {
if (type.mapData.claimsId(id)) return type;
}
// Each enemy occupies exactly 48 IDs across 3 states.
if (id >= 108 && id <= 155) return EnemyType.guard;
if (id >= 156 && id <= 203) return EnemyType.dog;
if (id >= 204 && id <= 251) return EnemyType.ss;
if (id >= 252 && id <= 299) return EnemyType.mutant;
if (id >= 300 && id <= 347) return EnemyType.officer;
return null;
}