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