diff --git a/lib/classes/cardinal_direction.dart b/lib/classes/cardinal_direction.dart new file mode 100644 index 0000000..b34eca4 --- /dev/null +++ b/lib/classes/cardinal_direction.dart @@ -0,0 +1,28 @@ +import 'dart:math' as math; + +enum CardinalDirection { + east(0.0), + south(math.pi / 2), + west(math.pi), + north(3 * math.pi / 2) + ; + + final double radians; + const CardinalDirection(this.radians); + + /// Helper to decode Wolf3D enemy directional blocks + static CardinalDirection fromEnemyIndex(int index) { + switch (index % 4) { + case 0: + return CardinalDirection.east; + case 1: + return CardinalDirection.north; + case 2: + return CardinalDirection.west; + case 3: + return CardinalDirection.south; + default: + return CardinalDirection.east; + } + } +} diff --git a/lib/classes/sprite.dart b/lib/classes/sprite.dart new file mode 100644 index 0000000..becb972 --- /dev/null +++ b/lib/classes/sprite.dart @@ -0,0 +1 @@ +typedef Sprite = List>; diff --git a/lib/features/difficulty/difficulty_screen.dart b/lib/features/difficulty/difficulty_screen.dart index 0f36b34..4321462 100644 --- a/lib/features/difficulty/difficulty_screen.dart +++ b/lib/features/difficulty/difficulty_screen.dart @@ -2,9 +2,16 @@ import 'package:flutter/material.dart'; import 'package:wolf_dart/features/difficulty/difficulty.dart'; import 'package:wolf_dart/features/renderer/renderer.dart'; -class DifficultyScreen extends StatelessWidget { +class DifficultyScreen extends StatefulWidget { const DifficultyScreen({super.key}); + @override + State createState() => _DifficultyScreenState(); +} + +class _DifficultyScreenState extends State { + bool isShareware = true; // Default to Shareware (WL1) + @override Widget build(BuildContext context) { return Scaffold( @@ -14,9 +21,9 @@ class DifficultyScreen extends StatelessWidget { onPressed: () { Navigator.of(context).push( MaterialPageRoute( - builder: (_) => const WolfRenderer( + builder: (_) => WolfRenderer( difficulty: Difficulty.bringEmOn, - isDemo: false, + isShareware: isShareware, showSpriteGallery: true, ), ), @@ -37,35 +44,59 @@ class DifficultyScreen extends StatelessWidget { fontFamily: 'Courier', ), ), - const SizedBox(height: 40), + const SizedBox(height: 20), + + // --- Version Toggle --- + Theme( + data: ThemeData(unselectedWidgetColor: Colors.grey), + child: CheckboxListTile( + title: const Text( + "Play Shareware Version (WL1)", + style: TextStyle(color: Colors.white), + ), + value: isShareware, + onChanged: (bool? value) { + setState(() { + isShareware = value ?? true; + }); + }, + controlAffinity: ListTileControlAffinity.leading, + contentPadding: const EdgeInsets.symmetric(horizontal: 100), + ), + ), + const SizedBox(height: 20), + + // --- Difficulty Buttons --- ListView.builder( shrinkWrap: true, itemCount: Difficulty.values.length, itemBuilder: (context, index) { final Difficulty difficulty = Difficulty.values[index]; - return ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blueGrey[900], - foregroundColor: Colors.white, - minimumSize: const Size(300, 50), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), - ), - ), - onPressed: () { - // Push the renderer and pass the selected difficulty - Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (_) => WolfRenderer( - difficulty: difficulty, - isDemo: false, - ), + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blueGrey[900], + foregroundColor: Colors.white, + minimumSize: const Size(300, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), ), - ); - }, - child: Text( - difficulty.title, - style: const TextStyle(fontSize: 18), + ), + onPressed: () { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (_) => WolfRenderer( + difficulty: difficulty, + isShareware: isShareware, + ), + ), + ); + }, + child: Text( + difficulty.title, + style: const TextStyle(fontSize: 18), + ), ), ); }, diff --git a/lib/features/entities/collectible.dart b/lib/features/entities/collectible.dart index e956863..bfa1bf4 100644 --- a/lib/features/entities/collectible.dart +++ b/lib/features/entities/collectible.dart @@ -1,3 +1,4 @@ +import 'package:wolf_dart/features/difficulty/difficulty.dart'; import 'package:wolf_dart/features/entities/entity.dart'; enum CollectibleType { ammo, health, treasure, weapon, key } @@ -31,7 +32,7 @@ class Collectible extends Entity { int objId, double x, double y, - int difficultyLevel, + Difficulty _, ) { if (isCollectible(objId)) { return Collectible( diff --git a/lib/features/entities/decorative.dart b/lib/features/entities/decorative.dart index 0ffd4c1..14f95af 100644 --- a/lib/features/entities/decorative.dart +++ b/lib/features/entities/decorative.dart @@ -1,3 +1,4 @@ +import 'package:wolf_dart/features/difficulty/difficulty.dart'; import 'package:wolf_dart/features/entities/entity.dart'; class Decorative extends Entity { @@ -32,7 +33,7 @@ class Decorative extends Entity { int objId, double x, double y, - int difficultyLevel, + Difficulty _, ) { if (isDecoration(objId)) { return Decorative( diff --git a/lib/features/entities/door_manager.dart b/lib/features/entities/door_manager.dart index 10a3537..0581e32 100644 --- a/lib/features/entities/door_manager.dart +++ b/lib/features/entities/door_manager.dart @@ -1,12 +1,13 @@ import 'dart:math' as math; +import 'package:wolf_dart/classes/sprite.dart'; import 'package:wolf_dart/features/map/door.dart'; class DoorManager { // Key is '$x,$y' final Map doors = {}; - void initDoors(List> wallGrid) { + void initDoors(Sprite wallGrid) { doors.clear(); for (int y = 0; y < wallGrid.length; y++) { for (int x = 0; x < wallGrid[y].length; x++) { diff --git a/lib/features/entities/enemies/brown_guard.dart b/lib/features/entities/enemies/brown_guard.dart index 8425a8c..725b7aa 100644 --- a/lib/features/entities/enemies/brown_guard.dart +++ b/lib/features/entities/enemies/brown_guard.dart @@ -1,8 +1,10 @@ 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 BrownGuard extends Enemy { static const double speed = 0.03; @@ -23,33 +25,18 @@ class BrownGuard extends Enemy { int objId, double x, double y, - int difficultyLevel, + Difficulty difficulty, ) { - bool canSpawn = false; - switch (difficultyLevel) { - case 0: - canSpawn = objId >= 108 && objId <= 115; - break; - case 1: - canSpawn = objId >= 144 && objId <= 151; - break; - case 2: - canSpawn = objId >= 180 && objId <= 187; - break; - case 3: - canSpawn = objId >= 216 && objId <= 223; - break; - } - - if (canSpawn) { + // Use the range constants we defined in MapObject! + if (objId >= MapObject.guardStart && objId <= MapObject.guardStart + 17) { return BrownGuard( x: x, y: y, - angle: Enemy.getInitialAngle(objId), + angle: MapObject.getAngle(objId), mapId: objId, ); } - return null; // Not a Brown Guard! + return null; } @override @@ -63,20 +50,11 @@ class BrownGuard extends Enemy { Coordinate2D movement = const Coordinate2D(0, 0); double newAngle = angle; - // 1. Wake up logic (Matches SightPlayer & FirstSighting) - if (state == EntityState.idle && - hasLineOfSight(playerPosition, isWalkable)) { - if (reactionTimeMs == 0) { - // Init reaction delay: ~1 to 4 tics in C (1 tic = ~14ms, but plays out longer in engine ticks). - // Let's approximate human-feeling reaction time: 200ms - 800ms - reactionTimeMs = elapsedMs + 200 + math.Random().nextInt(600); - } else if (elapsedMs >= reactionTimeMs) { - state = - EntityState.patrolling; // Equivalent to FirstSighting chase frame - lastActionTime = elapsedMs; - reactionTimeMs = 0; // Reset - } - } + checkWakeUp( + elapsedMs: elapsedMs, + playerPosition: playerPosition, + isWalkable: isWalkable, + ); double distance = position.distanceTo(playerPosition); double angleToPlayer = position.angleTo(playerPosition); @@ -85,14 +63,11 @@ class BrownGuard extends Enemy { newAngle = angleToPlayer; } - // Octant logic remains the same - double diff = newAngle - angleToPlayer; - while (diff <= -math.pi) { - diff += 2 * math.pi; - } - while (diff > math.pi) { - diff -= 2 * math.pi; - } + // Octant logic (Directional sprites) + 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; @@ -105,13 +80,10 @@ class BrownGuard extends Enemy { case EntityState.patrolling: if (distance > 0.8) { - // Jitter fix: Use continuous vector movement instead of single-axis snapping double moveX = math.cos(angleToPlayer) * speed; double moveY = math.sin(angleToPlayer) * speed; - Coordinate2D intendedMovement = Coordinate2D(moveX, moveY); - // Pass tryOpenDoor down! movement = getValidMovement( intendedMovement, isWalkable, @@ -119,12 +91,9 @@ class BrownGuard extends Enemy { ); } - // Animation fix: Update the sprite so he actually turns and walks! int walkFrame = (elapsedMs ~/ 150) % 4; spriteIndex = 58 + (walkFrame * 8) + octant; - // Shooting fix: Give him permission to stop and shoot you - // (1500ms delay between shots) if (distance < 6.0 && elapsedMs - lastActionTime > 1500) { if (hasLineOfSight(playerPosition, isWalkable)) { state = EntityState.shooting; @@ -132,20 +101,20 @@ class BrownGuard extends Enemy { _hasFiredThisCycle = false; } } - break; // Fallthrough fix: Don't forget the break! + break; case EntityState.shooting: int timeShooting = elapsedMs - lastActionTime; if (timeShooting < 150) { - spriteIndex = 96; + spriteIndex = 96; // Aiming } else if (timeShooting < 300) { - spriteIndex = 97; + spriteIndex = 97; // Firing if (!_hasFiredThisCycle) { - onDamagePlayer(10); // DAMAGING PLAYER + onDamagePlayer(10); _hasFiredThisCycle = true; } } else if (timeShooting < 450) { - spriteIndex = 98; + spriteIndex = 98; // Recoil } else { state = EntityState.patrolling; lastActionTime = elapsedMs; @@ -153,7 +122,8 @@ class BrownGuard extends Enemy { break; case EntityState.pain: - spriteIndex = 94; + spriteIndex = 94; // Ouch frame + // Stay in pain for a brief moment, then resume attacking if (elapsedMs - lastActionTime > 250) { state = EntityState.patrolling; lastActionTime = elapsedMs; @@ -164,9 +134,10 @@ class BrownGuard extends Enemy { if (isDying) { int deathFrame = (elapsedMs - lastActionTime) ~/ 150; if (deathFrame < 4) { - spriteIndex = 90 + deathFrame - 1; + // FIX: Removed the buggy "- 1" + spriteIndex = 90 + deathFrame; } else { - spriteIndex = 95; + spriteIndex = 95; // Final dead frame isDying = false; } } else { diff --git a/lib/features/entities/enemies/dog.dart b/lib/features/entities/enemies/dog.dart index 8877a6d..2339f15 100644 --- a/lib/features/entities/enemies/dog.dart +++ b/lib/features/entities/enemies/dog.dart @@ -1,11 +1,13 @@ 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'; // NEW class Dog extends Enemy { - static const double speed = 0.05; // Dogs are much faster than guards! + static const double speed = 0.05; bool _hasBittenThisCycle = false; Dog({ @@ -14,32 +16,17 @@ class Dog extends Enemy { required super.angle, required super.mapId, }) : super( - spriteIndex: 99, // Dogs start at index 99 in VSWAP + spriteIndex: 99, state: EntityState.idle, ); - static Dog? trySpawn(int objId, double x, double y, int difficultyLevel) { - bool canSpawn = false; - switch (difficultyLevel) { - case 0: - canSpawn = objId >= 116 && objId <= 119; - break; - case 1: - canSpawn = objId >= 152 && objId <= 155; - break; - case 2: - canSpawn = objId >= 188 && objId <= 191; - break; - case 3: - canSpawn = objId >= 224 && objId <= 227; - break; - } - - if (canSpawn) { + static Dog? trySpawn(int objId, double x, double y, Difficulty difficulty) { + // The renderer already checked difficulty, so we just check the ID block! + if (objId >= MapObject.dogStart && objId <= MapObject.dogStart + 17) { return Dog( x: x, y: y, - angle: Enemy.getInitialAngle(objId), + angle: MapObject.getAngle(objId), mapId: objId, ); } @@ -51,23 +38,19 @@ class Dog extends Enemy { required int elapsedMs, required Coordinate2D playerPosition, required bool Function(int x, int y) isWalkable, - required void Function(int x, int y) tryOpenDoor, // NEW + required void Function(int x, int y) tryOpenDoor, required void Function(int damage) onDamagePlayer, }) { Coordinate2D movement = const Coordinate2D(0, 0); double newAngle = angle; - // 1. Wake up logic - if (state == EntityState.idle && - hasLineOfSight(playerPosition, isWalkable)) { - if (reactionTimeMs == 0) { - reactionTimeMs = elapsedMs + 100 + math.Random().nextInt(200); - } else if (elapsedMs >= reactionTimeMs) { - state = EntityState.patrolling; - lastActionTime = elapsedMs; - reactionTimeMs = 0; - } - } + checkWakeUp( + elapsedMs: elapsedMs, + playerPosition: playerPosition, + isWalkable: isWalkable, + baseReactionMs: 100, + reactionVarianceMs: 200, + ); double distance = position.distanceTo(playerPosition); double angleToPlayer = position.angleTo(playerPosition); @@ -76,13 +59,10 @@ class Dog extends Enemy { newAngle = angleToPlayer; } - double diff = newAngle - angleToPlayer; - while (diff <= -math.pi) { - diff += 2 * math.pi; - } - while (diff > math.pi) { - diff -= 2 * math.pi; - } + 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; @@ -95,15 +75,12 @@ class Dog extends Enemy { case EntityState.patrolling: if (distance > 0.8) { - double deltaX = playerPosition.x - position.x; - double deltaY = playerPosition.y - position.y; - - double moveX = deltaX > 0 ? speed : (deltaX < 0 ? -speed : 0); - double moveY = deltaY > 0 ? speed : (deltaY < 0 ? -speed : 0); + // UPGRADED: Smooth vector movement instead of grid-snapping + double moveX = math.cos(angleToPlayer) * speed; + double moveY = math.sin(angleToPlayer) * speed; Coordinate2D intendedMovement = Coordinate2D(moveX, moveY); - // Pass tryOpenDoor down! movement = getValidMovement( intendedMovement, isWalkable, @@ -135,6 +112,22 @@ class Dog extends Enemy { } break; + // Make sure dogs have a death state so they don't stay standing! + case EntityState.dead: + if (isDying) { + int deathFrame = (elapsedMs - lastActionTime) ~/ 100; + if (deathFrame < 4) { + spriteIndex = + 140 + deathFrame; // Dog death frames usually start here + } else { + spriteIndex = 143; // Dead dog on floor + isDying = false; + } + } else { + spriteIndex = 143; + } + break; + default: break; } diff --git a/lib/features/entities/enemies/enemy.dart b/lib/features/entities/enemies/enemy.dart index fc914fe..d911960 100644 --- a/lib/features/entities/enemies/enemy.dart +++ b/lib/features/entities/enemies/enemy.dart @@ -55,6 +55,28 @@ abstract class Enemy extends Entity { } } + void checkWakeUp({ + required int elapsedMs, + required Coordinate2D playerPosition, + required bool Function(int x, int y) isWalkable, + int baseReactionMs = 200, + int reactionVarianceMs = 600, + }) { + if (state == EntityState.idle && + hasLineOfSight(playerPosition, isWalkable)) { + if (reactionTimeMs == 0) { + reactionTimeMs = + elapsedMs + + baseReactionMs + + math.Random().nextInt(reactionVarianceMs); + } else if (elapsedMs >= reactionTimeMs) { + state = EntityState.patrolling; + lastActionTime = elapsedMs; + reactionTimeMs = 0; + } + } + } + // Matches WL_STATE.C's 'CheckLine' using canonical Integer DDA traversal bool hasLineOfSight( Coordinate2D playerPosition, @@ -63,7 +85,7 @@ abstract class Enemy extends Entity { // 1. Proximity Check (Matches WL_STATE.C 'MINSIGHT') // If the player is very close, sight is automatic regardless of facing angle. // This compensates for our lack of a noise/gunshot alert system! - if (position.distanceTo(playerPosition) < 2.0) { + if (position.distanceTo(playerPosition) < 1.2) { return true; } diff --git a/lib/features/entities/entity_registry.dart b/lib/features/entities/entity_registry.dart index 12e5ecf..2bec763 100644 --- a/lib/features/entities/entity_registry.dart +++ b/lib/features/entities/entity_registry.dart @@ -1,15 +1,17 @@ +import 'package:wolf_dart/features/difficulty/difficulty.dart'; import 'package:wolf_dart/features/entities/collectible.dart'; import 'package:wolf_dart/features/entities/decorative.dart'; import 'package:wolf_dart/features/entities/enemies/brown_guard.dart'; import 'package:wolf_dart/features/entities/enemies/dog.dart'; import 'package:wolf_dart/features/entities/entity.dart'; +import 'package:wolf_dart/features/entities/map_objects.dart'; typedef EntitySpawner = Entity? Function( int objId, double x, double y, - int difficultyLevel, + Difficulty difficulty, ); abstract class EntityRegistry { @@ -24,11 +26,14 @@ abstract class EntityRegistry { int objId, double x, double y, - int difficultyLevel, + Difficulty difficulty, int maxSprites, ) { + // 1. Difficulty check before even looking for a spawner + if (!MapObject.shouldSpawn(objId, difficulty)) return null; + for (final spawner in _spawners) { - Entity? entity = spawner(objId, x, y, difficultyLevel); + Entity? entity = spawner(objId, x, y, difficulty); if (entity != null) { // Safety bounds check for the VSWAP array diff --git a/lib/features/entities/map_objects.dart b/lib/features/entities/map_objects.dart index 2aea815..799c430 100644 --- a/lib/features/entities/map_objects.dart +++ b/lib/features/entities/map_objects.dart @@ -1,4 +1,7 @@ -abstract class MapObjectId { +import 'package:wolf_dart/classes/cardinal_direction.dart'; +import 'package:wolf_dart/features/difficulty/difficulty.dart'; + +abstract class MapObject { // --- Player Spawns --- static const int playerNorth = 19; static const int playerEast = 20; @@ -12,7 +15,7 @@ abstract class MapObjectId { static const int floorLamp = 26; static const int chandelier = 27; static const int hangingSkeleton = 28; - static const int dogFood = 29; // Also used as decoration + static const int dogFoodDecoration = 29; static const int whiteColumn = 30; static const int pottedPlant = 31; static const int blueSkeleton = 32; @@ -25,12 +28,12 @@ abstract class MapObjectId { static const int emptyCage = 39; static const int cageWithSkeleton = 40; static const int bones = 41; - static const int goldenKeybowl = 42; // Decorative only + static const int goldenKeyBowl = 42; - // --- Collectibles & Items --- + // --- Collectibles --- static const int goldKey = 43; static const int silverKey = 44; - static const int bed = 45; // Bed is usually non-collectible but in this range + static const int bed = 45; static const int basket = 46; static const int food = 47; static const int medkit = 48; @@ -43,62 +46,110 @@ abstract class MapObjectId { static const int crown = 55; static const int extraLife = 56; - // --- More Decorations --- - static const int bloodPool = 57; + // --- Environmental --- + static const int bloodPoolSmall = 57; static const int barrel = 58; - static const int well = 59; - static const int emptyWell = 60; + static const int wellFull = 59; + static const int wellEmpty = 60; static const int bloodPoolLarge = 61; static const int flag = 62; - static const int callanard = 63; // Aardwolf sign / hidden message + static const int aardwolfSign = 63; static const int bonesAndSkull = 64; static const int wallHanging = 65; static const int stove = 66; static const int spearRack = 67; static const int vines = 68; - // --- Special Objects --- - static const int secretDoor = 98; // Often used for the Pushwall trigger - static const int elevatorToSecretLevel = 99; - static const int exitTrigger = 100; + // --- Logic & Triggers --- + static const int pushwallTrigger = 98; + static const int secretExitTrigger = 99; + static const int normalExitTrigger = 100; - // --- Enemies (Spawn Points) --- - // Guards - static const int guardEasyNorth = 108; - static const int guardEasyEast = 109; - static const int guardEasySouth = 110; - static const int guardEasyWest = 111; + // --- 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+ - // SS Guards - static const int ssEasyNorth = 124; - static const int ssEasyEast = 125; - static const int ssEasySouth = 126; - static const int ssEasyWest = 127; - - // Dogs - static const int dogEasyNorth = 140; - static const int dogEasyEast = 141; - static const int dogEasySouth = 142; - static const int dogEasyWest = 143; - - // Mutants - static const int mutantEasyNorth = 156; - static const int mutantEasyEast = 157; - static const int mutantEasySouth = 158; - static const int mutantEasyWest = 159; - - // Officers - static const int officerEasyNorth = 172; - static const int officerEasyEast = 173; - static const int officerEasySouth = 174; - static const int officerEasyWest = 175; - - // Bosses (Single spawn points) + // Bosses (Shared between WL1 and WL6) static const int bossHansGrosse = 214; + + // WL6 Exclusive Bosses static const int bossDrSchabbs = 215; - static const int bossFakeHitler = 216; - static const int bossMechaHitler = 217; - static const int bossOttoGiftmacher = 218; - static const int bossGretelGrosse = 219; - static const int bossGeneralFettgesicht = 220; + static const int bossTransGrosse = 216; + static const int bossUbermutant = 217; + static const int bossDeathKnight = 218; + static const int bossMechaHitler = 219; + static const int bossHitlerGhost = 220; + static const int bossGretelGrosse = 221; + static const int bossGiftmacher = 222; + static const int bossFettgesicht = 223; + + // --- Enemy Range Constants --- + static const int guardStart = 108; + static const int officerStart = 126; + static const int ssStart = 144; + static const int dogStart = 162; + static const int mutantStart = 180; + + /// Returns true if the object ID exists in the Shareware version. + static bool isSharewareCompatible(int id) { + // WL1 only had Guards (108-125), Dogs (162-179), and Hans Grosse (214) + if (id >= 126 && id < 162) return false; // No Officers or SS + if (id >= 180 && id < 214) return false; // No Mutants + if (id > 214) return false; // No other bosses + return true; + } + + /// Resolves which enemy type a map ID belongs to. + static String getEnemyType(int id) { + if (id >= 108 && id <= 125) return "Guard"; + if (id >= 126 && id <= 143) return "Officer"; + if (id >= 144 && id <= 161) return "SS"; + if (id >= 162 && id <= 179) return "Dog"; + if (id >= 180 && id <= 197) return "Mutant"; + return "Unknown"; + } + + /// Checks if an object should be spawned based on chosen difficulty. + static bool shouldSpawn(int id, Difficulty selectedDifficulty) { + if (id < 108 || id > 213) return true; // Items/Players/Bosses always spawn + + // Enemy blocks are 18 IDs wide (e.g., 108-125 for Guards) + int relativeId = (id - 108) % 18; + + // 0-3: Easy, 4-7: Medium, 8-11: Hard + if (relativeId < 4) return true; // Easy spawns on everything + if (relativeId < 8) { + return selectedDifficulty != Difficulty.canIPlayDaddy; // Medium/Hard + } + if (relativeId < 12) { + return selectedDifficulty == Difficulty.iAmDeathIncarnate; // Hard only + } + + // 12-15 are typically "Ambush" versions of the Easy/Medium/Hard guards + return true; + } + + /// Determines the spawn orientation of an enemy or player. + /// Determines the spawn orientation of an enemy or player. + static double getAngle(int id) { + // Player spawn angles + switch (id) { + case playerNorth: + return CardinalDirection.north.radians; + case playerEast: + return CardinalDirection.east.radians; + case playerSouth: + return CardinalDirection.south.radians; + case playerWest: + return CardinalDirection.west.radians; + } + + if (id < 108 || id > 213) return 0.0; + + // Enemy directions are mapped in groups of 4 + return CardinalDirection.fromEnemyIndex(id - 108).radians; + } } diff --git a/lib/features/entities/pushwall_manager.dart b/lib/features/entities/pushwall_manager.dart index b0a3244..bc7bb8a 100644 --- a/lib/features/entities/pushwall_manager.dart +++ b/lib/features/entities/pushwall_manager.dart @@ -25,7 +25,7 @@ class PushwallManager { for (int y = 0; y < objectGrid.length; y++) { for (int x = 0; x < objectGrid[y].length; x++) { - if (objectGrid[y][x] == MapObjectId.secretDoor) { + if (objectGrid[y][x] == MapObject.pushwallTrigger) { pushwalls['$x,$y'] = Pushwall(x, y, wallGrid[y][x]); } } diff --git a/lib/features/map/vswap_parser.dart b/lib/features/map/vswap_parser.dart index b502a6e..bc96e78 100644 --- a/lib/features/map/vswap_parser.dart +++ b/lib/features/map/vswap_parser.dart @@ -1,6 +1,7 @@ import 'dart:typed_data'; import 'package:wolf_dart/classes/matrix.dart'; +import 'package:wolf_dart/classes/sprite.dart'; class VswapParser { /// Extracts the 64x64 wall textures from VSWAP.WL1 @@ -17,7 +18,7 @@ class VswapParser { } // 3. Extract the Wall Textures - List>> textures = []; + List textures = []; // Walls are chunks 0 through (spriteStart - 1) for (int i = 0; i < spriteStart; i++) { @@ -26,7 +27,7 @@ class VswapParser { // Walls are always exactly 64x64 pixels (4096 bytes) // Note: Wolf3D stores pixels in COLUMN-MAJOR order (Top to bottom, then left to right) - List> texture = List.generate(64, (_) => List.filled(64, 0)); + Sprite texture = List.generate(64, (_) => List.filled(64, 0)); for (int x = 0; x < 64; x++) { for (int y = 0; y < 64; y++) { diff --git a/lib/features/map/wolf_map.dart b/lib/features/map/wolf_map.dart index f01c7b8..c1a7e08 100644 --- a/lib/features/map/wolf_map.dart +++ b/lib/features/map/wolf_map.dart @@ -18,14 +18,18 @@ class WolfMap { ); /// Asynchronously loads the map files and parses them into a new WolfMap instance. - static Future loadDemo() async { + static Future loadShareware() async { // 1. Load the binary data final mapHead = await rootBundle.load("assets/MAPHEAD.WL1"); final gameMaps = await rootBundle.load("assets/GAMEMAPS.WL1"); final vswap = await rootBundle.load("assets/VSWAP.WL1"); // 2. Parse the data using the parser we just built - final parsedLevels = WolfMapParser.parseMaps(mapHead, gameMaps); + final parsedLevels = WolfMapParser.parseMaps( + mapHead, + gameMaps, + isShareware: true, + ); final parsedTextures = VswapParser.parseWalls(vswap); final parsedSprites = VswapParser.parseSprites(vswap); @@ -38,7 +42,7 @@ class WolfMap { } /// Asynchronously loads the map files and parses them into a new WolfMap instance. - static Future load() async { + static Future loadRetail() async { // 1. Load the binary data final mapHead = await rootBundle.load("assets/MAPHEAD.WL6"); final gameMaps = await rootBundle.load("assets/GAMEMAPS.WL6"); diff --git a/lib/features/map/wolf_map_parser.dart b/lib/features/map/wolf_map_parser.dart index 720435c..9c5c274 100644 --- a/lib/features/map/wolf_map_parser.dart +++ b/lib/features/map/wolf_map_parser.dart @@ -2,11 +2,16 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:wolf_dart/classes/matrix.dart'; +import 'package:wolf_dart/features/entities/map_objects.dart'; import 'package:wolf_dart/features/map/wolf_level.dart'; abstract class WolfMapParser { /// Parses MAPHEAD and GAMEMAPS to extract the raw level data. - static List parseMaps(ByteData mapHead, ByteData gameMaps) { + static List parseMaps( + ByteData mapHead, + ByteData gameMaps, { + bool isShareware = false, + }) { List levels = []; // 1. READ MAPHEAD @@ -66,6 +71,21 @@ abstract class WolfMapParser { Uint16List carmackExpandedObjects = _expandCarmack(compressedObjectData); List flatObjectGrid = _expandRlew(carmackExpandedObjects, rlewTag); + for (int i = 0; i < flatObjectGrid.length; i++) { + int id = flatObjectGrid[i]; + + // Handle the 'secret' pushwalls (Logic check) + if (id == MapObject.pushwallTrigger) { + // In Wolf3D, ID 98 means the wall at this same index in Plane 0 is pushable. + // You might want to mark this in your engine state. + } + + // Filter out invalid IDs for Shareware to prevent crashes + if (isShareware && !MapObject.isSharewareCompatible(id)) { + flatObjectGrid[i] = 0; // Turn unknown objects into empty space + } + } + Matrix wallGrid = []; Matrix objectGrid = []; // NEW diff --git a/lib/features/player/player.dart b/lib/features/player/player.dart index d7971e8..f8b7307 100644 --- a/lib/features/player/player.dart +++ b/lib/features/player/player.dart @@ -132,7 +132,7 @@ class Player { switch (item.type) { case CollectibleType.health: if (health >= 100) return false; - heal(item.mapId == MapObjectId.dogFood ? 4 : 25); + heal(item.mapId == MapObject.dogFoodDecoration ? 4 : 25); pickedUp = true; break; @@ -147,11 +147,11 @@ class Player { break; case CollectibleType.treasure: - if (item.mapId == MapObjectId.cross) score += 100; - if (item.mapId == MapObjectId.chalice) score += 500; - if (item.mapId == MapObjectId.chest) score += 1000; - if (item.mapId == MapObjectId.crown) score += 5000; - if (item.mapId == MapObjectId.extraLife) { + if (item.mapId == MapObject.cross) score += 100; + if (item.mapId == MapObject.chalice) score += 500; + if (item.mapId == MapObject.chest) score += 1000; + if (item.mapId == MapObject.crown) score += 5000; + if (item.mapId == MapObject.extraLife) { heal(100); addAmmo(25); } @@ -159,7 +159,7 @@ class Player { break; case CollectibleType.weapon: - if (item.mapId == MapObjectId.machineGun) { + if (item.mapId == MapObject.machineGun) { if (weapons[WeaponType.machineGun] == null) { weapons[WeaponType.machineGun] = MachineGun(); hasMachineGun = true; @@ -168,7 +168,7 @@ class Player { requestWeaponSwitch(WeaponType.machineGun); pickedUp = true; } - if (item.mapId == MapObjectId.chainGun) { + if (item.mapId == MapObject.chainGun) { if (weapons[WeaponType.chainGun] == null) { weapons[WeaponType.chainGun] = ChainGun(); hasChainGun = true; @@ -180,8 +180,8 @@ class Player { break; case CollectibleType.key: - if (item.mapId == MapObjectId.goldKey) hasGoldKey = true; - if (item.mapId == MapObjectId.silverKey) hasSilverKey = true; + if (item.mapId == MapObject.goldKey) hasGoldKey = true; + if (item.mapId == MapObject.silverKey) hasSilverKey = true; pickedUp = true; break; } diff --git a/lib/features/renderer/renderer.dart b/lib/features/renderer/renderer.dart index 34f7afa..2fab44a 100644 --- a/lib/features/renderer/renderer.dart +++ b/lib/features/renderer/renderer.dart @@ -23,14 +23,14 @@ import 'package:wolf_dart/sprite_gallery.dart'; class WolfRenderer extends StatefulWidget { const WolfRenderer({ super.key, - required this.difficulty, + this.difficulty = Difficulty.bringEmOn, this.showSpriteGallery = false, - this.isDemo = true, + this.isShareware = true, }); final Difficulty difficulty; final bool showSpriteGallery; - final bool isDemo; + final bool isShareware; @override State createState() => _WolfRendererState(); @@ -60,11 +60,13 @@ class _WolfRendererState extends State @override void initState() { super.initState(); - _initGame(demo: widget.isDemo); + _initGame(widget.isShareware); } - Future _initGame({bool demo = true}) async { - gameMap = demo ? await WolfMap.loadDemo() : await WolfMap.load(); + Future _initGame(bool isShareware) async { + gameMap = isShareware + ? await WolfMap.loadShareware() + : await WolfMap.loadRetail(); currentLevel = gameMap.levels[0].wallGrid; doorManager.initDoors(currentLevel); @@ -77,30 +79,35 @@ class _WolfRendererState extends State for (int x = 0; x < 64; x++) { int objId = objectLevel[y][x]; - if (objId >= MapObjectId.playerNorth && - objId <= MapObjectId.playerWest) { + if (!MapObject.shouldSpawn(objId, widget.difficulty)) continue; + + if (objId >= MapObject.playerNorth && objId <= MapObject.playerWest) { double spawnAngle = 0.0; switch (objId) { - case MapObjectId.playerNorth: + case MapObject.playerNorth: spawnAngle = 3 * math.pi / 2; break; - case MapObjectId.playerEast: + case MapObject.playerEast: spawnAngle = 0.0; break; - case MapObjectId.playerSouth: + case MapObject.playerSouth: spawnAngle = math.pi / 2; break; - case MapObjectId.playerWest: + case MapObject.playerWest: spawnAngle = math.pi; break; } - player = Player(x: x + 0.5, y: y + 0.5, angle: spawnAngle); + player = Player( + x: x + 0.5, + y: y + 0.5, + angle: spawnAngle, + ); } else { Entity? newEntity = EntityRegistry.spawn( objId, x + 0.5, y + 0.5, - widget.difficulty.level, + widget.difficulty, gameMap.sprites.length, ); @@ -342,10 +349,10 @@ class _WolfRendererState extends State // Map ID 44 is usually the Ammo Clip in the Object Grid/Registry Entity? droppedAmmo = EntityRegistry.spawn( - MapObjectId.ammoClip, + MapObject.ammoClip, entity.x, entity.y, - widget.difficulty.level, + widget.difficulty, gameMap.sprites.length, ); @@ -436,28 +443,17 @@ class _WolfRendererState extends State right: 0, child: Center( child: Transform.translate( - offset: Offset( - // Replaced hidden step variables with a direct intention check! - (inputManager.isMovingForward || - inputManager.isMovingBackward) - ? math.sin( - DateTime.now() - .millisecondsSinceEpoch / - 100, - ) * - 12 - : 0, - player.weaponAnimOffset, - ), + offset: Offset(0, player.weaponAnimOffset), child: SizedBox( width: 500, height: 500, child: CustomPaint( painter: WeaponPainter( sprite: - gameMap.sprites[player - .currentWeapon - .currentSprite], + gameMap.sprites[player.currentWeapon + .getCurrentSpriteIndex( + gameMap.sprites.length, + )], ), ), ), diff --git a/lib/features/renderer/weapon_painter.dart b/lib/features/renderer/weapon_painter.dart index ad072f2..536701e 100644 --- a/lib/features/renderer/weapon_painter.dart +++ b/lib/features/renderer/weapon_painter.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:wolf_dart/classes/sprite.dart'; import 'package:wolf_dart/features/renderer/color_palette.dart'; class WeaponPainter extends CustomPainter { - final List> sprite; + final Sprite? sprite; // Initialize a reusable Paint object and disable anti-aliasing to keep the // pixels perfectly sharp and chunky. @@ -14,6 +15,8 @@ class WeaponPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { + if (sprite == null) return; + // Calculate width and height separately in case the container isn't a // perfect square double pixelWidth = size.width / 64; @@ -21,7 +24,7 @@ class WeaponPainter extends CustomPainter { for (int x = 0; x < 64; x++) { for (int y = 0; y < 64; y++) { - int colorByte = sprite[x][y]; + int colorByte = sprite![x][y]; if (colorByte != 255) { // 255 is our transparent magenta diff --git a/lib/features/weapon/weapon.dart b/lib/features/weapon/weapon.dart index df179fb..b24608a 100644 --- a/lib/features/weapon/weapon.dart +++ b/lib/features/weapon/weapon.dart @@ -30,8 +30,24 @@ abstract class Weapon { this.isAutomatic = true, }); - int get currentSprite => - state == WeaponState.idle ? idleSprite : fireFrames[frameIndex]; + int getCurrentSpriteIndex(int maxSprites) { + int baseSprite = state == WeaponState.idle + ? idleSprite + : fireFrames[frameIndex]; + + // Retail VSWAP typically has exactly 436 sprites (indices 0 to 435). + // The 20 weapon sprites are ALWAYS placed at the very end of the sprite block. + // This dynamically aligns the base index to the end of any VSWAP file! + int dynamicOffset = 436 - maxSprites; + int calculatedIndex = baseSprite - dynamicOffset; + + // Safety check! + if (calculatedIndex < 0 || calculatedIndex >= maxSprites) { + print("WARNING: Weapon sprite index $calculatedIndex out of bounds!"); + return 0; + } + return calculatedIndex; + } void releaseTrigger() { _triggerReleased = true;