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 leftPix = vswap.getUint16(offset, Endian.little);
|
||||||
int rightPix = vswap.getUint16(offset + 2, 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++) {
|
for (int x = leftPix; x <= rightPix; x++) {
|
||||||
|
// REVERTED to your original, correct math!
|
||||||
int colOffset = vswap.getUint16(
|
int colOffset = vswap.getUint16(
|
||||||
offset + 4 + ((x - leftPix) * 2),
|
offset + 4 + ((x - leftPix) * 2),
|
||||||
Endian.little,
|
Endian.little,
|
||||||
@@ -238,12 +249,17 @@ abstract class WLParser {
|
|||||||
if (endY == 0) break;
|
if (endY == 0) break;
|
||||||
endY ~/= 2;
|
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);
|
int startY = vswap.getUint16(cmdOffset + 4, Endian.little);
|
||||||
startY ~/= 2;
|
startY ~/= 2;
|
||||||
|
|
||||||
for (int y = startY; y < endY; y++) {
|
// Keep the safety clamps for retail version
|
||||||
// Write directly to the 1D array
|
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);
|
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
|
// Write the encoded Sixel characters for each color present in the band
|
||||||
bool firstColor = true;
|
bool firstColor = true;
|
||||||
for (var entry in colorMap.entries) {
|
for (var entry in colorMap.entries) {
|
||||||
if (!firstColor)
|
if (!firstColor) {
|
||||||
sb.write('\$'); // Carriage return to overlay colors on the same band
|
// Carriage return to overlay colors on the same band
|
||||||
|
sb.write('\$');
|
||||||
|
}
|
||||||
firstColor = false;
|
firstColor = false;
|
||||||
|
|
||||||
// Select color index
|
// 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_entities.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_input.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 {
|
class WolfEngine {
|
||||||
WolfEngine({
|
WolfEngine({
|
||||||
required this.data,
|
required this.data,
|
||||||
@@ -17,35 +22,60 @@ class WolfEngine {
|
|||||||
onPlaySound: (sfxId) => audio.playSoundEffect(sfxId),
|
onPlaySound: (sfxId) => audio.playSoundEffect(sfxId),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Total milliseconds elapsed since the engine was initialized.
|
||||||
int _timeAliveMs = 0;
|
int _timeAliveMs = 0;
|
||||||
|
|
||||||
|
/// The static game data (textures, sounds, maps) parsed from original files.
|
||||||
final WolfensteinData data;
|
final WolfensteinData data;
|
||||||
|
|
||||||
|
/// The active difficulty level, affecting enemy spawning and behavior.
|
||||||
final Difficulty difficulty;
|
final Difficulty difficulty;
|
||||||
|
|
||||||
|
/// The episode index where the game session begins.
|
||||||
final int startingEpisode;
|
final int startingEpisode;
|
||||||
|
|
||||||
|
/// Handles music and sound effect playback.
|
||||||
final EngineAudio audio;
|
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;
|
final void Function() onGameWon;
|
||||||
|
|
||||||
// Managers
|
// --- State Managers ---
|
||||||
|
|
||||||
|
/// Manages the state and animation of doors throughout the level.
|
||||||
final DoorManager doorManager;
|
final DoorManager doorManager;
|
||||||
|
|
||||||
|
/// Polls and processes raw user input into actionable engine commands.
|
||||||
final Wolf3dInput input;
|
final Wolf3dInput input;
|
||||||
|
|
||||||
|
/// Handles the detection and movement of secret "Pushwalls".
|
||||||
final PushwallManager pushwallManager = PushwallManager();
|
final PushwallManager pushwallManager = PushwallManager();
|
||||||
|
|
||||||
// State
|
// --- World State ---
|
||||||
|
|
||||||
|
/// The player's current position, stats, and inventory.
|
||||||
late Player player;
|
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;
|
late SpriteMap currentLevel;
|
||||||
|
|
||||||
|
/// The static level data source used for reloading or reference.
|
||||||
late WolfLevel activeLevel;
|
late WolfLevel activeLevel;
|
||||||
|
|
||||||
|
/// All dynamic entities currently in the level (Enemies, Pickups).
|
||||||
List<Entity> entities = [];
|
List<Entity> entities = [];
|
||||||
|
|
||||||
int _currentEpisodeIndex = 0;
|
int _currentEpisodeIndex = 0;
|
||||||
int _currentLevelIndex = 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;
|
int? _returnLevelIndex;
|
||||||
|
|
||||||
bool isInitialized = false;
|
bool isInitialized = false;
|
||||||
|
|
||||||
|
/// Initializes the engine, sets the starting episode, and loads the first level.
|
||||||
void init() {
|
void init() {
|
||||||
_currentEpisodeIndex = startingEpisode;
|
_currentEpisodeIndex = startingEpisode;
|
||||||
_currentLevelIndex = 0;
|
_currentLevelIndex = 0;
|
||||||
@@ -53,23 +83,29 @@ class WolfEngine {
|
|||||||
isInitialized = true;
|
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) {
|
void tick(Duration elapsed) {
|
||||||
if (!isInitialized) return;
|
if (!isInitialized) return;
|
||||||
|
|
||||||
_timeAliveMs += elapsed.inMilliseconds;
|
_timeAliveMs += elapsed.inMilliseconds;
|
||||||
|
|
||||||
|
// 1. Process User Input
|
||||||
input.update();
|
input.update();
|
||||||
final currentInput = input.currentInput;
|
final currentInput = input.currentInput;
|
||||||
|
|
||||||
final inputResult = _processInputs(elapsed, currentInput);
|
final inputResult = _processInputs(elapsed, currentInput);
|
||||||
|
|
||||||
|
// 2. Update Environment (Doors & Pushwalls)
|
||||||
doorManager.update(elapsed);
|
doorManager.update(elapsed);
|
||||||
pushwallManager.update(elapsed, currentLevel);
|
pushwallManager.update(elapsed, currentLevel);
|
||||||
player.tick(elapsed);
|
|
||||||
|
|
||||||
|
// 3. Update Physics & Movement
|
||||||
|
player.tick(elapsed);
|
||||||
player.angle += inputResult.dAngle;
|
player.angle += inputResult.dAngle;
|
||||||
|
|
||||||
|
// Normalize angle to [0, 2π]
|
||||||
if (player.angle < 0) player.angle += 2 * math.pi;
|
if (player.angle < 0) player.angle += 2 * math.pi;
|
||||||
if (player.angle >= 2 * math.pi) 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.x = validatedPos.x;
|
||||||
player.y = validatedPos.y;
|
player.y = validatedPos.y;
|
||||||
|
|
||||||
|
// 4. Update Dynamic World (Enemies & Combat)
|
||||||
_updateEntities(elapsed);
|
_updateEntities(elapsed);
|
||||||
|
|
||||||
player.updateWeapon(
|
player.updateWeapon(
|
||||||
@@ -90,38 +127,33 @@ class WolfEngine {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wipes the current world state and builds a new floor from map data.
|
||||||
void _loadLevel() {
|
void _loadLevel() {
|
||||||
entities.clear();
|
entities.clear();
|
||||||
|
|
||||||
final episode = data.episodes[_currentEpisodeIndex];
|
final episode = data.episodes[_currentEpisodeIndex];
|
||||||
activeLevel = episode.levels[_currentLevelIndex];
|
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]));
|
currentLevel = List.generate(64, (y) => List.from(activeLevel.wallGrid[y]));
|
||||||
final SpriteMap objectLevel = activeLevel.objectGrid;
|
final SpriteMap objectLevel = activeLevel.objectGrid;
|
||||||
|
|
||||||
doorManager.initDoors(currentLevel);
|
doorManager.initDoors(currentLevel);
|
||||||
|
|
||||||
pushwallManager.initPushwalls(currentLevel, objectLevel);
|
pushwallManager.initPushwalls(currentLevel, objectLevel);
|
||||||
|
|
||||||
audio.playLevelMusic(activeLevel);
|
audio.playLevelMusic(activeLevel);
|
||||||
|
|
||||||
|
// Spawn Player and Entities from the Object Grid
|
||||||
for (int y = 0; y < 64; y++) {
|
for (int y = 0; y < 64; y++) {
|
||||||
for (int x = 0; x < 64; x++) {
|
for (int x = 0; x < 64; x++) {
|
||||||
int objId = objectLevel[y][x];
|
int objId = objectLevel[y][x];
|
||||||
|
|
||||||
|
// Map IDs 19-22 are Reserved for Player Starts
|
||||||
if (objId >= MapObject.playerNorth && objId <= MapObject.playerWest) {
|
if (objId >= MapObject.playerNorth && objId <= MapObject.playerWest) {
|
||||||
double spawnAngle = 0.0;
|
player = Player(
|
||||||
if (objId == MapObject.playerNorth) {
|
x: x + 0.5,
|
||||||
spawnAngle = 3 * math.pi / 2;
|
y: y + 0.5,
|
||||||
} else if (objId == MapObject.playerEast) {
|
angle: MapObject.getAngle(objId),
|
||||||
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);
|
|
||||||
} else {
|
} else {
|
||||||
Entity? newEntity = EntityRegistry.spawn(
|
Entity? newEntity = EntityRegistry.spawn(
|
||||||
objId,
|
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 y = 0; y < 64; y++) {
|
||||||
for (int x = 0; x < 64; x++) {
|
for (int x = 0; x < 64; x++) {
|
||||||
int id = currentLevel[y][x];
|
int id = currentLevel[y][x];
|
||||||
@@ -146,18 +179,19 @@ class WolfEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_bumpPlayerIfStuck();
|
_bumpPlayerIfStuck();
|
||||||
print("Loaded Floor: ${_currentLevelIndex + 1} - ${activeLevel.name}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handles floor transitions, including the "Level 10" secret floor logic.
|
||||||
void _onLevelCompleted({bool isSecretExit = false}) {
|
void _onLevelCompleted({bool isSecretExit = false}) {
|
||||||
audio.stopMusic();
|
audio.stopMusic();
|
||||||
|
|
||||||
final currentEpisode = data.episodes[_currentEpisodeIndex];
|
final currentEpisode = data.episodes[_currentEpisodeIndex];
|
||||||
|
|
||||||
if (isSecretExit) {
|
if (isSecretExit) {
|
||||||
|
// Secret exits jump to map index 9 (Level 10)
|
||||||
_returnLevelIndex = _currentLevelIndex + 1;
|
_returnLevelIndex = _currentLevelIndex + 1;
|
||||||
_currentLevelIndex = 9;
|
_currentLevelIndex = 9;
|
||||||
} else {
|
} else {
|
||||||
|
// Returning from Level 10 or moving to the next sequential floor
|
||||||
if (_currentLevelIndex == 9 && _returnLevelIndex != null) {
|
if (_currentLevelIndex == 9 && _returnLevelIndex != null) {
|
||||||
_currentLevelIndex = _returnLevelIndex!;
|
_currentLevelIndex = _returnLevelIndex!;
|
||||||
_returnLevelIndex = null;
|
_returnLevelIndex = null;
|
||||||
@@ -168,13 +202,13 @@ class WolfEngine {
|
|||||||
|
|
||||||
if (_currentLevelIndex >= currentEpisode.levels.length ||
|
if (_currentLevelIndex >= currentEpisode.levels.length ||
|
||||||
_currentLevelIndex > 9) {
|
_currentLevelIndex > 9) {
|
||||||
print("Episode Completed! You win!");
|
|
||||||
onGameWon();
|
onGameWon();
|
||||||
} else {
|
} else {
|
||||||
_loadLevel();
|
_loadLevel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Translates [EngineInput] into movement vectors and rotation.
|
||||||
({Coordinate2D movement, double dAngle}) _processInputs(
|
({Coordinate2D movement, double dAngle}) _processInputs(
|
||||||
Duration elapsed,
|
Duration elapsed,
|
||||||
EngineInput input,
|
EngineInput input,
|
||||||
@@ -185,11 +219,9 @@ class WolfEngine {
|
|||||||
Coordinate2D movement = const Coordinate2D(0, 0);
|
Coordinate2D movement = const Coordinate2D(0, 0);
|
||||||
double dAngle = 0.0;
|
double dAngle = 0.0;
|
||||||
|
|
||||||
// Read directly from the passed-in EngineInput object
|
|
||||||
if (input.requestedWeapon != null) {
|
if (input.requestedWeapon != null) {
|
||||||
player.requestWeaponSwitch(input.requestedWeapon!);
|
player.requestWeaponSwitch(input.requestedWeapon!);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.isFiring) {
|
if (input.isFiring) {
|
||||||
player.fire(_timeAliveMs);
|
player.fire(_timeAliveMs);
|
||||||
} else {
|
} else {
|
||||||
@@ -203,16 +235,17 @@ class WolfEngine {
|
|||||||
math.cos(player.angle),
|
math.cos(player.angle),
|
||||||
math.sin(player.angle),
|
math.sin(player.angle),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (input.isMovingForward) movement += forwardVec * moveSpeed;
|
if (input.isMovingForward) movement += forwardVec * moveSpeed;
|
||||||
if (input.isMovingBackward) movement -= forwardVec * moveSpeed;
|
if (input.isMovingBackward) movement -= forwardVec * moveSpeed;
|
||||||
|
|
||||||
|
// Handle Wall Interactions (Switches, Doors, Secret Walls)
|
||||||
if (input.isInteracting) {
|
if (input.isInteracting) {
|
||||||
int targetX = (player.x + math.cos(player.angle)).toInt();
|
int targetX = (player.x + math.cos(player.angle)).toInt();
|
||||||
int targetY = (player.y + math.sin(player.angle)).toInt();
|
int targetY = (player.y + math.sin(player.angle)).toInt();
|
||||||
|
|
||||||
if (targetX >= 0 && targetX < 64 && targetY >= 0 && targetY < 64) {
|
if (targetX >= 0 && targetX < 64 && targetY >= 0 && targetY < 64) {
|
||||||
int wallId = currentLevel[targetY][targetX];
|
int wallId = currentLevel[targetY][targetX];
|
||||||
|
// Handle Elevator Switches
|
||||||
if (wallId == MapObject.normalElevatorSwitch) {
|
if (wallId == MapObject.normalElevatorSwitch) {
|
||||||
_onLevelCompleted(isSecretExit: false);
|
_onLevelCompleted(isSecretExit: false);
|
||||||
return (movement: const Coordinate2D(0, 0), dAngle: 0.0);
|
return (movement: const Coordinate2D(0, 0), dAngle: 0.0);
|
||||||
@@ -220,17 +253,7 @@ class WolfEngine {
|
|||||||
_onLevelCompleted(isSecretExit: true);
|
_onLevelCompleted(isSecretExit: true);
|
||||||
return (movement: const Coordinate2D(0, 0), dAngle: 0.0);
|
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);
|
doorManager.handleInteraction(player.x, player.y, player.angle);
|
||||||
pushwallManager.handleInteraction(
|
pushwallManager.handleInteraction(
|
||||||
player.x,
|
player.x,
|
||||||
@@ -243,16 +266,18 @@ class WolfEngine {
|
|||||||
return (movement: movement, dAngle: dAngle);
|
return (movement: movement, dAngle: dAngle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Performs axis-aligned collision detection with a wall-margin buffer.
|
||||||
Coordinate2D _calculateValidatedPosition(
|
Coordinate2D _calculateValidatedPosition(
|
||||||
Coordinate2D currentPos,
|
Coordinate2D currentPos,
|
||||||
Coordinate2D movement,
|
Coordinate2D movement,
|
||||||
) {
|
) {
|
||||||
const double margin = 0.3;
|
const double margin = 0.3; // Prevents clipping through wall edges
|
||||||
double newX = currentPos.x;
|
double newX = currentPos.x;
|
||||||
double newY = currentPos.y;
|
double newY = currentPos.y;
|
||||||
|
|
||||||
Coordinate2D target = currentPos + movement;
|
Coordinate2D target = currentPos + movement;
|
||||||
|
|
||||||
|
// Check X-axis Movement
|
||||||
if (movement.x != 0) {
|
if (movement.x != 0) {
|
||||||
int checkX = (movement.x > 0)
|
int checkX = (movement.x > 0)
|
||||||
? (target.x + margin).toInt()
|
? (target.x + margin).toInt()
|
||||||
@@ -260,6 +285,7 @@ class WolfEngine {
|
|||||||
if (isWalkable(checkX, currentPos.y.toInt())) newX = target.x;
|
if (isWalkable(checkX, currentPos.y.toInt())) newX = target.x;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check Y-axis Movement
|
||||||
if (movement.y != 0) {
|
if (movement.y != 0) {
|
||||||
int checkY = (movement.y > 0)
|
int checkY = (movement.y > 0)
|
||||||
? (target.y + margin).toInt()
|
? (target.y + margin).toInt()
|
||||||
@@ -270,26 +296,45 @@ class WolfEngine {
|
|||||||
return Coordinate2D(newX, newY);
|
return Coordinate2D(newX, newY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates all [Enemy] and [Collectible] entities in the world.
|
||||||
void _updateEntities(Duration elapsed) {
|
void _updateEntities(Duration elapsed) {
|
||||||
List<Entity> itemsToRemove = [];
|
List<Entity> itemsToRemove = [];
|
||||||
List<Entity> itemsToAdd = [];
|
List<Entity> itemsToAdd = [];
|
||||||
|
|
||||||
for (Entity entity in entities) {
|
for (Entity entity in entities) {
|
||||||
if (entity is Enemy) {
|
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(
|
final intent = entity.update(
|
||||||
elapsedMs: _timeAliveMs,
|
elapsedMs: _timeAliveMs,
|
||||||
playerPosition: player.position,
|
playerPosition: player.position,
|
||||||
isWalkable: isWalkable,
|
isWalkable: isWalkable,
|
||||||
tryOpenDoor: doorManager.tryOpenDoor,
|
tryOpenDoor: doorManager.tryOpenDoor,
|
||||||
onDamagePlayer: (int damage) {
|
onDamagePlayer: (int damage) => player.takeDamage(damage),
|
||||||
player.takeDamage(damage);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
entity.angle = intent.newAngle;
|
entity.angle = intent.newAngle;
|
||||||
entity.x += intent.movement.x;
|
entity.x += intent.movement.x;
|
||||||
entity.y += intent.movement.y;
|
entity.y += intent.movement.y;
|
||||||
|
|
||||||
|
// Handle Item Drops
|
||||||
if (entity.state == EntityState.dead &&
|
if (entity.state == EntityState.dead &&
|
||||||
entity.isDying &&
|
entity.isDying &&
|
||||||
!entity.hasDroppedItem) {
|
!entity.hasDroppedItem) {
|
||||||
@@ -318,12 +363,14 @@ class WolfEngine {
|
|||||||
if (itemsToAdd.isNotEmpty) entities.addAll(itemsToAdd);
|
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) {
|
bool isWalkable(int x, int y) {
|
||||||
if (currentLevel[y][x] == 0) return true;
|
if (currentLevel[y][x] == 0) return true;
|
||||||
if (currentLevel[y][x] >= 90) return doorManager.isDoorOpenEnough(x, y);
|
if (currentLevel[y][x] >= 90) return doorManager.isDoorOpenEnough(x, y);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Teleports the player to the nearest empty tile if they spawn inside a wall.
|
||||||
void _bumpPlayerIfStuck() {
|
void _bumpPlayerIfStuck() {
|
||||||
int pX = player.x.toInt();
|
int pX = player.x.toInt();
|
||||||
int pY = player.y.toInt();
|
int pY = player.y.toInt();
|
||||||
@@ -341,7 +388,6 @@ class WolfEngine {
|
|||||||
if (currentLevel[y][x] == 0) {
|
if (currentLevel[y][x] == 0) {
|
||||||
Coordinate2D safeSpot = Coordinate2D(x + 0.5, y + 0.5);
|
Coordinate2D safeSpot = Coordinate2D(x + 0.5, y + 0.5);
|
||||||
double dist = safeSpot.distanceTo(player.position);
|
double dist = safeSpot.distanceTo(player.position);
|
||||||
|
|
||||||
if (dist < shortestDist) {
|
if (dist < shortestDist) {
|
||||||
shortestDist = dist;
|
shortestDist = dist;
|
||||||
nearestSafeSpot = safeSpot;
|
nearestSafeSpot = safeSpot;
|
||||||
|
|||||||
@@ -1,14 +1,24 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
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/wolf_3d_entities.dart';
|
||||||
import 'package:wolf_3d_dart/src/entities/entity.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 {
|
class HansGrosse extends Enemy {
|
||||||
static const double speed = 0.04;
|
static const double speed = 0.04;
|
||||||
static const int _baseSprite = 291;
|
static const int _baseSprite = 291;
|
||||||
bool _hasFiredThisCycle = false;
|
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({
|
HansGrosse({
|
||||||
required super.x,
|
required super.x,
|
||||||
required super.y,
|
required super.y,
|
||||||
@@ -23,7 +33,7 @@ class HansGrosse extends Enemy {
|
|||||||
2 => 1050,
|
2 => 1050,
|
||||||
_ => 1200,
|
_ => 1200,
|
||||||
};
|
};
|
||||||
damage = 20; // Dual chainguns hit hard!
|
damage = 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
static HansGrosse? trySpawn(
|
static HansGrosse? trySpawn(
|
||||||
|
|||||||
@@ -1,25 +1,37 @@
|
|||||||
import 'dart:math' as math;
|
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.dart';
|
||||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_animation.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/entities/enemies/enemy_type.dart';
|
||||||
import 'package:wolf_3d_dart/src/entities/entity.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 {
|
class Dog extends Enemy {
|
||||||
|
/// The movement speed of the Dog in tiles per frame.
|
||||||
static const double speed = 0.05;
|
static const double speed = 0.05;
|
||||||
|
|
||||||
|
/// Ensures the dog only deals damage once per leap animation.
|
||||||
bool _hasBittenThisCycle = false;
|
bool _hasBittenThisCycle = false;
|
||||||
|
|
||||||
static EnemyType get type => EnemyType.dog;
|
@override
|
||||||
|
EnemyType get type => EnemyType.dog;
|
||||||
|
|
||||||
Dog({
|
Dog({
|
||||||
required super.x,
|
required super.x,
|
||||||
required super.y,
|
required super.y,
|
||||||
required super.angle,
|
required super.angle,
|
||||||
required super.mapId,
|
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;
|
health = 1;
|
||||||
damage = 5;
|
damage = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -68,19 +80,21 @@ class Dog extends Enemy {
|
|||||||
angleDiff: diff,
|
angleDiff: diff,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Dogs attack based on distance, so wrap the movement and attack in alert checks
|
// --- State: Patrolling ---
|
||||||
if (state == EntityState.patrolling) {
|
if (state == EntityState.patrolling) {
|
||||||
if (!isAlerted || distance > 1.0) {
|
if (!isAlerted || distance > 1.0) {
|
||||||
double currentMoveAngle = isAlerted ? angleToPlayer : angle;
|
double currentMoveAngle = isAlerted ? angleToPlayer : angle;
|
||||||
double moveX = math.cos(currentMoveAngle) * speed;
|
|
||||||
double moveY = math.sin(currentMoveAngle) * speed;
|
|
||||||
movement = getValidMovement(
|
movement = getValidMovement(
|
||||||
Coordinate2D(moveX, moveY),
|
Coordinate2D(
|
||||||
|
math.cos(currentMoveAngle) * speed,
|
||||||
|
math.sin(currentMoveAngle) * speed,
|
||||||
|
),
|
||||||
isWalkable,
|
isWalkable,
|
||||||
tryOpenDoor,
|
tryOpenDoor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dogs switch to attacking state based on melee proximity (1 tile).
|
||||||
if (isAlerted && distance < 1.0) {
|
if (isAlerted && distance < 1.0) {
|
||||||
state = EntityState.attacking;
|
state = EntityState.attacking;
|
||||||
lastActionTime = elapsedMs;
|
lastActionTime = elapsedMs;
|
||||||
@@ -88,8 +102,10 @@ class Dog extends Enemy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- State: Attacking (The Leap) ---
|
||||||
if (state == EntityState.attacking) {
|
if (state == EntityState.attacking) {
|
||||||
int time = elapsedMs - lastActionTime;
|
int time = elapsedMs - lastActionTime;
|
||||||
|
// Damage is applied mid-animation.
|
||||||
if (time >= 200 && !_hasBittenThisCycle) {
|
if (time >= 200 && !_hasBittenThisCycle) {
|
||||||
onDamagePlayer(damage);
|
onDamagePlayer(damage);
|
||||||
_hasBittenThisCycle = true;
|
_hasBittenThisCycle = true;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'dart:math' as math;
|
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/dog.dart';
|
||||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_type.dart';
|
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_type.dart';
|
||||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/guard.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/officer.dart';
|
||||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/ss.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/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 {
|
abstract class Enemy extends Entity {
|
||||||
Enemy({
|
Enemy({
|
||||||
required super.x,
|
required super.x,
|
||||||
@@ -20,33 +24,60 @@ abstract class Enemy extends Entity {
|
|||||||
super.lastActionTime,
|
super.lastActionTime,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// The amount of damage the enemy can take before dying.
|
||||||
int health = 25;
|
int health = 25;
|
||||||
|
|
||||||
|
/// The potential damage dealt to the player per successful attack.
|
||||||
int damage = 10;
|
int damage = 10;
|
||||||
|
|
||||||
|
/// Set to true when the enemy enters the [EntityState.dead] transition.
|
||||||
bool isDying = false;
|
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;
|
bool hasDroppedItem = false;
|
||||||
|
|
||||||
|
/// When true, the enemy has spotted the player and is actively pursuing or attacking.
|
||||||
bool isAlerted = false;
|
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;
|
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) {
|
void takeDamage(int amount, int currentTime) {
|
||||||
if (state == EntityState.dead) return;
|
if (state == EntityState.dead) return;
|
||||||
|
|
||||||
health -= amount;
|
health -= amount;
|
||||||
lastActionTime = currentTime;
|
lastActionTime = currentTime;
|
||||||
|
|
||||||
|
// Any hit from the player instantly alerts the enemy
|
||||||
isAlerted = true;
|
isAlerted = true;
|
||||||
|
|
||||||
if (health <= 0) {
|
if (health <= 0) {
|
||||||
state = EntityState.dead;
|
state = EntityState.dead;
|
||||||
isDying = true;
|
isDying = true;
|
||||||
} else if (math.Random().nextDouble() < 0.5) {
|
} else if (math.Random().nextDouble() < 0.5) {
|
||||||
|
// 50% chance to enter the 'Pain' state (flinching)
|
||||||
state = EntityState.pain;
|
state = EntityState.pain;
|
||||||
} else {
|
} else {
|
||||||
|
// If no pain state, ensure they are actively patrolling/attacking
|
||||||
state = EntityState.patrolling;
|
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({
|
void checkWakeUp({
|
||||||
required int elapsedMs,
|
required int elapsedMs,
|
||||||
required Coordinate2D playerPosition,
|
required Coordinate2D playerPosition,
|
||||||
@@ -56,11 +87,13 @@ abstract class Enemy extends Entity {
|
|||||||
}) {
|
}) {
|
||||||
if (!isAlerted && hasLineOfSight(playerPosition, isWalkable)) {
|
if (!isAlerted && hasLineOfSight(playerPosition, isWalkable)) {
|
||||||
if (reactionTimeMs == 0) {
|
if (reactionTimeMs == 0) {
|
||||||
|
// First frame of spotting: calculate how long until they "wake up"
|
||||||
reactionTimeMs =
|
reactionTimeMs =
|
||||||
elapsedMs +
|
elapsedMs +
|
||||||
baseReactionMs +
|
baseReactionMs +
|
||||||
math.Random().nextInt(reactionVarianceMs);
|
math.Random().nextInt(reactionVarianceMs);
|
||||||
} else if (elapsedMs >= reactionTimeMs) {
|
} else if (elapsedMs >= reactionTimeMs) {
|
||||||
|
// Reaction delay has passed
|
||||||
isAlerted = true;
|
isAlerted = true;
|
||||||
|
|
||||||
if (state == EntityState.idle) {
|
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(
|
bool hasLineOfSight(
|
||||||
Coordinate2D playerPosition,
|
Coordinate2D playerPosition,
|
||||||
bool Function(int x, int y) isWalkable,
|
bool Function(int x, int y) isWalkable,
|
||||||
) {
|
) {
|
||||||
// 1. Proximity Check (Matches WL_STATE.C 'MINSIGHT')
|
// 1. Proximity Check (Matches 'MINSIGHT' in original C)
|
||||||
// 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) < 1.2) {
|
if (position.distanceTo(playerPosition) < 1.2) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. FOV Check (Matches original sight angles)
|
// 2. FOV Check (Matches original 180-degree sight angles)
|
||||||
double angleToPlayer = position.angleTo(playerPosition);
|
double angleToPlayer = position.angleTo(playerPosition);
|
||||||
double diff = angle - angleToPlayer;
|
double diff = angle - angleToPlayer;
|
||||||
|
|
||||||
|
// Normalize angle difference to (-pi, pi]
|
||||||
while (diff <= -math.pi) {
|
while (diff <= -math.pi) {
|
||||||
diff += 2 * math.pi;
|
diff += 2 * math.pi;
|
||||||
}
|
}
|
||||||
@@ -96,9 +133,10 @@ abstract class Enemy extends Entity {
|
|||||||
diff -= 2 * math.pi;
|
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;
|
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 currentX = position.x.toInt();
|
||||||
int currentY = position.y.toInt();
|
int currentY = position.y.toInt();
|
||||||
int targetX = playerPosition.x.toInt();
|
int targetX = playerPosition.x.toInt();
|
||||||
@@ -111,8 +149,8 @@ abstract class Enemy extends Entity {
|
|||||||
int err = dx + dy;
|
int err = dx + dy;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
if (!isWalkable(currentX, currentY)) return false;
|
if (!isWalkable(currentX, currentY)) return false; // Hit a wall
|
||||||
if (currentX == targetX && currentY == targetY) break;
|
if (currentX == targetX && currentY == targetY) break; // Reached player
|
||||||
|
|
||||||
int e2 = 2 * err;
|
int e2 = 2 * err;
|
||||||
if (e2 >= dy) {
|
if (e2 >= dy) {
|
||||||
@@ -127,6 +165,11 @@ abstract class Enemy extends Entity {
|
|||||||
return true;
|
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 getValidMovement(
|
||||||
Coordinate2D intendedMovement,
|
Coordinate2D intendedMovement,
|
||||||
bool Function(int x, int y) isWalkable,
|
bool Function(int x, int y) isWalkable,
|
||||||
@@ -150,27 +193,28 @@ abstract class Enemy extends Entity {
|
|||||||
bool canMoveDiag = isWalkable(targetTileX, targetTileY);
|
bool canMoveDiag = isWalkable(targetTileX, targetTileY);
|
||||||
|
|
||||||
if (!canMoveX || !canMoveY || !canMoveDiag) {
|
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 (!canMoveX) tryOpenDoor(targetTileX, currentTileY);
|
||||||
if (!canMoveY) tryOpenDoor(currentTileX, targetTileY);
|
if (!canMoveY) tryOpenDoor(currentTileX, targetTileY);
|
||||||
if (!canMoveDiag) tryOpenDoor(targetTileX, 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 (canMoveX) return Coordinate2D(intendedMovement.x, 0);
|
||||||
if (canMoveY) return Coordinate2D(0, intendedMovement.y);
|
if (canMoveY) return Coordinate2D(0, intendedMovement.y);
|
||||||
return const Coordinate2D(0, 0);
|
return const Coordinate2D(0, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Check Cardinal Movement
|
// 2. Check Cardinal (Straight) Movement
|
||||||
if (movedX && !movedY) {
|
if (movedX && !movedY) {
|
||||||
if (!isWalkable(targetTileX, currentTileY)) {
|
if (!isWalkable(targetTileX, currentTileY)) {
|
||||||
tryOpenDoor(targetTileX, currentTileY); // Try to open!
|
tryOpenDoor(targetTileX, currentTileY);
|
||||||
return Coordinate2D(0, intendedMovement.y);
|
return Coordinate2D(0, intendedMovement.y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (movedY && !movedX) {
|
if (movedY && !movedX) {
|
||||||
if (!isWalkable(currentTileX, targetTileY)) {
|
if (!isWalkable(currentTileX, targetTileY)) {
|
||||||
tryOpenDoor(currentTileX, targetTileY); // Try to open!
|
tryOpenDoor(currentTileX, targetTileY);
|
||||||
return Coordinate2D(intendedMovement.x, 0);
|
return Coordinate2D(intendedMovement.x, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,6 +222,7 @@ abstract class Enemy extends Entity {
|
|||||||
return intendedMovement;
|
return intendedMovement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The per-frame update logic to be implemented by specific enemy types.
|
||||||
({Coordinate2D movement, double newAngle}) update({
|
({Coordinate2D movement, double newAngle}) update({
|
||||||
required int elapsedMs,
|
required int elapsedMs,
|
||||||
required Coordinate2D playerPosition,
|
required Coordinate2D playerPosition,
|
||||||
@@ -186,6 +231,10 @@ abstract class Enemy extends Entity {
|
|||||||
required void Function(int damage) onDamagePlayer,
|
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(
|
static Enemy? spawn(
|
||||||
int objId,
|
int objId,
|
||||||
double x,
|
double x,
|
||||||
@@ -193,7 +242,7 @@ abstract class Enemy extends Entity {
|
|||||||
Difficulty difficulty, {
|
Difficulty difficulty, {
|
||||||
bool isSharewareMode = false,
|
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) {
|
if (objId == MapObject.deadGuard || objId == MapObject.deadAardwolf) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -201,7 +250,7 @@ abstract class Enemy extends Entity {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent bosses from accidentally spawning as regular enemies!
|
// Prevent bosses from spawning via standard enemy logic
|
||||||
if (objId >= MapObject.bossHansGrosse &&
|
if (objId >= MapObject.bossHansGrosse &&
|
||||||
objId <= MapObject.bossFettgesicht) {
|
objId <= MapObject.bossFettgesicht) {
|
||||||
return null;
|
return null;
|
||||||
@@ -210,12 +259,12 @@ abstract class Enemy extends Entity {
|
|||||||
final type = EnemyType.fromMapId(objId);
|
final type = EnemyType.fromMapId(objId);
|
||||||
if (type == null) return null;
|
if (type == null) return null;
|
||||||
|
|
||||||
// Reject enemies that don't exist in the shareware data!
|
// Check version compatibility
|
||||||
if (isSharewareMode && !type.existsInShareware) return null;
|
if (isSharewareMode && !type.existsInShareware) return null;
|
||||||
|
|
||||||
final mapData = type.mapData;
|
final mapData = type.mapData;
|
||||||
|
|
||||||
// ALL enemies have explicit directional angles!
|
// Resolve spawn orientation and initial AI state
|
||||||
double spawnAngle = CardinalDirection.fromEnemyIndex(objId).radians;
|
double spawnAngle = CardinalDirection.fromEnemyIndex(objId).radians;
|
||||||
EntityState spawnState;
|
EntityState spawnState;
|
||||||
|
|
||||||
@@ -226,9 +275,11 @@ abstract class Enemy extends Entity {
|
|||||||
} else if (mapData.isAmbushForDifficulty(objId, difficulty)) {
|
} else if (mapData.isAmbushForDifficulty(objId, difficulty)) {
|
||||||
spawnState = EntityState.ambush;
|
spawnState = EntityState.ambush;
|
||||||
} else {
|
} 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) {
|
return switch (type) {
|
||||||
EnemyType.guard => Guard(x: x, y: y, angle: spawnAngle, mapId: objId),
|
EnemyType.guard => Guard(x: x, y: y, angle: spawnAngle, mapId: objId),
|
||||||
EnemyType.dog => Dog(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 '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.dart';
|
||||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_animation.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/entities/enemies/enemy_type.dart';
|
||||||
import 'package:wolf_3d_dart/src/entities/entity.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 {
|
class Guard extends Enemy {
|
||||||
|
/// The movement speed of the Guard in tiles per frame.
|
||||||
static const double speed = 0.03;
|
static const double speed = 0.03;
|
||||||
|
|
||||||
|
/// Internal flag to ensure the Guard only deals damage once per attack cycle.
|
||||||
bool _hasFiredThisCycle = false;
|
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({
|
Guard({
|
||||||
required super.x,
|
required super.x,
|
||||||
required super.y,
|
required super.y,
|
||||||
required super.angle,
|
required super.angle,
|
||||||
required super.mapId,
|
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
|
@override
|
||||||
({Coordinate2D movement, double newAngle}) update({
|
({Coordinate2D movement, double newAngle}) update({
|
||||||
required int elapsedMs,
|
required int elapsedMs,
|
||||||
@@ -30,6 +49,7 @@ class Guard extends Enemy {
|
|||||||
Coordinate2D movement = const Coordinate2D(0, 0);
|
Coordinate2D movement = const Coordinate2D(0, 0);
|
||||||
double newAngle = angle;
|
double newAngle = angle;
|
||||||
|
|
||||||
|
// Standard AI 'Wake Up' check for line-of-sight detection.
|
||||||
checkWakeUp(
|
checkWakeUp(
|
||||||
elapsedMs: elapsedMs,
|
elapsedMs: elapsedMs,
|
||||||
playerPosition: playerPosition,
|
playerPosition: playerPosition,
|
||||||
@@ -39,11 +59,12 @@ class Guard extends Enemy {
|
|||||||
double distance = position.distanceTo(playerPosition);
|
double distance = position.distanceTo(playerPosition);
|
||||||
double angleToPlayer = position.angleTo(playerPosition);
|
double angleToPlayer = position.angleTo(playerPosition);
|
||||||
|
|
||||||
|
// If the enemy is alerted, they constantly turn to face the player.
|
||||||
if (isAlerted && state != EntityState.dead) {
|
if (isAlerted && state != EntityState.dead) {
|
||||||
newAngle = angleToPlayer;
|
newAngle = angleToPlayer;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate angle diff for the octant logic
|
// Calculate normalized angle difference for the octant billboarding logic.
|
||||||
double diff = angleToPlayer - newAngle;
|
double diff = angleToPlayer - newAngle;
|
||||||
while (diff <= -math.pi) {
|
while (diff <= -math.pi) {
|
||||||
diff += 2 * math.pi;
|
diff += 2 * math.pi;
|
||||||
@@ -52,7 +73,7 @@ class Guard extends Enemy {
|
|||||||
diff -= 2 * math.pi;
|
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) {
|
EnemyAnimation currentAnim = switch (state) {
|
||||||
EntityState.patrolling => EnemyAnimation.walking,
|
EntityState.patrolling => EnemyAnimation.walking,
|
||||||
EntityState.attacking => EnemyAnimation.attacking,
|
EntityState.attacking => EnemyAnimation.attacking,
|
||||||
@@ -61,6 +82,7 @@ class Guard extends Enemy {
|
|||||||
_ => EnemyAnimation.idle,
|
_ => EnemyAnimation.idle,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Update the visual sprite based on state and viewing angle.
|
||||||
spriteIndex = type.getSpriteFromAnimation(
|
spriteIndex = type.getSpriteFromAnimation(
|
||||||
animation: currentAnim,
|
animation: currentAnim,
|
||||||
elapsedMs: elapsedMs,
|
elapsedMs: elapsedMs,
|
||||||
@@ -68,7 +90,9 @@ class Guard extends Enemy {
|
|||||||
angleDiff: diff,
|
angleDiff: diff,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// --- State: Patrolling ---
|
||||||
if (state == EntityState.patrolling) {
|
if (state == EntityState.patrolling) {
|
||||||
|
// Move toward the player if alerted, otherwise maintain patrol heading.
|
||||||
if (!isAlerted || distance > 0.8) {
|
if (!isAlerted || distance > 0.8) {
|
||||||
double currentMoveAngle = isAlerted ? angleToPlayer : angle;
|
double currentMoveAngle = isAlerted ? angleToPlayer : angle;
|
||||||
double moveX = math.cos(currentMoveAngle) * speed;
|
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 (isAlerted && distance < 6.0 && elapsedMs - lastActionTime > 1500) {
|
||||||
if (hasLineOfSight(playerPosition, isWalkable)) {
|
if (hasLineOfSight(playerPosition, isWalkable)) {
|
||||||
state = EntityState.attacking;
|
state = EntityState.attacking;
|
||||||
@@ -90,18 +115,23 @@ class Guard extends Enemy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- State: Attacking ---
|
||||||
if (state == EntityState.attacking) {
|
if (state == EntityState.attacking) {
|
||||||
int timeShooting = elapsedMs - lastActionTime;
|
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) {
|
if (timeShooting >= 100 && timeShooting < 200 && !_hasFiredThisCycle) {
|
||||||
onDamagePlayer(damage);
|
onDamagePlayer(damage);
|
||||||
_hasFiredThisCycle = true;
|
_hasFiredThisCycle = true;
|
||||||
} else if (timeShooting >= 300) {
|
} else if (timeShooting >= 300) {
|
||||||
|
// Return to pursuit after the attack animation completes.
|
||||||
state = EntityState.patrolling;
|
state = EntityState.patrolling;
|
||||||
lastActionTime = elapsedMs;
|
lastActionTime = elapsedMs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- State: Pain ---
|
||||||
|
// Brief recovery period after being hit by a player bullet.
|
||||||
if (state == EntityState.pain && elapsedMs - lastActionTime > 250) {
|
if (state == EntityState.pain && elapsedMs - lastActionTime > 250) {
|
||||||
state = EntityState.patrolling;
|
state = EntityState.patrolling;
|
||||||
lastActionTime = elapsedMs;
|
lastActionTime = elapsedMs;
|
||||||
|
|||||||
@@ -1,23 +1,27 @@
|
|||||||
import 'dart:math' as math;
|
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.dart';
|
||||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_animation.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/entities/enemies/enemy_type.dart';
|
||||||
import 'package:wolf_3d_dart/src/entities/entity.dart';
|
import 'package:wolf_3d_dart/src/entities/entity.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
|
||||||
class Mutant extends Enemy {
|
class Mutant extends Enemy {
|
||||||
static const double speed = 0.04;
|
static const double speed = 0.04;
|
||||||
bool _hasFiredThisCycle = false;
|
bool _hasFiredThisCycle = false;
|
||||||
|
|
||||||
static EnemyType get type => EnemyType.mutant;
|
@override
|
||||||
|
EnemyType get type => EnemyType.mutant;
|
||||||
|
|
||||||
Mutant({
|
Mutant({
|
||||||
required super.x,
|
required super.x,
|
||||||
required super.y,
|
required super.y,
|
||||||
required super.angle,
|
required super.angle,
|
||||||
required super.mapId,
|
required super.mapId,
|
||||||
}) : super(spriteIndex: type.animations.idle.start, state: EntityState.idle) {
|
}) : super(
|
||||||
|
spriteIndex: EnemyType.mutant.animations.idle.start,
|
||||||
|
state: EntityState.idle,
|
||||||
|
) {
|
||||||
health = 45;
|
health = 45;
|
||||||
damage = 10;
|
damage = 10;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,27 @@
|
|||||||
import 'dart:math' as math;
|
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.dart';
|
||||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_animation.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/entities/enemies/enemy_type.dart';
|
||||||
import 'package:wolf_3d_dart/src/entities/entity.dart';
|
import 'package:wolf_3d_dart/src/entities/entity.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
|
||||||
class Officer extends Enemy {
|
class Officer extends Enemy {
|
||||||
static const double speed = 0.055;
|
static const double speed = 0.055;
|
||||||
bool _hasFiredThisCycle = false;
|
bool _hasFiredThisCycle = false;
|
||||||
|
|
||||||
static EnemyType get type => EnemyType.officer;
|
@override
|
||||||
|
EnemyType get type => EnemyType.officer;
|
||||||
|
|
||||||
Officer({
|
Officer({
|
||||||
required super.x,
|
required super.x,
|
||||||
required super.y,
|
required super.y,
|
||||||
required super.angle,
|
required super.angle,
|
||||||
required super.mapId,
|
required super.mapId,
|
||||||
}) : super(spriteIndex: type.animations.idle.start, state: EntityState.idle) {
|
}) : super(
|
||||||
|
spriteIndex: EnemyType.officer.animations.idle.start,
|
||||||
|
state: EntityState.idle,
|
||||||
|
) {
|
||||||
health = 50;
|
health = 50;
|
||||||
damage = 15;
|
damage = 15;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,28 @@
|
|||||||
import 'dart:math' as math;
|
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.dart';
|
||||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_animation.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/entities/enemies/enemy_type.dart';
|
||||||
import 'package:wolf_3d_dart/src/entities/entity.dart';
|
import 'package:wolf_3d_dart/src/entities/entity.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
|
||||||
class SS extends Enemy {
|
class SS extends Enemy {
|
||||||
static const double speed = 0.04;
|
static const double speed = 0.04;
|
||||||
bool _hasFiredThisCycle = false;
|
bool _hasFiredThisCycle = false;
|
||||||
|
|
||||||
static EnemyType get type => EnemyType.ss;
|
/// Instance override required for engine-level animation lookup.
|
||||||
|
@override
|
||||||
|
EnemyType get type => EnemyType.ss;
|
||||||
|
|
||||||
SS({
|
SS({
|
||||||
required super.x,
|
required super.x,
|
||||||
required super.y,
|
required super.y,
|
||||||
required super.angle,
|
required super.angle,
|
||||||
required super.mapId,
|
required super.mapId,
|
||||||
}) : super(spriteIndex: type.animations.idle.start, state: EntityState.idle) {
|
}) : super(
|
||||||
|
spriteIndex: EnemyType.ss.animations.idle.start,
|
||||||
|
state: EntityState.idle,
|
||||||
|
) {
|
||||||
health = 100;
|
health = 100;
|
||||||
damage = 20;
|
damage = 20;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,53 @@
|
|||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
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;
|
double x;
|
||||||
|
|
||||||
|
/// Vertical position in the 64x64 grid.
|
||||||
double y;
|
double y;
|
||||||
|
|
||||||
|
/// The specific sprite index from the global [WolfensteinData.sprites] list.
|
||||||
int spriteIndex;
|
int spriteIndex;
|
||||||
|
|
||||||
|
/// Current rotation in radians.
|
||||||
double angle;
|
double angle;
|
||||||
|
|
||||||
|
/// Current behavior or animation state.
|
||||||
EntityState state;
|
EntityState state;
|
||||||
|
|
||||||
|
/// The original ID from the map's object layer.
|
||||||
int mapId;
|
int mapId;
|
||||||
|
|
||||||
|
/// Timestamp (in ms) of the last state change, used for animation timing.
|
||||||
int lastActionTime;
|
int lastActionTime;
|
||||||
|
|
||||||
Entity({
|
Entity({
|
||||||
@@ -21,21 +60,25 @@ abstract class Entity<T> {
|
|||||||
this.lastActionTime = 0,
|
this.lastActionTime = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Updates the spatial coordinates using a [Coordinate2D].
|
||||||
set position(Coordinate2D pos) {
|
set position(Coordinate2D pos) {
|
||||||
x = pos.x;
|
x = pos.x;
|
||||||
y = pos.y;
|
y = pos.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the current coordinates as a [Coordinate2D].
|
||||||
Coordinate2D get position => Coordinate2D(x, y);
|
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(
|
bool hasLineOfSightFrom(
|
||||||
Coordinate2D source,
|
Coordinate2D source,
|
||||||
double sourceAngle,
|
double sourceAngle,
|
||||||
double distance,
|
double distance,
|
||||||
bool Function(int x, int y) isWalkable,
|
bool Function(int x, int y) isWalkable,
|
||||||
) {
|
) {
|
||||||
// Corrected Integer Bresenham Algorithm
|
|
||||||
int currentX = source.x.toInt();
|
int currentX = source.x.toInt();
|
||||||
int currentY = source.y.toInt();
|
int currentY = source.y.toInt();
|
||||||
int targetX = x.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/collectible.dart';
|
||||||
import 'package:wolf_3d_dart/src/entities/entities/decorations/dead_aardwolf.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';
|
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/bosses/hans_grosse.dart';
|
||||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy.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/src/entities/entity.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
|
||||||
|
/// Defines a standard signature for entity spawning functions.
|
||||||
typedef EntitySpawner =
|
typedef EntitySpawner =
|
||||||
Entity? Function(
|
Entity? Function(
|
||||||
int objId,
|
int objId,
|
||||||
@@ -16,25 +17,34 @@ typedef EntitySpawner =
|
|||||||
bool isSharewareMode,
|
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 {
|
abstract class EntityRegistry {
|
||||||
|
/// Ordered list of spawner functions used to identify Map IDs.
|
||||||
static final List<EntitySpawner> _spawners = [
|
static final List<EntitySpawner> _spawners = [
|
||||||
// Special
|
// 1. Special Case Decorations (Legacy items from source)
|
||||||
DeadGuard.trySpawn,
|
DeadGuard.trySpawn,
|
||||||
DeadAardwolf.trySpawn,
|
DeadAardwolf.trySpawn,
|
||||||
|
|
||||||
// Bosses
|
// 2. Boss Entities
|
||||||
HansGrosse.trySpawn,
|
HansGrosse.trySpawn,
|
||||||
|
|
||||||
// Decorations
|
// 3. Static Decorative Objects (Puddles, Lamps, etc.)
|
||||||
Decorative.trySpawn,
|
Decorative.trySpawn,
|
||||||
|
|
||||||
// Enemies need to try to spawn first
|
// 4. Combat Enemies (Guard, SS, Mutant, Officer, Dog)
|
||||||
Enemy.spawn,
|
Enemy.spawn,
|
||||||
|
|
||||||
// Collectables
|
// 5. Pickups (Health, Ammo, Keys)
|
||||||
Collectible.trySpawn,
|
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(
|
static Entity? spawn(
|
||||||
int objId,
|
int objId,
|
||||||
double x,
|
double x,
|
||||||
|
|||||||
Reference in New Issue
Block a user