From 6927c902a7870182a8de09be5ae77ee5e5168def Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Tue, 17 Mar 2026 13:56:49 +0100 Subject: [PATCH] Fix enemy spawning Signed-off-by: Hans Kokx --- .../lib/src/data_types/enemy_map_data.dart | 24 ++-- .../entities/enemies/enemy_animation.dart | 121 +++++++++++++++++ .../entities/entities/enemies/enemy_type.dart | 127 +----------------- 3 files changed, 139 insertions(+), 133 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 bc96d13..59b9018 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,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; } } diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy_animation.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy_animation.dart index d159f0e..ab9b4c0 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy_animation.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy_animation.dart @@ -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.', + ); + } + } +} diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy_type.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy_type.dart index 5273f44..09b2e0b 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy_type.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy_type.dart @@ -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; }