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,
),

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:wolf_3d_data/wolf_3d_data.dart';
import 'package:wolf_dart/features/entities/enemies/enemy.dart';
import 'package:wolf_dart/features/renderer/color_palette.dart';
class SpriteGallery extends StatelessWidget {
@@ -11,7 +12,7 @@ class SpriteGallery extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("VSWAP Sprite Gallery"),
title: const Text("Sprite Gallery"),
automaticallyImplyLeading: true,
),
backgroundColor: Colors.black,
@@ -21,11 +22,22 @@ class SpriteGallery extends StatelessWidget {
),
itemCount: sprites.length,
itemBuilder: (context, index) {
// --- Check which enemy owns this sprite ---
String label = "Idx: $index";
for (final enemy in EnemyType.values) {
if (enemy.claimsSpriteIndex(index)) {
// Appends the enum name (e.g., "guard", "dog")
label += "\n${enemy.name}";
break;
}
}
return Column(
children: [
Text(
"Idx: $index",
label,
style: const TextStyle(color: Colors.white, fontSize: 10),
textAlign: TextAlign.center,
),
Expanded(
child: CustomPaint(

View File

@@ -1,3 +0,0 @@
import 'matrix.dart';
typedef Level = Matrix<int>;

View File

@@ -1 +0,0 @@
typedef Matrix<T> = List<List<T>>;

View File

@@ -1,3 +1,7 @@
import 'matrix.dart';
typedef Matrix<T> = List<List<T>>;
typedef Sprite = Matrix<int>;
typedef Wall = Sprite;
typedef Level = Matrix<int>;

View File

@@ -1,142 +0,0 @@
import 'dart:typed_data';
import 'classes/sprite.dart';
class VswapParser {
/// Extracts the 64x64 wall textures from VSWAP.WL1
static List<Sprite> parseWalls(ByteData vswap) {
// 1. Read Header
int chunks = vswap.getUint16(0, Endian.little);
int spriteStart = vswap.getUint16(2, Endian.little);
// int soundStart = vswap.getUint16(4, Endian.little); // We don't need this yet
// 2. Read Offsets (Where does each chunk start in the file?)
List<int> offsets = [];
for (int i = 0; i < spriteStart; i++) {
offsets.add(vswap.getUint32(6 + (i * 4), Endian.little));
}
// 3. Extract the Wall Textures
List<Sprite> textures = [];
// Walls are chunks 0 through (spriteStart - 1)
for (int i = 0; i < spriteStart; i++) {
int offset = offsets[i];
if (offset == 0) continue; // Empty chunk
// Walls are always exactly 64x64 pixels (4096 bytes)
// Note: Wolf3D stores pixels in COLUMN-MAJOR order (Top to bottom, then left to right)
Sprite texture = List.generate(64, (_) => List.filled(64, 0));
for (int x = 0; x < 64; x++) {
for (int y = 0; y < 64; y++) {
int byteIndex = offset + (x * 64) + y;
texture[x][y] = vswap.getUint8(byteIndex);
}
}
textures.add(texture);
}
return textures;
}
/// Extracts the compiled scaled sprites from VSWAP.WL1
static List<Sprite> parseSprites(ByteData vswap) {
int chunks = vswap.getUint16(0, Endian.little);
int spriteStart = vswap.getUint16(2, Endian.little);
int soundStart = vswap.getUint16(4, Endian.little);
List<int> offsets = [];
for (int i = 0; i < chunks; i++) {
offsets.add(vswap.getUint32(6 + (i * 4), Endian.little));
}
List<Sprite> sprites = [];
// Sprites are located between the walls and the sounds
for (int i = spriteStart; i < soundStart; i++) {
int offset = offsets[i];
if (offset == 0) continue; // Some chunks are empty placeholders
// Initialize the 64x64 grid with 255 (The Magenta Transparency Color!)
Sprite sprite = List.generate(64, (_) => List.filled(64, 255));
int leftPix = vswap.getUint16(offset, Endian.little);
int rightPix = vswap.getUint16(offset + 2, Endian.little);
// Read the offsets for each vertical column of the sprite
List<int> colOffsets = [];
for (int x = leftPix; x <= rightPix; x++) {
colOffsets.add(
vswap.getUint16(offset + 4 + ((x - leftPix) * 2), Endian.little),
);
}
for (int x = leftPix; x <= rightPix; x++) {
int colOffset = colOffsets[x - leftPix];
if (colOffset == 0) continue;
int cmdOffset = offset + colOffset;
// Execute the column drawing commands
while (true) {
int endY = vswap.getUint16(cmdOffset, Endian.little);
if (endY == 0) break; // 0 marks the end of the column
endY ~/= 2; // Wolf3D stores Y coordinates multiplied by 2
int pixelOfs = vswap.getUint16(cmdOffset + 2, Endian.little);
int startY = vswap.getUint16(cmdOffset + 4, Endian.little);
startY ~/= 2;
for (int y = startY; y < endY; y++) {
// The Carmack 286 Hack: pixelOfs + y gives the exact byte address!
sprite[x][y] = vswap.getUint8(offset + pixelOfs + y);
}
cmdOffset += 6; // Move to the next 6-byte instruction
}
}
sprites.add(sprite);
}
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;
}
}

View File

@@ -0,0 +1,130 @@
import 'dart:typed_data';
import 'classes/sprite.dart';
class WLParser {
/// Extracts the 64x64 wall textures from VSWAP.WL1
static List<Sprite> parseWalls(ByteData vswap) {
final header = _VswapHeader(vswap);
return header.offsets
.take(header.spriteStart)
.where((offset) => offset != 0) // Skip empty chunks
.map((offset) => _parseWallChunk(vswap, offset))
.toList();
}
/// Extracts the compiled scaled sprites from VSWAP.WL1
static List<Sprite> parseSprites(ByteData vswap) {
final header = _VswapHeader(vswap);
final sprites = <Sprite>[];
// Sprites are located between the walls and the sounds
for (int i = header.spriteStart; i < header.soundStart; i++) {
int offset = header.offsets[i];
if (offset != 0) {
sprites.add(_parseSingleSprite(vswap, offset));
}
}
return sprites;
}
/// Extracts digitized sound effects (PCM Audio) from VSWAP.WL1
static List<Uint8List> parseSounds(ByteData vswap) {
final header = _VswapHeader(vswap);
final lengthStart = 6 + (header.chunks * 4);
final sounds = <Uint8List>[];
// Sounds start after the sprites and go to the end of the chunks
for (int i = header.soundStart; i < header.chunks; i++) {
int offset = header.offsets[i];
int length = vswap.getUint16(lengthStart + (i * 2), Endian.little);
if (offset == 0 || length == 0) {
sounds.add(Uint8List(0)); // Empty placeholder
} else {
// Extract the raw 8-bit PCM audio bytes
sounds.add(vswap.buffer.asUint8List(offset, length));
}
}
return sounds;
}
// --- Private Helpers ---
static Sprite _parseWallChunk(ByteData vswap, int offset) {
// Generate the 64x64 pixel grid in column-major order functionally
return List.generate(
64,
(x) => List.generate(64, (y) => vswap.getUint8(offset + (x * 64) + y)),
);
}
static Sprite _parseSingleSprite(ByteData vswap, int offset) {
// Initialize the 64x64 grid with 255 (The Magenta Transparency Color!)
Sprite sprite = List.generate(64, (_) => List.filled(64, 255));
int leftPix = vswap.getUint16(offset, Endian.little);
int rightPix = vswap.getUint16(offset + 2, Endian.little);
// Parse vertical columns within the sprite bounds
for (int x = leftPix; x <= rightPix; x++) {
int colOffset = vswap.getUint16(
offset + 4 + ((x - leftPix) * 2),
Endian.little,
);
if (colOffset != 0) {
_parseSpriteColumn(vswap, sprite, x, offset, offset + colOffset);
}
}
return sprite;
}
static void _parseSpriteColumn(
ByteData vswap,
Sprite sprite,
int x,
int baseOffset,
int cmdOffset,
) {
// Execute the column drawing commands
while (true) {
int endY = vswap.getUint16(cmdOffset, Endian.little);
if (endY == 0) break; // 0 marks the end of the column
endY ~/= 2; // Wolf3D stores Y coordinates multiplied by 2
int pixelOfs = vswap.getUint16(cmdOffset + 2, Endian.little);
int startY = vswap.getUint16(cmdOffset + 4, Endian.little);
startY ~/= 2;
for (int y = startY; y < endY; y++) {
// The Carmack 286 Hack: pixelOfs + y gives the exact byte address
sprite[x][y] = vswap.getUint8(baseOffset + pixelOfs + y);
}
cmdOffset += 6; // Move to the next 6-byte instruction
}
}
}
/// Helper class to parse and store redundant VSWAP header data
class _VswapHeader {
final int chunks;
final int spriteStart;
final int soundStart;
final List<int> offsets;
_VswapHeader(ByteData vswap)
: chunks = vswap.getUint16(0, Endian.little),
spriteStart = vswap.getUint16(2, Endian.little),
soundStart = vswap.getUint16(4, Endian.little),
offsets = List.generate(
vswap.getUint16(0, Endian.little), // total chunks
(i) => vswap.getUint32(6 + (i * 4), Endian.little),
);
}

View File

@@ -3,6 +3,5 @@
/// More dartdocs go here.
library;
export 'src/classes/level.dart' show Level;
export 'src/classes/sprite.dart' show Sprite;
export 'src/vswap_parser.dart' show VswapParser;
export 'src/classes/sprite.dart' hide Matrix;
export 'src/wl_parser.dart' show WLParser;