diff --git a/lib/features/entities/decorative.dart b/lib/features/entities/decorative.dart index 14f95af..4404cb7 100644 --- a/lib/features/entities/decorative.dart +++ b/lib/features/entities/decorative.dart @@ -12,9 +12,13 @@ class Decorative extends Entity { // Checks if the Map ID belongs to a standard decoration static bool isDecoration(int objId) { - if (objId == 124) return true; // Dead guard + // ID 124 is a dead guard in WL1, but an SS guard in WL6. + // However, for spawning purposes, if the SS trySpawn fails, + // we only want to treat it as a decoration if it's not a live actor. + if (objId == 124 || objId == 125) return true; + if (objId >= 23 && objId <= 70) { - // Exclude the collectibles! + // Exclude collectibles defined in MapObject if ((objId >= 43 && objId <= 44) || (objId >= 47 && objId <= 56)) { return false; } @@ -23,9 +27,11 @@ class Decorative extends Entity { return false; } - // Safely calculates the VSWAP sprite index for standard decorations static int getSpriteIndex(int objId) { - if (objId == 124) return 95; // Dead guard + if (objId == 124) return 95; // Dead guard sprite index + if (objId == 125) return 96; // Dead Aardwolf/Other body + + // Standard decorations are typically offset by 21 in the VSWAP return objId - 21; } diff --git a/lib/features/entities/enemies/dog.dart b/lib/features/entities/enemies/dog.dart index 2339f15..4964826 100644 --- a/lib/features/entities/enemies/dog.dart +++ b/lib/features/entities/enemies/dog.dart @@ -20,15 +20,17 @@ class Dog extends Enemy { state: EntityState.idle, ); - static Dog? trySpawn(int objId, double x, double y, Difficulty difficulty) { - // The renderer already checked difficulty, so we just check the ID block! - if (objId >= MapObject.dogStart && objId <= MapObject.dogStart + 17) { + static Dog? trySpawn(int objId, double x, double y, Difficulty _) { + // Dogs span 216 to 251. + if (objId >= MapObject.dogStart && objId <= MapObject.dogStart + 35) { + bool isPatrolling = objId >= MapObject.dogStart + 18; + return Dog( x: x, y: y, angle: MapObject.getAngle(objId), mapId: objId, - ); + )..state = isPatrolling ? EntityState.patrolling : EntityState.idle; } return null; } @@ -61,8 +63,12 @@ class Dog extends Enemy { double diff = angleToPlayer - newAngle; - while (diff <= -math.pi) diff += 2 * math.pi; - while (diff > math.pi) diff -= 2 * math.pi; + while (diff <= -math.pi) { + diff += 2 * math.pi; + } + while (diff > math.pi) { + diff -= 2 * math.pi; + } int octant = ((diff + (math.pi / 8)) / (math.pi / 4)).floor() % 8; if (octant < 0) octant += 8; @@ -92,13 +98,13 @@ class Dog extends Enemy { spriteIndex = 107 + (walkFrame * 8) + octant; if (distance < 1.0 && elapsedMs - lastActionTime > 1000) { - state = EntityState.shooting; + state = EntityState.attacking; lastActionTime = elapsedMs; _hasBittenThisCycle = false; } break; - case EntityState.shooting: + case EntityState.attacking: int timeAttacking = elapsedMs - lastActionTime; if (timeAttacking < 200) { spriteIndex = 139; diff --git a/lib/features/entities/enemies/enemy.dart b/lib/features/entities/enemies/enemy.dart index d911960..95d960f 100644 --- a/lib/features/entities/enemies/enemy.dart +++ b/lib/features/entities/enemies/enemy.dart @@ -38,23 +38,6 @@ abstract class Enemy extends Entity { } } - static double getInitialAngle(int objId) { - int normalizedId = (objId - 108) % 36; - int direction = normalizedId % 4; - switch (direction) { - case 0: - return 0.0; - case 1: - return 3 * math.pi / 2; - case 2: - return math.pi; - case 3: - return math.pi / 2; - default: - return 0.0; - } - } - void checkWakeUp({ required int elapsedMs, required Coordinate2D playerPosition, @@ -187,7 +170,7 @@ abstract class Enemy extends Entity { required int elapsedMs, required Coordinate2D playerPosition, required bool Function(int x, int y) isWalkable, - required void Function(int x, int y) tryOpenDoor, // NEW + required void Function(int x, int y) tryOpenDoor, required void Function(int damage) onDamagePlayer, }); } diff --git a/lib/features/entities/enemies/brown_guard.dart b/lib/features/entities/enemies/guard.dart similarity index 83% rename from lib/features/entities/enemies/brown_guard.dart rename to lib/features/entities/enemies/guard.dart index 725b7aa..796c6b3 100644 --- a/lib/features/entities/enemies/brown_guard.dart +++ b/lib/features/entities/enemies/guard.dart @@ -6,35 +6,35 @@ import 'package:wolf_dart/features/entities/enemies/enemy.dart'; import 'package:wolf_dart/features/entities/entity.dart'; import 'package:wolf_dart/features/entities/map_objects.dart'; -class BrownGuard extends Enemy { +class Guard extends Enemy { static const double speed = 0.03; bool _hasFiredThisCycle = false; - BrownGuard({ + Guard({ required super.x, required super.y, required super.angle, required super.mapId, }) : super( - // Default front-facing idle spriteIndex: 50, state: EntityState.idle, ); - static BrownGuard? trySpawn( - int objId, - double x, - double y, - Difficulty difficulty, - ) { - // Use the range constants we defined in MapObject! - if (objId >= MapObject.guardStart && objId <= MapObject.guardStart + 17) { - return BrownGuard( + static Guard? trySpawn(int objId, double x, double y, Difficulty difficulty) { + // Guards span 108 to 143. (124 and 125 are decorative dead bodies). + if (objId >= MapObject.guardStart && + objId <= MapObject.guardStart + 35 && + objId != 124 && + objId != 125) { + // If the ID is in the second half of the block, it's a Patrolling guard + bool isPatrolling = objId >= MapObject.guardStart + 18; + + return Guard( x: x, y: y, angle: MapObject.getAngle(objId), mapId: objId, - ); + )..state = isPatrolling ? EntityState.patrolling : EntityState.idle; } return null; } @@ -66,8 +66,12 @@ class BrownGuard extends Enemy { // Octant logic (Directional sprites) double diff = angleToPlayer - newAngle; - while (diff <= -math.pi) diff += 2 * math.pi; - while (diff > math.pi) diff -= 2 * math.pi; + while (diff <= -math.pi) { + diff += 2 * math.pi; + } + while (diff > math.pi) { + diff -= 2 * math.pi; + } int octant = ((diff + (math.pi / 8)) / (math.pi / 4)).floor() % 8; if (octant < 0) octant += 8; @@ -96,14 +100,14 @@ class BrownGuard extends Enemy { if (distance < 6.0 && elapsedMs - lastActionTime > 1500) { if (hasLineOfSight(playerPosition, isWalkable)) { - state = EntityState.shooting; + state = EntityState.attacking; lastActionTime = elapsedMs; _hasFiredThisCycle = false; } } break; - case EntityState.shooting: + case EntityState.attacking: int timeShooting = elapsedMs - lastActionTime; if (timeShooting < 150) { spriteIndex = 96; // Aiming diff --git a/lib/features/entities/enemies/mutant.dart b/lib/features/entities/enemies/mutant.dart new file mode 100644 index 0000000..53e8638 --- /dev/null +++ b/lib/features/entities/enemies/mutant.dart @@ -0,0 +1,156 @@ +import 'dart:math' as math; + +import 'package:wolf_dart/classes/coordinate_2d.dart'; +import 'package:wolf_dart/features/difficulty/difficulty.dart'; +import 'package:wolf_dart/features/entities/enemies/enemy.dart'; +import 'package:wolf_dart/features/entities/entity.dart'; +import 'package:wolf_dart/features/entities/map_objects.dart'; + +class Mutant extends Enemy { + static const double speed = 0.045; + static const int _baseSprite = 187; + bool _hasFiredThisCycle = false; + + Mutant({ + required super.x, + required super.y, + required super.angle, + required super.mapId, + }) : super( + spriteIndex: _baseSprite, + state: EntityState.idle, + ) { + health = 45; + damage = 10; + } + + static Mutant? trySpawn( + int objId, + double x, + double y, + Difficulty difficulty, + ) { + // Mutants span 252 to 287 + if (objId >= MapObject.mutantStart && objId <= MapObject.mutantStart + 35) { + bool isPatrolling = objId >= MapObject.mutantStart + 18; + + return Mutant( + x: x, + y: y, + angle: MapObject.getAngle(objId), + mapId: objId, + )..state = isPatrolling ? EntityState.patrolling : EntityState.idle; + } + return null; + } + + @override + ({Coordinate2D movement, double newAngle}) update({ + required int elapsedMs, + required Coordinate2D playerPosition, + required bool Function(int x, int y) isWalkable, + required void Function(int damage) onDamagePlayer, + required void Function(int x, int y) tryOpenDoor, + }) { + Coordinate2D movement = const Coordinate2D(0, 0); + double newAngle = angle; + + // Mutants don't make wake-up noises in the original game! + checkWakeUp( + elapsedMs: elapsedMs, + playerPosition: playerPosition, + isWalkable: isWalkable, + ); + + double distance = position.distanceTo(playerPosition); + double angleToPlayer = position.angleTo(playerPosition); + + if (state != EntityState.idle && state != EntityState.dead) { + newAngle = angleToPlayer; + } + + double diff = angleToPlayer - newAngle; + while (diff <= -math.pi) { + diff += 2 * math.pi; + } + while (diff > math.pi) { + diff -= 2 * math.pi; + } + int octant = ((diff + (math.pi / 8)) / (math.pi / 4)).floor() % 8; + if (octant < 0) octant += 8; + + switch (state) { + case EntityState.idle: + spriteIndex = _baseSprite + octant; + break; + + 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 = (_baseSprite + 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 = _baseSprite + 46; // Aiming + } else if (timeShooting < 300) { + spriteIndex = _baseSprite + 47; // Firing + if (!_hasFiredThisCycle) { + onDamagePlayer(damage); + _hasFiredThisCycle = true; + } + } else if (timeShooting < 450) { + spriteIndex = _baseSprite + 48; // Recoil + } else { + state = EntityState.patrolling; + lastActionTime = elapsedMs; + } + break; + + case EntityState.pain: + spriteIndex = _baseSprite + 44; + if (elapsedMs - lastActionTime > 250) { + state = EntityState.patrolling; + lastActionTime = elapsedMs; + } + break; + + case EntityState.dead: + if (isDying) { + int deathFrame = (elapsedMs - lastActionTime) ~/ 150; + if (deathFrame < 4) { + spriteIndex = (_baseSprite + 40) + deathFrame; + } else { + spriteIndex = _baseSprite + 45; + isDying = false; + } + } else { + spriteIndex = _baseSprite + 45; + } + break; + + default: + break; + } + + return (movement: movement, newAngle: newAngle); + } +} diff --git a/lib/features/entities/enemies/officer.dart b/lib/features/entities/enemies/officer.dart new file mode 100644 index 0000000..a4e307a --- /dev/null +++ b/lib/features/entities/enemies/officer.dart @@ -0,0 +1,156 @@ +import 'dart:math' as math; + +import 'package:wolf_dart/classes/coordinate_2d.dart'; +import 'package:wolf_dart/features/difficulty/difficulty.dart'; +import 'package:wolf_dart/features/entities/enemies/enemy.dart'; +import 'package:wolf_dart/features/entities/entity.dart'; +import 'package:wolf_dart/features/entities/map_objects.dart'; + +class Officer extends Enemy { + static const double speed = 0.055; + static const int _baseSprite = 50; + bool _hasFiredThisCycle = false; + + Officer({ + required super.x, + required super.y, + required super.angle, + required super.mapId, + }) : super( + spriteIndex: _baseSprite, + state: EntityState.idle, + ) { + health = 50; + damage = 15; + } + + static Officer? trySpawn( + int objId, + double x, + double y, + Difficulty difficulty, + ) { + if (objId >= MapObject.officerStart && + objId <= MapObject.officerStart + 35) { + bool isPatrolling = objId >= MapObject.officerStart + 18; + + return Officer( + x: x, + y: y, + angle: MapObject.getAngle(objId), + mapId: objId, + )..state = isPatrolling ? EntityState.patrolling : EntityState.idle; + } + return null; + } + + @override + ({Coordinate2D movement, double newAngle}) update({ + required int elapsedMs, + required Coordinate2D playerPosition, + required bool Function(int x, int y) isWalkable, + required void Function(int damage) onDamagePlayer, + required void Function(int x, int y) tryOpenDoor, + }) { + Coordinate2D movement = const Coordinate2D(0, 0); + double newAngle = angle; + + checkWakeUp( + elapsedMs: elapsedMs, + playerPosition: playerPosition, + isWalkable: isWalkable, + ); + + double distance = position.distanceTo(playerPosition); + double angleToPlayer = position.angleTo(playerPosition); + + if (state != EntityState.idle && state != EntityState.dead) { + newAngle = angleToPlayer; + } + + double diff = angleToPlayer - newAngle; + while (diff <= -math.pi) { + diff += 2 * math.pi; + } + while (diff > math.pi) { + diff -= 2 * math.pi; + } + + int octant = ((diff + (math.pi / 8)) / (math.pi / 4)).floor() % 8; + if (octant < 0) octant += 8; + + switch (state) { + case EntityState.idle: + spriteIndex = _baseSprite + octant; + break; + + 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 = (_baseSprite + 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 = _baseSprite + 46; // Aiming + } else if (timeShooting < 300) { + spriteIndex = _baseSprite + 47; // Firing + if (!_hasFiredThisCycle) { + onDamagePlayer(damage); + _hasFiredThisCycle = true; + } + } else if (timeShooting < 450) { + spriteIndex = _baseSprite + 48; // Recoil + } else { + state = EntityState.patrolling; + lastActionTime = elapsedMs; + } + break; + + case EntityState.pain: + spriteIndex = _baseSprite + 44; + if (elapsedMs - lastActionTime > 250) { + state = EntityState.patrolling; + lastActionTime = elapsedMs; + } + break; + + case EntityState.dead: + if (isDying) { + int deathFrame = (elapsedMs - lastActionTime) ~/ 150; + if (deathFrame < 4) { + spriteIndex = (_baseSprite + 40) + deathFrame; + } else { + spriteIndex = _baseSprite + 45; + isDying = false; + } + } else { + spriteIndex = _baseSprite + 45; + } + break; + + default: + break; + } + + return (movement: movement, newAngle: newAngle); + } +} diff --git a/lib/features/entities/enemies/ss.dart b/lib/features/entities/enemies/ss.dart new file mode 100644 index 0000000..29b0c9c --- /dev/null +++ b/lib/features/entities/enemies/ss.dart @@ -0,0 +1,151 @@ +import 'dart:math' as math; + +import 'package:wolf_dart/classes/coordinate_2d.dart'; +import 'package:wolf_dart/features/difficulty/difficulty.dart'; +import 'package:wolf_dart/features/entities/enemies/enemy.dart'; +import 'package:wolf_dart/features/entities/entity.dart'; +import 'package:wolf_dart/features/entities/map_objects.dart'; + +class SS extends Enemy { + static const double speed = 0.04; + static const int _baseSprite = 138; + bool _hasFiredThisCycle = false; + + SS({ + required super.x, + required super.y, + required super.angle, + required super.mapId, + }) : super( + spriteIndex: _baseSprite, + state: EntityState.idle, + ) { + health = 100; + damage = 20; + } + + static SS? trySpawn(int objId, double x, double y, Difficulty difficulty) { + if (objId >= MapObject.ssStart && objId <= MapObject.ssStart + 35) { + bool isPatrolling = objId >= MapObject.ssStart + 18; + + return SS( + x: x, + y: y, + angle: MapObject.getAngle(objId), + mapId: objId, + )..state = isPatrolling ? EntityState.patrolling : EntityState.idle; + } + return null; + } + + @override + ({Coordinate2D movement, double newAngle}) update({ + required int elapsedMs, + required Coordinate2D playerPosition, + required bool Function(int x, int y) isWalkable, + required void Function(int damage) onDamagePlayer, + required void Function(int x, int y) tryOpenDoor, + }) { + Coordinate2D movement = const Coordinate2D(0, 0); + double newAngle = angle; + + checkWakeUp( + elapsedMs: elapsedMs, + playerPosition: playerPosition, + isWalkable: isWalkable, + ); + + double distance = position.distanceTo(playerPosition); + double angleToPlayer = position.angleTo(playerPosition); + + if (state != EntityState.idle && state != EntityState.dead) { + newAngle = angleToPlayer; + } + + double diff = angleToPlayer - newAngle; + while (diff <= -math.pi) { + diff += 2 * math.pi; + } + while (diff > math.pi) { + diff -= 2 * math.pi; + } + + int octant = ((diff + (math.pi / 8)) / (math.pi / 4)).floor() % 8; + if (octant < 0) octant += 8; + + switch (state) { + case EntityState.idle: + spriteIndex = _baseSprite + octant; + break; + + 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 = (_baseSprite + 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 = _baseSprite + 46; // Aiming + } else if (timeShooting < 200) { + spriteIndex = _baseSprite + 47; // Firing + if (!_hasFiredThisCycle) { + onDamagePlayer(damage); + _hasFiredThisCycle = true; + } + } else if (timeShooting < 300) { + spriteIndex = _baseSprite + 48; // Recoil + } else { + state = EntityState.patrolling; + lastActionTime = elapsedMs; + } + break; + + case EntityState.pain: + spriteIndex = _baseSprite + 44; + if (elapsedMs - lastActionTime > 250) { + state = EntityState.patrolling; + lastActionTime = elapsedMs; + } + break; + + case EntityState.dead: + if (isDying) { + int deathFrame = (elapsedMs - lastActionTime) ~/ 150; + if (deathFrame < 4) { + spriteIndex = (_baseSprite + 40) + deathFrame; + } else { + spriteIndex = _baseSprite + 45; + isDying = false; + } + } else { + spriteIndex = _baseSprite + 45; + } + break; + + default: + break; + } + + return (movement: movement, newAngle: newAngle); + } +} diff --git a/lib/features/entities/entity.dart b/lib/features/entities/entity.dart index f5f9e04..fbdb85c 100644 --- a/lib/features/entities/entity.dart +++ b/lib/features/entities/entity.dart @@ -1,6 +1,6 @@ import 'package:wolf_dart/classes/coordinate_2d.dart'; -enum EntityState { staticObj, idle, patrolling, shooting, pain, dead } +enum EntityState { staticObj, idle, patrolling, attacking, pain, dead } abstract class Entity { double x; diff --git a/lib/features/entities/entity_registry.dart b/lib/features/entities/entity_registry.dart index 2bec763..db4a0b2 100644 --- a/lib/features/entities/entity_registry.dart +++ b/lib/features/entities/entity_registry.dart @@ -1,8 +1,11 @@ import 'package:wolf_dart/features/difficulty/difficulty.dart'; import 'package:wolf_dart/features/entities/collectible.dart'; import 'package:wolf_dart/features/entities/decorative.dart'; -import 'package:wolf_dart/features/entities/enemies/brown_guard.dart'; import 'package:wolf_dart/features/entities/enemies/dog.dart'; +import 'package:wolf_dart/features/entities/enemies/guard.dart'; +import 'package:wolf_dart/features/entities/enemies/mutant.dart'; +import 'package:wolf_dart/features/entities/enemies/officer.dart'; +import 'package:wolf_dart/features/entities/enemies/ss.dart'; import 'package:wolf_dart/features/entities/entity.dart'; import 'package:wolf_dart/features/entities/map_objects.dart'; @@ -16,8 +19,14 @@ typedef EntitySpawner = abstract class EntityRegistry { static final List _spawners = [ - BrownGuard.trySpawn, + // Enemies need to try to spawn first + Guard.trySpawn, + Officer.trySpawn, + SS.trySpawn, + Mutant.trySpawn, Dog.trySpawn, + + // Everything else Collectible.trySpawn, Decorative.trySpawn, ]; @@ -32,17 +41,22 @@ abstract class EntityRegistry { // 1. Difficulty check before even looking for a spawner if (!MapObject.shouldSpawn(objId, difficulty)) return null; + if (objId == 0) return null; + for (final spawner in _spawners) { Entity? entity = spawner(objId, x, y, difficulty); if (entity != null) { // Safety bounds check for the VSWAP array if (entity.spriteIndex >= 0 && entity.spriteIndex < maxSprites) { + print("Spawned entity with objId $objId"); return entity; } + print("VSWAP doesn't have this sprite! objId $objId"); return null; // VSWAP doesn't have this sprite! } } + print("No class claimed this Map ID > objId $objId"); return null; // No class claimed this Map ID } } diff --git a/lib/features/entities/map_objects.dart b/lib/features/entities/map_objects.dart index 799c430..1213b7e 100644 --- a/lib/features/entities/map_objects.dart +++ b/lib/features/entities/map_objects.dart @@ -65,13 +65,6 @@ abstract class MapObject { static const int secretExitTrigger = 99; static const int normalExitTrigger = 100; - // --- Enemy Base IDs (Easy, North) --- - static const int _guardBase = 108; - static const int _officerBase = 126; // WL6 only - static const int _ssBase = 144; // WL6 only - static const int _dogBase = 162; - static const int _mutantBase = 180; // Episode 2+ - // Bosses (Shared between WL1 and WL6) static const int bossHansGrosse = 214; @@ -87,55 +80,38 @@ abstract class MapObject { static const int bossFettgesicht = 223; // --- Enemy Range Constants --- - static const int guardStart = 108; - static const int officerStart = 126; - static const int ssStart = 144; - static const int dogStart = 162; - static const int mutantStart = 180; + static const int guardStart = 108; // 108-143 + static const int officerStart = 144; // 144-179 (WL6) + static const int ssStart = 180; // 180-215 (WL6) + static const int dogStart = 216; // 216-251 + static const int mutantStart = 252; // 252-287 (WL6) + + // --- Missing Decorative Bodies --- + static const int deadGuard = 124; // Decorative only in WL1 + static const int deadAardwolf = 125; // Decorative only in WL1 + /// Returns true if the object ID exists in the Shareware version. /// Returns true if the object ID exists in the Shareware version. static bool isSharewareCompatible(int id) { - // WL1 only had Guards (108-125), Dogs (162-179), and Hans Grosse (214) - if (id >= 126 && id < 162) return false; // No Officers or SS - if (id >= 180 && id < 214) return false; // No Mutants - if (id > 214) return false; // No other bosses - return true; + // Standard Decorations & Collectibles + if (id <= vines) return true; + + // Logic Triggers (Exits/Pushwalls) + if (id >= pushwallTrigger && id <= normalExitTrigger) return true; + + // Guards (108-143 includes dead bodies at 124/125) + if (id >= guardStart && id < officerStart) return true; + + // Dogs (216-251) - These ARE in Shareware! + if (id >= dogStart && id < mutantStart) return true; + + // Episode 1 Boss + if (id == bossHansGrosse) return true; + + return false; } - /// Resolves which enemy type a map ID belongs to. - static String getEnemyType(int id) { - if (id >= 108 && id <= 125) return "Guard"; - if (id >= 126 && id <= 143) return "Officer"; - if (id >= 144 && id <= 161) return "SS"; - if (id >= 162 && id <= 179) return "Dog"; - if (id >= 180 && id <= 197) return "Mutant"; - return "Unknown"; - } - - /// Checks if an object should be spawned based on chosen difficulty. - static bool shouldSpawn(int id, Difficulty selectedDifficulty) { - if (id < 108 || id > 213) return true; // Items/Players/Bosses always spawn - - // Enemy blocks are 18 IDs wide (e.g., 108-125 for Guards) - int relativeId = (id - 108) % 18; - - // 0-3: Easy, 4-7: Medium, 8-11: Hard - if (relativeId < 4) return true; // Easy spawns on everything - if (relativeId < 8) { - return selectedDifficulty != Difficulty.canIPlayDaddy; // Medium/Hard - } - if (relativeId < 12) { - return selectedDifficulty == Difficulty.iAmDeathIncarnate; // Hard only - } - - // 12-15 are typically "Ambush" versions of the Easy/Medium/Hard guards - return true; - } - - /// Determines the spawn orientation of an enemy or player. - /// Determines the spawn orientation of an enemy or player. static double getAngle(int id) { - // Player spawn angles switch (id) { case playerNorth: return CardinalDirection.north.radians; @@ -147,9 +123,52 @@ abstract class MapObject { return CardinalDirection.west.radians; } - if (id < 108 || id > 213) return 0.0; + // FIX: Expand the boundary to include ALL enemies (Dogs and Mutants) + if (id < guardStart || id > (mutantStart + 35)) return 0.0; - // Enemy directions are mapped in groups of 4 - return CardinalDirection.fromEnemyIndex(id - 108).radians; + int baseId; + if (id >= mutantStart) { + baseId = mutantStart; + } else if (id >= dogStart) { + baseId = dogStart; + } else if (id >= ssStart) { + baseId = ssStart; + } else if (id >= officerStart) { + baseId = officerStart; + } else { + baseId = guardStart; + } + + // FIX: Normalize patrolling enemies back to the standing block, THEN get the 4-way angle + int directionIndex = ((id - baseId) % 18) % 4; + return CardinalDirection.fromEnemyIndex(directionIndex).radians; + } + + static bool shouldSpawn(int id, Difficulty selectedDifficulty) { + // FIX: Expand the boundary so Dogs and Mutants aren't bypassing difficulty checks + if (id < guardStart || id > (mutantStart + 35)) return true; + + int baseId; + if (id >= mutantStart) { + baseId = mutantStart; + } else if (id >= dogStart) { + baseId = dogStart; + } else if (id >= ssStart) { + baseId = ssStart; + } else if (id >= officerStart) { + baseId = officerStart; + } else { + baseId = guardStart; + } + + int relativeId = (id - baseId) % 18; + + return switch (relativeId) { + < 4 => true, + < 8 => selectedDifficulty.level >= Difficulty.dontHurtMe.level, + < 12 => selectedDifficulty.level >= Difficulty.bringEmOn.level, + < 16 => selectedDifficulty.level >= Difficulty.iAmDeathIncarnate.level, + _ => true, + }; } } diff --git a/lib/features/map/wolf_map.dart b/lib/features/map/wolf_map.dart index c1a7e08..c31548a 100644 --- a/lib/features/map/wolf_map.dart +++ b/lib/features/map/wolf_map.dart @@ -1,8 +1,8 @@ import 'package:flutter/services.dart'; import 'package:wolf_dart/classes/matrix.dart'; -import 'package:wolf_dart/features/map/vswap_parser.dart'; import 'package:wolf_dart/features/map/wolf_level.dart'; import 'package:wolf_dart/features/map/wolf_map_parser.dart'; +import 'package:wolf_dart/vswap_parser.dart'; class WolfMap { /// The fully parsed and decompressed levels from the game files. diff --git a/lib/features/map/vswap_parser.dart b/lib/vswap_parser.dart similarity index 75% rename from lib/features/map/vswap_parser.dart rename to lib/vswap_parser.dart index bc96e78..3805d26 100644 --- a/lib/features/map/vswap_parser.dart +++ b/lib/vswap_parser.dart @@ -101,4 +101,43 @@ class VswapParser { } return sprites; } + + /// Extracts digitized sound effects (PCM Audio) from VSWAP.WL1 + static List parseSounds(ByteData vswap) { + int chunks = vswap.getUint16(0, Endian.little); + int soundStart = vswap.getUint16(4, Endian.little); + + List offsets = []; + List lengths = []; + + // Offsets are 32-bit integers starting at byte 6 + for (int i = 0; i < chunks; i++) { + offsets.add(vswap.getUint32(6 + (i * 4), Endian.little)); + } + + // Lengths are 16-bit integers immediately following the offset array + int lengthStart = 6 + (chunks * 4); + for (int i = 0; i < chunks; i++) { + lengths.add(vswap.getUint16(lengthStart + (i * 2), Endian.little)); + } + + List sounds = []; + + // Sounds start after the sprites and go to the end of the chunks + for (int i = soundStart; i < chunks; i++) { + int offset = offsets[i]; + int length = lengths[i]; + + if (offset == 0 || length == 0) { + sounds.add(Uint8List(0)); // Empty placeholder + continue; + } + + // Extract the raw 8-bit PCM audio bytes + final soundData = vswap.buffer.asUint8List(offset, length); + sounds.add(soundData); + } + + return sounds; + } }