diff --git a/packages/wolf_3d_dart/lib/src/data_types/map_objects.dart b/packages/wolf_3d_dart/lib/src/data_types/map_objects.dart index 51729c0..84f0168 100644 --- a/packages/wolf_3d_dart/lib/src/data_types/map_objects.dart +++ b/packages/wolf_3d_dart/lib/src/data_types/map_objects.dart @@ -89,13 +89,13 @@ abstract class MapObject { static const int bossFettgesicht = 223; // --- Enemy Range Constants --- - // Every enemy type occupies a block of 36 IDs. - // Modulo math is used on these ranges to determine orientation and patrol status. + // Enemies are encoded in 8-ID blocks: 4 standing directions and 4 patrol directions. + // Most enemy families shift by 36 IDs between difficulty tiers, while mutants shift by 18. static const int guardStart = 108; // Claims 108-115, 144-151, 180-187 - static const int dogStart = 116; // Claims 116-123, 152-159, 188-195 + static const int officerStart = 116; // Claims 116-123, 152-159, 188-195 static const int ssStart = 126; // Claims 126-133, 162-169, 198-205 - static const int mutantStart = 136; // Claims 136-143, 172-179, 208-215 - static const int officerStart = 252; // Claims 252-259, 288-295, 324-331 + static const int dogStart = 134; // Claims 134-141, 170-177, 206-213 + static const int mutantStart = 216; // Claims 216-223, 234-241, 252-259 // --- Missing Decorative Bodies --- static const int deadGuard = 124; // Decorative only in WL1 @@ -119,9 +119,14 @@ abstract class MapObject { final EnemyType? type = EnemyType.fromMapId(id); if (type == null) return 0.0; - // Because all enemies are grouped in blocks of 4 (one for each CardinalDirection), - // modulo 4 extracts the specific orientation index. - return CardinalDirection.fromEnemyIndex(id % 4).radians; + final int? normalizedId = type.mapData.getNormalizedId(id, Difficulty.hard); + if (normalizedId == null) return 0.0; + + // Enemy IDs are stored as 8-ID blocks per family. + // Offsets 0-3 are standing directions and 4-7 are patrol directions. + return CardinalDirection.fromEnemyIndex( + normalizedId - type.mapData.baseId, + ).radians; } /// Validates if an object should exist on the map given the current [difficulty]. diff --git a/packages/wolf_3d_dart/lib/src/engine/player/player.dart b/packages/wolf_3d_dart/lib/src/engine/player/player.dart index 9d76156..7352584 100644 --- a/packages/wolf_3d_dart/lib/src/engine/player/player.dart +++ b/packages/wolf_3d_dart/lib/src/engine/player/player.dart @@ -197,8 +197,8 @@ class Player { return pickedUp; } - void fire(int currentTime) { - if (switchState != WeaponSwitchState.idle) return; + bool fire(int currentTime) { + if (switchState != WeaponSwitchState.idle) return false; // We pass the isFiring state to handle automatic vs semi-auto behavior bool shotFired = currentWeapon.fire(currentTime, currentAmmo: ammo); @@ -206,6 +206,8 @@ class Player { if (shotFired && currentWeapon.type != WeaponType.knife) { ammo--; } + + return shotFired; } void releaseTrigger() { diff --git a/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart b/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart index 9bb0066..0a23022 100644 --- a/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart +++ b/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart @@ -85,6 +85,7 @@ class WolfEngine { /// Initializes the engine, sets the starting episode, and loads the first level. void init() { + audio.activeGame = data; _currentEpisodeIndex = startingEpisode; _currentLevelIndex = 0; _loadLevel(); @@ -247,9 +248,13 @@ class WolfEngine { } if (input.isFiring) { - player.fire(_timeAliveMs); + final bool shotFired = player.fire(_timeAliveMs); - if (_timeAliveMs - _lastAcousticAlertTime > 400) { + if (shotFired) { + _playPlayerWeaponSound(); + } + + if (shotFired && _timeAliveMs - _lastAcousticAlertTime > 400) { _propagateGunfire(); _lastAcousticAlertTime = _timeAliveMs; } @@ -356,6 +361,7 @@ class WolfEngine { isWalkable: isWalkable, tryOpenDoor: doorManager.tryOpenDoor, onDamagePlayer: (int damage) => player.takeDamage(damage), + onPlaySound: audio.playSoundEffect, ); // Scale the enemy's movement intent to prevent super-speed on 90Hz/120Hz displays @@ -429,6 +435,7 @@ class WolfEngine { if (entity.position.x.toInt() == tile.x && entity.position.y.toInt() == tile.y) { entity.isAlerted = true; + audio.playSoundEffect(entity.alertSoundId); // Wake them up! if (entity.state == EntityState.idle || @@ -480,6 +487,17 @@ class WolfEngine { return false; } + void _playPlayerWeaponSound() { + final sfxId = switch (player.currentWeapon.type) { + WeaponType.knife => WolfSound.knifeAttack, + WeaponType.pistol => WolfSound.pistolFire, + WeaponType.machineGun => WolfSound.machineGunFire, + WeaponType.chainGun => WolfSound.gatlingFire, + }; + + audio.playSoundEffect(sfxId); + } + /// Clamps movement to a maximum of 0.2 tiles per step to prevent wall-clipping /// and map-boundary jumps during low framerate/high delta spikes. Coordinate2D _clampMovement(Coordinate2D intent) { diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/bosses/hans_grosse.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/bosses/hans_grosse.dart index c686338..43294f2 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/bosses/hans_grosse.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/bosses/hans_grosse.dart @@ -11,6 +11,12 @@ class HansGrosse extends Enemy { EnemyType get type => throw UnimplementedError("Hans Grosse uses manual animation logic."); + @override + int get alertSoundId => WolfSound.bossActive; + + @override + int get attackSoundId => WolfSound.naziFire; + HansGrosse({ required super.x, required super.y, @@ -69,6 +75,7 @@ class HansGrosse extends Enemy { required bool Function(int x, int y) isWalkable, required void Function(int damage) onDamagePlayer, required void Function(int x, int y) tryOpenDoor, + required void Function(int sfxId) onPlaySound, }) { Coordinate2D movement = const Coordinate2D(0, 0); double newAngle = angle; @@ -77,12 +84,14 @@ class HansGrosse extends Enemy { newAngle = position.angleTo(playerPosition); } - checkWakeUp( + if (checkWakeUp( elapsedMs: elapsedMs, playerPosition: playerPosition, isWalkable: isWalkable, baseReactionMs: 50, - ); + )) { + onPlaySound(alertSoundId); + } double distance = position.distanceTo(playerPosition); @@ -130,6 +139,7 @@ class HansGrosse extends Enemy { setTics(10); } else if (currentFrame == 2) { spriteIndex = _baseSprite + 6; // Fire + onPlaySound(attackSoundId); onDamagePlayer(damage); setTics(4); } else if (currentFrame == 3) { diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/dog.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/dog.dart index 7e876dc..c0a9362 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/dog.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/dog.dart @@ -38,17 +38,20 @@ class Dog extends Enemy { required bool Function(int x, int y) isWalkable, required void Function(int damage) onDamagePlayer, required void Function(int x, int y) tryOpenDoor, + required void Function(int sfxId) onPlaySound, }) { Coordinate2D movement = const Coordinate2D(0, 0); double newAngle = angle; // 1. Perception if (state != EntityState.dead && !isDying) { - checkWakeUp( + if (checkWakeUp( elapsedMs: elapsedMs, playerPosition: playerPosition, isWalkable: isWalkable, - ); + )) { + onPlaySound(alertSoundId); + } } bool ticReady = processTics(elapsedDeltaMs, moveSpeed: 0); @@ -59,6 +62,7 @@ class Dog extends Enemy { currentFrame++; // Phase 2: The actual bite if (currentFrame == 1) { + onPlaySound(attackSoundId); final bool attackSuccessful = math.Random().nextDouble() < (180 / 256); diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy.dart index 6d8f851..f42effb 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy.dart @@ -48,6 +48,12 @@ abstract class Enemy extends Entity { /// knowing the specific subclass. EnemyType get type; + /// The sound played when this enemy notices the player or hears combat. + int get alertSoundId => type.alertSoundId; + + /// The sound played when this enemy performs its attack animation. + int get attackSoundId => type.attackSoundId; + /// Ensures enemies drop only one item (like ammo or a key) upon death. bool hasDroppedItem = false; @@ -111,6 +117,7 @@ abstract class Enemy extends Entity { void handleAttackState({ required int elapsedDeltaMs, required void Function(int damage) onDamagePlayer, + required void Function() onAttack, required int shootTics, required int cooldownTics, required int postAttackTics, @@ -122,6 +129,7 @@ abstract class Enemy extends Entity { if (currentFrame == 1) { // Phase 1: Bang! Calculate hit chance based on distance. // Drops by ~5% per tile distance, capped at a minimum 10% chance to hit. + onAttack(); double hitChance = (1.0 - (distance * 0.05)).clamp(0.1, 1.0); if (math.Random().nextDouble() <= hitChance) { @@ -147,13 +155,15 @@ abstract class Enemy extends Entity { /// /// Includes a randomized delay based on [baseReactionMs] and [reactionVarianceMs] /// to prevent all enemies in a room from reacting on the exact same frame. - void checkWakeUp({ + bool checkWakeUp({ required int elapsedMs, required Coordinate2D playerPosition, required bool Function(int x, int y) isWalkable, int baseReactionMs = 200, int reactionVarianceMs = 600, }) { + bool didAlert = false; + if (!isAlerted && hasLineOfSight(playerPosition, isWalkable)) { if (reactionTimeMs == 0) { // First frame of spotting: calculate how long until they "wake up" @@ -164,6 +174,7 @@ abstract class Enemy extends Entity { } else if (elapsedMs >= reactionTimeMs) { // Reaction delay has passed isAlerted = true; + didAlert = true; if (state == EntityState.idle || state == EntityState.ambushing) { state = EntityState.patrolling; @@ -174,6 +185,8 @@ abstract class Enemy extends Entity { reactionTimeMs = 0; } } + + return didAlert; } /// Determines if there is a clear, unobstructed path between the enemy and the player. @@ -312,6 +325,7 @@ abstract class Enemy extends Entity { required bool Function(int x, int y) isWalkable, required void Function(int x, int y) tryOpenDoor, required void Function(int damage) onDamagePlayer, + required void Function(int sfxId) onPlaySound, }); /// Factory method to spawn the correct [Enemy] subclass based on a Map ID. @@ -358,7 +372,9 @@ abstract class Enemy extends Entity { // Resolve spawn orientation using the NORMALIZED ID (0-7 offset from base) // This prevents offset math bugs (like the Mutant's 18-ID shift) from breaking facing directions. - double spawnAngle = CardinalDirection.fromEnemyIndex(normalizedId).radians; + double spawnAngle = CardinalDirection.fromEnemyIndex( + normalizedId - matchedType.mapData.baseId, + ).radians; EntityState spawnState; 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 1c48888..6621fa8 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 @@ -11,6 +11,8 @@ enum EnemyType { /// Standard Brown Guard (The most common enemy). guard( mapData: EnemyMapData(MapObject.guardStart), + alertSoundId: WolfSound.guardHalt, + attackSoundId: WolfSound.naziFire, animations: EnemyAnimationMap( idle: SpriteFrameRange(50, 57), walking: SpriteFrameRange(58, 89), @@ -24,6 +26,8 @@ enum EnemyType { /// Attack Dog (Fast melee enemy). dog( mapData: EnemyMapData(MapObject.dogStart), + alertSoundId: WolfSound.dogBark, + attackSoundId: WolfSound.dogAttack, animations: EnemyAnimationMap( // Dogs don't have true idle sprites, so map idle to the first walk frame safely idle: SpriteFrameRange(99, 106), @@ -43,6 +47,8 @@ enum EnemyType { /// SS Officer (Blue uniform, machine gun). ss( mapData: EnemyMapData(MapObject.ssStart), + alertSoundId: WolfSound.ssSchutzstaffel, + attackSoundId: WolfSound.naziFire, animations: EnemyAnimationMap( idle: SpriteFrameRange(138, 145), walking: SpriteFrameRange(146, 177), @@ -55,7 +61,9 @@ enum EnemyType { /// Undead Mutant (Exclusive to later episodes/retail). mutant( - mapData: EnemyMapData(MapObject.mutantStart), + mapData: EnemyMapData(MapObject.mutantStart, tierOffset: 18), + alertSoundId: WolfSound.guardHalt, + attackSoundId: WolfSound.naziFire, animations: EnemyAnimationMap( idle: SpriteFrameRange(187, 194), walking: SpriteFrameRange(195, 226), @@ -70,6 +78,8 @@ enum EnemyType { /// High-ranking Officer (White uniform, fast pistol). officer( mapData: EnemyMapData(MapObject.officerStart), + alertSoundId: WolfSound.guardHalt, + attackSoundId: WolfSound.naziFire, animations: EnemyAnimationMap( idle: SpriteFrameRange(238, 245), walking: SpriteFrameRange(246, 277), @@ -88,12 +98,20 @@ enum EnemyType { /// The animation ranges for this enemy type. final EnemyAnimationMap animations; + /// The sound played when this enemy first becomes alerted. + final int alertSoundId; + + /// The sound played when this enemy attacks. + final int attackSoundId; + /// If false, this enemy type will be ignored when loading shareware data. final bool existsInShareware; const EnemyType({ required this.mapData, required this.animations, + required this.alertSoundId, + required this.attackSoundId, this.existsInShareware = true, }); @@ -151,7 +169,6 @@ enum EnemyType { // --- Octant Calculation --- // We split the circle into 8 segments (octants). - // Adding pi/8 offsets the segments so "North" covers the +/- 22.5 degree range. int octant = ((angleDiff + (math.pi / 8)) / (math.pi / 4)).floor() % 8; if (octant < 0) octant += 8; diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/guard.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/guard.dart index 3cace7f..89fdc95 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/guard.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/guard.dart @@ -35,17 +35,20 @@ class Guard extends Enemy { required bool Function(int x, int y) isWalkable, required void Function(int damage) onDamagePlayer, required void Function(int x, int y) tryOpenDoor, + required void Function(int sfxId) onPlaySound, }) { Coordinate2D movement = const Coordinate2D(0, 0); double newAngle = angle; double distance = position.distanceTo(playerPosition); // 1. Perception (SightPlayer) - checkWakeUp( + if (checkWakeUp( elapsedMs: elapsedMs, playerPosition: playerPosition, isWalkable: isWalkable, - ); + )) { + onPlaySound(alertSoundId); + } // 2. Discrete AI Logic (Decisions happen every 10 tics) bool ticReady = processTics(elapsedDeltaMs, moveSpeed: 0); @@ -55,6 +58,7 @@ class Guard extends Enemy { elapsedDeltaMs: elapsedDeltaMs, distance: distance, onDamagePlayer: onDamagePlayer, + onAttack: () => onPlaySound(attackSoundId), shootTics: 20, cooldownTics: 20, postAttackTics: 20, @@ -123,7 +127,7 @@ class Guard extends Enemy { double newAngle, Coordinate2D playerPosition, ) { - double diff = position.angleTo(playerPosition) - newAngle; + double diff = newAngle - position.angleTo(playerPosition); while (diff <= -math.pi) { diff += 2 * math.pi; } diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/mutant.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/mutant.dart index c01edd0..6caecd1 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/mutant.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/mutant.dart @@ -33,22 +33,25 @@ class Mutant extends Enemy { required bool Function(int x, int y) isWalkable, required void Function(int damage) onDamagePlayer, required void Function(int x, int y) tryOpenDoor, + required void Function(int sfxId) onPlaySound, }) { Coordinate2D movement = const Coordinate2D(0, 0); double newAngle = angle; - checkWakeUp( + if (checkWakeUp( elapsedMs: elapsedMs, playerPosition: playerPosition, isWalkable: isWalkable, - ); + )) { + onPlaySound(alertSoundId); + } double distance = position.distanceTo(playerPosition); double angleToPlayer = position.angleTo(playerPosition); if (isAlerted && state != EntityState.dead) newAngle = angleToPlayer; - double diff = angleToPlayer - newAngle; + double diff = newAngle - angleToPlayer; while (diff <= -math.pi) { diff += 2 * math.pi; } @@ -105,6 +108,7 @@ class Mutant extends Enemy { if (processTics(elapsedDeltaMs, moveSpeed: 0)) { currentFrame++; if (currentFrame == 1) { + onPlaySound(attackSoundId); onDamagePlayer(damage); setTics(4); } else if (currentFrame == 2) { diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/officer.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/officer.dart index 7b148b6..422ed5f 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/officer.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/officer.dart @@ -33,22 +33,25 @@ class Officer extends Enemy { required bool Function(int x, int y) isWalkable, required void Function(int damage) onDamagePlayer, required void Function(int x, int y) tryOpenDoor, + required void Function(int sfxId) onPlaySound, }) { Coordinate2D movement = const Coordinate2D(0, 0); double newAngle = angle; - checkWakeUp( + if (checkWakeUp( elapsedMs: elapsedMs, playerPosition: playerPosition, isWalkable: isWalkable, - ); + )) { + onPlaySound(alertSoundId); + } double distance = position.distanceTo(playerPosition); double angleToPlayer = position.angleTo(playerPosition); if (isAlerted && state != EntityState.dead) newAngle = angleToPlayer; - double diff = angleToPlayer - newAngle; + double diff = newAngle - angleToPlayer; while (diff <= -math.pi) { diff += 2 * math.pi; } @@ -105,6 +108,7 @@ class Officer extends Enemy { if (processTics(elapsedDeltaMs, moveSpeed: 0)) { currentFrame++; if (currentFrame == 1) { + onPlaySound(attackSoundId); onDamagePlayer(damage); setTics(4); // Bang! } else if (currentFrame == 2) { diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/ss.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/ss.dart index 0f95b0e..323270e 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/ss.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/ss.dart @@ -32,22 +32,25 @@ class SS extends Enemy { required bool Function(int x, int y) isWalkable, required void Function(int damage) onDamagePlayer, required void Function(int x, int y) tryOpenDoor, + required void Function(int sfxId) onPlaySound, }) { Coordinate2D movement = const Coordinate2D(0, 0); double newAngle = angle; - checkWakeUp( + if (checkWakeUp( elapsedMs: elapsedMs, playerPosition: playerPosition, isWalkable: isWalkable, - ); + )) { + onPlaySound(alertSoundId); + } double distance = position.distanceTo(playerPosition); double angleToPlayer = position.angleTo(playerPosition); if (isAlerted && state != EntityState.dead) newAngle = angleToPlayer; - double diff = angleToPlayer - newAngle; + double diff = newAngle - angleToPlayer; while (diff <= -math.pi) { diff += 2 * math.pi; } @@ -104,6 +107,7 @@ class SS extends Enemy { if (processTics(elapsedDeltaMs, moveSpeed: 0)) { currentFrame++; if (currentFrame == 1) { + onPlaySound(attackSoundId); onDamagePlayer(damage); setTics(5); // Bang! } else if (currentFrame == 2) { @@ -114,6 +118,7 @@ class SS extends Enemy { if (math.Random().nextDouble() > 0.5) { // 50% chance to burst currentFrame = 1; + onPlaySound(attackSoundId); onDamagePlayer(damage); setTics(5); return (movement: movement, newAngle: newAngle); diff --git a/packages/wolf_3d_dart/test/entities/enemy_spawn_test.dart b/packages/wolf_3d_dart/test/entities/enemy_spawn_test.dart new file mode 100644 index 0000000..cb72f04 --- /dev/null +++ b/packages/wolf_3d_dart/test/entities/enemy_spawn_test.dart @@ -0,0 +1,47 @@ +import 'package:test/test.dart'; +import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; +import 'package:wolf_3d_dart/wolf_3d_entities.dart'; + +void main() { + group('Enemy map IDs', () { + test('maps officers, dogs, and mutants to the correct enemy types', () { + expect(EnemyType.fromMapId(116), EnemyType.officer); + expect(EnemyType.fromMapId(134), EnemyType.dog); + expect(EnemyType.fromMapId(216), EnemyType.mutant); + }); + + test('uses the original mutant 18-ID difficulty offset', () { + expect(EnemyType.mutant.mapData.getNormalizedId(216, Difficulty.easy), 216); + expect( + EnemyType.mutant.mapData.getNormalizedId(234, Difficulty.medium), + 216, + ); + expect( + EnemyType.mutant.mapData.getNormalizedId(252, Difficulty.hard), + 216, + ); + }); + }); + + group('Enemy spawn facing', () { + test('spawns dog directions using the original dog map IDs', () { + expect(_spawnEnemy(134).angle, CardinalDirection.east.radians); + expect(_spawnEnemy(135).angle, CardinalDirection.north.radians); + expect(_spawnEnemy(136).angle, CardinalDirection.west.radians); + expect(_spawnEnemy(137).angle, CardinalDirection.south.radians); + }); + + test('maps patrol variants to the same facing directions', () { + expect(_spawnEnemy(138).angle, CardinalDirection.east.radians); + expect(_spawnEnemy(139).angle, CardinalDirection.north.radians); + expect(_spawnEnemy(140).angle, CardinalDirection.west.radians); + expect(_spawnEnemy(141).angle, CardinalDirection.south.radians); + }); + }); +} + +Enemy _spawnEnemy(int mapId) { + final enemy = Enemy.spawn(mapId, 8.5, 8.5, Difficulty.hard); + expect(enemy, isNotNull); + return enemy!; +} \ No newline at end of file diff --git a/packages/wolf_3d_dart/test/entities/enemy_validation_test.dart b/packages/wolf_3d_dart/test/entities/enemy_validation_test.dart index 1003663..14c5433 100644 --- a/packages/wolf_3d_dart/test/entities/enemy_validation_test.dart +++ b/packages/wolf_3d_dart/test/entities/enemy_validation_test.dart @@ -1,7 +1,7 @@ import 'dart:math' as math; import 'package:test/test.dart'; -import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_type.dart'; +import 'package:wolf_3d_dart/wolf_3d_entities.dart'; void main() { group('Enemy Sprite Range Validation', () { @@ -22,6 +22,16 @@ void main() { // Skip if we are comparing the exact same animation on the same enemy if (enemyA == enemyB && animA == animB) return; + // Dogs intentionally reuse some standing frames as walking frames. + final isDogIdleWalkReuse = + enemyA == EnemyType.dog && + enemyB == EnemyType.dog && + ((animA == EnemyAnimation.idle && + animB == EnemyAnimation.walking) || + (animA == EnemyAnimation.walking && + animB == EnemyAnimation.idle)); + if (isDogIdleWalkReuse) return; + if (rangeA.overlaps(rangeB)) { // Determine the specific frames that are clashing final start = math.max(rangeA.start, rangeB.start);