Fixed sprite rendering bug and death animations
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Entity> 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<Entity> itemsToRemove = [];
|
||||
List<Entity> 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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<T> {
|
||||
/// 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<T> {
|
||||
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();
|
||||
|
||||
@@ -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<EntitySpawner> _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,
|
||||
|
||||
Reference in New Issue
Block a user