From 1ec891d9a089a766cd4e3da1798953f51b39adf8 Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Sat, 14 Mar 2026 21:06:31 +0100 Subject: [PATCH] Working on fixing enemy identification Signed-off-by: Hans Kokx --- lib/features/entities/enemies/dog.dart | 7 +- lib/features/entities/enemies/enemy.dart | 43 ++++++ lib/features/entities/enemies/guard.dart | 29 ++-- lib/features/entities/enemies/mutant.dart | 34 ++--- lib/features/entities/enemies/officer.dart | 36 ++--- lib/features/entities/enemies/ss.dart | 28 ++-- lib/features/entities/entity_registry.dart | 6 + lib/features/entities/map_objects.dart | 64 +++----- lib/features/map/wolf_level.dart | 4 - lib/features/map/wolf_map.dart | 8 +- lib/features/map/wolf_map_parser.dart | 23 +-- lib/sprite_gallery.dart | 16 +- .../wolf_3d_data/lib/src/classes/level.dart | 3 - .../wolf_3d_data/lib/src/classes/matrix.dart | 1 - .../wolf_3d_data/lib/src/classes/sprite.dart | 6 +- .../wolf_3d_data/lib/src/vswap_parser.dart | 142 ------------------ packages/wolf_3d_data/lib/src/wl_parser.dart | 130 ++++++++++++++++ packages/wolf_3d_data/lib/wolf_3d_data.dart | 5 +- 18 files changed, 293 insertions(+), 292 deletions(-) delete mode 100644 packages/wolf_3d_data/lib/src/classes/level.dart delete mode 100644 packages/wolf_3d_data/lib/src/classes/matrix.dart delete mode 100644 packages/wolf_3d_data/lib/src/vswap_parser.dart create mode 100644 packages/wolf_3d_data/lib/src/wl_parser.dart diff --git a/lib/features/entities/enemies/dog.dart b/lib/features/entities/enemies/dog.dart index 4964826..147e49e 100644 --- a/lib/features/entities/enemies/dog.dart +++ b/lib/features/entities/enemies/dog.dart @@ -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, diff --git a/lib/features/entities/enemies/enemy.dart b/lib/features/entities/enemies/enemy.dart index 95d960f..1b6c243 100644 --- a/lib/features/entities/enemies/enemy.dart +++ b/lib/features/entities/enemies/enemy.dart @@ -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, diff --git a/lib/features/entities/enemies/guard.dart b/lib/features/entities/enemies/guard.dart index 796c6b3..fc00c7a 100644 --- a/lib/features/entities/enemies/guard.dart +++ b/lib/features/entities/enemies/guard.dart @@ -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; diff --git a/lib/features/entities/enemies/mutant.dart b/lib/features/entities/enemies/mutant.dart index 53e8638..3e1a92a 100644 --- a/lib/features/entities/enemies/mutant.dart +++ b/lib/features/entities/enemies/mutant.dart @@ -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; diff --git a/lib/features/entities/enemies/officer.dart b/lib/features/entities/enemies/officer.dart index a4e307a..581a2a1 100644 --- a/lib/features/entities/enemies/officer.dart +++ b/lib/features/entities/enemies/officer.dart @@ -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; diff --git a/lib/features/entities/enemies/ss.dart b/lib/features/entities/enemies/ss.dart index 29b0c9c..e7329ab 100644 --- a/lib/features/entities/enemies/ss.dart +++ b/lib/features/entities/enemies/ss.dart @@ -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; diff --git a/lib/features/entities/entity_registry.dart b/lib/features/entities/entity_registry.dart index d86dc36..a27514e 100644 --- a/lib/features/entities/entity_registry.dart +++ b/lib/features/entities/entity_registry.dart @@ -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) { diff --git a/lib/features/entities/map_objects.dart b/lib/features/entities/map_objects.dart index 1213b7e..7dadc8f 100644 --- a/lib/features/entities/map_objects.dart +++ b/lib/features/entities/map_objects.dart @@ -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) }; } } diff --git a/lib/features/map/wolf_level.dart b/lib/features/map/wolf_level.dart index 6c3c997..e10e208 100644 --- a/lib/features/map/wolf_level.dart +++ b/lib/features/map/wolf_level.dart @@ -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, }); diff --git a/lib/features/map/wolf_map.dart b/lib/features/map/wolf_map.dart index 9dba27d..3fc6fd8 100644 --- a/lib/features/map/wolf_map.dart +++ b/lib/features/map/wolf_map.dart @@ -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._( diff --git a/lib/features/map/wolf_map_parser.dart b/lib/features/map/wolf_map_parser.dart index 9c49a22..eec13ba 100644 --- a/lib/features/map/wolf_map_parser.dart +++ b/lib/features/map/wolf_map_parser.dart @@ -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 nameBytes = []; for (int n = 0; n < 16; n++) { @@ -86,25 +81,23 @@ abstract class WolfMapParser { } } - Sprite wallGrid = []; - Sprite objectGrid = []; // NEW + List> wallGrid = []; + List> objectGrid = []; - for (int y = 0; y < height; y++) { + for (int y = 0; y < 64; y++) { List wallRow = []; - List objectRow = []; // NEW - for (int x = 0; x < width; x++) { - wallRow.add(flatWallGrid[y * width + x]); - objectRow.add(flatObjectGrid[y * width + x]); // NEW + List 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, ), diff --git a/lib/sprite_gallery.dart b/lib/sprite_gallery.dart index 91d66ef..b65d4aa 100644 --- a/lib/sprite_gallery.dart +++ b/lib/sprite_gallery.dart @@ -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( diff --git a/packages/wolf_3d_data/lib/src/classes/level.dart b/packages/wolf_3d_data/lib/src/classes/level.dart deleted file mode 100644 index c179a9c..0000000 --- a/packages/wolf_3d_data/lib/src/classes/level.dart +++ /dev/null @@ -1,3 +0,0 @@ -import 'matrix.dart'; - -typedef Level = Matrix; diff --git a/packages/wolf_3d_data/lib/src/classes/matrix.dart b/packages/wolf_3d_data/lib/src/classes/matrix.dart deleted file mode 100644 index d892678..0000000 --- a/packages/wolf_3d_data/lib/src/classes/matrix.dart +++ /dev/null @@ -1 +0,0 @@ -typedef Matrix = List>; diff --git a/packages/wolf_3d_data/lib/src/classes/sprite.dart b/packages/wolf_3d_data/lib/src/classes/sprite.dart index bdc14aa..cbdab04 100644 --- a/packages/wolf_3d_data/lib/src/classes/sprite.dart +++ b/packages/wolf_3d_data/lib/src/classes/sprite.dart @@ -1,3 +1,7 @@ -import 'matrix.dart'; +typedef Matrix = List>; typedef Sprite = Matrix; + +typedef Wall = Sprite; + +typedef Level = Matrix; diff --git a/packages/wolf_3d_data/lib/src/vswap_parser.dart b/packages/wolf_3d_data/lib/src/vswap_parser.dart deleted file mode 100644 index c0b5156..0000000 --- a/packages/wolf_3d_data/lib/src/vswap_parser.dart +++ /dev/null @@ -1,142 +0,0 @@ -import 'dart:typed_data'; - -import 'classes/sprite.dart'; - -class VswapParser { - /// Extracts the 64x64 wall textures from VSWAP.WL1 - static List 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 offsets = []; - for (int i = 0; i < spriteStart; i++) { - offsets.add(vswap.getUint32(6 + (i * 4), Endian.little)); - } - - // 3. Extract the Wall Textures - List 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 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 offsets = []; - for (int i = 0; i < chunks; i++) { - offsets.add(vswap.getUint32(6 + (i * 4), Endian.little)); - } - - List 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 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 parseSounds(ByteData vswap) { - int chunks = vswap.getUint16(0, Endian.little); - int soundStart = vswap.getUint16(4, Endian.little); - - List offsets = []; - List lengths = []; - - // Offsets are 32-bit integers starting at byte 6 - for (int i = 0; i < chunks; i++) { - offsets.add(vswap.getUint32(6 + (i * 4), Endian.little)); - } - - // Lengths are 16-bit integers immediately following the offset array - int lengthStart = 6 + (chunks * 4); - for (int i = 0; i < chunks; i++) { - lengths.add(vswap.getUint16(lengthStart + (i * 2), Endian.little)); - } - - List sounds = []; - - // Sounds start after the sprites and go to the end of the chunks - for (int i = soundStart; i < chunks; i++) { - int offset = offsets[i]; - int length = lengths[i]; - - if (offset == 0 || length == 0) { - sounds.add(Uint8List(0)); // Empty placeholder - continue; - } - - // Extract the raw 8-bit PCM audio bytes - final soundData = vswap.buffer.asUint8List(offset, length); - sounds.add(soundData); - } - - return sounds; - } -} diff --git a/packages/wolf_3d_data/lib/src/wl_parser.dart b/packages/wolf_3d_data/lib/src/wl_parser.dart new file mode 100644 index 0000000..d7b4a2f --- /dev/null +++ b/packages/wolf_3d_data/lib/src/wl_parser.dart @@ -0,0 +1,130 @@ +import 'dart:typed_data'; + +import 'classes/sprite.dart'; + +class WLParser { + /// Extracts the 64x64 wall textures from VSWAP.WL1 + static List 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 parseSprites(ByteData vswap) { + final header = _VswapHeader(vswap); + final sprites = []; + + // 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 parseSounds(ByteData vswap) { + final header = _VswapHeader(vswap); + final lengthStart = 6 + (header.chunks * 4); + final sounds = []; + + // 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 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), + ); +} diff --git a/packages/wolf_3d_data/lib/wolf_3d_data.dart b/packages/wolf_3d_data/lib/wolf_3d_data.dart index 1141d5f..1a1bd6d 100644 --- a/packages/wolf_3d_data/lib/wolf_3d_data.dart +++ b/packages/wolf_3d_data/lib/wolf_3d_data.dart @@ -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;