Refactor enemy sound handling and improve enemy type definitions
- Updated enemy classes to include alert and attack sound IDs. - Refactored checkWakeUp and attack logic to play sounds appropriately. - Enhanced enemy type definitions with sound mappings. - Added unit tests for enemy spawn and validation. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -89,13 +89,13 @@ abstract class MapObject {
|
||||
static const int bossFettgesicht = 223;
|
||||
|
||||
// --- Enemy Range Constants ---
|
||||
// Every enemy type occupies a block of 36 IDs.
|
||||
// Modulo math is used on these ranges to determine orientation and patrol status.
|
||||
// Enemies are encoded in 8-ID blocks: 4 standing directions and 4 patrol directions.
|
||||
// Most enemy families shift by 36 IDs between difficulty tiers, while mutants shift by 18.
|
||||
static const int guardStart = 108; // Claims 108-115, 144-151, 180-187
|
||||
static const int dogStart = 116; // Claims 116-123, 152-159, 188-195
|
||||
static const int officerStart = 116; // Claims 116-123, 152-159, 188-195
|
||||
static const int ssStart = 126; // Claims 126-133, 162-169, 198-205
|
||||
static const int mutantStart = 136; // Claims 136-143, 172-179, 208-215
|
||||
static const int officerStart = 252; // Claims 252-259, 288-295, 324-331
|
||||
static const int dogStart = 134; // Claims 134-141, 170-177, 206-213
|
||||
static const int mutantStart = 216; // Claims 216-223, 234-241, 252-259
|
||||
|
||||
// --- Missing Decorative Bodies ---
|
||||
static const int deadGuard = 124; // Decorative only in WL1
|
||||
@@ -119,9 +119,14 @@ abstract class MapObject {
|
||||
final EnemyType? type = EnemyType.fromMapId(id);
|
||||
if (type == null) return 0.0;
|
||||
|
||||
// Because all enemies are grouped in blocks of 4 (one for each CardinalDirection),
|
||||
// modulo 4 extracts the specific orientation index.
|
||||
return CardinalDirection.fromEnemyIndex(id % 4).radians;
|
||||
final int? normalizedId = type.mapData.getNormalizedId(id, Difficulty.hard);
|
||||
if (normalizedId == null) return 0.0;
|
||||
|
||||
// Enemy IDs are stored as 8-ID blocks per family.
|
||||
// Offsets 0-3 are standing directions and 4-7 are patrol directions.
|
||||
return CardinalDirection.fromEnemyIndex(
|
||||
normalizedId - type.mapData.baseId,
|
||||
).radians;
|
||||
}
|
||||
|
||||
/// Validates if an object should exist on the map given the current [difficulty].
|
||||
|
||||
@@ -197,8 +197,8 @@ class Player {
|
||||
return pickedUp;
|
||||
}
|
||||
|
||||
void fire(int currentTime) {
|
||||
if (switchState != WeaponSwitchState.idle) return;
|
||||
bool fire(int currentTime) {
|
||||
if (switchState != WeaponSwitchState.idle) return false;
|
||||
|
||||
// We pass the isFiring state to handle automatic vs semi-auto behavior
|
||||
bool shotFired = currentWeapon.fire(currentTime, currentAmmo: ammo);
|
||||
@@ -206,6 +206,8 @@ class Player {
|
||||
if (shotFired && currentWeapon.type != WeaponType.knife) {
|
||||
ammo--;
|
||||
}
|
||||
|
||||
return shotFired;
|
||||
}
|
||||
|
||||
void releaseTrigger() {
|
||||
|
||||
@@ -85,6 +85,7 @@ class WolfEngine {
|
||||
|
||||
/// Initializes the engine, sets the starting episode, and loads the first level.
|
||||
void init() {
|
||||
audio.activeGame = data;
|
||||
_currentEpisodeIndex = startingEpisode;
|
||||
_currentLevelIndex = 0;
|
||||
_loadLevel();
|
||||
@@ -247,9 +248,13 @@ class WolfEngine {
|
||||
}
|
||||
|
||||
if (input.isFiring) {
|
||||
player.fire(_timeAliveMs);
|
||||
final bool shotFired = player.fire(_timeAliveMs);
|
||||
|
||||
if (_timeAliveMs - _lastAcousticAlertTime > 400) {
|
||||
if (shotFired) {
|
||||
_playPlayerWeaponSound();
|
||||
}
|
||||
|
||||
if (shotFired && _timeAliveMs - _lastAcousticAlertTime > 400) {
|
||||
_propagateGunfire();
|
||||
_lastAcousticAlertTime = _timeAliveMs;
|
||||
}
|
||||
@@ -356,6 +361,7 @@ class WolfEngine {
|
||||
isWalkable: isWalkable,
|
||||
tryOpenDoor: doorManager.tryOpenDoor,
|
||||
onDamagePlayer: (int damage) => player.takeDamage(damage),
|
||||
onPlaySound: audio.playSoundEffect,
|
||||
);
|
||||
|
||||
// Scale the enemy's movement intent to prevent super-speed on 90Hz/120Hz displays
|
||||
@@ -429,6 +435,7 @@ class WolfEngine {
|
||||
if (entity.position.x.toInt() == tile.x &&
|
||||
entity.position.y.toInt() == tile.y) {
|
||||
entity.isAlerted = true;
|
||||
audio.playSoundEffect(entity.alertSoundId);
|
||||
|
||||
// Wake them up!
|
||||
if (entity.state == EntityState.idle ||
|
||||
@@ -480,6 +487,17 @@ class WolfEngine {
|
||||
return false;
|
||||
}
|
||||
|
||||
void _playPlayerWeaponSound() {
|
||||
final sfxId = switch (player.currentWeapon.type) {
|
||||
WeaponType.knife => WolfSound.knifeAttack,
|
||||
WeaponType.pistol => WolfSound.pistolFire,
|
||||
WeaponType.machineGun => WolfSound.machineGunFire,
|
||||
WeaponType.chainGun => WolfSound.gatlingFire,
|
||||
};
|
||||
|
||||
audio.playSoundEffect(sfxId);
|
||||
}
|
||||
|
||||
/// Clamps movement to a maximum of 0.2 tiles per step to prevent wall-clipping
|
||||
/// and map-boundary jumps during low framerate/high delta spikes.
|
||||
Coordinate2D _clampMovement(Coordinate2D intent) {
|
||||
|
||||
@@ -11,6 +11,12 @@ class HansGrosse extends Enemy {
|
||||
EnemyType get type =>
|
||||
throw UnimplementedError("Hans Grosse uses manual animation logic.");
|
||||
|
||||
@override
|
||||
int get alertSoundId => WolfSound.bossActive;
|
||||
|
||||
@override
|
||||
int get attackSoundId => WolfSound.naziFire;
|
||||
|
||||
HansGrosse({
|
||||
required super.x,
|
||||
required super.y,
|
||||
@@ -69,6 +75,7 @@ class HansGrosse extends Enemy {
|
||||
required bool Function(int x, int y) isWalkable,
|
||||
required void Function(int damage) onDamagePlayer,
|
||||
required void Function(int x, int y) tryOpenDoor,
|
||||
required void Function(int sfxId) onPlaySound,
|
||||
}) {
|
||||
Coordinate2D movement = const Coordinate2D(0, 0);
|
||||
double newAngle = angle;
|
||||
@@ -77,12 +84,14 @@ class HansGrosse extends Enemy {
|
||||
newAngle = position.angleTo(playerPosition);
|
||||
}
|
||||
|
||||
checkWakeUp(
|
||||
if (checkWakeUp(
|
||||
elapsedMs: elapsedMs,
|
||||
playerPosition: playerPosition,
|
||||
isWalkable: isWalkable,
|
||||
baseReactionMs: 50,
|
||||
);
|
||||
)) {
|
||||
onPlaySound(alertSoundId);
|
||||
}
|
||||
|
||||
double distance = position.distanceTo(playerPosition);
|
||||
|
||||
@@ -130,6 +139,7 @@ class HansGrosse extends Enemy {
|
||||
setTics(10);
|
||||
} else if (currentFrame == 2) {
|
||||
spriteIndex = _baseSprite + 6; // Fire
|
||||
onPlaySound(attackSoundId);
|
||||
onDamagePlayer(damage);
|
||||
setTics(4);
|
||||
} else if (currentFrame == 3) {
|
||||
|
||||
@@ -38,17 +38,20 @@ class Dog extends Enemy {
|
||||
required bool Function(int x, int y) isWalkable,
|
||||
required void Function(int damage) onDamagePlayer,
|
||||
required void Function(int x, int y) tryOpenDoor,
|
||||
required void Function(int sfxId) onPlaySound,
|
||||
}) {
|
||||
Coordinate2D movement = const Coordinate2D(0, 0);
|
||||
double newAngle = angle;
|
||||
|
||||
// 1. Perception
|
||||
if (state != EntityState.dead && !isDying) {
|
||||
checkWakeUp(
|
||||
if (checkWakeUp(
|
||||
elapsedMs: elapsedMs,
|
||||
playerPosition: playerPosition,
|
||||
isWalkable: isWalkable,
|
||||
);
|
||||
)) {
|
||||
onPlaySound(alertSoundId);
|
||||
}
|
||||
}
|
||||
|
||||
bool ticReady = processTics(elapsedDeltaMs, moveSpeed: 0);
|
||||
@@ -59,6 +62,7 @@ class Dog extends Enemy {
|
||||
currentFrame++;
|
||||
// Phase 2: The actual bite
|
||||
if (currentFrame == 1) {
|
||||
onPlaySound(attackSoundId);
|
||||
final bool attackSuccessful =
|
||||
math.Random().nextDouble() < (180 / 256);
|
||||
|
||||
|
||||
@@ -48,6 +48,12 @@ abstract class Enemy extends Entity {
|
||||
/// knowing the specific subclass.
|
||||
EnemyType get type;
|
||||
|
||||
/// The sound played when this enemy notices the player or hears combat.
|
||||
int get alertSoundId => type.alertSoundId;
|
||||
|
||||
/// The sound played when this enemy performs its attack animation.
|
||||
int get attackSoundId => type.attackSoundId;
|
||||
|
||||
/// Ensures enemies drop only one item (like ammo or a key) upon death.
|
||||
bool hasDroppedItem = false;
|
||||
|
||||
@@ -111,6 +117,7 @@ abstract class Enemy extends Entity {
|
||||
void handleAttackState({
|
||||
required int elapsedDeltaMs,
|
||||
required void Function(int damage) onDamagePlayer,
|
||||
required void Function() onAttack,
|
||||
required int shootTics,
|
||||
required int cooldownTics,
|
||||
required int postAttackTics,
|
||||
@@ -122,6 +129,7 @@ abstract class Enemy extends Entity {
|
||||
if (currentFrame == 1) {
|
||||
// Phase 1: Bang! Calculate hit chance based on distance.
|
||||
// Drops by ~5% per tile distance, capped at a minimum 10% chance to hit.
|
||||
onAttack();
|
||||
double hitChance = (1.0 - (distance * 0.05)).clamp(0.1, 1.0);
|
||||
|
||||
if (math.Random().nextDouble() <= hitChance) {
|
||||
@@ -147,13 +155,15 @@ abstract class Enemy extends Entity {
|
||||
///
|
||||
/// 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({
|
||||
bool checkWakeUp({
|
||||
required int elapsedMs,
|
||||
required Coordinate2D playerPosition,
|
||||
required bool Function(int x, int y) isWalkable,
|
||||
int baseReactionMs = 200,
|
||||
int reactionVarianceMs = 600,
|
||||
}) {
|
||||
bool didAlert = false;
|
||||
|
||||
if (!isAlerted && hasLineOfSight(playerPosition, isWalkable)) {
|
||||
if (reactionTimeMs == 0) {
|
||||
// First frame of spotting: calculate how long until they "wake up"
|
||||
@@ -164,6 +174,7 @@ abstract class Enemy extends Entity {
|
||||
} else if (elapsedMs >= reactionTimeMs) {
|
||||
// Reaction delay has passed
|
||||
isAlerted = true;
|
||||
didAlert = true;
|
||||
|
||||
if (state == EntityState.idle || state == EntityState.ambushing) {
|
||||
state = EntityState.patrolling;
|
||||
@@ -174,6 +185,8 @@ abstract class Enemy extends Entity {
|
||||
reactionTimeMs = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return didAlert;
|
||||
}
|
||||
|
||||
/// Determines if there is a clear, unobstructed path between the enemy and the player.
|
||||
@@ -312,6 +325,7 @@ abstract class Enemy extends Entity {
|
||||
required bool Function(int x, int y) isWalkable,
|
||||
required void Function(int x, int y) tryOpenDoor,
|
||||
required void Function(int damage) onDamagePlayer,
|
||||
required void Function(int sfxId) onPlaySound,
|
||||
});
|
||||
|
||||
/// Factory method to spawn the correct [Enemy] subclass based on a Map ID.
|
||||
@@ -358,7 +372,9 @@ abstract class Enemy extends Entity {
|
||||
|
||||
// Resolve spawn orientation using the NORMALIZED ID (0-7 offset from base)
|
||||
// This prevents offset math bugs (like the Mutant's 18-ID shift) from breaking facing directions.
|
||||
double spawnAngle = CardinalDirection.fromEnemyIndex(normalizedId).radians;
|
||||
double spawnAngle = CardinalDirection.fromEnemyIndex(
|
||||
normalizedId - matchedType.mapData.baseId,
|
||||
).radians;
|
||||
|
||||
EntityState spawnState;
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ enum EnemyType {
|
||||
/// Standard Brown Guard (The most common enemy).
|
||||
guard(
|
||||
mapData: EnemyMapData(MapObject.guardStart),
|
||||
alertSoundId: WolfSound.guardHalt,
|
||||
attackSoundId: WolfSound.naziFire,
|
||||
animations: EnemyAnimationMap(
|
||||
idle: SpriteFrameRange(50, 57),
|
||||
walking: SpriteFrameRange(58, 89),
|
||||
@@ -24,6 +26,8 @@ enum EnemyType {
|
||||
/// Attack Dog (Fast melee enemy).
|
||||
dog(
|
||||
mapData: EnemyMapData(MapObject.dogStart),
|
||||
alertSoundId: WolfSound.dogBark,
|
||||
attackSoundId: WolfSound.dogAttack,
|
||||
animations: EnemyAnimationMap(
|
||||
// Dogs don't have true idle sprites, so map idle to the first walk frame safely
|
||||
idle: SpriteFrameRange(99, 106),
|
||||
@@ -43,6 +47,8 @@ enum EnemyType {
|
||||
/// SS Officer (Blue uniform, machine gun).
|
||||
ss(
|
||||
mapData: EnemyMapData(MapObject.ssStart),
|
||||
alertSoundId: WolfSound.ssSchutzstaffel,
|
||||
attackSoundId: WolfSound.naziFire,
|
||||
animations: EnemyAnimationMap(
|
||||
idle: SpriteFrameRange(138, 145),
|
||||
walking: SpriteFrameRange(146, 177),
|
||||
@@ -55,7 +61,9 @@ enum EnemyType {
|
||||
|
||||
/// Undead Mutant (Exclusive to later episodes/retail).
|
||||
mutant(
|
||||
mapData: EnemyMapData(MapObject.mutantStart),
|
||||
mapData: EnemyMapData(MapObject.mutantStart, tierOffset: 18),
|
||||
alertSoundId: WolfSound.guardHalt,
|
||||
attackSoundId: WolfSound.naziFire,
|
||||
animations: EnemyAnimationMap(
|
||||
idle: SpriteFrameRange(187, 194),
|
||||
walking: SpriteFrameRange(195, 226),
|
||||
@@ -70,6 +78,8 @@ enum EnemyType {
|
||||
/// High-ranking Officer (White uniform, fast pistol).
|
||||
officer(
|
||||
mapData: EnemyMapData(MapObject.officerStart),
|
||||
alertSoundId: WolfSound.guardHalt,
|
||||
attackSoundId: WolfSound.naziFire,
|
||||
animations: EnemyAnimationMap(
|
||||
idle: SpriteFrameRange(238, 245),
|
||||
walking: SpriteFrameRange(246, 277),
|
||||
@@ -88,12 +98,20 @@ enum EnemyType {
|
||||
/// The animation ranges for this enemy type.
|
||||
final EnemyAnimationMap animations;
|
||||
|
||||
/// The sound played when this enemy first becomes alerted.
|
||||
final int alertSoundId;
|
||||
|
||||
/// The sound played when this enemy attacks.
|
||||
final int attackSoundId;
|
||||
|
||||
/// If false, this enemy type will be ignored when loading shareware data.
|
||||
final bool existsInShareware;
|
||||
|
||||
const EnemyType({
|
||||
required this.mapData,
|
||||
required this.animations,
|
||||
required this.alertSoundId,
|
||||
required this.attackSoundId,
|
||||
this.existsInShareware = true,
|
||||
});
|
||||
|
||||
@@ -151,7 +169,6 @@ enum EnemyType {
|
||||
|
||||
// --- Octant Calculation ---
|
||||
// We split the circle into 8 segments (octants).
|
||||
// Adding pi/8 offsets the segments so "North" covers the +/- 22.5 degree range.
|
||||
int octant = ((angleDiff + (math.pi / 8)) / (math.pi / 4)).floor() % 8;
|
||||
if (octant < 0) octant += 8;
|
||||
|
||||
|
||||
@@ -35,17 +35,20 @@ class Guard extends Enemy {
|
||||
required bool Function(int x, int y) isWalkable,
|
||||
required void Function(int damage) onDamagePlayer,
|
||||
required void Function(int x, int y) tryOpenDoor,
|
||||
required void Function(int sfxId) onPlaySound,
|
||||
}) {
|
||||
Coordinate2D movement = const Coordinate2D(0, 0);
|
||||
double newAngle = angle;
|
||||
double distance = position.distanceTo(playerPosition);
|
||||
|
||||
// 1. Perception (SightPlayer)
|
||||
checkWakeUp(
|
||||
if (checkWakeUp(
|
||||
elapsedMs: elapsedMs,
|
||||
playerPosition: playerPosition,
|
||||
isWalkable: isWalkable,
|
||||
);
|
||||
)) {
|
||||
onPlaySound(alertSoundId);
|
||||
}
|
||||
|
||||
// 2. Discrete AI Logic (Decisions happen every 10 tics)
|
||||
bool ticReady = processTics(elapsedDeltaMs, moveSpeed: 0);
|
||||
@@ -55,6 +58,7 @@ class Guard extends Enemy {
|
||||
elapsedDeltaMs: elapsedDeltaMs,
|
||||
distance: distance,
|
||||
onDamagePlayer: onDamagePlayer,
|
||||
onAttack: () => onPlaySound(attackSoundId),
|
||||
shootTics: 20,
|
||||
cooldownTics: 20,
|
||||
postAttackTics: 20,
|
||||
@@ -123,7 +127,7 @@ class Guard extends Enemy {
|
||||
double newAngle,
|
||||
Coordinate2D playerPosition,
|
||||
) {
|
||||
double diff = position.angleTo(playerPosition) - newAngle;
|
||||
double diff = newAngle - position.angleTo(playerPosition);
|
||||
while (diff <= -math.pi) {
|
||||
diff += 2 * math.pi;
|
||||
}
|
||||
|
||||
@@ -33,22 +33,25 @@ class Mutant extends Enemy {
|
||||
required bool Function(int x, int y) isWalkable,
|
||||
required void Function(int damage) onDamagePlayer,
|
||||
required void Function(int x, int y) tryOpenDoor,
|
||||
required void Function(int sfxId) onPlaySound,
|
||||
}) {
|
||||
Coordinate2D movement = const Coordinate2D(0, 0);
|
||||
double newAngle = angle;
|
||||
|
||||
checkWakeUp(
|
||||
if (checkWakeUp(
|
||||
elapsedMs: elapsedMs,
|
||||
playerPosition: playerPosition,
|
||||
isWalkable: isWalkable,
|
||||
);
|
||||
)) {
|
||||
onPlaySound(alertSoundId);
|
||||
}
|
||||
|
||||
double distance = position.distanceTo(playerPosition);
|
||||
double angleToPlayer = position.angleTo(playerPosition);
|
||||
|
||||
if (isAlerted && state != EntityState.dead) newAngle = angleToPlayer;
|
||||
|
||||
double diff = angleToPlayer - newAngle;
|
||||
double diff = newAngle - angleToPlayer;
|
||||
while (diff <= -math.pi) {
|
||||
diff += 2 * math.pi;
|
||||
}
|
||||
@@ -105,6 +108,7 @@ class Mutant extends Enemy {
|
||||
if (processTics(elapsedDeltaMs, moveSpeed: 0)) {
|
||||
currentFrame++;
|
||||
if (currentFrame == 1) {
|
||||
onPlaySound(attackSoundId);
|
||||
onDamagePlayer(damage);
|
||||
setTics(4);
|
||||
} else if (currentFrame == 2) {
|
||||
|
||||
@@ -33,22 +33,25 @@ class Officer extends Enemy {
|
||||
required bool Function(int x, int y) isWalkable,
|
||||
required void Function(int damage) onDamagePlayer,
|
||||
required void Function(int x, int y) tryOpenDoor,
|
||||
required void Function(int sfxId) onPlaySound,
|
||||
}) {
|
||||
Coordinate2D movement = const Coordinate2D(0, 0);
|
||||
double newAngle = angle;
|
||||
|
||||
checkWakeUp(
|
||||
if (checkWakeUp(
|
||||
elapsedMs: elapsedMs,
|
||||
playerPosition: playerPosition,
|
||||
isWalkable: isWalkable,
|
||||
);
|
||||
)) {
|
||||
onPlaySound(alertSoundId);
|
||||
}
|
||||
|
||||
double distance = position.distanceTo(playerPosition);
|
||||
double angleToPlayer = position.angleTo(playerPosition);
|
||||
|
||||
if (isAlerted && state != EntityState.dead) newAngle = angleToPlayer;
|
||||
|
||||
double diff = angleToPlayer - newAngle;
|
||||
double diff = newAngle - angleToPlayer;
|
||||
while (diff <= -math.pi) {
|
||||
diff += 2 * math.pi;
|
||||
}
|
||||
@@ -105,6 +108,7 @@ class Officer extends Enemy {
|
||||
if (processTics(elapsedDeltaMs, moveSpeed: 0)) {
|
||||
currentFrame++;
|
||||
if (currentFrame == 1) {
|
||||
onPlaySound(attackSoundId);
|
||||
onDamagePlayer(damage);
|
||||
setTics(4); // Bang!
|
||||
} else if (currentFrame == 2) {
|
||||
|
||||
@@ -32,22 +32,25 @@ class SS extends Enemy {
|
||||
required bool Function(int x, int y) isWalkable,
|
||||
required void Function(int damage) onDamagePlayer,
|
||||
required void Function(int x, int y) tryOpenDoor,
|
||||
required void Function(int sfxId) onPlaySound,
|
||||
}) {
|
||||
Coordinate2D movement = const Coordinate2D(0, 0);
|
||||
double newAngle = angle;
|
||||
|
||||
checkWakeUp(
|
||||
if (checkWakeUp(
|
||||
elapsedMs: elapsedMs,
|
||||
playerPosition: playerPosition,
|
||||
isWalkable: isWalkable,
|
||||
);
|
||||
)) {
|
||||
onPlaySound(alertSoundId);
|
||||
}
|
||||
|
||||
double distance = position.distanceTo(playerPosition);
|
||||
double angleToPlayer = position.angleTo(playerPosition);
|
||||
|
||||
if (isAlerted && state != EntityState.dead) newAngle = angleToPlayer;
|
||||
|
||||
double diff = angleToPlayer - newAngle;
|
||||
double diff = newAngle - angleToPlayer;
|
||||
while (diff <= -math.pi) {
|
||||
diff += 2 * math.pi;
|
||||
}
|
||||
@@ -104,6 +107,7 @@ class SS extends Enemy {
|
||||
if (processTics(elapsedDeltaMs, moveSpeed: 0)) {
|
||||
currentFrame++;
|
||||
if (currentFrame == 1) {
|
||||
onPlaySound(attackSoundId);
|
||||
onDamagePlayer(damage);
|
||||
setTics(5); // Bang!
|
||||
} else if (currentFrame == 2) {
|
||||
@@ -114,6 +118,7 @@ class SS extends Enemy {
|
||||
if (math.Random().nextDouble() > 0.5) {
|
||||
// 50% chance to burst
|
||||
currentFrame = 1;
|
||||
onPlaySound(attackSoundId);
|
||||
onDamagePlayer(damage);
|
||||
setTics(5);
|
||||
return (movement: movement, newAngle: newAngle);
|
||||
|
||||
47
packages/wolf_3d_dart/test/entities/enemy_spawn_test.dart
Normal file
47
packages/wolf_3d_dart/test/entities/enemy_spawn_test.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
import 'package:test/test.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
||||
|
||||
void main() {
|
||||
group('Enemy map IDs', () {
|
||||
test('maps officers, dogs, and mutants to the correct enemy types', () {
|
||||
expect(EnemyType.fromMapId(116), EnemyType.officer);
|
||||
expect(EnemyType.fromMapId(134), EnemyType.dog);
|
||||
expect(EnemyType.fromMapId(216), EnemyType.mutant);
|
||||
});
|
||||
|
||||
test('uses the original mutant 18-ID difficulty offset', () {
|
||||
expect(EnemyType.mutant.mapData.getNormalizedId(216, Difficulty.easy), 216);
|
||||
expect(
|
||||
EnemyType.mutant.mapData.getNormalizedId(234, Difficulty.medium),
|
||||
216,
|
||||
);
|
||||
expect(
|
||||
EnemyType.mutant.mapData.getNormalizedId(252, Difficulty.hard),
|
||||
216,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('Enemy spawn facing', () {
|
||||
test('spawns dog directions using the original dog map IDs', () {
|
||||
expect(_spawnEnemy(134).angle, CardinalDirection.east.radians);
|
||||
expect(_spawnEnemy(135).angle, CardinalDirection.north.radians);
|
||||
expect(_spawnEnemy(136).angle, CardinalDirection.west.radians);
|
||||
expect(_spawnEnemy(137).angle, CardinalDirection.south.radians);
|
||||
});
|
||||
|
||||
test('maps patrol variants to the same facing directions', () {
|
||||
expect(_spawnEnemy(138).angle, CardinalDirection.east.radians);
|
||||
expect(_spawnEnemy(139).angle, CardinalDirection.north.radians);
|
||||
expect(_spawnEnemy(140).angle, CardinalDirection.west.radians);
|
||||
expect(_spawnEnemy(141).angle, CardinalDirection.south.radians);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Enemy _spawnEnemy(int mapId) {
|
||||
final enemy = Enemy.spawn(mapId, 8.5, 8.5, Difficulty.hard);
|
||||
expect(enemy, isNotNull);
|
||||
return enemy!;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_type.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
||||
|
||||
void main() {
|
||||
group('Enemy Sprite Range Validation', () {
|
||||
@@ -22,6 +22,16 @@ void main() {
|
||||
// Skip if we are comparing the exact same animation on the same enemy
|
||||
if (enemyA == enemyB && animA == animB) return;
|
||||
|
||||
// Dogs intentionally reuse some standing frames as walking frames.
|
||||
final isDogIdleWalkReuse =
|
||||
enemyA == EnemyType.dog &&
|
||||
enemyB == EnemyType.dog &&
|
||||
((animA == EnemyAnimation.idle &&
|
||||
animB == EnemyAnimation.walking) ||
|
||||
(animA == EnemyAnimation.walking &&
|
||||
animB == EnemyAnimation.idle));
|
||||
if (isDogIdleWalkReuse) return;
|
||||
|
||||
if (rangeA.overlaps(rangeB)) {
|
||||
// Determine the specific frames that are clashing
|
||||
final start = math.max(rangeA.start, rangeB.start);
|
||||
|
||||
Reference in New Issue
Block a user