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 // Checks if the Map ID belongs to a standard decoration
static bool isDecoration(int objId) { 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) { if (objId >= 23 && objId <= 70) {
// Exclude the collectibles! // Exclude collectibles defined in MapObject
if ((objId >= 43 && objId <= 44) || (objId >= 47 && objId <= 56)) { if ((objId >= 43 && objId <= 44) || (objId >= 47 && objId <= 56)) {
return false; return false;
} }
@@ -23,9 +27,11 @@ class Decorative extends Entity {
return false; return false;
} }
// Safely calculates the VSWAP sprite index for standard decorations
static int getSpriteIndex(int objId) { 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; return objId - 21;
} }

View File

@@ -20,15 +20,17 @@ class Dog extends Enemy {
state: EntityState.idle, state: EntityState.idle,
); );
static Dog? trySpawn(int objId, double x, double y, Difficulty difficulty) { static Dog? trySpawn(int objId, double x, double y, Difficulty _) {
// The renderer already checked difficulty, so we just check the ID block! // Dogs span 216 to 251.
if (objId >= MapObject.dogStart && objId <= MapObject.dogStart + 17) { if (objId >= MapObject.dogStart && objId <= MapObject.dogStart + 35) {
bool isPatrolling = objId >= MapObject.dogStart + 18;
return Dog( return Dog(
x: x, x: x,
y: y, y: y,
angle: MapObject.getAngle(objId), angle: MapObject.getAngle(objId),
mapId: objId, mapId: objId,
); )..state = isPatrolling ? EntityState.patrolling : EntityState.idle;
} }
return null; return null;
} }
@@ -61,8 +63,12 @@ class Dog extends Enemy {
double diff = angleToPlayer - newAngle; double diff = angleToPlayer - newAngle;
while (diff <= -math.pi) diff += 2 * math.pi; while (diff <= -math.pi) {
while (diff > math.pi) diff -= 2 * 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; int octant = ((diff + (math.pi / 8)) / (math.pi / 4)).floor() % 8;
if (octant < 0) octant += 8; if (octant < 0) octant += 8;
@@ -92,13 +98,13 @@ class Dog extends Enemy {
spriteIndex = 107 + (walkFrame * 8) + octant; spriteIndex = 107 + (walkFrame * 8) + octant;
if (distance < 1.0 && elapsedMs - lastActionTime > 1000) { if (distance < 1.0 && elapsedMs - lastActionTime > 1000) {
state = EntityState.shooting; state = EntityState.attacking;
lastActionTime = elapsedMs; lastActionTime = elapsedMs;
_hasBittenThisCycle = false; _hasBittenThisCycle = false;
} }
break; break;
case EntityState.shooting: case EntityState.attacking:
int timeAttacking = elapsedMs - lastActionTime; int timeAttacking = elapsedMs - lastActionTime;
if (timeAttacking < 200) { if (timeAttacking < 200) {
spriteIndex = 139; 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({ void checkWakeUp({
required int elapsedMs, required int elapsedMs,
required Coordinate2D playerPosition, required Coordinate2D playerPosition,
@@ -187,7 +170,7 @@ abstract class Enemy extends Entity {
required int elapsedMs, required int elapsedMs,
required Coordinate2D playerPosition, required Coordinate2D playerPosition,
required bool Function(int x, int y) isWalkable, 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, 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/entity.dart';
import 'package:wolf_dart/features/entities/map_objects.dart'; import 'package:wolf_dart/features/entities/map_objects.dart';
class BrownGuard extends Enemy { class Guard extends Enemy {
static const double speed = 0.03; static const double speed = 0.03;
bool _hasFiredThisCycle = false; bool _hasFiredThisCycle = false;
BrownGuard({ Guard({
required super.x, required super.x,
required super.y, required super.y,
required super.angle, required super.angle,
required super.mapId, required super.mapId,
}) : super( }) : super(
// Default front-facing idle
spriteIndex: 50, spriteIndex: 50,
state: EntityState.idle, state: EntityState.idle,
); );
static BrownGuard? trySpawn( static Guard? trySpawn(int objId, double x, double y, Difficulty difficulty) {
int objId, // Guards span 108 to 143. (124 and 125 are decorative dead bodies).
double x, if (objId >= MapObject.guardStart &&
double y, objId <= MapObject.guardStart + 35 &&
Difficulty difficulty, objId != 124 &&
) { objId != 125) {
// Use the range constants we defined in MapObject! // If the ID is in the second half of the block, it's a Patrolling guard
if (objId >= MapObject.guardStart && objId <= MapObject.guardStart + 17) { bool isPatrolling = objId >= MapObject.guardStart + 18;
return BrownGuard(
return Guard(
x: x, x: x,
y: y, y: y,
angle: MapObject.getAngle(objId), angle: MapObject.getAngle(objId),
mapId: objId, mapId: objId,
); )..state = isPatrolling ? EntityState.patrolling : EntityState.idle;
} }
return null; return null;
} }
@@ -66,8 +66,12 @@ class BrownGuard extends Enemy {
// Octant logic (Directional sprites) // Octant logic (Directional sprites)
double diff = angleToPlayer - newAngle; double diff = angleToPlayer - newAngle;
while (diff <= -math.pi) diff += 2 * math.pi; while (diff <= -math.pi) {
while (diff > math.pi) diff -= 2 * 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; int octant = ((diff + (math.pi / 8)) / (math.pi / 4)).floor() % 8;
if (octant < 0) octant += 8; if (octant < 0) octant += 8;
@@ -96,14 +100,14 @@ class BrownGuard extends Enemy {
if (distance < 6.0 && elapsedMs - lastActionTime > 1500) { if (distance < 6.0 && elapsedMs - lastActionTime > 1500) {
if (hasLineOfSight(playerPosition, isWalkable)) { if (hasLineOfSight(playerPosition, isWalkable)) {
state = EntityState.shooting; state = EntityState.attacking;
lastActionTime = elapsedMs; lastActionTime = elapsedMs;
_hasFiredThisCycle = false; _hasFiredThisCycle = false;
} }
} }
break; break;
case EntityState.shooting: case EntityState.attacking:
int timeShooting = elapsedMs - lastActionTime; int timeShooting = elapsedMs - lastActionTime;
if (timeShooting < 150) { if (timeShooting < 150) {
spriteIndex = 96; // Aiming 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'; 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> { abstract class Entity<T> {
double x; double x;

View File

@@ -1,8 +1,11 @@
import 'package:wolf_dart/features/difficulty/difficulty.dart'; import 'package:wolf_dart/features/difficulty/difficulty.dart';
import 'package:wolf_dart/features/entities/collectible.dart'; import 'package:wolf_dart/features/entities/collectible.dart';
import 'package:wolf_dart/features/entities/decorative.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/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/entity.dart';
import 'package:wolf_dart/features/entities/map_objects.dart'; import 'package:wolf_dart/features/entities/map_objects.dart';
@@ -16,8 +19,14 @@ typedef EntitySpawner =
abstract class EntityRegistry { abstract class EntityRegistry {
static final List<EntitySpawner> _spawners = [ static final List<EntitySpawner> _spawners = [
BrownGuard.trySpawn, // Enemies need to try to spawn first
Guard.trySpawn,
Officer.trySpawn,
SS.trySpawn,
Mutant.trySpawn,
Dog.trySpawn, Dog.trySpawn,
// Everything else
Collectible.trySpawn, Collectible.trySpawn,
Decorative.trySpawn, Decorative.trySpawn,
]; ];
@@ -32,17 +41,22 @@ abstract class EntityRegistry {
// 1. Difficulty check before even looking for a spawner // 1. Difficulty check before even looking for a spawner
if (!MapObject.shouldSpawn(objId, difficulty)) return null; if (!MapObject.shouldSpawn(objId, difficulty)) return null;
if (objId == 0) return null;
for (final spawner in _spawners) { for (final spawner in _spawners) {
Entity? entity = spawner(objId, x, y, difficulty); Entity? entity = spawner(objId, x, y, difficulty);
if (entity != null) { if (entity != null) {
// Safety bounds check for the VSWAP array // Safety bounds check for the VSWAP array
if (entity.spriteIndex >= 0 && entity.spriteIndex < maxSprites) { if (entity.spriteIndex >= 0 && entity.spriteIndex < maxSprites) {
print("Spawned entity with objId $objId");
return entity; return entity;
} }
print("VSWAP doesn't have this sprite! objId $objId");
return null; // VSWAP doesn't have this sprite! 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 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 secretExitTrigger = 99;
static const int normalExitTrigger = 100; 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) // Bosses (Shared between WL1 and WL6)
static const int bossHansGrosse = 214; static const int bossHansGrosse = 214;
@@ -87,55 +80,38 @@ abstract class MapObject {
static const int bossFettgesicht = 223; static const int bossFettgesicht = 223;
// --- Enemy Range Constants --- // --- Enemy Range Constants ---
static const int guardStart = 108; static const int guardStart = 108; // 108-143
static const int officerStart = 126; static const int officerStart = 144; // 144-179 (WL6)
static const int ssStart = 144; static const int ssStart = 180; // 180-215 (WL6)
static const int dogStart = 162; static const int dogStart = 216; // 216-251
static const int mutantStart = 180; 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. /// Returns true if the object ID exists in the Shareware version.
static bool isSharewareCompatible(int id) { static bool isSharewareCompatible(int id) {
// WL1 only had Guards (108-125), Dogs (162-179), and Hans Grosse (214) // Standard Decorations & Collectibles
if (id >= 126 && id < 162) return false; // No Officers or SS if (id <= vines) return true;
if (id >= 180 && id < 214) return false; // No Mutants
if (id > 214) return false; // No other bosses // Logic Triggers (Exits/Pushwalls)
return true; 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) { static double getAngle(int id) {
// Player spawn angles
switch (id) { switch (id) {
case playerNorth: case playerNorth:
return CardinalDirection.north.radians; return CardinalDirection.north.radians;
@@ -147,9 +123,52 @@ abstract class MapObject {
return CardinalDirection.west.radians; 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 int baseId;
return CardinalDirection.fromEnemyIndex(id - 108).radians; 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:flutter/services.dart';
import 'package:wolf_dart/classes/matrix.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_level.dart';
import 'package:wolf_dart/features/map/wolf_map_parser.dart'; import 'package:wolf_dart/features/map/wolf_map_parser.dart';
import 'package:wolf_dart/vswap_parser.dart';
class WolfMap { class WolfMap {
/// The fully parsed and decompressed levels from the game files. /// The fully parsed and decompressed levels from the game files.

View File

@@ -101,4 +101,43 @@ class VswapParser {
} }
return sprites; 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;
}
} }