Adding more enemy types

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-14 19:19:45 +01:00
parent 278c73a256
commit 9915aec674
12 changed files with 638 additions and 104 deletions

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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,
});
}

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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<T> {
double x;

View File

@@ -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<EntitySpawner> _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
}
}

View File

@@ -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,
};
}
}

View File

@@ -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.

View File

@@ -101,4 +101,43 @@ class VswapParser {
}
return sprites;
}
/// Extracts digitized sound effects (PCM Audio) from VSWAP.WL1
static List<Uint8List> parseSounds(ByteData vswap) {
int chunks = vswap.getUint16(0, Endian.little);
int soundStart = vswap.getUint16(4, Endian.little);
List<int> offsets = [];
List<int> 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<Uint8List> 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;
}
}