Enhance enemy AI and area connectivity
- Introduced area grid management in WolfEngine to track player-connected areas. - Updated enemy behavior to consider area connectivity when alerting and moving. - Added debugging logs for enemy states and movements to assist in tracking AI behavior. - Implemented fallback area generation for levels lacking area data. - Enhanced patrol behavior for dogs and guards to prevent rapid direction changes after hitting walls. - Updated tests to validate new area connectivity logic and enemy behavior under various conditions. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:developer';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@ Future<Map<GameVersion, WolfensteinData>> discoverInDirectory({
|
|||||||
}) async {
|
}) async {
|
||||||
final dir = Directory(directoryPath ?? Directory.current.path);
|
final dir = Directory(directoryPath ?? Directory.current.path);
|
||||||
if (!await dir.exists()) {
|
if (!await dir.exists()) {
|
||||||
print('Warning: Directory does not exist -> ${dir.path}');
|
log('Warning: Directory does not exist -> ${dir.path}');
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +61,9 @@ Future<Map<GameVersion, WolfensteinData>> discoverInDirectory({
|
|||||||
.where((f) => !foundFiles.containsKey(f))
|
.where((f) => !foundFiles.containsKey(f))
|
||||||
.map((f) => '${f.baseName}.$ext')
|
.map((f) => '${f.baseName}.$ext')
|
||||||
.join(', ');
|
.join(', ');
|
||||||
print('Found partial data for ${version.name}. Missing: $missingFiles');
|
log(
|
||||||
|
'Found partial data for ${version.name}. Missing: $missingFiles',
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,10 +76,10 @@ Future<Map<GameVersion, WolfensteinData>> discoverInDirectory({
|
|||||||
final hash = md5.convert(vswapBytes).toString();
|
final hash = md5.convert(vswapBytes).toString();
|
||||||
final identity = DataVersion.fromChecksum(hash);
|
final identity = DataVersion.fromChecksum(hash);
|
||||||
|
|
||||||
print('--- Found ${version.name} ---');
|
log('--- Found ${version.name} ---');
|
||||||
print('MD5 Identity: ${identity.name} ($hash)');
|
log('MD5 Identity: ${identity.name} ($hash)');
|
||||||
if (identity == DataVersion.version10Retail) {
|
if (identity == DataVersion.version10Retail) {
|
||||||
print(
|
log(
|
||||||
'Note: Detected v1.0 specific file structure (MAPTEMP support active).',
|
'Note: Detected v1.0 specific file structure (MAPTEMP support active).',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -97,7 +100,7 @@ Future<Map<GameVersion, WolfensteinData>> discoverInDirectory({
|
|||||||
|
|
||||||
loadedVersions[version] = data;
|
loadedVersions[version] = data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error parsing data for ${version.name}: $e');
|
log('Error parsing data for ${version.name}: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
|||||||
/// objects (walls, sprites, audio, levels, and UI elements) using original
|
/// objects (walls, sprites, audio, levels, and UI elements) using original
|
||||||
/// algorithms like Carmackization, RLEW, and Huffman expansion.
|
/// algorithms like Carmackization, RLEW, and Huffman expansion.
|
||||||
abstract class WLParser {
|
abstract class WLParser {
|
||||||
|
static const int _areaTileBase = 107;
|
||||||
|
|
||||||
// --- Original Song Lookup Tables ---
|
// --- Original Song Lookup Tables ---
|
||||||
static const List<int> _sharewareMusicMap = [
|
static const List<int> _sharewareMusicMap = [
|
||||||
2, 3, 4, 5, 2, 3, 4, 5, 6, 7, // Episode 1
|
2, 3, 4, 5, 2, 3, 4, 5, 6, 7, // Episode 1
|
||||||
@@ -486,16 +488,25 @@ abstract class WLParser {
|
|||||||
// --- BUILD 64x64 GRIDS ---
|
// --- BUILD 64x64 GRIDS ---
|
||||||
List<List<int>> wallGrid = [];
|
List<List<int>> wallGrid = [];
|
||||||
List<List<int>> objectGrid = [];
|
List<List<int>> objectGrid = [];
|
||||||
|
List<List<int>> areaGrid = [];
|
||||||
|
|
||||||
for (int y = 0; y < 64; y++) {
|
for (int y = 0; y < 64; y++) {
|
||||||
List<int> wallRow = [];
|
List<int> wallRow = [];
|
||||||
List<int> objectRow = [];
|
List<int> objectRow = [];
|
||||||
|
List<int> areaRow = [];
|
||||||
for (int x = 0; x < 64; x++) {
|
for (int x = 0; x < 64; x++) {
|
||||||
wallRow.add(flatWallGrid[y * 64 + x]);
|
final rawWallId = flatWallGrid[y * 64 + x];
|
||||||
|
wallRow.add(rawWallId);
|
||||||
objectRow.add(flatObjectGrid[y * 64 + x]);
|
objectRow.add(flatObjectGrid[y * 64 + x]);
|
||||||
|
if (rawWallId >= _areaTileBase) {
|
||||||
|
areaRow.add(rawWallId - _areaTileBase);
|
||||||
|
} else {
|
||||||
|
areaRow.add(-1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
wallGrid.add(wallRow);
|
wallGrid.add(wallRow);
|
||||||
objectGrid.add(objectRow);
|
objectGrid.add(objectRow);
|
||||||
|
areaGrid.add(areaRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- ASSIGN MUSIC ---
|
// --- ASSIGN MUSIC ---
|
||||||
@@ -508,6 +519,7 @@ abstract class WLParser {
|
|||||||
name: parsedName,
|
name: parsedName,
|
||||||
wallGrid: wallGrid,
|
wallGrid: wallGrid,
|
||||||
objectGrid: objectGrid,
|
objectGrid: objectGrid,
|
||||||
|
areaGrid: areaGrid,
|
||||||
musicIndex: trackIndex,
|
musicIndex: trackIndex,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ class WolfLevel {
|
|||||||
/// A 64x64 grid of indices pointing to game objects, entities, and triggers.
|
/// A 64x64 grid of indices pointing to game objects, entities, and triggers.
|
||||||
final SpriteMap objectGrid;
|
final SpriteMap objectGrid;
|
||||||
|
|
||||||
|
/// A 64x64 grid of area numbers extracted from plane 0.
|
||||||
|
///
|
||||||
|
/// Cells with `-1` are non-area solids/unknown tiles. Area numbers are
|
||||||
|
/// zero-based and correspond to original AREATILE-derived sectors.
|
||||||
|
final SpriteMap areaGrid;
|
||||||
|
|
||||||
/// The index of the [ImfMusic] track to play while this level is active.
|
/// The index of the [ImfMusic] track to play while this level is active.
|
||||||
final int musicIndex;
|
final int musicIndex;
|
||||||
|
|
||||||
@@ -23,6 +29,7 @@ class WolfLevel {
|
|||||||
required this.name,
|
required this.name,
|
||||||
required this.wallGrid,
|
required this.wallGrid,
|
||||||
required this.objectGrid,
|
required this.objectGrid,
|
||||||
|
required this.areaGrid,
|
||||||
required this.musicIndex,
|
required this.musicIndex,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class CliSilentAudio implements EngineAudio {
|
|||||||
@override
|
@override
|
||||||
void playLevelMusic(WolfLevel level) {
|
void playLevelMusic(WolfLevel level) {
|
||||||
// Optional: Print a log so you know it's working!
|
// Optional: Print a log so you know it's working!
|
||||||
// print("🎵 Playing music for: ${level.name} 🎵");
|
// debugPrint("🎵 Playing music for: ${level.name} 🎵");
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:developer';
|
||||||
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';
|
||||||
@@ -46,16 +47,25 @@ class DoorManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Handles player-initiated interaction with doors based on facing direction.
|
/// Handles player-initiated interaction with doors based on facing direction.
|
||||||
void handleInteraction(double playerX, double playerY, double playerAngle) {
|
///
|
||||||
|
/// Returns the door tile coordinates when a door actually transitions/opening
|
||||||
|
/// starts, otherwise returns `null`.
|
||||||
|
({int x, int y})? handleInteraction(
|
||||||
|
double playerX,
|
||||||
|
double playerY,
|
||||||
|
double playerAngle,
|
||||||
|
) {
|
||||||
int targetX = (playerX + math.cos(playerAngle)).toInt();
|
int targetX = (playerX + math.cos(playerAngle)).toInt();
|
||||||
int targetY = (playerY + math.sin(playerAngle)).toInt();
|
int targetY = (playerY + math.sin(playerAngle)).toInt();
|
||||||
|
|
||||||
final int key = _key(targetX, targetY);
|
final int key = _key(targetX, targetY);
|
||||||
if (doors.containsKey(key)) {
|
if (doors.containsKey(key) && doors[key]!.interact()) {
|
||||||
if (doors[key]!.interact()) {
|
|
||||||
onPlaySound(WolfSound.openDoor);
|
onPlaySound(WolfSound.openDoor);
|
||||||
|
log('[DEBUG] Player opened door at ($targetX, $targetY)');
|
||||||
|
return (x: targetX, y: targetY);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempted by AI entities to open a door blocking their path.
|
/// Attempted by AI entities to open a door blocking their path.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:developer';
|
||||||
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';
|
||||||
@@ -103,7 +104,7 @@ class Player {
|
|||||||
pendingWeaponType = weaponType;
|
pendingWeaponType = weaponType;
|
||||||
switchState = WeaponSwitchState.lowering;
|
switchState = WeaponSwitchState.lowering;
|
||||||
|
|
||||||
print("[PLAYER] Requesting weapon switch to ${weaponType.name}.");
|
log("[PLAYER] Requesting weapon switch to ${weaponType.name}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Health & Damage ---
|
// --- Health & Damage ---
|
||||||
@@ -116,16 +117,16 @@ class Player {
|
|||||||
damageFlash = math.min(1.0, damageFlash + (damage * 0.05));
|
damageFlash = math.min(1.0, damageFlash + (damage * 0.05));
|
||||||
|
|
||||||
if (health <= 0) {
|
if (health <= 0) {
|
||||||
print("[PLAYER] Died! Final Score: $score");
|
log("[PLAYER] Died! Final Score: $score");
|
||||||
} else {
|
} else {
|
||||||
print("[PLAYER] Took $damage damage ($health)");
|
log("[PLAYER] Took $damage damage ($health)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void heal(int amount) {
|
void heal(int amount) {
|
||||||
final int newHealth = math.min(100, health + amount);
|
final int newHealth = math.min(100, health + amount);
|
||||||
if (health < 100) {
|
if (health < 100) {
|
||||||
print("[PLAYER] Healed for $amount ($health -> $newHealth)");
|
log("[PLAYER] Healed for $amount ($health -> $newHealth)");
|
||||||
}
|
}
|
||||||
health = newHealth;
|
health = newHealth;
|
||||||
}
|
}
|
||||||
@@ -133,14 +134,14 @@ class Player {
|
|||||||
void addAmmo(int amount) {
|
void addAmmo(int amount) {
|
||||||
final int newAmmo = math.min(99, ammo + amount);
|
final int newAmmo = math.min(99, ammo + amount);
|
||||||
if (ammo < 99) {
|
if (ammo < 99) {
|
||||||
print("[PLAYER] Picked up ammo $amount ($ammo -> $newAmmo)");
|
log("[PLAYER] Picked up ammo $amount ($ammo -> $newAmmo)");
|
||||||
}
|
}
|
||||||
ammo = newAmmo;
|
ammo = newAmmo;
|
||||||
}
|
}
|
||||||
|
|
||||||
void addLives(int amount) {
|
void addLives(int amount) {
|
||||||
lives = math.min(9, lives + amount);
|
lives = math.min(9, lives + amount);
|
||||||
print("[PLAYER] Adding $amount extra lives (Total Lives: $lives)");
|
log("[PLAYER] Adding $amount extra lives (Total Lives: $lives)");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to collect [item] and returns the SFX to play.
|
/// Attempts to collect [item] and returns the SFX to play.
|
||||||
@@ -160,7 +161,7 @@ class Player {
|
|||||||
);
|
);
|
||||||
if (effect == null) return null;
|
if (effect == null) return null;
|
||||||
|
|
||||||
print("[PLAYER] Collected ${item.runtimeType} - Effect: $effect");
|
log("[PLAYER] Collected ${item.runtimeType} - Effect: $effect");
|
||||||
|
|
||||||
if (effect.healthToRestore > 0) {
|
if (effect.healthToRestore > 0) {
|
||||||
heal(effect.healthToRestore);
|
heal(effect.healthToRestore);
|
||||||
@@ -171,7 +172,7 @@ class Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (effect.scoreToAdd > 0) {
|
if (effect.scoreToAdd > 0) {
|
||||||
print("[PLAYER] Adding ${effect.scoreToAdd} score.");
|
log("[PLAYER] Adding ${effect.scoreToAdd} score.");
|
||||||
|
|
||||||
score += effect.scoreToAdd;
|
score += effect.scoreToAdd;
|
||||||
}
|
}
|
||||||
@@ -181,12 +182,12 @@ class Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (effect.grantGoldKey) {
|
if (effect.grantGoldKey) {
|
||||||
print("[PLAYER] Collected Gold Key.");
|
log("[PLAYER] Collected Gold Key.");
|
||||||
hasGoldKey = true;
|
hasGoldKey = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (effect.grantSilverKey) {
|
if (effect.grantSilverKey) {
|
||||||
print("[PLAYER] Collected Silver Key.");
|
log("[PLAYER] Collected Silver Key.");
|
||||||
hasSilverKey = true;
|
hasSilverKey = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +204,7 @@ class Player {
|
|||||||
if (weaponType == WeaponType.machineGun) hasMachineGun = true;
|
if (weaponType == WeaponType.machineGun) hasMachineGun = true;
|
||||||
if (weaponType == WeaponType.chainGun) hasChainGun = true;
|
if (weaponType == WeaponType.chainGun) hasChainGun = true;
|
||||||
|
|
||||||
print("[PLAYER] Collected ${weaponType.name}.");
|
log("[PLAYER] Collected ${weaponType.name}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (effect.requestWeaponSwitch case final weaponType?) {
|
if (effect.requestWeaponSwitch case final weaponType?) {
|
||||||
@@ -252,7 +253,7 @@ class Player {
|
|||||||
currentTime: currentTime,
|
currentTime: currentTime,
|
||||||
onEnemyKilled: (Enemy killedEnemy) {
|
onEnemyKilled: (Enemy killedEnemy) {
|
||||||
score += killedEnemy.scoreValue;
|
score += killedEnemy.scoreValue;
|
||||||
print(
|
log(
|
||||||
"[PLAYER] Killed ${killedEnemy.runtimeType}! +${killedEnemy.scoreValue} (Score: $score)",
|
"[PLAYER] Killed ${killedEnemy.runtimeType}! +${killedEnemy.scoreValue} (Score: $score)",
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -262,7 +263,7 @@ class Player {
|
|||||||
if (currentWeapon.state == WeaponState.idle &&
|
if (currentWeapon.state == WeaponState.idle &&
|
||||||
ammo <= 0 &&
|
ammo <= 0 &&
|
||||||
currentWeapon.type != WeaponType.knife) {
|
currentWeapon.type != WeaponType.knife) {
|
||||||
print("[PLAYER] Out of ammo, switching to knife.");
|
log("[PLAYER] Out of ammo, switching to knife.");
|
||||||
requestWeaponSwitch(WeaponType.knife);
|
requestWeaponSwitch(WeaponType.knife);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:developer';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:wolf_3d_dart/src/menu/menu_manager.dart';
|
import 'package:wolf_3d_dart/src/menu/menu_manager.dart';
|
||||||
@@ -124,6 +125,15 @@ class WolfEngine {
|
|||||||
/// All dynamic entities currently in the level (Enemies, Pickups).
|
/// All dynamic entities currently in the level (Enemies, Pickups).
|
||||||
List<Entity> entities = [];
|
List<Entity> entities = [];
|
||||||
|
|
||||||
|
/// Per-tile area numbers derived from map plane 0 (or fallback fill).
|
||||||
|
late SpriteMap _areaGrid;
|
||||||
|
|
||||||
|
/// True for areas currently connected to the player's area.
|
||||||
|
List<bool> _areasByPlayer = [];
|
||||||
|
|
||||||
|
/// Total number of known area indices in the active level.
|
||||||
|
int _areaCount = 0;
|
||||||
|
|
||||||
int _currentEpisodeIndex = 0;
|
int _currentEpisodeIndex = 0;
|
||||||
|
|
||||||
bool _isPlayerMovingFast = false;
|
bool _isPlayerMovingFast = false;
|
||||||
@@ -175,6 +185,7 @@ class WolfEngine {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
frameBuffer = FrameBuffer(width, height);
|
frameBuffer = FrameBuffer(width, height);
|
||||||
|
log("[DEBUG] FrameBuffer resized to ${width}x$height");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The primary heartbeat of the engine.
|
/// The primary heartbeat of the engine.
|
||||||
@@ -222,6 +233,8 @@ class WolfEngine {
|
|||||||
player.x = validatedPos.x;
|
player.x = validatedPos.x;
|
||||||
player.y = validatedPos.y;
|
player.y = validatedPos.y;
|
||||||
|
|
||||||
|
_refreshPlayerConnectedAreas();
|
||||||
|
|
||||||
// 4. Update Dynamic World
|
// 4. Update Dynamic World
|
||||||
_updateEntities(delta);
|
_updateEntities(delta);
|
||||||
|
|
||||||
@@ -329,6 +342,7 @@ class WolfEngine {
|
|||||||
|
|
||||||
// Create a mutable copy of the wall grid so pushwalls can modify it
|
// 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]));
|
||||||
|
_areaGrid = List.generate(64, (y) => List.from(activeLevel.areaGrid[y]));
|
||||||
final SpriteMap objectLevel = activeLevel.objectGrid;
|
final SpriteMap objectLevel = activeLevel.objectGrid;
|
||||||
|
|
||||||
doorManager.initDoors(currentLevel);
|
doorManager.initDoors(currentLevel);
|
||||||
@@ -371,6 +385,9 @@ class WolfEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_buildFallbackAreasIfNeeded();
|
||||||
|
_refreshPlayerConnectedAreas();
|
||||||
|
|
||||||
_bumpPlayerIfStuck();
|
_bumpPlayerIfStuck();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -542,6 +559,8 @@ class WolfEngine {
|
|||||||
playerAngle: player.angle,
|
playerAngle: player.angle,
|
||||||
isPlayerRunning: _isPlayerMovingFast,
|
isPlayerRunning: _isPlayerMovingFast,
|
||||||
isWalkable: isWalkable,
|
isWalkable: isWalkable,
|
||||||
|
areaAt: _areaAt,
|
||||||
|
isAreaConnectedToPlayer: _isAreaConnectedToPlayer,
|
||||||
tryOpenDoor: doorManager.tryOpenDoor,
|
tryOpenDoor: doorManager.tryOpenDoor,
|
||||||
onDamagePlayer: (int damage) {
|
onDamagePlayer: (int damage) {
|
||||||
final difficultyMode = difficulty ?? Difficulty.medium;
|
final difficultyMode = difficulty ?? Difficulty.medium;
|
||||||
@@ -555,22 +574,42 @@ class WolfEngine {
|
|||||||
|
|
||||||
Coordinate2D scaledMovement = intent.movement * timeScale;
|
Coordinate2D scaledMovement = intent.movement * timeScale;
|
||||||
Coordinate2D safeMovement = _clampMovement(scaledMovement);
|
Coordinate2D safeMovement = _clampMovement(scaledMovement);
|
||||||
|
final validatedPosition = _calculateValidatedPosition(
|
||||||
|
entity.position,
|
||||||
|
safeMovement,
|
||||||
|
);
|
||||||
|
|
||||||
entity.angle = intent.newAngle;
|
entity.angle = intent.newAngle;
|
||||||
|
|
||||||
// Final sanity check: only move if the destination is on-map
|
// Final sanity check: only move if the destination is on-map
|
||||||
double nextX = entity.x + safeMovement.x;
|
if (validatedPosition.x >= 0 &&
|
||||||
double nextY = entity.y + safeMovement.y;
|
validatedPosition.x < 64 &&
|
||||||
|
validatedPosition.y >= 0 &&
|
||||||
if (nextX >= 0 && nextX < 64 && nextY >= 0 && nextY < 64) {
|
validatedPosition.y < 64) {
|
||||||
entity.x = nextX;
|
entity.x = validatedPosition.x;
|
||||||
entity.y = nextY;
|
entity.y = validatedPosition.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Item Drops
|
// Handle Item Drops
|
||||||
if (entity.state == EntityState.dead &&
|
if (entity.state == EntityState.dead &&
|
||||||
entity.isDying &&
|
entity.isDying &&
|
||||||
!entity.hasDroppedItem) {
|
!entity.hasDroppedItem) {
|
||||||
|
final dx = player.x - entity.x;
|
||||||
|
final dy = player.y - entity.y;
|
||||||
|
final distance = math.sqrt(dx * dx + dy * dy);
|
||||||
|
final bearingToPlayer = math.atan2(dy, dx);
|
||||||
|
|
||||||
|
log(
|
||||||
|
'[DEBUG] Enemy #${entity.debugId} (${entity.type.name}) death context '
|
||||||
|
'enemyPos=(${entity.x.toStringAsFixed(2)}, ${entity.y.toStringAsFixed(2)}) '
|
||||||
|
'enemyTile=(${entity.x.toInt()}, ${entity.y.toInt()}) '
|
||||||
|
'playerPos=(${player.x.toStringAsFixed(2)}, ${player.y.toStringAsFixed(2)}) '
|
||||||
|
'playerTile=(${player.x.toInt()}, ${player.y.toInt()}) '
|
||||||
|
'delta=(${dx.toStringAsFixed(2)}, ${dy.toStringAsFixed(2)}) '
|
||||||
|
'dist=${distance.toStringAsFixed(3)} '
|
||||||
|
'bearing=${bearingToPlayer.toStringAsFixed(3)}',
|
||||||
|
);
|
||||||
|
|
||||||
entity.hasDroppedItem = true;
|
entity.hasDroppedItem = true;
|
||||||
|
|
||||||
Entity? droppedItem;
|
Entity? droppedItem;
|
||||||
@@ -609,44 +648,80 @@ class WolfEngine {
|
|||||||
if (itemsToAdd.isNotEmpty) entities.addAll(itemsToAdd);
|
if (itemsToAdd.isNotEmpty) entities.addAll(itemsToAdd);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Propagates weapon noise through corridors and open doors using a Breadth-First Search.
|
/// Canonical-style gunfire alert propagation: enemies in areas connected to
|
||||||
|
/// the player area hear the shot (unless they are ambush-only placements).
|
||||||
void _propagateGunfire() {
|
void _propagateGunfire() {
|
||||||
int maxAcousticRange = 20; // How many tiles the sound wave travels
|
for (final entity in entities) {
|
||||||
int startX = player.x.toInt();
|
if (entity is! Enemy ||
|
||||||
int startY = player.y.toInt();
|
entity.isAlerted ||
|
||||||
|
entity.state == EntityState.dead) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Track visited tiles using a 1D index to prevent infinite loops
|
final int area = _areaAt(
|
||||||
Set<int> visited = {startY * 64 + startX};
|
entity.position.x.toInt(),
|
||||||
|
entity.position.y.toInt(),
|
||||||
|
);
|
||||||
|
if (area < 0 || !_isAreaConnectedToPlayer(area)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
List<({int x, int y})> queue = [(x: startX, y: startY)];
|
// Ambush enemies are intentionally deaf to noise until they see the player.
|
||||||
|
if (entity.state == EntityState.ambushing) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
int distance = 0;
|
|
||||||
|
|
||||||
while (queue.isNotEmpty && distance < maxAcousticRange) {
|
|
||||||
List<({int x, int y})> nextQueue = [];
|
|
||||||
|
|
||||||
for (var tile in queue) {
|
|
||||||
// 1. Alert any enemies standing on this specific tile
|
|
||||||
for (var entity in entities) {
|
|
||||||
if (entity is Enemy &&
|
|
||||||
!entity.isAlerted &&
|
|
||||||
entity.state != EntityState.dead) {
|
|
||||||
if (entity.position.x.toInt() == tile.x &&
|
|
||||||
entity.position.y.toInt() == tile.y) {
|
|
||||||
entity.isAlerted = true;
|
entity.isAlerted = true;
|
||||||
audio.playSoundEffect(entity.alertSoundId);
|
audio.playSoundEffect(entity.alertSoundId);
|
||||||
|
log(
|
||||||
|
'[DEBUG] Enemy #${entity.debugId} (${entity.type.name}) '
|
||||||
|
'alerted by gunfire in area $area',
|
||||||
|
);
|
||||||
|
|
||||||
// Wake them up!
|
if (entity.state == EntityState.idle) {
|
||||||
if (entity.state == EntityState.idle ||
|
|
||||||
entity.state == EntityState.ambushing) {
|
|
||||||
entity.state = EntityState.patrolling;
|
entity.state = EntityState.patrolling;
|
||||||
entity.lastActionTime = _timeAliveMs;
|
entity.lastActionTime = _timeAliveMs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int _areaAt(int x, int y) {
|
||||||
|
if (x < 0 || x >= 64 || y < 0 || y >= 64) return -1;
|
||||||
|
return _areaGrid[y][x];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Expand the sound wave outward to North, East, South, West
|
bool _isAreaConnectedToPlayer(int area) {
|
||||||
|
return area >= 0 && area < _areasByPlayer.length && _areasByPlayer[area];
|
||||||
|
}
|
||||||
|
|
||||||
|
void _buildFallbackAreasIfNeeded() {
|
||||||
|
int maxArea = -1;
|
||||||
|
for (int y = 0; y < 64; y++) {
|
||||||
|
for (int x = 0; x < 64; x++) {
|
||||||
|
if (_areaGrid[y][x] > maxArea) {
|
||||||
|
maxArea = _areaGrid[y][x];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxArea >= 0) {
|
||||||
|
_areaCount = maxArea + 1;
|
||||||
|
_areasByPlayer = List<bool>.filled(_areaCount, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compatibility fallback for maps without canonical area data: infer area
|
||||||
|
// sectors by flood-filling contiguous walkable/door tiles.
|
||||||
|
int nextArea = 0;
|
||||||
|
for (int y = 0; y < 64; y++) {
|
||||||
|
for (int x = 0; x < 64; x++) {
|
||||||
|
if (_areaGrid[y][x] >= 0 || !_isAreaFillPassable(x, y)) continue;
|
||||||
|
|
||||||
|
final queue = <({int x, int y})>[(x: x, y: y)];
|
||||||
|
_areaGrid[y][x] = nextArea;
|
||||||
|
|
||||||
|
for (int i = 0; i < queue.length; i++) {
|
||||||
|
final tile = queue[i];
|
||||||
final neighbors = <({int x, int y})>[
|
final neighbors = <({int x, int y})>[
|
||||||
(x: tile.x + 1, y: tile.y),
|
(x: tile.x + 1, y: tile.y),
|
||||||
(x: tile.x - 1, y: tile.y),
|
(x: tile.x - 1, y: tile.y),
|
||||||
@@ -654,23 +729,79 @@ class WolfEngine {
|
|||||||
(x: tile.x, y: tile.y - 1),
|
(x: tile.x, y: tile.y - 1),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (var n in neighbors) {
|
for (final n in neighbors) {
|
||||||
// Keep it within the 64x64 grid limits
|
if (n.x < 0 || n.x >= 64 || n.y < 0 || n.y >= 64) continue;
|
||||||
if (n.x >= 0 && n.x < 64 && n.y >= 0 && n.y < 64) {
|
if (_areaGrid[n.y][n.x] >= 0 || !_isAreaFillPassable(n.x, n.y)) {
|
||||||
int idx = n.y * 64 + n.x;
|
continue;
|
||||||
if (!visited.contains(idx)) {
|
}
|
||||||
visited.add(idx);
|
_areaGrid[n.y][n.x] = nextArea;
|
||||||
|
queue.add(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nextArea++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Sound only travels through walkable tiles (air and OPEN doors).
|
_areaCount = nextArea;
|
||||||
if (isWalkable(n.x, n.y)) {
|
_areasByPlayer = List<bool>.filled(_areaCount, false);
|
||||||
nextQueue.add(n);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _isAreaFillPassable(int x, int y) {
|
||||||
|
final id = currentLevel[y][x];
|
||||||
|
return id == 0 || (id >= 90 && id <= 101);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _isAreaTraversalPassable(int x, int y) {
|
||||||
|
if (x < 0 || x >= 64 || y < 0 || y >= 64) return false;
|
||||||
|
final id = currentLevel[y][x];
|
||||||
|
if (id == 0) return true;
|
||||||
|
if (id >= 90 && id <= 101) {
|
||||||
|
// For area connectivity, opening/closing doors already bridge sectors.
|
||||||
|
final door = doorManager.doors[((y & 0xFFFF) << 16) | (x & 0xFFFF)];
|
||||||
|
return door != null && door.state != DoorState.closed;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _refreshPlayerConnectedAreas() {
|
||||||
|
if (_areaCount <= 0) return;
|
||||||
|
|
||||||
|
for (int i = 0; i < _areasByPlayer.length; i++) {
|
||||||
|
_areasByPlayer[i] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int startX = player.x.toInt();
|
||||||
|
final int startY = player.y.toInt();
|
||||||
|
final int playerArea = _areaAt(startX, startY);
|
||||||
|
if (playerArea < 0) return;
|
||||||
|
|
||||||
|
final visited = List<List<bool>>.generate(
|
||||||
|
64,
|
||||||
|
(_) => List<bool>.filled(64, false),
|
||||||
|
);
|
||||||
|
final queue = <({int x, int y})>[(x: startX, y: startY)];
|
||||||
|
visited[startY][startX] = true;
|
||||||
|
|
||||||
|
for (int i = 0; i < queue.length; i++) {
|
||||||
|
final tile = queue[i];
|
||||||
|
final area = _areaAt(tile.x, tile.y);
|
||||||
|
if (area >= 0 && area < _areasByPlayer.length) {
|
||||||
|
_areasByPlayer[area] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final neighbors = <({int x, int y})>[
|
||||||
|
(x: tile.x + 1, y: tile.y),
|
||||||
|
(x: tile.x - 1, y: tile.y),
|
||||||
|
(x: tile.x, y: tile.y + 1),
|
||||||
|
(x: tile.x, y: tile.y - 1),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (final n in neighbors) {
|
||||||
|
if (n.x < 0 || n.x >= 64 || n.y < 0 || n.y >= 64) continue;
|
||||||
|
if (visited[n.y][n.x] || !_isAreaTraversalPassable(n.x, n.y)) continue;
|
||||||
|
visited[n.y][n.x] = true;
|
||||||
|
queue.add(n);
|
||||||
}
|
}
|
||||||
queue = nextQueue;
|
|
||||||
distance++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ class HansGrosse extends Enemy {
|
|||||||
required double playerAngle,
|
required double playerAngle,
|
||||||
required bool isPlayerRunning,
|
required bool isPlayerRunning,
|
||||||
required bool Function(int x, int y) isWalkable,
|
required bool Function(int x, int y) isWalkable,
|
||||||
|
required int Function(int x, int y) areaAt,
|
||||||
|
required bool Function(int area) isAreaConnectedToPlayer,
|
||||||
required void Function(int damage) onDamagePlayer,
|
required void Function(int damage) onDamagePlayer,
|
||||||
required void Function(int x, int y) tryOpenDoor,
|
required void Function(int x, int y) tryOpenDoor,
|
||||||
required void Function(int sfxId) onPlaySound,
|
required void Function(int sfxId) onPlaySound,
|
||||||
@@ -96,6 +98,8 @@ class HansGrosse extends Enemy {
|
|||||||
elapsedMs: elapsedMs,
|
elapsedMs: elapsedMs,
|
||||||
playerPosition: playerPosition,
|
playerPosition: playerPosition,
|
||||||
isWalkable: isWalkable,
|
isWalkable: isWalkable,
|
||||||
|
areaAt: areaAt,
|
||||||
|
isAreaConnectedToPlayer: isAreaConnectedToPlayer,
|
||||||
baseReactionMs: 50,
|
baseReactionMs: 50,
|
||||||
)) {
|
)) {
|
||||||
onPlaySound(alertSoundId);
|
onPlaySound(alertSoundId);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:developer';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy.dart';
|
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy.dart';
|
||||||
@@ -13,6 +14,12 @@ class Dog extends Enemy {
|
|||||||
// Used to simulate SelectDodgeDir's zigzag behavior
|
// Used to simulate SelectDodgeDir's zigzag behavior
|
||||||
double _dodgeAngleOffset = 0.0;
|
double _dodgeAngleOffset = 0.0;
|
||||||
int _dodgeTicTimer = 0;
|
int _dodgeTicTimer = 0;
|
||||||
|
int _stuckFrames = 0;
|
||||||
|
bool _wasMoving = false;
|
||||||
|
|
||||||
|
/// Prevents rapid patrol direction oscillation after hitting a wall.
|
||||||
|
/// After each reversal the dog stands still for this many milliseconds.
|
||||||
|
int _patrolReversalCooldownMs = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
EnemyType get type => EnemyType.dog;
|
EnemyType get type => EnemyType.dog;
|
||||||
@@ -38,10 +45,13 @@ class Dog extends Enemy {
|
|||||||
required double playerAngle,
|
required double playerAngle,
|
||||||
required bool isPlayerRunning,
|
required bool isPlayerRunning,
|
||||||
required bool Function(int x, int y) isWalkable,
|
required bool Function(int x, int y) isWalkable,
|
||||||
|
required int Function(int x, int y) areaAt,
|
||||||
|
required bool Function(int area) isAreaConnectedToPlayer,
|
||||||
required void Function(int damage) onDamagePlayer,
|
required void Function(int damage) onDamagePlayer,
|
||||||
required void Function(int x, int y) tryOpenDoor,
|
required void Function(int x, int y) tryOpenDoor,
|
||||||
required void Function(int sfxId) onPlaySound,
|
required void Function(int sfxId) onPlaySound,
|
||||||
}) {
|
}) {
|
||||||
|
final previousState = state;
|
||||||
Coordinate2D movement = const Coordinate2D(0, 0);
|
Coordinate2D movement = const Coordinate2D(0, 0);
|
||||||
double newAngle = angle;
|
double newAngle = angle;
|
||||||
|
|
||||||
@@ -51,6 +61,8 @@ class Dog extends Enemy {
|
|||||||
elapsedMs: elapsedMs,
|
elapsedMs: elapsedMs,
|
||||||
playerPosition: playerPosition,
|
playerPosition: playerPosition,
|
||||||
isWalkable: isWalkable,
|
isWalkable: isWalkable,
|
||||||
|
areaAt: areaAt,
|
||||||
|
isAreaConnectedToPlayer: isAreaConnectedToPlayer,
|
||||||
)) {
|
)) {
|
||||||
onPlaySound(alertSoundId);
|
onPlaySound(alertSoundId);
|
||||||
}
|
}
|
||||||
@@ -91,7 +103,17 @@ class Dog extends Enemy {
|
|||||||
double currentMoveSpeed = speedPerTic * ticsThisFrame;
|
double currentMoveSpeed = speedPerTic * ticsThisFrame;
|
||||||
|
|
||||||
if (isAlerted) {
|
if (isAlerted) {
|
||||||
state = EntityState.ambushing;
|
// Only commit to the chase (ambushing) state once we have a valid
|
||||||
|
// movement path. While blocked, the dog waits in its current state
|
||||||
|
// so it does not visually appear to charge a closed door.
|
||||||
|
final bool startedChaseThisFrame = state != EntityState.ambushing;
|
||||||
|
|
||||||
|
if (startedChaseThisFrame) {
|
||||||
|
// Reset dodge at chase start so the first pursuit vector is not biased
|
||||||
|
// by stale/random strafe offset from previous behavior.
|
||||||
|
_dodgeAngleOffset = 0.0;
|
||||||
|
_dodgeTicTimer = 0;
|
||||||
|
}
|
||||||
|
|
||||||
double dx = (position.x - playerPosition.x).abs() - currentMoveSpeed;
|
double dx = (position.x - playerPosition.x).abs() - currentMoveSpeed;
|
||||||
double dy = (position.y - playerPosition.y).abs() - currentMoveSpeed;
|
double dy = (position.y - playerPosition.y).abs() - currentMoveSpeed;
|
||||||
@@ -115,12 +137,56 @@ class Dog extends Enemy {
|
|||||||
_dodgeTicTimer--;
|
_dodgeTicTimer--;
|
||||||
}
|
}
|
||||||
|
|
||||||
newAngle = position.angleTo(playerPosition) + _dodgeAngleOffset;
|
final baseAngle = position.angleTo(playerPosition);
|
||||||
|
final preferredAngle = baseAngle + _dodgeAngleOffset;
|
||||||
|
|
||||||
movement = getValidMovement(
|
// Build a SelectDodgeDir-inspired candidate list. The first two entries
|
||||||
|
// are the dodge-shifted diagonal and the direct diagonal (original
|
||||||
|
// behaviour). The next two are the pure cardinal directions toward the
|
||||||
|
// player along each axis — these align with doorways and corridors and
|
||||||
|
// handle cases where all diagonal variants clip a wall beside the opening.
|
||||||
|
final double toDx = playerPosition.x - position.x;
|
||||||
|
final double toDy = playerPosition.y - position.y;
|
||||||
|
final double primaryCardinal = toDx.abs() >= toDy.abs()
|
||||||
|
? (toDx >= 0 ? 0.0 : math.pi)
|
||||||
|
: (toDy >= 0 ? math.pi / 2 : -math.pi / 2);
|
||||||
|
final double secondaryCardinal = toDx.abs() < toDy.abs()
|
||||||
|
? (toDx >= 0 ? 0.0 : math.pi)
|
||||||
|
: (toDy >= 0 ? math.pi / 2 : -math.pi / 2);
|
||||||
|
|
||||||
|
// Diagonal-toward-player is the primary candidate. In a corridor the
|
||||||
|
// diagonal naturally wall-slides into the correct lane (e.g. northwest
|
||||||
|
// intent → blocked north → slides west along the corridor floor toward
|
||||||
|
// the door). Cardinal directions are kept only as last-resort fallbacks
|
||||||
|
// for when the entity is pinned in a corner.
|
||||||
|
final candidateAngles = <double>[
|
||||||
|
if (startedChaseThisFrame) ...[
|
||||||
|
baseAngle, // direct diagonal toward player on first chase frame
|
||||||
|
preferredAngle, // dodge-offset diagonal
|
||||||
|
] else ...[
|
||||||
|
preferredAngle, // dodge-offset diagonal (established chase)
|
||||||
|
baseAngle, // direct diagonal fallback
|
||||||
|
],
|
||||||
|
primaryCardinal,
|
||||||
|
secondaryCardinal,
|
||||||
|
baseAngle + math.pi / 2,
|
||||||
|
baseAngle - math.pi / 2,
|
||||||
|
];
|
||||||
|
|
||||||
|
newAngle = preferredAngle;
|
||||||
|
movement = const Coordinate2D(0, 0);
|
||||||
|
|
||||||
|
// A movement magnitude threshold prevents accepting near-zero floating-
|
||||||
|
// point residuals (e.g. cos(π/2) ≈ 6e-17) as valid movement.
|
||||||
|
final double minEffective = currentMoveSpeed * 0.5;
|
||||||
|
|
||||||
|
int selectedCandidateIndex = -1;
|
||||||
|
for (int i = 0; i < candidateAngles.length; i++) {
|
||||||
|
final candidateAngle = candidateAngles[i];
|
||||||
|
final candidateMovement = getValidMovement(
|
||||||
intendedMovement: Coordinate2D(
|
intendedMovement: Coordinate2D(
|
||||||
math.cos(newAngle) * currentMoveSpeed,
|
math.cos(candidateAngle) * currentMoveSpeed,
|
||||||
math.sin(newAngle) * currentMoveSpeed,
|
math.sin(candidateAngle) * currentMoveSpeed,
|
||||||
),
|
),
|
||||||
playerPosition: playerPosition,
|
playerPosition: playerPosition,
|
||||||
isWalkable: isWalkable,
|
isWalkable: isWalkable,
|
||||||
@@ -128,10 +194,61 @@ class Dog extends Enemy {
|
|||||||
tryOpenDoor: (x, y) {},
|
tryOpenDoor: (x, y) {},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (candidateMovement.x.abs() + candidateMovement.y.abs() >=
|
||||||
|
minEffective) {
|
||||||
|
newAngle = candidateAngle;
|
||||||
|
movement = candidateMovement;
|
||||||
|
selectedCandidateIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (movement.x == 0 && movement.y == 0) {
|
||||||
|
// Blocked — don't visually enter chase state yet.
|
||||||
|
// Keep current state (patrolling) until a path opens.
|
||||||
|
} else {
|
||||||
|
state = EntityState.ambushing;
|
||||||
|
}
|
||||||
|
|
||||||
if (movement.x == 0 && movement.y == 0) {
|
if (movement.x == 0 && movement.y == 0) {
|
||||||
_dodgeTicTimer = 0;
|
_dodgeTicTimer = 0;
|
||||||
|
_stuckFrames++;
|
||||||
|
|
||||||
|
// Emit periodic diagnostics for dogs that appear to keep facing/moving
|
||||||
|
// into blocked geometry. This is intentionally sparse to avoid log spam.
|
||||||
|
if (_stuckFrames == 1 || _stuckFrames % 20 == 0) {
|
||||||
|
final int tileX = x.toInt();
|
||||||
|
final int tileY = y.toInt();
|
||||||
|
final int playerTileX = playerPosition.x.toInt();
|
||||||
|
final int playerTileY = playerPosition.y.toInt();
|
||||||
|
|
||||||
|
log(
|
||||||
|
'[DEBUG] Dog #$debugId stuck=$_stuckFrames '
|
||||||
|
'at ($tileX,$tileY)->player($playerTileX,$playerTileY) '
|
||||||
|
'base=${baseAngle.toStringAsFixed(3)} '
|
||||||
|
'pref=${preferredAngle.toStringAsFixed(3)} '
|
||||||
|
'cand=$selectedCandidateIndex '
|
||||||
|
'walk N=${isWalkable(tileX, tileY - 1)} '
|
||||||
|
'E=${isWalkable(tileX + 1, tileY)} '
|
||||||
|
'S=${isWalkable(tileX, tileY + 1)} '
|
||||||
|
'W=${isWalkable(tileX - 1, tileY)}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (_stuckFrames >= 20) {
|
||||||
|
log(
|
||||||
|
'[DEBUG] Dog #$debugId unstuck after $_stuckFrames frames '
|
||||||
|
'using candidate $selectedCandidateIndex '
|
||||||
|
'move=(${movement.x.toStringAsFixed(4)},${movement.y.toStringAsFixed(4)})',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_stuckFrames = 0;
|
||||||
}
|
}
|
||||||
} else if (state == EntityState.patrolling) {
|
} else if (state == EntityState.patrolling) {
|
||||||
|
if (_patrolReversalCooldownMs > 0) {
|
||||||
|
// Stand still during the post-reversal pause.
|
||||||
|
_patrolReversalCooldownMs -= elapsedDeltaMs;
|
||||||
|
} else {
|
||||||
movement = getValidMovement(
|
movement = getValidMovement(
|
||||||
intendedMovement: Coordinate2D(
|
intendedMovement: Coordinate2D(
|
||||||
math.cos(angle) * currentMoveSpeed,
|
math.cos(angle) * currentMoveSpeed,
|
||||||
@@ -142,6 +259,13 @@ class Dog extends Enemy {
|
|||||||
// Patrolling dogs using T_Path CAN open doors in the original logic.
|
// Patrolling dogs using T_Path CAN open doors in the original logic.
|
||||||
tryOpenDoor: tryOpenDoor,
|
tryOpenDoor: tryOpenDoor,
|
||||||
);
|
);
|
||||||
|
// Matches T_Path behavior: when patrol is blocked, reverse direction.
|
||||||
|
// A cooldown prevents immediate re-reversal and rapid oscillation.
|
||||||
|
if (movement.x == 0 && movement.y == 0) {
|
||||||
|
newAngle = angle + math.pi;
|
||||||
|
_patrolReversalCooldownMs = 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ticReady) {
|
if (ticReady) {
|
||||||
@@ -150,6 +274,29 @@ class Dog extends Enemy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state != previousState) {
|
||||||
|
log(
|
||||||
|
'[DEBUG] Dog #$debugId state ${previousState.name} -> ${state.name} '
|
||||||
|
'at (${x.toStringAsFixed(2)}, ${y.toStringAsFixed(2)})',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final bool isMovingNow = movement.x.abs() + movement.y.abs() > 0.0001;
|
||||||
|
if (isMovingNow && !_wasMoving) {
|
||||||
|
final moveAngle = math.atan2(movement.y, movement.x);
|
||||||
|
final cardinalDirection = movement.x.abs() >= movement.y.abs()
|
||||||
|
? (movement.x >= 0 ? 'east' : 'west')
|
||||||
|
: (movement.y >= 0 ? 'south' : 'north');
|
||||||
|
log(
|
||||||
|
'[DEBUG] Dog #$debugId movement start '
|
||||||
|
'from (${x.toStringAsFixed(2)}, ${y.toStringAsFixed(2)}) '
|
||||||
|
'tile (${x.toInt()}, ${y.toInt()}) '
|
||||||
|
'dir=$cardinalDirection angle=${moveAngle.toStringAsFixed(3)} '
|
||||||
|
'vec=(${movement.x.toStringAsFixed(4)}, ${movement.y.toStringAsFixed(4)})',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_wasMoving = isMovingNow;
|
||||||
|
|
||||||
_updateAnimation(elapsedMs, newAngle, playerPosition);
|
_updateAnimation(elapsedMs, newAngle, playerPosition);
|
||||||
return (movement: movement, newAngle: newAngle);
|
return (movement: movement, newAngle: newAngle);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:developer';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/dog.dart';
|
import 'package:wolf_3d_dart/src/entities/entities/enemies/dog.dart';
|
||||||
@@ -14,6 +15,11 @@ import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
|||||||
/// This class encapsulates the shared AI behaviors, including movement physics,
|
/// This class encapsulates the shared AI behaviors, including movement physics,
|
||||||
/// difficulty-based spawning, and the signature Wolfenstein 3D sight-detection system.
|
/// difficulty-based spawning, and the signature Wolfenstein 3D sight-detection system.
|
||||||
abstract class Enemy extends Entity {
|
abstract class Enemy extends Entity {
|
||||||
|
static int _nextDebugId = 0;
|
||||||
|
|
||||||
|
/// A sequential ID assigned at spawn for debug identification.
|
||||||
|
final int debugId = ++_nextDebugId;
|
||||||
|
|
||||||
Enemy({
|
Enemy({
|
||||||
required super.x,
|
required super.x,
|
||||||
required super.y,
|
required super.y,
|
||||||
@@ -99,7 +105,9 @@ abstract class Enemy extends Entity {
|
|||||||
|
|
||||||
/// Canonical Wolf3D ranged damage buckets by tile distance.
|
/// Canonical Wolf3D ranged damage buckets by tile distance.
|
||||||
int rollRangedDamage(double distance, {bool isSharpShooter = false}) {
|
int rollRangedDamage(double distance, {bool isSharpShooter = false}) {
|
||||||
final adjustedDistance = isSharpShooter ? (distance * (2.0 / 3.0)) : distance;
|
final adjustedDistance = isSharpShooter
|
||||||
|
? (distance * (2.0 / 3.0))
|
||||||
|
: distance;
|
||||||
|
|
||||||
if (adjustedDistance < 2.0) {
|
if (adjustedDistance < 2.0) {
|
||||||
return math.Random().nextInt(64); // 0..63
|
return math.Random().nextInt(64); // 0..63
|
||||||
@@ -138,7 +146,9 @@ abstract class Enemy extends Entity {
|
|||||||
required bool isPlayerRunning,
|
required bool isPlayerRunning,
|
||||||
bool isSharpShooter = false,
|
bool isSharpShooter = false,
|
||||||
}) {
|
}) {
|
||||||
final adjustedDistance = isSharpShooter ? (distance * (2.0 / 3.0)) : distance;
|
final adjustedDistance = isSharpShooter
|
||||||
|
? (distance * (2.0 / 3.0))
|
||||||
|
: distance;
|
||||||
final dist = adjustedDistance.floor();
|
final dist = adjustedDistance.floor();
|
||||||
final isVisible = isVisibleFromPlayer(
|
final isVisible = isVisibleFromPlayer(
|
||||||
playerPosition: playerPosition,
|
playerPosition: playerPosition,
|
||||||
@@ -170,6 +180,9 @@ abstract class Enemy extends Entity {
|
|||||||
if (health <= 0) {
|
if (health <= 0) {
|
||||||
state = EntityState.dead;
|
state = EntityState.dead;
|
||||||
isDying = true;
|
isDying = true;
|
||||||
|
log(
|
||||||
|
'[DEBUG] Enemy #$debugId (${type.name}) killed at tile (${x.toInt()}, ${y.toInt()})',
|
||||||
|
);
|
||||||
} else if (math.Random().nextDouble() < 0.5) {
|
} else if (math.Random().nextDouble() < 0.5) {
|
||||||
state = EntityState.pain;
|
state = EntityState.pain;
|
||||||
setTics(5); // Add this so they actually pause to flinch
|
setTics(5); // Add this so they actually pause to flinch
|
||||||
@@ -227,11 +240,18 @@ abstract class Enemy extends Entity {
|
|||||||
required int elapsedMs,
|
required int elapsedMs,
|
||||||
required Coordinate2D playerPosition,
|
required Coordinate2D playerPosition,
|
||||||
required bool Function(int x, int y) isWalkable,
|
required bool Function(int x, int y) isWalkable,
|
||||||
|
required int Function(int x, int y) areaAt,
|
||||||
|
required bool Function(int area) isAreaConnectedToPlayer,
|
||||||
int baseReactionMs = 200,
|
int baseReactionMs = 200,
|
||||||
int reactionVarianceMs = 600,
|
int reactionVarianceMs = 600,
|
||||||
}) {
|
}) {
|
||||||
bool didAlert = false;
|
bool didAlert = false;
|
||||||
|
|
||||||
|
final int area = areaAt(x.toInt(), y.toInt());
|
||||||
|
if (area >= 0 && !isAreaConnectedToPlayer(area)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
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"
|
// First frame of spotting: calculate how long until they "wake up"
|
||||||
@@ -244,6 +264,12 @@ abstract class Enemy extends Entity {
|
|||||||
isAlerted = true;
|
isAlerted = true;
|
||||||
didAlert = true;
|
didAlert = true;
|
||||||
|
|
||||||
|
log(
|
||||||
|
'[DEBUG] Enemy #$debugId (${type.name}) alerted by sight '
|
||||||
|
'enemy=(${x.toStringAsFixed(2)}, ${y.toStringAsFixed(2)}) '
|
||||||
|
'player=(${playerPosition.x.toStringAsFixed(2)}, ${playerPosition.y.toStringAsFixed(2)})',
|
||||||
|
);
|
||||||
|
|
||||||
if (state == EntityState.idle || state == EntityState.ambushing) {
|
if (state == EntityState.idle || state == EntityState.ambushing) {
|
||||||
state = EntityState.patrolling;
|
state = EntityState.patrolling;
|
||||||
setTics(10);
|
setTics(10);
|
||||||
@@ -361,9 +387,17 @@ abstract class Enemy extends Entity {
|
|||||||
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
|
// Slide in the primary direction of travel. This prevents the entity from
|
||||||
|
// being deflected sideways when navigating through a doorway: if the dog
|
||||||
|
// is moving mostly north and clips a wall tile beside an open door, the
|
||||||
|
// Y (northward) slide is preferred over the X (sideways) slide.
|
||||||
|
if (intendedMovement.y.abs() >= intendedMovement.x.abs()) {
|
||||||
|
if (canMoveY) return Coordinate2D(0, intendedMovement.y);
|
||||||
|
if (canMoveX) return Coordinate2D(intendedMovement.x, 0);
|
||||||
|
} else {
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -393,6 +427,8 @@ abstract class Enemy extends Entity {
|
|||||||
required double playerAngle,
|
required double playerAngle,
|
||||||
required bool isPlayerRunning,
|
required bool isPlayerRunning,
|
||||||
required bool Function(int x, int y) isWalkable,
|
required bool Function(int x, int y) isWalkable,
|
||||||
|
required int Function(int x, int y) areaAt,
|
||||||
|
required bool Function(int area) isAreaConnectedToPlayer,
|
||||||
required void Function(int x, int y) tryOpenDoor,
|
required void Function(int x, int y) tryOpenDoor,
|
||||||
required void Function(int damage) onDamagePlayer,
|
required void Function(int damage) onDamagePlayer,
|
||||||
required void Function(int sfxId) onPlaySound,
|
required void Function(int sfxId) onPlaySound,
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ class Guard extends Enemy {
|
|||||||
@override
|
@override
|
||||||
EnemyType get type => EnemyType.guard;
|
EnemyType get type => EnemyType.guard;
|
||||||
|
|
||||||
|
/// Prevents rapid patrol direction oscillation after hitting a wall.
|
||||||
|
int _patrolReversalCooldownMs = 0;
|
||||||
|
|
||||||
Guard({
|
Guard({
|
||||||
required super.x,
|
required super.x,
|
||||||
required super.y,
|
required super.y,
|
||||||
@@ -34,6 +37,8 @@ class Guard extends Enemy {
|
|||||||
required double playerAngle,
|
required double playerAngle,
|
||||||
required bool isPlayerRunning,
|
required bool isPlayerRunning,
|
||||||
required bool Function(int x, int y) isWalkable,
|
required bool Function(int x, int y) isWalkable,
|
||||||
|
required int Function(int x, int y) areaAt,
|
||||||
|
required bool Function(int area) isAreaConnectedToPlayer,
|
||||||
required void Function(int damage) onDamagePlayer,
|
required void Function(int damage) onDamagePlayer,
|
||||||
required void Function(int x, int y) tryOpenDoor,
|
required void Function(int x, int y) tryOpenDoor,
|
||||||
required void Function(int sfxId) onPlaySound,
|
required void Function(int sfxId) onPlaySound,
|
||||||
@@ -47,6 +52,8 @@ class Guard extends Enemy {
|
|||||||
elapsedMs: elapsedMs,
|
elapsedMs: elapsedMs,
|
||||||
playerPosition: playerPosition,
|
playerPosition: playerPosition,
|
||||||
isWalkable: isWalkable,
|
isWalkable: isWalkable,
|
||||||
|
areaAt: areaAt,
|
||||||
|
isAreaConnectedToPlayer: isAreaConnectedToPlayer,
|
||||||
)) {
|
)) {
|
||||||
onPlaySound(alertSoundId);
|
onPlaySound(alertSoundId);
|
||||||
}
|
}
|
||||||
@@ -113,6 +120,9 @@ class Guard extends Enemy {
|
|||||||
tryOpenDoor: tryOpenDoor,
|
tryOpenDoor: tryOpenDoor,
|
||||||
);
|
);
|
||||||
} else if (state == EntityState.patrolling) {
|
} else if (state == EntityState.patrolling) {
|
||||||
|
if (_patrolReversalCooldownMs > 0) {
|
||||||
|
_patrolReversalCooldownMs -= elapsedDeltaMs;
|
||||||
|
} else {
|
||||||
// Normal patrol movement
|
// Normal patrol movement
|
||||||
movement = getValidMovement(
|
movement = getValidMovement(
|
||||||
intendedMovement: Coordinate2D(
|
intendedMovement: Coordinate2D(
|
||||||
@@ -123,6 +133,13 @@ class Guard extends Enemy {
|
|||||||
isWalkable: isWalkable,
|
isWalkable: isWalkable,
|
||||||
tryOpenDoor: tryOpenDoor,
|
tryOpenDoor: tryOpenDoor,
|
||||||
);
|
);
|
||||||
|
// Matches T_Path behavior: when patrol is blocked, reverse direction.
|
||||||
|
// A cooldown prevents immediate re-reversal and rapid oscillation.
|
||||||
|
if (movement.x == 0 && movement.y == 0) {
|
||||||
|
newAngle = angle + math.pi;
|
||||||
|
_patrolReversalCooldownMs = 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ticReady) {
|
if (ticReady) {
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ class Mutant extends Enemy {
|
|||||||
required double playerAngle,
|
required double playerAngle,
|
||||||
required bool isPlayerRunning,
|
required bool isPlayerRunning,
|
||||||
required bool Function(int x, int y) isWalkable,
|
required bool Function(int x, int y) isWalkable,
|
||||||
|
required int Function(int x, int y) areaAt,
|
||||||
|
required bool Function(int area) isAreaConnectedToPlayer,
|
||||||
required void Function(int damage) onDamagePlayer,
|
required void Function(int damage) onDamagePlayer,
|
||||||
required void Function(int x, int y) tryOpenDoor,
|
required void Function(int x, int y) tryOpenDoor,
|
||||||
required void Function(int sfxId) onPlaySound,
|
required void Function(int sfxId) onPlaySound,
|
||||||
@@ -43,6 +45,8 @@ class Mutant extends Enemy {
|
|||||||
elapsedMs: elapsedMs,
|
elapsedMs: elapsedMs,
|
||||||
playerPosition: playerPosition,
|
playerPosition: playerPosition,
|
||||||
isWalkable: isWalkable,
|
isWalkable: isWalkable,
|
||||||
|
areaAt: areaAt,
|
||||||
|
isAreaConnectedToPlayer: isAreaConnectedToPlayer,
|
||||||
)) {
|
)) {
|
||||||
onPlaySound(alertSoundId);
|
onPlaySound(alertSoundId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ class Officer extends Enemy {
|
|||||||
required double playerAngle,
|
required double playerAngle,
|
||||||
required bool isPlayerRunning,
|
required bool isPlayerRunning,
|
||||||
required bool Function(int x, int y) isWalkable,
|
required bool Function(int x, int y) isWalkable,
|
||||||
|
required int Function(int x, int y) areaAt,
|
||||||
|
required bool Function(int area) isAreaConnectedToPlayer,
|
||||||
required void Function(int damage) onDamagePlayer,
|
required void Function(int damage) onDamagePlayer,
|
||||||
required void Function(int x, int y) tryOpenDoor,
|
required void Function(int x, int y) tryOpenDoor,
|
||||||
required void Function(int sfxId) onPlaySound,
|
required void Function(int sfxId) onPlaySound,
|
||||||
@@ -43,6 +45,8 @@ class Officer extends Enemy {
|
|||||||
elapsedMs: elapsedMs,
|
elapsedMs: elapsedMs,
|
||||||
playerPosition: playerPosition,
|
playerPosition: playerPosition,
|
||||||
isWalkable: isWalkable,
|
isWalkable: isWalkable,
|
||||||
|
areaAt: areaAt,
|
||||||
|
isAreaConnectedToPlayer: isAreaConnectedToPlayer,
|
||||||
)) {
|
)) {
|
||||||
onPlaySound(alertSoundId);
|
onPlaySound(alertSoundId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ class SS extends Enemy {
|
|||||||
required double playerAngle,
|
required double playerAngle,
|
||||||
required bool isPlayerRunning,
|
required bool isPlayerRunning,
|
||||||
required bool Function(int x, int y) isWalkable,
|
required bool Function(int x, int y) isWalkable,
|
||||||
|
required int Function(int x, int y) areaAt,
|
||||||
|
required bool Function(int area) isAreaConnectedToPlayer,
|
||||||
required void Function(int damage) onDamagePlayer,
|
required void Function(int damage) onDamagePlayer,
|
||||||
required void Function(int x, int y) tryOpenDoor,
|
required void Function(int x, int y) tryOpenDoor,
|
||||||
required void Function(int sfxId) onPlaySound,
|
required void Function(int sfxId) onPlaySound,
|
||||||
@@ -42,6 +44,8 @@ class SS extends Enemy {
|
|||||||
elapsedMs: elapsedMs,
|
elapsedMs: elapsedMs,
|
||||||
playerPosition: playerPosition,
|
playerPosition: playerPosition,
|
||||||
isWalkable: isWalkable,
|
isWalkable: isWalkable,
|
||||||
|
areaAt: areaAt,
|
||||||
|
isAreaConnectedToPlayer: isAreaConnectedToPlayer,
|
||||||
)) {
|
)) {
|
||||||
onPlaySound(alertSoundId);
|
onPlaySound(alertSoundId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:developer';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy.dart';
|
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy.dart';
|
||||||
@@ -43,7 +44,7 @@ abstract class Weapon {
|
|||||||
|
|
||||||
// Safety check!
|
// Safety check!
|
||||||
if (calculatedIndex < 0 || calculatedIndex >= maxSprites) {
|
if (calculatedIndex < 0 || calculatedIndex >= maxSprites) {
|
||||||
print("WARNING: Weapon sprite index $calculatedIndex out of bounds!");
|
log("WARNING: Weapon sprite index $calculatedIndex out of bounds!");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return calculatedIndex;
|
return calculatedIndex;
|
||||||
@@ -64,7 +65,7 @@ abstract class Weapon {
|
|||||||
lastFrameTime = currentTime;
|
lastFrameTime = currentTime;
|
||||||
_triggerReleased = false;
|
_triggerReleased = false;
|
||||||
|
|
||||||
print("[WEAPON] $type fired.");
|
log("[WEAPON] $type fired.");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:developer';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:audioplayers/audioplayers.dart';
|
import 'package:audioplayers/audioplayers.dart';
|
||||||
@@ -11,7 +12,7 @@ class WolfAudio implements EngineAudio {
|
|||||||
// Play the first 50 sounds with a 2-second gap to identify them
|
// Play the first 50 sounds with a 2-second gap to identify them
|
||||||
for (int i = 0; i < 50; i++) {
|
for (int i = 0; i < 50; i++) {
|
||||||
Future.delayed(Duration(seconds: i * 2), () {
|
Future.delayed(Duration(seconds: i * 2), () {
|
||||||
print("[AUDIO] Testing Sound ID: $i");
|
log("[AUDIO] Testing Sound ID: $i");
|
||||||
playSoundEffect(i);
|
playSoundEffect(i);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -50,11 +51,11 @@ class WolfAudio implements EngineAudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_isInitialized = true;
|
_isInitialized = true;
|
||||||
print(
|
log(
|
||||||
"[AUDIO] AudioPlayers initialized successfully with $_maxSfxChannels SFX channels.",
|
"[AUDIO] AudioPlayers initialized successfully with $_maxSfxChannels SFX channels.",
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("[AUDIO] Failed to initialize AudioPlayers - $e");
|
log("[AUDIO] Failed to initialize AudioPlayers - $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +91,7 @@ class WolfAudio implements EngineAudio {
|
|||||||
);
|
);
|
||||||
await _musicPlayer.play(BytesSource(wavBytes));
|
await _musicPlayer.play(BytesSource(wavBytes));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("[AUDIO] Error playing music track - $e");
|
log("[AUDIO] Error playing music track - $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +125,7 @@ class WolfAudio implements EngineAudio {
|
|||||||
if (index < data.music.length) {
|
if (index < data.music.length) {
|
||||||
await playMusic(data.music[index]);
|
await playMusic(data.music[index]);
|
||||||
} else {
|
} else {
|
||||||
print("[AUDIO] Warning - Track index $index out of bounds.");
|
log("[AUDIO] Warning - Track index $index out of bounds.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +135,7 @@ class WolfAudio implements EngineAudio {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> playSoundEffect(int sfxId) async {
|
Future<void> playSoundEffect(int sfxId) async {
|
||||||
print("[AUDIO] Playing sfx id $sfxId");
|
log("[AUDIO] Playing sfx id $sfxId");
|
||||||
// The original engine uses a specific starting chunk for digitized sounds.
|
// The original engine uses a specific starting chunk for digitized sounds.
|
||||||
// In many loaders, the 'sounds' list is already just the digitized ones.
|
// In many loaders, the 'sounds' list is already just the digitized ones.
|
||||||
// If your list contains EVERYTHING, you need to add the offset (174).
|
// If your list contains EVERYTHING, you need to add the offset (174).
|
||||||
@@ -168,7 +169,7 @@ class WolfAudio implements EngineAudio {
|
|||||||
// Note: We use BytesSource because createWavFile returns Uint8List (the file bytes)
|
// Note: We use BytesSource because createWavFile returns Uint8List (the file bytes)
|
||||||
await player.play(BytesSource(wavBytes));
|
await player.play(BytesSource(wavBytes));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("[AUDIO] SFX Error - $e");
|
log("[AUDIO] SFX Error - $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ WolfEngine _buildEngine({
|
|||||||
WolfLevel(
|
WolfLevel(
|
||||||
name: 'Test Level',
|
name: 'Test Level',
|
||||||
wallGrid: wallGrid,
|
wallGrid: wallGrid,
|
||||||
|
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
|
||||||
objectGrid: objectGrid,
|
objectGrid: objectGrid,
|
||||||
musicIndex: 0,
|
musicIndex: 0,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ void main() {
|
|||||||
ss.takeDamage(999, engine.timeAliveMs);
|
ss.takeDamage(999, engine.timeAliveMs);
|
||||||
engine.tick(const Duration(milliseconds: 16));
|
engine.tick(const Duration(milliseconds: 16));
|
||||||
|
|
||||||
final droppedMachineGun =
|
final droppedMachineGun = engine.entities
|
||||||
engine.entities.whereType<WeaponCollectible>().any(
|
.whereType<WeaponCollectible>()
|
||||||
|
.any(
|
||||||
(item) => item.mapId == MapObject.machineGun,
|
(item) => item.mapId == MapObject.machineGun,
|
||||||
);
|
);
|
||||||
expect(droppedMachineGun, isTrue);
|
expect(droppedMachineGun, isTrue);
|
||||||
@@ -85,6 +86,7 @@ WolfEngine _buildEngine() {
|
|||||||
WolfLevel(
|
WolfLevel(
|
||||||
name: 'Test Level',
|
name: 'Test Level',
|
||||||
wallGrid: wallGrid,
|
wallGrid: wallGrid,
|
||||||
|
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
|
||||||
objectGrid: objectGrid,
|
objectGrid: objectGrid,
|
||||||
musicIndex: 0,
|
musicIndex: 0,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ void main() {
|
|||||||
playerAngle: 0,
|
playerAngle: 0,
|
||||||
isPlayerRunning: false,
|
isPlayerRunning: false,
|
||||||
isWalkable: (_, _) => false,
|
isWalkable: (_, _) => false,
|
||||||
|
areaAt: (_, _) => 0,
|
||||||
|
isAreaConnectedToPlayer: (_) => true,
|
||||||
onDamagePlayer: (_) {},
|
onDamagePlayer: (_) {},
|
||||||
tryOpenDoor: (_, _) {},
|
tryOpenDoor: (_, _) {},
|
||||||
onPlaySound: (_) {},
|
onPlaySound: (_) {},
|
||||||
@@ -78,6 +80,8 @@ void main() {
|
|||||||
playerAngle: 0,
|
playerAngle: 0,
|
||||||
isPlayerRunning: false,
|
isPlayerRunning: false,
|
||||||
isWalkable: (_, _) => false,
|
isWalkable: (_, _) => false,
|
||||||
|
areaAt: (_, _) => 0,
|
||||||
|
isAreaConnectedToPlayer: (_) => true,
|
||||||
onDamagePlayer: (_) {},
|
onDamagePlayer: (_) {},
|
||||||
tryOpenDoor: (_, _) {},
|
tryOpenDoor: (_, _) {},
|
||||||
onPlaySound: (_) {},
|
onPlaySound: (_) {},
|
||||||
@@ -100,6 +104,8 @@ void main() {
|
|||||||
playerAngle: 0,
|
playerAngle: 0,
|
||||||
isPlayerRunning: false,
|
isPlayerRunning: false,
|
||||||
isWalkable: (_, _) => true,
|
isWalkable: (_, _) => true,
|
||||||
|
areaAt: (_, _) => 0,
|
||||||
|
isAreaConnectedToPlayer: (_) => true,
|
||||||
onDamagePlayer: (_) {},
|
onDamagePlayer: (_) {},
|
||||||
tryOpenDoor: (_, _) {},
|
tryOpenDoor: (_, _) {},
|
||||||
onPlaySound: (_) {},
|
onPlaySound: (_) {},
|
||||||
@@ -112,16 +118,53 @@ void main() {
|
|||||||
playerAngle: 0,
|
playerAngle: 0,
|
||||||
isPlayerRunning: false,
|
isPlayerRunning: false,
|
||||||
isWalkable: (_, _) => true,
|
isWalkable: (_, _) => true,
|
||||||
|
areaAt: (_, _) => 0,
|
||||||
|
isAreaConnectedToPlayer: (_) => true,
|
||||||
onDamagePlayer: (_) {},
|
onDamagePlayer: (_) {},
|
||||||
tryOpenDoor: (_, _) {},
|
tryOpenDoor: (_, _) {},
|
||||||
onPlaySound: (_) {},
|
onPlaySound: (_) {},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(guardIntent.movement.x.abs() + guardIntent.movement.y.abs(),
|
|
||||||
greaterThan(0));
|
|
||||||
expect(
|
expect(
|
||||||
dogIntent.movement.x.abs() + dogIntent.movement.y.abs(), greaterThan(0));
|
guardIntent.movement.x.abs() + guardIntent.movement.y.abs(),
|
||||||
|
greaterThan(0),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
dogIntent.movement.x.abs() + dogIntent.movement.y.abs(),
|
||||||
|
greaterThan(0),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'alerted dog can choose alternate path when direct chase is blocked',
|
||||||
|
() {
|
||||||
|
final dog = Dog(x: 8.99, y: 8.99, angle: 0, mapId: MapObject.dogStart);
|
||||||
|
dog.isAlerted = true;
|
||||||
|
|
||||||
|
final intent = dog.update(
|
||||||
|
elapsedMs: 1000,
|
||||||
|
elapsedDeltaMs: 16,
|
||||||
|
playerPosition: const Coordinate2D(11.99, 8.99),
|
||||||
|
playerAngle: 0,
|
||||||
|
isPlayerRunning: false,
|
||||||
|
isWalkable: (x, y) {
|
||||||
|
// Block the direct east chase tile only.
|
||||||
|
if (x == 9 && y == 8) return false;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
areaAt: (_, _) => 0,
|
||||||
|
isAreaConnectedToPlayer: (_) => true,
|
||||||
|
onDamagePlayer: (_) {},
|
||||||
|
tryOpenDoor: (_, _) {},
|
||||||
|
onPlaySound: (_) {},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
intent.movement.x.abs() + intent.movement.y.abs(),
|
||||||
|
greaterThan(0),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ void main() {
|
|||||||
WolfLevel(
|
WolfLevel(
|
||||||
name: 'Test Level',
|
name: 'Test Level',
|
||||||
wallGrid: wallGrid,
|
wallGrid: wallGrid,
|
||||||
|
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
|
||||||
objectGrid: objectGrid,
|
objectGrid: objectGrid,
|
||||||
musicIndex: 0,
|
musicIndex: 0,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ void main() {
|
|||||||
WolfLevel(
|
WolfLevel(
|
||||||
name: 'Test Level',
|
name: 'Test Level',
|
||||||
wallGrid: wallGrid,
|
wallGrid: wallGrid,
|
||||||
|
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
|
||||||
objectGrid: objectGrid,
|
objectGrid: objectGrid,
|
||||||
musicIndex: 0,
|
musicIndex: 0,
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user