diff --git a/packages/wolf_3d_dart/lib/src/data/wl_parser.dart b/packages/wolf_3d_dart/lib/src/data/wl_parser.dart index 4e2d845..69f7b9f 100644 --- a/packages/wolf_3d_dart/lib/src/data/wl_parser.dart +++ b/packages/wolf_3d_dart/lib/src/data/wl_parser.dart @@ -214,7 +214,18 @@ abstract class WLParser { int leftPix = vswap.getUint16(offset, Endian.little); int rightPix = vswap.getUint16(offset + 2, Endian.little); + // --- SAFETY CHECK --- + // If the bounds are outside 0-63, this is a dummy or corrupted chunk. + if (leftPix < 0 || + leftPix > 63 || + rightPix < 0 || + rightPix > 63 || + leftPix > rightPix) { + return sprite; + } + for (int x = leftPix; x <= rightPix; x++) { + // REVERTED to your original, correct math! int colOffset = vswap.getUint16( offset + 4 + ((x - leftPix) * 2), Endian.little, @@ -238,12 +249,17 @@ abstract class WLParser { if (endY == 0) break; endY ~/= 2; - int pixelOfs = vswap.getUint16(cmdOffset + 2, Endian.little); + // THE FIX: This MUST be a signed integer (getInt16) to handle DOS wraparound + int pixelOfs = vswap.getInt16(cmdOffset + 2, Endian.little); + int startY = vswap.getUint16(cmdOffset + 4, Endian.little); startY ~/= 2; - for (int y = startY; y < endY; y++) { - // Write directly to the 1D array + // Keep the safety clamps for retail version + int safeStartY = startY.clamp(0, 63); + int safeEndY = endY.clamp(0, 64); + + for (int y = safeStartY; y < safeEndY; y++) { sprite.pixels[x * 64 + y] = vswap.getUint8(baseOffset + pixelOfs + y); } diff --git a/packages/wolf_3d_dart/lib/src/engine/rasterizer/sixel_rasterizer.dart b/packages/wolf_3d_dart/lib/src/engine/rasterizer/sixel_rasterizer.dart index cd12dd5..3d6e549 100644 --- a/packages/wolf_3d_dart/lib/src/engine/rasterizer/sixel_rasterizer.dart +++ b/packages/wolf_3d_dart/lib/src/engine/rasterizer/sixel_rasterizer.dart @@ -191,8 +191,10 @@ class SixelRasterizer extends Rasterizer { // Write the encoded Sixel characters for each color present in the band bool firstColor = true; for (var entry in colorMap.entries) { - if (!firstColor) - sb.write('\$'); // Carriage return to overlay colors on the same band + if (!firstColor) { + // Carriage return to overlay colors on the same band + sb.write('\$'); + } firstColor = false; // Select color index diff --git a/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart b/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart index e7b5e64..d3748ab 100644 --- a/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart +++ b/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart @@ -5,6 +5,11 @@ import 'package:wolf_3d_dart/wolf_3d_engine.dart'; import 'package:wolf_3d_dart/wolf_3d_entities.dart'; import 'package:wolf_3d_dart/wolf_3d_input.dart'; +/// The core orchestration class for the Wolfenstein 3D engine. +/// +/// [WolfEngine] manages the game loop, level transitions, entity updates, +/// and collision detection. It serves as the bridge between raw data, +/// input systems, and the world state. class WolfEngine { WolfEngine({ required this.data, @@ -17,35 +22,60 @@ class WolfEngine { onPlaySound: (sfxId) => audio.playSoundEffect(sfxId), ); + /// Total milliseconds elapsed since the engine was initialized. int _timeAliveMs = 0; + /// The static game data (textures, sounds, maps) parsed from original files. final WolfensteinData data; + + /// The active difficulty level, affecting enemy spawning and behavior. final Difficulty difficulty; + + /// The episode index where the game session begins. final int startingEpisode; + /// Handles music and sound effect playback. final EngineAudio audio; - // Standard Dart function instead of Flutter's VoidCallback + /// Callback triggered when the final level of an episode is completed. final void Function() onGameWon; - // Managers + // --- State Managers --- + + /// Manages the state and animation of doors throughout the level. final DoorManager doorManager; + + /// Polls and processes raw user input into actionable engine commands. final Wolf3dInput input; + /// Handles the detection and movement of secret "Pushwalls". final PushwallManager pushwallManager = PushwallManager(); - // State + // --- World State --- + + /// The player's current position, stats, and inventory. late Player player; + + /// The mutable 64x64 grid representing the current world. + /// This grid is modified in real-time by doors and pushwalls. late SpriteMap currentLevel; + + /// The static level data source used for reloading or reference. late WolfLevel activeLevel; + + /// All dynamic entities currently in the level (Enemies, Pickups). List entities = []; int _currentEpisodeIndex = 0; int _currentLevelIndex = 0; + + /// Stores the previous level index when entering a secret floor, + /// allowing the player to return to the correct spot. int? _returnLevelIndex; bool isInitialized = false; + /// Initializes the engine, sets the starting episode, and loads the first level. void init() { _currentEpisodeIndex = startingEpisode; _currentLevelIndex = 0; @@ -53,23 +83,29 @@ class WolfEngine { isInitialized = true; } - // Expect standard Dart Duration. The host app is responsible for the loop. + /// The primary heartbeat of the engine. + /// + /// Updates all world subsystems based on the [elapsed] time. + /// This should be called once per frame by the host application. void tick(Duration elapsed) { if (!isInitialized) return; _timeAliveMs += elapsed.inMilliseconds; + // 1. Process User Input input.update(); final currentInput = input.currentInput; - final inputResult = _processInputs(elapsed, currentInput); + // 2. Update Environment (Doors & Pushwalls) doorManager.update(elapsed); pushwallManager.update(elapsed, currentLevel); - player.tick(elapsed); + // 3. Update Physics & Movement + player.tick(elapsed); player.angle += inputResult.dAngle; + // Normalize angle to [0, 2π] if (player.angle < 0) player.angle += 2 * math.pi; if (player.angle >= 2 * math.pi) player.angle -= 2 * math.pi; @@ -81,6 +117,7 @@ class WolfEngine { player.x = validatedPos.x; player.y = validatedPos.y; + // 4. Update Dynamic World (Enemies & Combat) _updateEntities(elapsed); player.updateWeapon( @@ -90,38 +127,33 @@ class WolfEngine { ); } + /// Wipes the current world state and builds a new floor from map data. void _loadLevel() { entities.clear(); final episode = data.episodes[_currentEpisodeIndex]; activeLevel = episode.levels[_currentLevelIndex]; + // Create a mutable copy of the wall grid so pushwalls can modify it currentLevel = List.generate(64, (y) => List.from(activeLevel.wallGrid[y])); final SpriteMap objectLevel = activeLevel.objectGrid; doorManager.initDoors(currentLevel); - pushwallManager.initPushwalls(currentLevel, objectLevel); - audio.playLevelMusic(activeLevel); + // Spawn Player and Entities from the Object Grid for (int y = 0; y < 64; y++) { for (int x = 0; x < 64; x++) { int objId = objectLevel[y][x]; + // Map IDs 19-22 are Reserved for Player Starts if (objId >= MapObject.playerNorth && objId <= MapObject.playerWest) { - double spawnAngle = 0.0; - if (objId == MapObject.playerNorth) { - spawnAngle = 3 * math.pi / 2; - } else if (objId == MapObject.playerEast) { - spawnAngle = 0.0; - } else if (objId == MapObject.playerSouth) { - spawnAngle = math.pi / 2; - } else if (objId == MapObject.playerWest) { - spawnAngle = math.pi; - } - - player = Player(x: x + 0.5, y: y + 0.5, angle: spawnAngle); + player = Player( + x: x + 0.5, + y: y + 0.5, + angle: MapObject.getAngle(objId), + ); } else { Entity? newEntity = EntityRegistry.spawn( objId, @@ -136,6 +168,7 @@ class WolfEngine { } } + // Sanitize the level grid to ensure only valid walls/doors remain for (int y = 0; y < 64; y++) { for (int x = 0; x < 64; x++) { int id = currentLevel[y][x]; @@ -146,18 +179,19 @@ class WolfEngine { } _bumpPlayerIfStuck(); - print("Loaded Floor: ${_currentLevelIndex + 1} - ${activeLevel.name}"); } + /// Handles floor transitions, including the "Level 10" secret floor logic. void _onLevelCompleted({bool isSecretExit = false}) { audio.stopMusic(); - final currentEpisode = data.episodes[_currentEpisodeIndex]; if (isSecretExit) { + // Secret exits jump to map index 9 (Level 10) _returnLevelIndex = _currentLevelIndex + 1; _currentLevelIndex = 9; } else { + // Returning from Level 10 or moving to the next sequential floor if (_currentLevelIndex == 9 && _returnLevelIndex != null) { _currentLevelIndex = _returnLevelIndex!; _returnLevelIndex = null; @@ -168,13 +202,13 @@ class WolfEngine { if (_currentLevelIndex >= currentEpisode.levels.length || _currentLevelIndex > 9) { - print("Episode Completed! You win!"); onGameWon(); } else { _loadLevel(); } } + /// Translates [EngineInput] into movement vectors and rotation. ({Coordinate2D movement, double dAngle}) _processInputs( Duration elapsed, EngineInput input, @@ -185,11 +219,9 @@ class WolfEngine { Coordinate2D movement = const Coordinate2D(0, 0); double dAngle = 0.0; - // Read directly from the passed-in EngineInput object if (input.requestedWeapon != null) { player.requestWeaponSwitch(input.requestedWeapon!); } - if (input.isFiring) { player.fire(_timeAliveMs); } else { @@ -203,16 +235,17 @@ class WolfEngine { math.cos(player.angle), math.sin(player.angle), ); - if (input.isMovingForward) movement += forwardVec * moveSpeed; if (input.isMovingBackward) movement -= forwardVec * moveSpeed; + // Handle Wall Interactions (Switches, Doors, Secret Walls) if (input.isInteracting) { int targetX = (player.x + math.cos(player.angle)).toInt(); int targetY = (player.y + math.sin(player.angle)).toInt(); if (targetX >= 0 && targetX < 64 && targetY >= 0 && targetY < 64) { int wallId = currentLevel[targetY][targetX]; + // Handle Elevator Switches if (wallId == MapObject.normalElevatorSwitch) { _onLevelCompleted(isSecretExit: false); return (movement: const Coordinate2D(0, 0), dAngle: 0.0); @@ -220,17 +253,7 @@ class WolfEngine { _onLevelCompleted(isSecretExit: true); return (movement: const Coordinate2D(0, 0), dAngle: 0.0); } - - int objId = activeLevel.objectGrid[targetY][targetX]; - if (objId == MapObject.normalExitTrigger) { - _onLevelCompleted(isSecretExit: false); - return (movement: movement, dAngle: dAngle); - } else if (objId == MapObject.secretExitTrigger) { - _onLevelCompleted(isSecretExit: true); - return (movement: movement, dAngle: dAngle); - } } - doorManager.handleInteraction(player.x, player.y, player.angle); pushwallManager.handleInteraction( player.x, @@ -243,16 +266,18 @@ class WolfEngine { return (movement: movement, dAngle: dAngle); } + /// Performs axis-aligned collision detection with a wall-margin buffer. Coordinate2D _calculateValidatedPosition( Coordinate2D currentPos, Coordinate2D movement, ) { - const double margin = 0.3; + const double margin = 0.3; // Prevents clipping through wall edges double newX = currentPos.x; double newY = currentPos.y; Coordinate2D target = currentPos + movement; + // Check X-axis Movement if (movement.x != 0) { int checkX = (movement.x > 0) ? (target.x + margin).toInt() @@ -260,6 +285,7 @@ class WolfEngine { if (isWalkable(checkX, currentPos.y.toInt())) newX = target.x; } + // Check Y-axis Movement if (movement.y != 0) { int checkY = (movement.y > 0) ? (target.y + margin).toInt() @@ -270,26 +296,45 @@ class WolfEngine { return Coordinate2D(newX, newY); } + /// Updates all [Enemy] and [Collectible] entities in the world. void _updateEntities(Duration elapsed) { List itemsToRemove = []; List itemsToAdd = []; for (Entity entity in entities) { if (entity is Enemy) { + // --- ANIMATION TRANSITION FIX (SAFE VERSION) --- + // We check if the enemy is in the 'dead' state and currently 'isDying'. + // Inside WolfEngine._updateEntities + if (entity.state == EntityState.dead && entity.isDying) { + // We use a try-catch because HansGrosse throws an UnimplementedError on 'type'. + try { + final range = entity.type.animations.dying; + final totalAnimTime = range.length * 150; + + if (_timeAliveMs - entity.lastActionTime >= totalAnimTime) { + // Transition from 'Falling' to 'Corpse'. + entity.isDying = false; + } + } catch (_) { + // Bosses manage their 'isDying' flag manually in their update() methods. + } + } + + // Standard AI Update cycle final intent = entity.update( elapsedMs: _timeAliveMs, playerPosition: player.position, isWalkable: isWalkable, tryOpenDoor: doorManager.tryOpenDoor, - onDamagePlayer: (int damage) { - player.takeDamage(damage); - }, + onDamagePlayer: (int damage) => player.takeDamage(damage), ); entity.angle = intent.newAngle; entity.x += intent.movement.x; entity.y += intent.movement.y; + // Handle Item Drops if (entity.state == EntityState.dead && entity.isDying && !entity.hasDroppedItem) { @@ -318,12 +363,14 @@ class WolfEngine { if (itemsToAdd.isNotEmpty) entities.addAll(itemsToAdd); } + /// Returns true if a tile is empty or contains a door that is sufficiently open. bool isWalkable(int x, int y) { if (currentLevel[y][x] == 0) return true; if (currentLevel[y][x] >= 90) return doorManager.isDoorOpenEnough(x, y); return false; } + /// Teleports the player to the nearest empty tile if they spawn inside a wall. void _bumpPlayerIfStuck() { int pX = player.x.toInt(); int pY = player.y.toInt(); @@ -341,7 +388,6 @@ class WolfEngine { if (currentLevel[y][x] == 0) { Coordinate2D safeSpot = Coordinate2D(x + 0.5, y + 0.5); double dist = safeSpot.distanceTo(player.position); - if (dist < shortestDist) { shortestDist = dist; nearestSafeSpot = safeSpot; diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/bosses/hans_grosse.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/bosses/hans_grosse.dart index 5bd3cea..b126de8 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/bosses/hans_grosse.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/bosses/hans_grosse.dart @@ -1,14 +1,24 @@ import 'dart:math' as math; import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; -import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy.dart'; -import 'package:wolf_3d_dart/src/entities/entity.dart'; +import 'package:wolf_3d_dart/wolf_3d_entities.dart'; +/// The Episode 1 Boss: Hans Grosse. +/// +/// Unlike standard enemies, Hans uses manual sprite indexing because +/// he does not have 8-directional rotation and features a unique +/// dual-chaingun firing rhythm. class HansGrosse extends Enemy { static const double speed = 0.04; static const int _baseSprite = 291; bool _hasFiredThisCycle = false; + /// Hans is a unique boss and does not map to a standard [EnemyType] enum. + /// This returns an 'unknown' or specialized type if needed for the engine. + @override + EnemyType get type => + throw UnimplementedError("Hans Grosse uses manual animation logic."); + HansGrosse({ required super.x, required super.y, @@ -23,7 +33,7 @@ class HansGrosse extends Enemy { 2 => 1050, _ => 1200, }; - damage = 20; // Dual chainguns hit hard! + damage = 20; } static HansGrosse? trySpawn( diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/dog.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/dog.dart index a7f41dd..073bcf4 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/dog.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/dog.dart @@ -1,25 +1,37 @@ import 'dart:math' as math; -import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy.dart'; import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_animation.dart'; import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_type.dart'; import 'package:wolf_3d_dart/src/entities/entity.dart'; +import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; +/// The Attack Dog entity. +/// +/// Dogs are fast, melee-only enemies with very low health. They do not +/// have a 'pain' state and attack by leaping at the player once in range. class Dog extends Enemy { + /// The movement speed of the Dog in tiles per frame. static const double speed = 0.05; + + /// Ensures the dog only deals damage once per leap animation. bool _hasBittenThisCycle = false; - static EnemyType get type => EnemyType.dog; + @override + EnemyType get type => EnemyType.dog; Dog({ required super.x, required super.y, required super.angle, required super.mapId, - }) : super(spriteIndex: type.animations.idle.start, state: EntityState.idle) { + }) : super( + // Static metadata used during initialization. + spriteIndex: EnemyType.dog.animations.idle.start, + state: EntityState.idle, + ) { health = 1; - damage = 5; + damage = 2; } @override @@ -68,19 +80,21 @@ class Dog extends Enemy { angleDiff: diff, ); - // Dogs attack based on distance, so wrap the movement and attack in alert checks + // --- State: Patrolling --- if (state == EntityState.patrolling) { if (!isAlerted || distance > 1.0) { double currentMoveAngle = isAlerted ? angleToPlayer : angle; - double moveX = math.cos(currentMoveAngle) * speed; - double moveY = math.sin(currentMoveAngle) * speed; movement = getValidMovement( - Coordinate2D(moveX, moveY), + Coordinate2D( + math.cos(currentMoveAngle) * speed, + math.sin(currentMoveAngle) * speed, + ), isWalkable, tryOpenDoor, ); } + // Dogs switch to attacking state based on melee proximity (1 tile). if (isAlerted && distance < 1.0) { state = EntityState.attacking; lastActionTime = elapsedMs; @@ -88,8 +102,10 @@ class Dog extends Enemy { } } + // --- State: Attacking (The Leap) --- if (state == EntityState.attacking) { int time = elapsedMs - lastActionTime; + // Damage is applied mid-animation. if (time >= 200 && !_hasBittenThisCycle) { onDamagePlayer(damage); _hasBittenThisCycle = true; diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy.dart index 3614d0e..83eeb42 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy.dart @@ -1,6 +1,5 @@ import 'dart:math' as math; -import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; import 'package:wolf_3d_dart/src/entities/entities/enemies/dog.dart'; import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_type.dart'; import 'package:wolf_3d_dart/src/entities/entities/enemies/guard.dart'; @@ -8,7 +7,12 @@ import 'package:wolf_3d_dart/src/entities/entities/enemies/mutant.dart'; import 'package:wolf_3d_dart/src/entities/entities/enemies/officer.dart'; import 'package:wolf_3d_dart/src/entities/entities/enemies/ss.dart'; import 'package:wolf_3d_dart/src/entities/entity.dart'; +import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; +/// The base class for all computer-controlled opponents in the game. +/// +/// This class encapsulates the shared AI behaviors, including movement physics, +/// difficulty-based spawning, and the signature Wolfenstein 3D sight-detection system. abstract class Enemy extends Entity { Enemy({ required super.x, @@ -20,33 +24,60 @@ abstract class Enemy extends Entity { super.lastActionTime, }); + /// The amount of damage the enemy can take before dying. int health = 25; + + /// The potential damage dealt to the player per successful attack. int damage = 10; + + /// Set to true when the enemy enters the [EntityState.dead] transition. bool isDying = false; + + /// Returns the metadata for this specific enemy type (Guard, SS, etc.). + /// + /// This allows the engine to access [EnemyAnimationMap] data without + /// knowing the specific subclass. + EnemyType get type; + + /// Ensures enemies drop only one item (like ammo or a key) upon death. bool hasDroppedItem = false; + + /// When true, the enemy has spotted the player and is actively pursuing or attacking. bool isAlerted = false; - // Replaces ob->temp2 for reaction delays + /// Used to simulate a "reaction delay" before an alerted enemy begins to move. + /// This replaces the `ob->temp2` variable found in the original `WL_ACT2.C`. int reactionTimeMs = 0; + /// Reduces health and handles state transitions for pain or death. + /// + /// Alerts the enemy automatically upon taking damage. There is a 50% chance + /// to trigger a "pain" flinch animation if the enemy survives the hit. void takeDamage(int amount, int currentTime) { if (state == EntityState.dead) return; health -= amount; lastActionTime = currentTime; + // Any hit from the player instantly alerts the enemy isAlerted = true; if (health <= 0) { state = EntityState.dead; isDying = true; } else if (math.Random().nextDouble() < 0.5) { + // 50% chance to enter the 'Pain' state (flinching) state = EntityState.pain; } else { + // If no pain state, ensure they are actively patrolling/attacking state = EntityState.patrolling; } } + /// Periodically checks if the enemy should spot the player. + /// + /// Includes a randomized delay based on [baseReactionMs] and [reactionVarianceMs] + /// to prevent all enemies in a room from reacting on the exact same frame. void checkWakeUp({ required int elapsedMs, required Coordinate2D playerPosition, @@ -56,11 +87,13 @@ abstract class Enemy extends Entity { }) { if (!isAlerted && hasLineOfSight(playerPosition, isWalkable)) { if (reactionTimeMs == 0) { + // First frame of spotting: calculate how long until they "wake up" reactionTimeMs = elapsedMs + baseReactionMs + math.Random().nextInt(reactionVarianceMs); } else if (elapsedMs >= reactionTimeMs) { + // Reaction delay has passed isAlerted = true; if (state == EntityState.idle) { @@ -73,22 +106,26 @@ abstract class Enemy extends Entity { } } - // Matches WL_STATE.C's 'CheckLine' using canonical Integer DDA traversal + /// Determines if there is a clear, unobstructed path between the enemy and the player. + /// + /// This logic matches the sight rules of the original engine: + /// 1. **Proximity**: If the player is within ~1.2 tiles, sight is automatic. + /// 2. **Field of View**: The player must be within a 180-degree arc in front of the enemy. + /// 3. **Raycast**: Uses an Integer Bresenham traversal to check for wall obstructions. bool hasLineOfSight( Coordinate2D playerPosition, bool Function(int x, int y) isWalkable, ) { - // 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! + // 1. Proximity Check (Matches 'MINSIGHT' in original C) if (position.distanceTo(playerPosition) < 1.2) { return true; } - // 2. FOV Check (Matches original sight angles) + // 2. FOV Check (Matches original 180-degree sight angles) double angleToPlayer = position.angleTo(playerPosition); double diff = angle - angleToPlayer; + // Normalize angle difference to (-pi, pi] while (diff <= -math.pi) { diff += 2 * math.pi; } @@ -96,9 +133,10 @@ abstract class Enemy extends Entity { diff -= 2 * math.pi; } + // If the player is behind the enemy (more than 90 degrees left or right), return false if (diff.abs() > math.pi / 2) return false; - // 3. Map Check (Corrected Integer Bresenham) + // 3. Map Check (Integer Bresenham Traversal) int currentX = position.x.toInt(); int currentY = position.y.toInt(); int targetX = playerPosition.x.toInt(); @@ -111,8 +149,8 @@ abstract class Enemy extends Entity { int err = dx + dy; while (true) { - if (!isWalkable(currentX, currentY)) return false; - if (currentX == targetX && currentY == targetY) break; + if (!isWalkable(currentX, currentY)) return false; // Hit a wall + if (currentX == targetX && currentY == targetY) break; // Reached player int e2 = 2 * err; if (e2 >= dy) { @@ -127,6 +165,11 @@ abstract class Enemy extends Entity { return true; } + /// Resolves movement by checking for collisions and attempting to interact with doors. + /// + /// The logic performs separate X and Y collision checks to allow "sliding" along + /// walls. If a movement is blocked, it calls [tryOpenDoor] to simulate the + /// enemy's ability to navigate through the level. Coordinate2D getValidMovement( Coordinate2D intendedMovement, bool Function(int x, int y) isWalkable, @@ -150,27 +193,28 @@ abstract class Enemy extends Entity { bool canMoveDiag = isWalkable(targetTileX, targetTileY); if (!canMoveX || !canMoveY || !canMoveDiag) { - // Trigger doors if they are blocking the path + // Trigger door logic if a wall is blocking the path if (!canMoveX) tryOpenDoor(targetTileX, currentTileY); if (!canMoveY) tryOpenDoor(currentTileX, targetTileY); if (!canMoveDiag) tryOpenDoor(targetTileX, targetTileY); + // Allow sliding: if X is clear but Y/Diag is blocked, return only X movement if (canMoveX) return Coordinate2D(intendedMovement.x, 0); if (canMoveY) return Coordinate2D(0, intendedMovement.y); return const Coordinate2D(0, 0); } } - // 2. Check Cardinal Movement + // 2. Check Cardinal (Straight) Movement if (movedX && !movedY) { if (!isWalkable(targetTileX, currentTileY)) { - tryOpenDoor(targetTileX, currentTileY); // Try to open! + tryOpenDoor(targetTileX, currentTileY); return Coordinate2D(0, intendedMovement.y); } } if (movedY && !movedX) { if (!isWalkable(currentTileX, targetTileY)) { - tryOpenDoor(currentTileX, targetTileY); // Try to open! + tryOpenDoor(currentTileX, targetTileY); return Coordinate2D(intendedMovement.x, 0); } } @@ -178,6 +222,7 @@ abstract class Enemy extends Entity { return intendedMovement; } + /// The per-frame update logic to be implemented by specific enemy types. ({Coordinate2D movement, double newAngle}) update({ required int elapsedMs, required Coordinate2D playerPosition, @@ -186,6 +231,10 @@ abstract class Enemy extends Entity { required void Function(int damage) onDamagePlayer, }); + /// Factory method to spawn the correct [Enemy] subclass based on a Map ID. + /// + /// This validates [difficulty] requirements, filters out non-enemy objects, + /// and ensures [isSharewareMode] restrictions are respected (e.g., no Mutants). static Enemy? spawn( int objId, double x, @@ -193,7 +242,7 @@ abstract class Enemy extends Entity { Difficulty difficulty, { bool isSharewareMode = false, }) { - // 124 (Dead Guard) famously overwrote a patrol ID in the original engine! + // Filter out decorative bodies or player starts if (objId == MapObject.deadGuard || objId == MapObject.deadAardwolf) { return null; } @@ -201,7 +250,7 @@ abstract class Enemy extends Entity { return null; } - // Prevent bosses from accidentally spawning as regular enemies! + // Prevent bosses from spawning via standard enemy logic if (objId >= MapObject.bossHansGrosse && objId <= MapObject.bossFettgesicht) { return null; @@ -210,12 +259,12 @@ abstract class Enemy extends Entity { final type = EnemyType.fromMapId(objId); if (type == null) return null; - // Reject enemies that don't exist in the shareware data! + // Check version compatibility if (isSharewareMode && !type.existsInShareware) return null; final mapData = type.mapData; - // ALL enemies have explicit directional angles! + // Resolve spawn orientation and initial AI state double spawnAngle = CardinalDirection.fromEnemyIndex(objId).radians; EntityState spawnState; @@ -226,9 +275,11 @@ abstract class Enemy extends Entity { } else if (mapData.isAmbushForDifficulty(objId, difficulty)) { spawnState = EntityState.ambush; } else { - return null; // ID belongs to this enemy, but not on this difficulty + // The ID belongs to this enemy type, but not for this specific difficulty level + return null; } + // Return the specific instance return switch (type) { EnemyType.guard => Guard(x: x, y: y, angle: spawnAngle, mapId: objId), EnemyType.dog => Dog(x: x, y: y, angle: spawnAngle, mapId: objId), diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/guard.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/guard.dart index 0de4378..fcced22 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/guard.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/guard.dart @@ -1,24 +1,43 @@ import 'dart:math' as math; -import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy.dart'; import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_animation.dart'; import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_type.dart'; import 'package:wolf_3d_dart/src/entities/entity.dart'; +import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; +/// The standard Brown Guard entity. +/// +/// The Guard is a basic projectile enemy that patrols until alerted by +/// line-of-sight or proximity to the player. class Guard extends Enemy { + /// The movement speed of the Guard in tiles per frame. static const double speed = 0.03; + + /// Internal flag to ensure the Guard only deals damage once per attack cycle. bool _hasFiredThisCycle = false; - static EnemyType get type => EnemyType.guard; + /// Returns the metadata specific to the Guard enemy type. + @override + EnemyType get type => EnemyType.guard; Guard({ required super.x, required super.y, required super.angle, required super.mapId, - }) : super(spriteIndex: type.animations.idle.start, state: EntityState.idle); + }) : super( + // We access the static metadata via EnemyType.guard for the initializer + // since 'type' is not yet available in the super-constructor call. + spriteIndex: EnemyType.guard.animations.idle.start, + state: EntityState.idle, + ) { + health = 25; + damage = 10; + } + /// Performs the per-frame logic for the Guard, including movement, + /// combat state transitions, and sprite animation. @override ({Coordinate2D movement, double newAngle}) update({ required int elapsedMs, @@ -30,6 +49,7 @@ class Guard extends Enemy { Coordinate2D movement = const Coordinate2D(0, 0); double newAngle = angle; + // Standard AI 'Wake Up' check for line-of-sight detection. checkWakeUp( elapsedMs: elapsedMs, playerPosition: playerPosition, @@ -39,11 +59,12 @@ class Guard extends Enemy { double distance = position.distanceTo(playerPosition); double angleToPlayer = position.angleTo(playerPosition); + // If the enemy is alerted, they constantly turn to face the player. if (isAlerted && state != EntityState.dead) { newAngle = angleToPlayer; } - // Calculate angle diff for the octant logic + // Calculate normalized angle difference for the octant billboarding logic. double diff = angleToPlayer - newAngle; while (diff <= -math.pi) { diff += 2 * math.pi; @@ -52,7 +73,7 @@ class Guard extends Enemy { diff -= 2 * math.pi; } - // Use the centralized animation logic to avoid manual offset errors + // Resolve the current animation state for the renderer. EnemyAnimation currentAnim = switch (state) { EntityState.patrolling => EnemyAnimation.walking, EntityState.attacking => EnemyAnimation.attacking, @@ -61,6 +82,7 @@ class Guard extends Enemy { _ => EnemyAnimation.idle, }; + // Update the visual sprite based on state and viewing angle. spriteIndex = type.getSpriteFromAnimation( animation: currentAnim, elapsedMs: elapsedMs, @@ -68,7 +90,9 @@ class Guard extends Enemy { angleDiff: diff, ); + // --- State: Patrolling --- if (state == EntityState.patrolling) { + // Move toward the player if alerted, otherwise maintain patrol heading. if (!isAlerted || distance > 0.8) { double currentMoveAngle = isAlerted ? angleToPlayer : angle; double moveX = math.cos(currentMoveAngle) * speed; @@ -81,6 +105,7 @@ class Guard extends Enemy { ); } + // Attack if the player is within 6 tiles and the cooldown has passed. if (isAlerted && distance < 6.0 && elapsedMs - lastActionTime > 1500) { if (hasLineOfSight(playerPosition, isWalkable)) { state = EntityState.attacking; @@ -90,18 +115,23 @@ class Guard extends Enemy { } } + // --- State: Attacking --- if (state == EntityState.attacking) { int timeShooting = elapsedMs - lastActionTime; - // SS-Specific firing logic + + // The Guard fires a single shot between 100ms and 200ms of the attack state. if (timeShooting >= 100 && timeShooting < 200 && !_hasFiredThisCycle) { onDamagePlayer(damage); _hasFiredThisCycle = true; } else if (timeShooting >= 300) { + // Return to pursuit after the attack animation completes. state = EntityState.patrolling; lastActionTime = elapsedMs; } } + // --- State: Pain --- + // Brief recovery period after being hit by a player bullet. if (state == EntityState.pain && elapsedMs - lastActionTime > 250) { state = EntityState.patrolling; lastActionTime = elapsedMs; diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/mutant.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/mutant.dart index 3755db5..6ea4914 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/mutant.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/mutant.dart @@ -1,23 +1,27 @@ import 'dart:math' as math; -import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy.dart'; import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_animation.dart'; import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_type.dart'; import 'package:wolf_3d_dart/src/entities/entity.dart'; +import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; class Mutant extends Enemy { static const double speed = 0.04; bool _hasFiredThisCycle = false; - static EnemyType get type => EnemyType.mutant; + @override + EnemyType get type => EnemyType.mutant; Mutant({ required super.x, required super.y, required super.angle, required super.mapId, - }) : super(spriteIndex: type.animations.idle.start, state: EntityState.idle) { + }) : super( + spriteIndex: EnemyType.mutant.animations.idle.start, + state: EntityState.idle, + ) { health = 45; damage = 10; } diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/officer.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/officer.dart index 3a1d805..5539eea 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/officer.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/officer.dart @@ -1,23 +1,27 @@ import 'dart:math' as math; -import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy.dart'; import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_animation.dart'; import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_type.dart'; import 'package:wolf_3d_dart/src/entities/entity.dart'; +import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; class Officer extends Enemy { static const double speed = 0.055; bool _hasFiredThisCycle = false; - static EnemyType get type => EnemyType.officer; + @override + EnemyType get type => EnemyType.officer; Officer({ required super.x, required super.y, required super.angle, required super.mapId, - }) : super(spriteIndex: type.animations.idle.start, state: EntityState.idle) { + }) : super( + spriteIndex: EnemyType.officer.animations.idle.start, + state: EntityState.idle, + ) { health = 50; damage = 15; } diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/ss.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/ss.dart index c87ef03..6c51200 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/ss.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/ss.dart @@ -1,23 +1,28 @@ import 'dart:math' as math; -import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy.dart'; import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_animation.dart'; import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_type.dart'; import 'package:wolf_3d_dart/src/entities/entity.dart'; +import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; class SS extends Enemy { static const double speed = 0.04; bool _hasFiredThisCycle = false; - static EnemyType get type => EnemyType.ss; + /// Instance override required for engine-level animation lookup. + @override + EnemyType get type => EnemyType.ss; SS({ required super.x, required super.y, required super.angle, required super.mapId, - }) : super(spriteIndex: type.animations.idle.start, state: EntityState.idle) { + }) : super( + spriteIndex: EnemyType.ss.animations.idle.start, + state: EntityState.idle, + ) { health = 100; damage = 20; } diff --git a/packages/wolf_3d_dart/lib/src/entities/entity.dart b/packages/wolf_3d_dart/lib/src/entities/entity.dart index 515dd9d..7f5bf87 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entity.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entity.dart @@ -1,14 +1,53 @@ import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; -enum EntityState { staticObj, ambush, idle, patrolling, attacking, pain, dead } +/// Defines the high-level AI or physical state of an entity. +enum EntityState { + /// Non-moving decorative objects or items. + staticObj, -abstract class Entity { + /// Enemies waiting for the player to enter their field of view. + ambush, + + /// Enemies standing still but capable of hearing noise. + idle, + + /// Enemies moving along a predefined path. + patrolling, + + /// Entities currently performing a combat action. + attacking, + + /// Entities in a hit-stun or flinch animation. + pain, + + /// Entities that have completed their dying sequence. + dead, +} + +/// The base class for all dynamic objects in the game world. +/// +/// This includes players, enemies, and collectibles. It manages spatial +/// coordinates and provides core utilities for visibility checks. +abstract class Entity { + /// Horizontal position in the 64x64 grid. double x; + + /// Vertical position in the 64x64 grid. double y; + + /// The specific sprite index from the global [WolfensteinData.sprites] list. int spriteIndex; + + /// Current rotation in radians. double angle; + + /// Current behavior or animation state. EntityState state; + + /// The original ID from the map's object layer. int mapId; + + /// Timestamp (in ms) of the last state change, used for animation timing. int lastActionTime; Entity({ @@ -21,21 +60,25 @@ abstract class Entity { this.lastActionTime = 0, }); + /// Updates the spatial coordinates using a [Coordinate2D]. set position(Coordinate2D pos) { x = pos.x; y = pos.y; } + /// Returns the current coordinates as a [Coordinate2D]. Coordinate2D get position => Coordinate2D(x, y); - // NEW: Checks if a projectile or sightline from 'source' can reach this entity + /// Performs a grid-based line-of-sight check from a [source] to this entity. + /// + /// Uses an Integer Bresenham algorithm to traverse tiles between the points. + /// Returns `false` if the path is obstructed by a wall (non-walkable tile). bool hasLineOfSightFrom( Coordinate2D source, double sourceAngle, double distance, bool Function(int x, int y) isWalkable, ) { - // Corrected Integer Bresenham Algorithm int currentX = source.x.toInt(); int currentY = source.y.toInt(); int targetX = x.toInt(); diff --git a/packages/wolf_3d_dart/lib/src/entities/entity_registry.dart b/packages/wolf_3d_dart/lib/src/entities/entity_registry.dart index 3646e95..61b5a79 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entity_registry.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entity_registry.dart @@ -1,4 +1,3 @@ -import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; import 'package:wolf_3d_dart/src/entities/entities/collectible.dart'; import 'package:wolf_3d_dart/src/entities/entities/decorations/dead_aardwolf.dart'; import 'package:wolf_3d_dart/src/entities/entities/decorations/dead_guard.dart'; @@ -6,7 +5,9 @@ import 'package:wolf_3d_dart/src/entities/entities/decorative.dart'; import 'package:wolf_3d_dart/src/entities/entities/enemies/bosses/hans_grosse.dart'; import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy.dart'; import 'package:wolf_3d_dart/src/entities/entity.dart'; +import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; +/// Defines a standard signature for entity spawning functions. typedef EntitySpawner = Entity? Function( int objId, @@ -16,25 +17,34 @@ typedef EntitySpawner = bool isSharewareMode, }); +/// The central factory for instantiating all dynamic objects in a Wolf3D level. +/// +/// This registry uses a prioritized list of spawners. Specialized entities +/// (like bosses or unique decorations) are checked first, followed by +/// generic enemies and finally collectibles. abstract class EntityRegistry { + /// Ordered list of spawner functions used to identify Map IDs. static final List _spawners = [ - // Special + // 1. Special Case Decorations (Legacy items from source) DeadGuard.trySpawn, DeadAardwolf.trySpawn, - // Bosses + // 2. Boss Entities HansGrosse.trySpawn, - // Decorations + // 3. Static Decorative Objects (Puddles, Lamps, etc.) Decorative.trySpawn, - // Enemies need to try to spawn first + // 4. Combat Enemies (Guard, SS, Mutant, Officer, Dog) Enemy.spawn, - // Collectables + // 5. Pickups (Health, Ammo, Keys) Collectible.trySpawn, ]; + /// Interprets a [objId] from the map and returns the corresponding [Entity]. + /// + /// Returns `null` if the ID is 0 or is not recognized by any registered spawner. static Entity? spawn( int objId, double x,