Fixed some enemy movement logic

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-17 17:20:24 +01:00
parent a2f01da515
commit 8cb1ea8d9b
8 changed files with 138 additions and 96 deletions

View File

@@ -96,12 +96,13 @@ class HansGrosse extends Enemy {
if (!isAlerted || distance > 1.5) {
double currentMoveAngle = isAlerted ? newAngle : angle;
movement = getValidMovement(
Coordinate2D(
intendedMovement: Coordinate2D(
math.cos(currentMoveAngle) * speed,
math.sin(currentMoveAngle) * speed,
),
isWalkable,
tryOpenDoor,
playerPosition: playerPosition,
isWalkable: isWalkable,
tryOpenDoor: tryOpenDoor,
);
}

View File

@@ -7,7 +7,9 @@ import 'package:wolf_3d_dart/src/entities/entity.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
class Dog extends Enemy {
static const double speed = 0.05;
/// Original SPDDOG is 1024. 1 Tile is 65536 units.
/// 1024 / 65536 = ~0.0156 tiles per tic.
static const double speedPerTic = 0.0156;
@override
EnemyType get type => EnemyType.dog;
@@ -21,8 +23,7 @@ class Dog extends Enemy {
spriteIndex: EnemyType.dog.animations.idle.start,
state: EntityState.idle,
) {
health = 1;
damage = 2;
health = 1; // Dogs always have 1 HP in the original engine
}
@override
@@ -36,19 +37,96 @@ class Dog extends Enemy {
}) {
Coordinate2D movement = const Coordinate2D(0, 0);
double newAngle = angle;
double distance = position.distanceTo(playerPosition);
// 1. Perception
checkWakeUp(
elapsedMs: elapsedMs,
playerPosition: playerPosition,
isWalkable: isWalkable,
);
double distance = position.distanceTo(playerPosition);
double angleToPlayer = position.angleTo(playerPosition);
// 2. Discrete AI Decision Rhythm
bool ticReady = processTics(elapsedDeltaMs, moveSpeed: 0);
if (isAlerted && state != EntityState.dead) newAngle = angleToPlayer;
if (state == EntityState.attacking) {
// --- AUTHENTIC T_Bite / Jump Sequence ---
// Original Jump sequence (s_dogjump1 to s_dogjump5) is 5 frames, 10 tics each
if (ticReady) {
currentFrame++;
double diff = angleToPlayer - newAngle;
if (currentFrame == 1) {
// Phase 2: The actual bite (s_dogjump2)
// Original hit chance is US_RndT() < 180 (~70% chance)
if (distance <= 1.2 && math.Random().nextDouble() < (180 / 256)) {
// Original damage: US_RndT() >> 4 (0 to 15 damage)
int actualDamage = math.Random().nextInt(16);
onDamagePlayer(actualDamage);
}
setTics(10);
} else if (currentFrame < 5) {
setTics(10); // Phases 3-5: Mid-air and Landing
} else {
// Sequence complete, return to chase
state = EntityState.patrolling;
currentFrame = 0;
setTics(10);
}
}
} else if (state != EntityState.dead) {
// 3. Continuous Movement (T_DogChase / T_Path)
double ticsThisFrame = elapsedDeltaMs / 14.28;
double currentMoveSpeed = speedPerTic * ticsThisFrame;
if (isAlerted) {
newAngle = position.angleTo(playerPosition);
// Trigger Jump: Original uses MINACTORDIST (approx 0.8 tiles)
if (distance <= 0.8) {
state = EntityState.attacking;
currentFrame = 0;
lastActionTime = elapsedMs;
setTics(10);
return (movement: const Coordinate2D(0, 0), newAngle: newAngle);
}
movement = getValidMovement(
intendedMovement: Coordinate2D(
math.cos(newAngle) * currentMoveSpeed,
math.sin(newAngle) * currentMoveSpeed,
),
playerPosition: playerPosition,
isWalkable: isWalkable,
tryOpenDoor: tryOpenDoor,
);
} else if (state == EntityState.patrolling) {
movement = getValidMovement(
intendedMovement: Coordinate2D(
math.cos(angle) * currentMoveSpeed,
math.sin(angle) * currentMoveSpeed,
),
playerPosition: playerPosition,
isWalkable: isWalkable,
tryOpenDoor: tryOpenDoor,
);
}
if (ticReady) {
currentFrame = (currentFrame + 1) % 4;
setTics(10); // Chase rhythm (s_dogchase1-4)
}
}
_updateAnimation(elapsedMs, newAngle, playerPosition);
return (movement: movement, newAngle: newAngle);
}
void _updateAnimation(
int elapsedMs,
double newAngle,
Coordinate2D playerPosition,
) {
double diff = position.angleTo(playerPosition) - newAngle;
while (diff <= -math.pi) {
diff += 2 * math.pi;
}
@@ -68,51 +146,9 @@ class Dog extends Enemy {
elapsedMs: elapsedMs,
lastActionTime: lastActionTime,
angleDiff: diff,
walkFrameOverride: state == EntityState.patrolling ? currentFrame : null,
);
if (state == EntityState.patrolling) {
if (!isAlerted || distance > 1.0) {
double currentMoveAngle = isAlerted ? angleToPlayer : angle;
movement = getValidMovement(
Coordinate2D(
math.cos(currentMoveAngle) * speed,
math.sin(currentMoveAngle) * speed,
),
isWalkable,
tryOpenDoor,
walkFrameOverride: (state == EntityState.patrolling || isAlerted)
? currentFrame
: null,
);
}
if (processTics(elapsedDeltaMs, moveSpeed: speed)) {
currentFrame = (currentFrame + 1) % 4;
setTics(5);
if (isAlerted && distance < 1.0) {
state = EntityState.attacking;
currentFrame = 0;
lastActionTime = elapsedMs;
setTics(5); // Leap
}
}
}
if (state == EntityState.attacking) {
if (processTics(elapsedDeltaMs, moveSpeed: 0)) {
currentFrame++;
if (currentFrame == 1) {
onDamagePlayer(damage); // Bite
setTics(5);
} else if (currentFrame == 2) {
setTics(5); // Land
} else {
state = EntityState.patrolling;
currentFrame = 0;
setTics(5);
}
}
}
return (movement: movement, newAngle: newAngle);
}
}

View File

@@ -240,11 +240,23 @@ abstract class Enemy extends Entity {
/// The logic performs separate X and Y collision checks to allow "sliding" along
/// walls. If a movement is blocked, it calls [tryOpenDoor] to simulate the
/// enemy's ability to navigate through the level.
Coordinate2D getValidMovement(
Coordinate2D intendedMovement,
bool Function(int x, int y) isWalkable,
void Function(int x, int y) tryOpenDoor,
) {
Coordinate2D getValidMovement({
required Coordinate2D intendedMovement,
required Coordinate2D playerPosition,
required bool Function(int x, int y) isWalkable,
required void Function(int x, int y) tryOpenDoor,
}) {
final double distToPlayer = position.distanceTo(playerPosition);
const double minDistance = 0.9;
if (distToPlayer < minDistance) {
// If already too close, only allow movement if it increases distance
Coordinate2D nextPos = position + intendedMovement;
if (nextPos.distanceTo(playerPosition) < distToPlayer) {
return const Coordinate2D(0, 0);
}
}
double newX = position.x + intendedMovement.x;
double newY = position.y + intendedMovement.y;

View File

@@ -28,7 +28,7 @@ enum EnemyType {
idle: SpriteFrameRange(99, 106),
walking: SpriteFrameRange(107, 130),
attacking: SpriteFrameRange(135, 137),
pain: SpriteFrameRange(137, 137),
pain: SpriteFrameRange(0, 0),
dying: SpriteFrameRange(131, 133),
dead: SpriteFrameRange(134, 134),
),

View File

@@ -86,19 +86,25 @@ class Guard extends Enemy {
}
// Pursuit movement
movement = _calculateMovement(
newAngle,
currentMoveSpeed,
isWalkable,
tryOpenDoor,
movement = getValidMovement(
intendedMovement: Coordinate2D(
math.cos(newAngle) * currentMoveSpeed,
math.sin(newAngle) * currentMoveSpeed,
),
playerPosition: playerPosition,
isWalkable: isWalkable,
tryOpenDoor: tryOpenDoor,
);
} else if (state == EntityState.patrolling) {
// Normal patrol movement
movement = _calculateMovement(
angle,
currentMoveSpeed,
isWalkable,
tryOpenDoor,
movement = getValidMovement(
intendedMovement: Coordinate2D(
math.cos(angle) * currentMoveSpeed,
math.sin(angle) * currentMoveSpeed,
),
playerPosition: playerPosition,
isWalkable: isWalkable,
tryOpenDoor: tryOpenDoor,
);
}
@@ -112,22 +118,6 @@ class Guard extends Enemy {
return (movement: movement, newAngle: newAngle);
}
Coordinate2D _calculateMovement(
double moveAngle,
double moveSpeed,
bool Function(int x, int y) isWalkable,
void Function(int x, int y) tryOpenDoor,
) {
return getValidMovement(
Coordinate2D(
math.cos(moveAngle) * moveSpeed,
math.sin(moveAngle) * moveSpeed,
),
isWalkable,
tryOpenDoor,
);
}
void _updateAnimation(
int elapsedMs,
double newAngle,

View File

@@ -76,12 +76,13 @@ class Mutant extends Enemy {
if (!isAlerted || distance > 0.8) {
double currentMoveAngle = isAlerted ? angleToPlayer : angle;
movement = getValidMovement(
Coordinate2D(
intendedMovement: Coordinate2D(
math.cos(currentMoveAngle) * speed,
math.sin(currentMoveAngle) * speed,
),
isWalkable,
tryOpenDoor,
playerPosition: playerPosition,
isWalkable: isWalkable,
tryOpenDoor: tryOpenDoor,
);
}

View File

@@ -76,12 +76,13 @@ class Officer extends Enemy {
if (!isAlerted || distance > 0.8) {
double currentMoveAngle = isAlerted ? angleToPlayer : angle;
movement = getValidMovement(
Coordinate2D(
intendedMovement: Coordinate2D(
math.cos(currentMoveAngle) * speed,
math.sin(currentMoveAngle) * speed,
),
isWalkable,
tryOpenDoor,
playerPosition: playerPosition,
isWalkable: isWalkable,
tryOpenDoor: tryOpenDoor,
);
}

View File

@@ -75,12 +75,13 @@ class SS extends Enemy {
if (!isAlerted || distance > 0.8) {
double currentMoveAngle = isAlerted ? angleToPlayer : angle;
movement = getValidMovement(
Coordinate2D(
intendedMovement: Coordinate2D(
math.cos(currentMoveAngle) * speed,
math.sin(currentMoveAngle) * speed,
),
isWalkable,
tryOpenDoor,
playerPosition: playerPosition,
isWalkable: isWalkable,
tryOpenDoor: tryOpenDoor,
);
}