Fix dog AI and animations
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -416,7 +416,7 @@ class WolfEngine {
|
||||
|
||||
// Wake them up!
|
||||
if (entity.state == EntityState.idle ||
|
||||
entity.state == EntityState.ambush) {
|
||||
entity.state == EntityState.chasing) {
|
||||
entity.state = EntityState.patrolling;
|
||||
entity.lastActionTime = _timeAliveMs;
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ class HansGrosse extends Enemy {
|
||||
|
||||
switch (state) {
|
||||
case EntityState.idle:
|
||||
case EntityState.ambush:
|
||||
case EntityState.chasing:
|
||||
spriteIndex = _baseSprite;
|
||||
break;
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@ class Dog extends Enemy {
|
||||
/// 1024 / 65536 = ~0.0156 tiles per tic.
|
||||
static const double speedPerTic = 0.0156;
|
||||
|
||||
// Used to simulate SelectDodgeDir's zigzag behavior
|
||||
double _dodgeAngleOffset = 0.0;
|
||||
int _dodgeTicTimer = 0;
|
||||
|
||||
@override
|
||||
EnemyType get type => EnemyType.dog;
|
||||
|
||||
@@ -20,10 +24,10 @@ class Dog extends Enemy {
|
||||
required super.angle,
|
||||
required super.mapId,
|
||||
}) : super(
|
||||
spriteIndex: EnemyType.dog.animations.idle.start,
|
||||
state: EntityState.idle,
|
||||
spriteIndex: EnemyType.dog.animations.walking.start,
|
||||
state: EntityState.patrolling,
|
||||
) {
|
||||
health = 1; // Dogs always have 1 HP in the original engine
|
||||
health = 1;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -37,59 +41,76 @@ 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,
|
||||
);
|
||||
if (state != EntityState.dead && !isDying) {
|
||||
checkWakeUp(
|
||||
elapsedMs: elapsedMs,
|
||||
playerPosition: playerPosition,
|
||||
isWalkable: isWalkable,
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Discrete AI Decision Rhythm
|
||||
bool ticReady = processTics(elapsedDeltaMs, moveSpeed: 0);
|
||||
|
||||
// 2. State-Based Logic Gate
|
||||
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++;
|
||||
|
||||
// Phase 2: The actual bite
|
||||
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);
|
||||
final bool attackSuccessful =
|
||||
math.Random().nextDouble() < (180 / 256);
|
||||
|
||||
double dx = (position.x - playerPosition.x).abs() - speedPerTic;
|
||||
double dy = (position.y - playerPosition.y).abs() - speedPerTic;
|
||||
bool inBiteRange = dx <= 1.0 && dy <= 1.0;
|
||||
|
||||
if (inBiteRange && attackSuccessful) {
|
||||
onDamagePlayer(math.Random().nextInt(16));
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
if (currentFrame >= 5) {
|
||||
state = EntityState.chasing;
|
||||
currentFrame = 0;
|
||||
}
|
||||
setTics(10);
|
||||
}
|
||||
} else if (state != EntityState.dead) {
|
||||
// 3. Continuous Movement (T_DogChase / T_Path)
|
||||
_updateAnimation(elapsedMs, newAngle, playerPosition);
|
||||
return (movement: const Coordinate2D(0, 0), newAngle: newAngle);
|
||||
} else if (state != EntityState.dead && !isDying) {
|
||||
// 3. Chase / Movement Logic
|
||||
double ticsThisFrame = elapsedDeltaMs / 14.28;
|
||||
double currentMoveSpeed = speedPerTic * ticsThisFrame;
|
||||
|
||||
if (isAlerted) {
|
||||
newAngle = position.angleTo(playerPosition);
|
||||
if (isAlerted || state == EntityState.chasing) {
|
||||
state = EntityState.chasing;
|
||||
|
||||
// Trigger Jump: Original uses MINACTORDIST (approx 0.8 tiles)
|
||||
if (distance <= 0.8) {
|
||||
double dx = (position.x - playerPosition.x).abs() - currentMoveSpeed;
|
||||
double dy = (position.y - playerPosition.y).abs() - currentMoveSpeed;
|
||||
|
||||
if (dx <= 1.0 && dy <= 1.0) {
|
||||
state = EntityState.attacking;
|
||||
currentFrame = 0;
|
||||
lastActionTime = elapsedMs;
|
||||
setTics(10);
|
||||
return (movement: const Coordinate2D(0, 0), newAngle: newAngle);
|
||||
return (
|
||||
movement: const Coordinate2D(0, 0),
|
||||
newAngle: position.angleTo(playerPosition),
|
||||
);
|
||||
}
|
||||
|
||||
if (_dodgeTicTimer <= 0) {
|
||||
_dodgeAngleOffset =
|
||||
(math.Random().nextBool() ? 1 : -1) * (math.pi / 4);
|
||||
_dodgeTicTimer = 15;
|
||||
} else if (ticReady) {
|
||||
_dodgeTicTimer--;
|
||||
}
|
||||
|
||||
newAngle = position.angleTo(playerPosition) + _dodgeAngleOffset;
|
||||
|
||||
movement = getValidMovement(
|
||||
intendedMovement: Coordinate2D(
|
||||
math.cos(newAngle) * currentMoveSpeed,
|
||||
@@ -97,8 +118,13 @@ class Dog extends Enemy {
|
||||
),
|
||||
playerPosition: playerPosition,
|
||||
isWalkable: isWalkable,
|
||||
tryOpenDoor: tryOpenDoor,
|
||||
// FIX: Alerted dogs cannot open doors! Pass an empty closure to treat doors as walls.
|
||||
tryOpenDoor: (x, y) {},
|
||||
);
|
||||
|
||||
if (movement.x == 0 && movement.y == 0) {
|
||||
_dodgeTicTimer = 0;
|
||||
}
|
||||
} else if (state == EntityState.patrolling) {
|
||||
movement = getValidMovement(
|
||||
intendedMovement: Coordinate2D(
|
||||
@@ -107,13 +133,14 @@ class Dog extends Enemy {
|
||||
),
|
||||
playerPosition: playerPosition,
|
||||
isWalkable: isWalkable,
|
||||
// Patrolling dogs using T_Path CAN open doors in the original logic.
|
||||
tryOpenDoor: tryOpenDoor,
|
||||
);
|
||||
}
|
||||
|
||||
if (ticReady) {
|
||||
currentFrame = (currentFrame + 1) % 4;
|
||||
setTics(10); // Chase rhythm (s_dogchase1-4)
|
||||
setTics(10);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +153,7 @@ class Dog 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;
|
||||
}
|
||||
@@ -135,8 +162,9 @@ class Dog extends Enemy {
|
||||
}
|
||||
|
||||
EnemyAnimation currentAnim = switch (state) {
|
||||
EntityState.patrolling => EnemyAnimation.walking,
|
||||
EntityState.patrolling || EntityState.chasing => EnemyAnimation.walking,
|
||||
EntityState.attacking => EnemyAnimation.attacking,
|
||||
EntityState.pain => EnemyAnimation.pain,
|
||||
EntityState.dead => isDying ? EnemyAnimation.dying : EnemyAnimation.dead,
|
||||
_ => EnemyAnimation.idle,
|
||||
};
|
||||
@@ -146,7 +174,10 @@ class Dog extends Enemy {
|
||||
elapsedMs: elapsedMs,
|
||||
lastActionTime: lastActionTime,
|
||||
angleDiff: diff,
|
||||
walkFrameOverride: (state == EntityState.patrolling || isAlerted)
|
||||
walkFrameOverride:
|
||||
(state == EntityState.patrolling ||
|
||||
state == EntityState.chasing ||
|
||||
isAlerted)
|
||||
? currentFrame
|
||||
: null,
|
||||
);
|
||||
|
||||
@@ -165,7 +165,7 @@ abstract class Enemy extends Entity {
|
||||
// Reaction delay has passed
|
||||
isAlerted = true;
|
||||
|
||||
if (state == EntityState.idle || state == EntityState.ambush) {
|
||||
if (state == EntityState.idle || state == EntityState.chasing) {
|
||||
state = EntityState.patrolling;
|
||||
setTics(10);
|
||||
}
|
||||
@@ -356,7 +356,7 @@ abstract class Enemy extends Entity {
|
||||
} else if (mapData.isStaticForDifficulty(objId, difficulty)) {
|
||||
spawnState = EntityState.idle;
|
||||
} else if (mapData.isAmbushForDifficulty(objId, difficulty)) {
|
||||
spawnState = EntityState.ambush;
|
||||
spawnState = EntityState.chasing;
|
||||
} else {
|
||||
// The ID belongs to this enemy type, but not for this specific difficulty level
|
||||
return null;
|
||||
|
||||
@@ -25,11 +25,17 @@ enum EnemyType {
|
||||
dog(
|
||||
mapData: EnemyMapData(MapObject.dogStart),
|
||||
animations: EnemyAnimationMap(
|
||||
// Dogs don't have true idle sprites, so map idle to the first walk frame safely
|
||||
idle: SpriteFrameRange(99, 106),
|
||||
walking: SpriteFrameRange(107, 130),
|
||||
// Dogs have 4 walk frames * 8 octants = 32 sprites
|
||||
walking: SpriteFrameRange(99, 130),
|
||||
// Jump / Attack is 3 global sprites
|
||||
attacking: SpriteFrameRange(135, 137),
|
||||
// Dogs don't have pain frames in Wolf3D
|
||||
pain: SpriteFrameRange(0, 0),
|
||||
// Die is 3 global sprites
|
||||
dying: SpriteFrameRange(131, 133),
|
||||
// Dead is 1 global sprite
|
||||
dead: SpriteFrameRange(134, 134),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -132,7 +132,7 @@ class Guard extends Enemy {
|
||||
}
|
||||
|
||||
EnemyAnimation currentAnim = switch (state) {
|
||||
EntityState.patrolling => EnemyAnimation.walking,
|
||||
EntityState.patrolling || EntityState.chasing => EnemyAnimation.walking,
|
||||
EntityState.attacking => EnemyAnimation.attacking,
|
||||
EntityState.pain => EnemyAnimation.pain,
|
||||
EntityState.dead => isDying ? EnemyAnimation.dying : EnemyAnimation.dead,
|
||||
|
||||
@@ -57,7 +57,7 @@ class Mutant extends Enemy {
|
||||
}
|
||||
|
||||
EnemyAnimation currentAnim = switch (state) {
|
||||
EntityState.patrolling => EnemyAnimation.walking,
|
||||
EntityState.patrolling || EntityState.chasing => EnemyAnimation.walking,
|
||||
EntityState.attacking => EnemyAnimation.attacking,
|
||||
EntityState.pain => EnemyAnimation.pain,
|
||||
EntityState.dead => isDying ? EnemyAnimation.dying : EnemyAnimation.dead,
|
||||
|
||||
@@ -57,7 +57,7 @@ class Officer extends Enemy {
|
||||
}
|
||||
|
||||
EnemyAnimation currentAnim = switch (state) {
|
||||
EntityState.patrolling => EnemyAnimation.walking,
|
||||
EntityState.patrolling || EntityState.chasing => EnemyAnimation.walking,
|
||||
EntityState.attacking => EnemyAnimation.attacking,
|
||||
EntityState.pain => EnemyAnimation.pain,
|
||||
EntityState.dead => isDying ? EnemyAnimation.dying : EnemyAnimation.dead,
|
||||
|
||||
@@ -56,7 +56,7 @@ class SS extends Enemy {
|
||||
}
|
||||
|
||||
EnemyAnimation currentAnim = switch (state) {
|
||||
EntityState.patrolling => EnemyAnimation.walking,
|
||||
EntityState.patrolling || EntityState.chasing => EnemyAnimation.walking,
|
||||
EntityState.attacking => EnemyAnimation.attacking,
|
||||
EntityState.pain => EnemyAnimation.pain,
|
||||
EntityState.dead => isDying ? EnemyAnimation.dying : EnemyAnimation.dead,
|
||||
|
||||
@@ -6,7 +6,7 @@ enum EntityState {
|
||||
staticObj,
|
||||
|
||||
/// Enemies waiting for the player to enter their field of view.
|
||||
ambush,
|
||||
chasing,
|
||||
|
||||
/// Enemies standing still but capable of hearing noise.
|
||||
idle,
|
||||
|
||||
Reference in New Issue
Block a user