Fixed some enemy movement logic
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -96,12 +96,13 @@ class HansGrosse extends Enemy {
|
|||||||
if (!isAlerted || distance > 1.5) {
|
if (!isAlerted || distance > 1.5) {
|
||||||
double currentMoveAngle = isAlerted ? newAngle : angle;
|
double currentMoveAngle = isAlerted ? newAngle : angle;
|
||||||
movement = getValidMovement(
|
movement = getValidMovement(
|
||||||
Coordinate2D(
|
intendedMovement: Coordinate2D(
|
||||||
math.cos(currentMoveAngle) * speed,
|
math.cos(currentMoveAngle) * speed,
|
||||||
math.sin(currentMoveAngle) * speed,
|
math.sin(currentMoveAngle) * speed,
|
||||||
),
|
),
|
||||||
isWalkable,
|
playerPosition: playerPosition,
|
||||||
tryOpenDoor,
|
isWalkable: isWalkable,
|
||||||
|
tryOpenDoor: tryOpenDoor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import 'package:wolf_3d_dart/src/entities/entity.dart';
|
|||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
|
||||||
class Dog extends Enemy {
|
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
|
@override
|
||||||
EnemyType get type => EnemyType.dog;
|
EnemyType get type => EnemyType.dog;
|
||||||
@@ -21,8 +23,7 @@ class Dog extends Enemy {
|
|||||||
spriteIndex: EnemyType.dog.animations.idle.start,
|
spriteIndex: EnemyType.dog.animations.idle.start,
|
||||||
state: EntityState.idle,
|
state: EntityState.idle,
|
||||||
) {
|
) {
|
||||||
health = 1;
|
health = 1; // Dogs always have 1 HP in the original engine
|
||||||
damage = 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -36,19 +37,96 @@ 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
|
||||||
checkWakeUp(
|
checkWakeUp(
|
||||||
elapsedMs: elapsedMs,
|
elapsedMs: elapsedMs,
|
||||||
playerPosition: playerPosition,
|
playerPosition: playerPosition,
|
||||||
isWalkable: isWalkable,
|
isWalkable: isWalkable,
|
||||||
);
|
);
|
||||||
|
|
||||||
double distance = position.distanceTo(playerPosition);
|
// 2. Discrete AI Decision Rhythm
|
||||||
double angleToPlayer = position.angleTo(playerPosition);
|
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) {
|
while (diff <= -math.pi) {
|
||||||
diff += 2 * math.pi;
|
diff += 2 * math.pi;
|
||||||
}
|
}
|
||||||
@@ -68,51 +146,9 @@ class Dog extends Enemy {
|
|||||||
elapsedMs: elapsedMs,
|
elapsedMs: elapsedMs,
|
||||||
lastActionTime: lastActionTime,
|
lastActionTime: lastActionTime,
|
||||||
angleDiff: diff,
|
angleDiff: diff,
|
||||||
walkFrameOverride: state == EntityState.patrolling ? currentFrame : null,
|
walkFrameOverride: (state == EntityState.patrolling || isAlerted)
|
||||||
);
|
? 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,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -240,11 +240,23 @@ abstract class Enemy extends Entity {
|
|||||||
/// The logic performs separate X and Y collision checks to allow "sliding" along
|
/// 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
|
/// walls. If a movement is blocked, it calls [tryOpenDoor] to simulate the
|
||||||
/// enemy's ability to navigate through the level.
|
/// enemy's ability to navigate through the level.
|
||||||
Coordinate2D getValidMovement(
|
Coordinate2D getValidMovement({
|
||||||
Coordinate2D intendedMovement,
|
required Coordinate2D intendedMovement,
|
||||||
bool Function(int x, int y) isWalkable,
|
required Coordinate2D playerPosition,
|
||||||
void Function(int x, int y) tryOpenDoor,
|
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 newX = position.x + intendedMovement.x;
|
||||||
double newY = position.y + intendedMovement.y;
|
double newY = position.y + intendedMovement.y;
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ enum EnemyType {
|
|||||||
idle: SpriteFrameRange(99, 106),
|
idle: SpriteFrameRange(99, 106),
|
||||||
walking: SpriteFrameRange(107, 130),
|
walking: SpriteFrameRange(107, 130),
|
||||||
attacking: SpriteFrameRange(135, 137),
|
attacking: SpriteFrameRange(135, 137),
|
||||||
pain: SpriteFrameRange(137, 137),
|
pain: SpriteFrameRange(0, 0),
|
||||||
dying: SpriteFrameRange(131, 133),
|
dying: SpriteFrameRange(131, 133),
|
||||||
dead: SpriteFrameRange(134, 134),
|
dead: SpriteFrameRange(134, 134),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -86,19 +86,25 @@ class Guard extends Enemy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pursuit movement
|
// Pursuit movement
|
||||||
movement = _calculateMovement(
|
movement = getValidMovement(
|
||||||
newAngle,
|
intendedMovement: Coordinate2D(
|
||||||
currentMoveSpeed,
|
math.cos(newAngle) * currentMoveSpeed,
|
||||||
isWalkable,
|
math.sin(newAngle) * currentMoveSpeed,
|
||||||
tryOpenDoor,
|
),
|
||||||
|
playerPosition: playerPosition,
|
||||||
|
isWalkable: isWalkable,
|
||||||
|
tryOpenDoor: tryOpenDoor,
|
||||||
);
|
);
|
||||||
} else if (state == EntityState.patrolling) {
|
} else if (state == EntityState.patrolling) {
|
||||||
// Normal patrol movement
|
// Normal patrol movement
|
||||||
movement = _calculateMovement(
|
movement = getValidMovement(
|
||||||
angle,
|
intendedMovement: Coordinate2D(
|
||||||
currentMoveSpeed,
|
math.cos(angle) * currentMoveSpeed,
|
||||||
isWalkable,
|
math.sin(angle) * currentMoveSpeed,
|
||||||
tryOpenDoor,
|
),
|
||||||
|
playerPosition: playerPosition,
|
||||||
|
isWalkable: isWalkable,
|
||||||
|
tryOpenDoor: tryOpenDoor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,22 +118,6 @@ class Guard extends Enemy {
|
|||||||
return (movement: movement, newAngle: newAngle);
|
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(
|
void _updateAnimation(
|
||||||
int elapsedMs,
|
int elapsedMs,
|
||||||
double newAngle,
|
double newAngle,
|
||||||
|
|||||||
@@ -76,12 +76,13 @@ class Mutant extends Enemy {
|
|||||||
if (!isAlerted || distance > 0.8) {
|
if (!isAlerted || distance > 0.8) {
|
||||||
double currentMoveAngle = isAlerted ? angleToPlayer : angle;
|
double currentMoveAngle = isAlerted ? angleToPlayer : angle;
|
||||||
movement = getValidMovement(
|
movement = getValidMovement(
|
||||||
Coordinate2D(
|
intendedMovement: Coordinate2D(
|
||||||
math.cos(currentMoveAngle) * speed,
|
math.cos(currentMoveAngle) * speed,
|
||||||
math.sin(currentMoveAngle) * speed,
|
math.sin(currentMoveAngle) * speed,
|
||||||
),
|
),
|
||||||
isWalkable,
|
playerPosition: playerPosition,
|
||||||
tryOpenDoor,
|
isWalkable: isWalkable,
|
||||||
|
tryOpenDoor: tryOpenDoor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,12 +76,13 @@ class Officer extends Enemy {
|
|||||||
if (!isAlerted || distance > 0.8) {
|
if (!isAlerted || distance > 0.8) {
|
||||||
double currentMoveAngle = isAlerted ? angleToPlayer : angle;
|
double currentMoveAngle = isAlerted ? angleToPlayer : angle;
|
||||||
movement = getValidMovement(
|
movement = getValidMovement(
|
||||||
Coordinate2D(
|
intendedMovement: Coordinate2D(
|
||||||
math.cos(currentMoveAngle) * speed,
|
math.cos(currentMoveAngle) * speed,
|
||||||
math.sin(currentMoveAngle) * speed,
|
math.sin(currentMoveAngle) * speed,
|
||||||
),
|
),
|
||||||
isWalkable,
|
playerPosition: playerPosition,
|
||||||
tryOpenDoor,
|
isWalkable: isWalkable,
|
||||||
|
tryOpenDoor: tryOpenDoor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,12 +75,13 @@ class SS extends Enemy {
|
|||||||
if (!isAlerted || distance > 0.8) {
|
if (!isAlerted || distance > 0.8) {
|
||||||
double currentMoveAngle = isAlerted ? angleToPlayer : angle;
|
double currentMoveAngle = isAlerted ? angleToPlayer : angle;
|
||||||
movement = getValidMovement(
|
movement = getValidMovement(
|
||||||
Coordinate2D(
|
intendedMovement: Coordinate2D(
|
||||||
math.cos(currentMoveAngle) * speed,
|
math.cos(currentMoveAngle) * speed,
|
||||||
math.sin(currentMoveAngle) * speed,
|
math.sin(currentMoveAngle) * speed,
|
||||||
),
|
),
|
||||||
isWalkable,
|
playerPosition: playerPosition,
|
||||||
tryOpenDoor,
|
isWalkable: isWalkable,
|
||||||
|
tryOpenDoor: tryOpenDoor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user