Working on fixing enemy identification

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-14 21:06:31 +01:00
parent 12e2e7e3a8
commit 1ec891d9a0
18 changed files with 293 additions and 292 deletions

View File

@@ -16,14 +16,13 @@ class Dog extends Enemy {
required super.angle,
required super.mapId,
}) : super(
spriteIndex: 99,
spriteIndex: EnemyType.dog.spriteBaseIdx,
state: EntityState.idle,
);
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;
if (EnemyType.dog.claimsMapId(objId)) {
bool isPatrolling = objId >= EnemyType.dog.mapBaseId + 18;
return Dog(
x: x,

View File

@@ -3,6 +3,49 @@ import 'dart:math' as math;
import 'package:wolf_dart/classes/coordinate_2d.dart';
import 'package:wolf_dart/features/entities/entity.dart';
enum EnemyType {
guard(mapBaseId: 108, spriteBaseIdx: 50),
dog(mapBaseId: 216, spriteBaseIdx: 99),
ss(mapBaseId: 180, spriteBaseIdx: 138),
mutant(mapBaseId: 252, spriteBaseIdx: 187),
officer(mapBaseId: 144, spriteBaseIdx: 238),
;
final int mapBaseId;
final int spriteBaseIdx;
const EnemyType({
required this.mapBaseId,
required this.spriteBaseIdx,
});
/// Helper to check if a specific TED5 Map ID belongs to this enemy
bool claimsMapId(int id) => id >= mapBaseId && id <= mapBaseId + 35;
/// Helper to find which EnemyType a given Map ID belongs to
static EnemyType? fromMapId(int id) {
for (final type in EnemyType.values) {
if (type.claimsMapId(id)) return type;
}
return null;
}
bool claimsSpriteIndex(int index) {
return switch (this) {
// Walk, Action, & Death: 50-98
EnemyType.guard => index >= 50 && index <= 98,
// Walk, Action, & Death: 99-137
EnemyType.dog => index >= 99 && index <= 137,
// Walk, Action, & Death: 138-186
EnemyType.ss => index >= 138 && index <= 186,
// Walk, Action, & Death: 187-237
EnemyType.mutant => index >= 187 && index <= 237,
// Walk, Action, & Death: 238-287
EnemyType.officer => index >= 238 && index <= 287,
};
}
}
abstract class Enemy extends Entity {
Enemy({
required super.x,

View File

@@ -16,18 +16,13 @@ class Guard extends Enemy {
required super.angle,
required super.mapId,
}) : super(
spriteIndex: 50,
spriteIndex: EnemyType.guard.spriteBaseIdx,
state: EntityState.idle,
);
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;
static Guard? trySpawn(int objId, double x, double y, Difficulty _) {
if (EnemyType.guard.claimsMapId(objId) && objId != 124 && objId != 125) {
bool isPatrolling = objId >= EnemyType.guard.mapBaseId + 18;
return Guard(
x: x,
@@ -110,15 +105,15 @@ class Guard extends Enemy {
case EntityState.attacking:
int timeShooting = elapsedMs - lastActionTime;
if (timeShooting < 150) {
spriteIndex = 96; // Aiming
spriteIndex = 90; // Aiming
} else if (timeShooting < 300) {
spriteIndex = 97; // Firing
spriteIndex = 91; // Firing
if (!_hasFiredThisCycle) {
onDamagePlayer(10);
_hasFiredThisCycle = true;
}
} else if (timeShooting < 450) {
spriteIndex = 98; // Recoil
spriteIndex = 90; // Recoil (back to aim pose)
} else {
state = EntityState.patrolling;
lastActionTime = elapsedMs;
@@ -126,8 +121,7 @@ class Guard extends Enemy {
break;
case EntityState.pain:
spriteIndex = 94; // Ouch frame
// Stay in pain for a brief moment, then resume attacking
spriteIndex = 92; // Ouch frame
if (elapsedMs - lastActionTime > 250) {
state = EntityState.patrolling;
lastActionTime = elapsedMs;
@@ -137,15 +131,14 @@ class Guard extends Enemy {
case EntityState.dead:
if (isDying) {
int deathFrame = (elapsedMs - lastActionTime) ~/ 150;
if (deathFrame < 4) {
// FIX: Removed the buggy "- 1"
spriteIndex = 90 + deathFrame;
if (deathFrame < 3) {
spriteIndex = 93 + deathFrame; // Cycles 93, 94, 95
} else {
spriteIndex = 95; // Final dead frame
isDying = false;
}
} else {
spriteIndex = 95;
spriteIndex = 95; // Final dead frame
}
break;

View File

@@ -8,7 +8,6 @@ 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({
@@ -17,22 +16,16 @@ class Mutant extends Enemy {
required super.angle,
required super.mapId,
}) : super(
spriteIndex: _baseSprite,
spriteIndex: EnemyType.mutant.spriteBaseIdx,
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;
static Mutant? trySpawn(int objId, double x, double y, Difficulty _) {
if (EnemyType.mutant.claimsMapId(objId)) {
bool isPatrolling = objId >= EnemyType.mutant.mapBaseId + 18;
return Mutant(
x: x,
@@ -81,7 +74,7 @@ class Mutant extends Enemy {
switch (state) {
case EntityState.idle:
spriteIndex = _baseSprite + octant;
spriteIndex = EnemyType.mutant.spriteBaseIdx + octant;
break;
case EntityState.patrolling:
@@ -96,7 +89,8 @@ class Mutant extends Enemy {
}
int walkFrame = (elapsedMs ~/ 150) % 4;
spriteIndex = (_baseSprite + 8) + (walkFrame * 8) + octant;
spriteIndex =
(EnemyType.mutant.spriteBaseIdx + 8) + (walkFrame * 8) + octant;
if (distance < 6.0 && elapsedMs - lastActionTime > 1000) {
if (hasLineOfSight(playerPosition, isWalkable)) {
@@ -110,15 +104,15 @@ class Mutant extends Enemy {
case EntityState.attacking:
int timeShooting = elapsedMs - lastActionTime;
if (timeShooting < 150) {
spriteIndex = _baseSprite + 46; // Aiming
spriteIndex = EnemyType.mutant.spriteBaseIdx + 46; // Aiming
} else if (timeShooting < 300) {
spriteIndex = _baseSprite + 47; // Firing
spriteIndex = EnemyType.mutant.spriteBaseIdx + 47; // Firing
if (!_hasFiredThisCycle) {
onDamagePlayer(damage);
_hasFiredThisCycle = true;
}
} else if (timeShooting < 450) {
spriteIndex = _baseSprite + 48; // Recoil
spriteIndex = EnemyType.mutant.spriteBaseIdx + 48; // Recoil
} else {
state = EntityState.patrolling;
lastActionTime = elapsedMs;
@@ -126,7 +120,7 @@ class Mutant extends Enemy {
break;
case EntityState.pain:
spriteIndex = _baseSprite + 44;
spriteIndex = EnemyType.mutant.spriteBaseIdx + 44;
if (elapsedMs - lastActionTime > 250) {
state = EntityState.patrolling;
lastActionTime = elapsedMs;
@@ -137,13 +131,13 @@ class Mutant extends Enemy {
if (isDying) {
int deathFrame = (elapsedMs - lastActionTime) ~/ 150;
if (deathFrame < 4) {
spriteIndex = (_baseSprite + 40) + deathFrame;
spriteIndex = (EnemyType.mutant.spriteBaseIdx + 40) + deathFrame;
} else {
spriteIndex = _baseSprite + 45;
spriteIndex = EnemyType.mutant.spriteBaseIdx + 45;
isDying = false;
}
} else {
spriteIndex = _baseSprite + 45;
spriteIndex = EnemyType.mutant.spriteBaseIdx + 45;
}
break;

View File

@@ -8,7 +8,6 @@ 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({
@@ -17,22 +16,16 @@ class Officer extends Enemy {
required super.angle,
required super.mapId,
}) : super(
spriteIndex: _baseSprite,
spriteIndex: EnemyType.officer.spriteBaseIdx,
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;
static Officer? trySpawn(int objId, double x, double y, Difficulty _) {
if (EnemyType.officer.claimsMapId(objId)) {
bool isPatrolling = objId >= EnemyType.officer.mapBaseId + 18;
return Officer(
x: x,
@@ -81,7 +74,7 @@ class Officer extends Enemy {
switch (state) {
case EntityState.idle:
spriteIndex = _baseSprite + octant;
spriteIndex = EnemyType.officer.spriteBaseIdx + octant;
break;
case EntityState.patrolling:
@@ -96,7 +89,8 @@ class Officer extends Enemy {
}
int walkFrame = (elapsedMs ~/ 150) % 4;
spriteIndex = (_baseSprite + 8) + (walkFrame * 8) + octant;
spriteIndex =
(EnemyType.officer.spriteBaseIdx + 8) + (walkFrame * 8) + octant;
if (distance < 6.0 && elapsedMs - lastActionTime > 1000) {
if (hasLineOfSight(playerPosition, isWalkable)) {
@@ -110,15 +104,15 @@ class Officer extends Enemy {
case EntityState.attacking:
int timeShooting = elapsedMs - lastActionTime;
if (timeShooting < 150) {
spriteIndex = _baseSprite + 46; // Aiming
spriteIndex = EnemyType.officer.spriteBaseIdx + 40; // Aiming
} else if (timeShooting < 300) {
spriteIndex = _baseSprite + 47; // Firing
spriteIndex = EnemyType.officer.spriteBaseIdx + 41; // Firing
if (!_hasFiredThisCycle) {
onDamagePlayer(damage);
_hasFiredThisCycle = true;
}
} else if (timeShooting < 450) {
spriteIndex = _baseSprite + 48; // Recoil
spriteIndex = EnemyType.officer.spriteBaseIdx + 40; // Recoil
} else {
state = EntityState.patrolling;
lastActionTime = elapsedMs;
@@ -126,7 +120,7 @@ class Officer extends Enemy {
break;
case EntityState.pain:
spriteIndex = _baseSprite + 44;
spriteIndex = EnemyType.officer.spriteBaseIdx + 42;
if (elapsedMs - lastActionTime > 250) {
state = EntityState.patrolling;
lastActionTime = elapsedMs;
@@ -136,14 +130,14 @@ class Officer extends Enemy {
case EntityState.dead:
if (isDying) {
int deathFrame = (elapsedMs - lastActionTime) ~/ 150;
if (deathFrame < 4) {
spriteIndex = (_baseSprite + 40) + deathFrame;
if (deathFrame < 3) {
spriteIndex = (EnemyType.officer.spriteBaseIdx + 43) + deathFrame;
} else {
spriteIndex = _baseSprite + 45;
spriteIndex = EnemyType.officer.spriteBaseIdx + 45;
isDying = false;
}
} else {
spriteIndex = _baseSprite + 45;
spriteIndex = EnemyType.officer.spriteBaseIdx + 45;
}
break;

View File

@@ -8,7 +8,6 @@ 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({
@@ -17,16 +16,16 @@ class SS extends Enemy {
required super.angle,
required super.mapId,
}) : super(
spriteIndex: _baseSprite,
spriteIndex: EnemyType.ss.spriteBaseIdx,
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;
static SS? trySpawn(int objId, double x, double y, Difficulty _) {
if (EnemyType.ss.claimsMapId(objId)) {
bool isPatrolling = objId >= EnemyType.ss.mapBaseId + 18;
return SS(
x: x,
@@ -75,7 +74,7 @@ class SS extends Enemy {
switch (state) {
case EntityState.idle:
spriteIndex = _baseSprite + octant;
spriteIndex = EnemyType.ss.spriteBaseIdx + octant;
break;
case EntityState.patrolling:
@@ -90,7 +89,8 @@ class SS extends Enemy {
}
int walkFrame = (elapsedMs ~/ 150) % 4;
spriteIndex = (_baseSprite + 8) + (walkFrame * 8) + octant;
spriteIndex =
(EnemyType.ss.spriteBaseIdx + 8) + (walkFrame * 8) + octant;
if (distance < 6.0 && elapsedMs - lastActionTime > 1500) {
if (hasLineOfSight(playerPosition, isWalkable)) {
@@ -105,15 +105,15 @@ class SS extends Enemy {
// SS machine gun fires much faster than a standard pistol!
int timeShooting = elapsedMs - lastActionTime;
if (timeShooting < 100) {
spriteIndex = _baseSprite + 46; // Aiming
spriteIndex = EnemyType.ss.spriteBaseIdx + 46; // Aiming
} else if (timeShooting < 200) {
spriteIndex = _baseSprite + 47; // Firing
spriteIndex = EnemyType.ss.spriteBaseIdx + 47; // Firing
if (!_hasFiredThisCycle) {
onDamagePlayer(damage);
_hasFiredThisCycle = true;
}
} else if (timeShooting < 300) {
spriteIndex = _baseSprite + 48; // Recoil
spriteIndex = EnemyType.ss.spriteBaseIdx + 48; // Recoil
} else {
state = EntityState.patrolling;
lastActionTime = elapsedMs;
@@ -121,7 +121,7 @@ class SS extends Enemy {
break;
case EntityState.pain:
spriteIndex = _baseSprite + 44;
spriteIndex = EnemyType.ss.spriteBaseIdx + 44;
if (elapsedMs - lastActionTime > 250) {
state = EntityState.patrolling;
lastActionTime = elapsedMs;
@@ -132,13 +132,13 @@ class SS extends Enemy {
if (isDying) {
int deathFrame = (elapsedMs - lastActionTime) ~/ 150;
if (deathFrame < 4) {
spriteIndex = (_baseSprite + 40) + deathFrame;
spriteIndex = (EnemyType.ss.spriteBaseIdx + 40) + deathFrame;
} else {
spriteIndex = _baseSprite + 45;
spriteIndex = EnemyType.ss.spriteBaseIdx + 45;
isDying = false;
}
} else {
spriteIndex = _baseSprite + 45;
spriteIndex = EnemyType.ss.spriteBaseIdx + 45;
}
break;

View File

@@ -3,6 +3,7 @@ import 'package:wolf_dart/features/entities/collectible.dart';
import 'package:wolf_dart/features/entities/decorative.dart';
import 'package:wolf_dart/features/entities/enemies/bosses/hans_grosse.dart';
import 'package:wolf_dart/features/entities/enemies/dog.dart';
import 'package:wolf_dart/features/entities/enemies/enemy.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';
@@ -50,6 +51,11 @@ abstract class EntityRegistry {
for (final spawner in _spawners) {
Entity? entity = spawner(objId, x, y, difficulty);
final EnemyType? type = EnemyType.fromMapId(objId);
if (type != null) {
print("Spawning ${type.name} enemy");
}
if (entity != null) {
// Safety bounds check for the VSWAP array
if (entity.spriteIndex >= 0 && entity.spriteIndex < maxSprites) {

View File

@@ -1,5 +1,6 @@
import 'package:wolf_dart/classes/cardinal_direction.dart';
import 'package:wolf_dart/features/difficulty/difficulty.dart';
import 'package:wolf_dart/features/entities/enemies/enemy.dart';
abstract class MapObject {
// --- Player Spawns ---
@@ -81,10 +82,10 @@ abstract class MapObject {
// --- Enemy Range Constants ---
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 officerStart = 144; // 144-179
static const int ssStart = 180; // 180-215
static const int dogStart = 216; // 216-251
static const int mutantStart = 252; // 252-287 (WL6)
static const int mutantStart = 252; // 252-287
// --- Missing Decorative Bodies ---
static const int deadGuard = 124; // Decorative only in WL1
@@ -112,6 +113,7 @@ abstract class MapObject {
}
static double getAngle(int id) {
// Player spawn
switch (id) {
case playerNorth:
return CardinalDirection.north.radians;
@@ -123,52 +125,34 @@ abstract class MapObject {
return CardinalDirection.west.radians;
}
// FIX: Expand the boundary to include ALL enemies (Dogs and Mutants)
if (id < guardStart || id > (mutantStart + 35)) return 0.0;
// Boss check
if (id == bossHansGrosse) return 0.0;
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;
}
final EnemyType? type = EnemyType.fromMapId(id);
if (type == null) return 0.0; // Not a standard directional enemy
// FIX: Normalize patrolling enemies back to the standing block, THEN get the 4-way angle
int directionIndex = ((id - baseId) % 18) % 4;
// Normalize patrolling enemies back to the standing block, THEN get the
// 4-way angle
int directionIndex = ((id - type.mapBaseId) % 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;
EnemyType? type = EnemyType.fromMapId(id);
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;
}
// If it's not a standard enemy (it's a decoration, boss, or player), spawn it
if (type == null) return true;
int relativeId = (id - baseId) % 18;
int offset = id - type.mapBaseId;
int normalizedOffset = offset >= 18 ? offset - 18 : offset;
return switch (relativeId) {
< 4 => true,
< 8 => selectedDifficulty.level >= Difficulty.dontHurtMe.level,
< 12 => selectedDifficulty.level >= Difficulty.bringEmOn.level,
< 16 => selectedDifficulty.level >= Difficulty.iAmDeathIncarnate.level,
_ => true,
return switch (normalizedOffset) {
< 4 => true, // Spawns on all difficulties
< 8 => selectedDifficulty.level >= Difficulty.bringEmOn.level, // Normal
< 16 =>
selectedDifficulty.level >=
Difficulty.iAmDeathIncarnate.level, // Hard & Ambush
_ => true, // Dead bodies (decorations)
};
}
}

View File

@@ -2,15 +2,11 @@ import 'package:wolf_3d_data/wolf_3d_data.dart';
class WolfLevel {
final String name;
final int width; // Always 64 in standard Wolf3D
final int height; // Always 64
final Sprite wallGrid;
final Sprite objectGrid;
WolfLevel({
required this.name,
required this.width,
required this.height,
required this.wallGrid,
required this.objectGrid,
});

View File

@@ -29,8 +29,8 @@ class WolfMap {
gameMaps,
isShareware: true,
);
final parsedTextures = VswapParser.parseWalls(vswap);
final parsedSprites = VswapParser.parseSprites(vswap);
final parsedTextures = WLParser.parseWalls(vswap);
final parsedSprites = WLParser.parseSprites(vswap);
// 3. Return the populated instance!
return WolfMap._(
@@ -49,8 +49,8 @@ class WolfMap {
// 2. Parse the data using the parser we just built
final parsedLevels = WolfMapParser.parseMaps(mapHead, gameMaps);
final parsedTextures = VswapParser.parseWalls(vswap);
final parsedSprites = VswapParser.parseSprites(vswap);
final parsedTextures = WLParser.parseWalls(vswap);
final parsedSprites = WLParser.parseSprites(vswap);
// 3. Return the populated instance!
return WolfMap._(

View File

@@ -1,7 +1,6 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:wolf_3d_data/wolf_3d_data.dart';
import 'package:wolf_dart/features/entities/map_objects.dart';
import 'package:wolf_dart/features/map/wolf_level.dart';
@@ -40,10 +39,6 @@ abstract class WolfMapParser {
int plane0Length = gameMaps.getUint16(mapOffset + 12, Endian.little);
int plane1Length = gameMaps.getUint16(mapOffset + 14, Endian.little);
// Dimensions (Always 64x64, but we read it anyway for accuracy)
int width = gameMaps.getUint16(mapOffset + 18, Endian.little);
int height = gameMaps.getUint16(mapOffset + 20, Endian.little);
// Map Name (16 bytes of ASCII text)
List<int> nameBytes = [];
for (int n = 0; n < 16; n++) {
@@ -86,25 +81,23 @@ abstract class WolfMapParser {
}
}
Sprite wallGrid = [];
Sprite objectGrid = []; // NEW
List<List<int>> wallGrid = [];
List<List<int>> objectGrid = [];
for (int y = 0; y < height; y++) {
for (int y = 0; y < 64; y++) {
List<int> wallRow = [];
List<int> objectRow = []; // NEW
for (int x = 0; x < width; x++) {
wallRow.add(flatWallGrid[y * width + x]);
objectRow.add(flatObjectGrid[y * width + x]); // NEW
List<int> objectRow = [];
for (int x = 0; x < 64; x++) {
wallRow.add(flatWallGrid[y * 64 + x]);
objectRow.add(flatObjectGrid[y * 64 + x]);
}
wallGrid.add(wallRow);
objectGrid.add(objectRow); // NEW
objectGrid.add(objectRow);
}
levels.add(
WolfLevel(
name: name,
width: width,
height: height,
wallGrid: wallGrid,
objectGrid: objectGrid,
),