Can now open secret walls and pick up machine gun

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-14 15:34:27 +01:00
parent 9f5f29100b
commit 001c7c3131
13 changed files with 545 additions and 120 deletions

View File

@@ -58,18 +58,26 @@ class BrownGuard extends Enemy {
required Coordinate2D playerPosition,
required bool Function(int x, int y) isWalkable,
required void Function(int damage) onDamagePlayer,
required void Function(int x, int y) tryOpenDoor,
}) {
Coordinate2D movement = const Coordinate2D(0, 0);
double newAngle = angle;
// 1. Wake up logic
// 1. Wake up logic (Matches SightPlayer & FirstSighting)
if (state == EntityState.idle &&
hasLineOfSight(playerPosition, isWalkable)) {
state = EntityState.patrolling;
lastActionTime = elapsedMs;
if (reactionTimeMs == 0) {
// Init reaction delay: ~1 to 4 tics in C (1 tic = ~14ms, but plays out longer in engine ticks).
// Let's approximate human-feeling reaction time: 200ms - 800ms
reactionTimeMs = elapsedMs + 200 + math.Random().nextInt(600);
} else if (elapsedMs >= reactionTimeMs) {
state =
EntityState.patrolling; // Equivalent to FirstSighting chase frame
lastActionTime = elapsedMs;
reactionTimeMs = 0; // Reset
}
}
// 2. Pre-calculate spatial relations
double distance = position.distanceTo(playerPosition);
double angleToPlayer = position.angleTo(playerPosition);
@@ -77,7 +85,7 @@ class BrownGuard extends Enemy {
newAngle = angleToPlayer;
}
// Calculate Octant for sprite direction
// Octant logic remains the same
double diff = newAngle - angleToPlayer;
while (diff <= -math.pi) {
diff += 2 * math.pi;
@@ -85,6 +93,7 @@ class BrownGuard extends Enemy {
while (diff > math.pi) {
diff -= 2 * math.pi;
}
int octant = ((diff + (math.pi / 8)) / (math.pi / 4)).floor() % 8;
if (octant < 0) octant += 8;
@@ -96,22 +105,34 @@ class BrownGuard extends Enemy {
case EntityState.patrolling:
if (distance > 0.8) {
// Calculate movement intent
movement =
Coordinate2D(math.cos(newAngle), math.sin(newAngle)) * speed;
// Jitter fix: Use continuous vector movement instead of single-axis snapping
double moveX = math.cos(angleToPlayer) * speed;
double moveY = math.sin(angleToPlayer) * speed;
Coordinate2D intendedMovement = Coordinate2D(moveX, moveY);
// Pass tryOpenDoor down!
movement = getValidMovement(
intendedMovement,
isWalkable,
tryOpenDoor,
);
}
// Animation fix: Update the sprite so he actually turns and walks!
int walkFrame = (elapsedMs ~/ 150) % 4;
spriteIndex = 58 + (walkFrame * 8) + octant;
if (distance < 5.0 && elapsedMs - lastActionTime > 2000) {
// Shooting fix: Give him permission to stop and shoot you
// (1500ms delay between shots)
if (distance < 6.0 && elapsedMs - lastActionTime > 1500) {
if (hasLineOfSight(playerPosition, isWalkable)) {
state = EntityState.shooting;
lastActionTime = elapsedMs;
_hasFiredThisCycle = false;
}
}
break;
break; // Fallthrough fix: Don't forget the break!
case EntityState.shooting:
int timeShooting = elapsedMs - lastActionTime;
@@ -152,6 +173,7 @@ class BrownGuard extends Enemy {
spriteIndex = 95;
}
break;
default:
break;
}

View File

@@ -51,21 +51,28 @@ class Dog extends Enemy {
required int elapsedMs,
required Coordinate2D playerPosition,
required bool Function(int x, int y) isWalkable,
required void Function(int x, int y) tryOpenDoor, // NEW
required void Function(int damage) onDamagePlayer,
}) {
Coordinate2D movement = const Coordinate2D(0, 0);
double newAngle = angle;
// 1. Wake up logic
if (state == EntityState.idle &&
hasLineOfSight(playerPosition, isWalkable)) {
state = EntityState.patrolling;
lastActionTime = elapsedMs;
if (reactionTimeMs == 0) {
reactionTimeMs = elapsedMs + 100 + math.Random().nextInt(200);
} else if (elapsedMs >= reactionTimeMs) {
state = EntityState.patrolling;
lastActionTime = elapsedMs;
reactionTimeMs = 0;
}
}
double distance = position.distanceTo(playerPosition);
double angleToPlayer = position.angleTo(playerPosition);
if (state == EntityState.patrolling || state == EntityState.shooting) {
if (state != EntityState.idle && state != EntityState.dead) {
newAngle = angleToPlayer;
}
@@ -76,36 +83,60 @@ class Dog extends Enemy {
while (diff > math.pi) {
diff -= 2 * math.pi;
}
int octant = ((diff + (math.pi / 8)) / (math.pi / 4)).floor() % 8;
if (octant < 0) octant += 8;
if (state == EntityState.idle) {
spriteIndex = 99 + octant;
} else if (state == EntityState.patrolling) {
if (distance > 0.8) {
movement = Coordinate2D(math.cos(newAngle), math.sin(newAngle)) * speed;
}
// 3. Clean State Machine
switch (state) {
case EntityState.idle:
spriteIndex = 99 + octant;
break;
int walkFrame = (elapsedMs ~/ 100) % 4;
spriteIndex = 107 + (walkFrame * 8) + octant;
case EntityState.patrolling:
if (distance > 0.8) {
double deltaX = playerPosition.x - position.x;
double deltaY = playerPosition.y - position.y;
if (distance < 1.0 && elapsedMs - lastActionTime > 1000) {
state = EntityState.shooting;
lastActionTime = elapsedMs;
_hasBittenThisCycle = false;
}
} else if (state == EntityState.shooting) {
int timeAttacking = elapsedMs - lastActionTime;
if (timeAttacking < 200) {
spriteIndex = 139;
if (!_hasBittenThisCycle) {
onDamagePlayer(5); // DOG BITE
_hasBittenThisCycle = true;
double moveX = deltaX > 0 ? speed : (deltaX < 0 ? -speed : 0);
double moveY = deltaY > 0 ? speed : (deltaY < 0 ? -speed : 0);
Coordinate2D intendedMovement = Coordinate2D(moveX, moveY);
// Pass tryOpenDoor down!
movement = getValidMovement(
intendedMovement,
isWalkable,
tryOpenDoor,
);
}
} else {
state = EntityState.patrolling;
lastActionTime = elapsedMs;
}
int walkFrame = (elapsedMs ~/ 100) % 4;
spriteIndex = 107 + (walkFrame * 8) + octant;
if (distance < 1.0 && elapsedMs - lastActionTime > 1000) {
state = EntityState.shooting;
lastActionTime = elapsedMs;
_hasBittenThisCycle = false;
}
break;
case EntityState.shooting:
int timeAttacking = elapsedMs - lastActionTime;
if (timeAttacking < 200) {
spriteIndex = 139;
if (!_hasBittenThisCycle) {
onDamagePlayer(5);
_hasBittenThisCycle = true;
}
} else {
state = EntityState.patrolling;
lastActionTime = elapsedMs;
}
break;
default:
break;
}
return (movement: movement, newAngle: newAngle);

View File

@@ -14,21 +14,22 @@ abstract class Enemy extends Entity {
super.lastActionTime,
});
// Standard guard health
int health = 25;
int damage = 10;
bool isDying = false;
bool hasDroppedItem = false;
// Replaces ob->temp2 for reaction delays
int reactionTimeMs = 0;
void takeDamage(int amount, int currentTime) {
if (state == EntityState.dead) return;
health -= amount;
// Mark the start of the death/pain
lastActionTime = currentTime;
if (health <= 0) {
state = EntityState.dead;
// This triggers the dying animation
isDying = true;
} else if (math.Random().nextDouble() < 0.5) {
state = EntityState.pain;
@@ -37,12 +38,9 @@ abstract class Enemy extends Entity {
}
}
// Decodes the Map ID to figure out which way the enemy is facing
static double getInitialAngle(int objId) {
int normalizedId = (objId - 108) % 36;
// 0=East, 1=North, 2=West, 3=South
int direction = normalizedId % 4;
switch (direction) {
case 0:
return 0.0;
@@ -57,14 +55,19 @@ abstract class Enemy extends Entity {
}
}
// The enemy can now check its own line of sight!
// Matches WL_STATE.C's 'CheckLine' using canonical Integer DDA traversal
bool hasLineOfSight(
Coordinate2D playerPosition,
bool Function(int x, int y) isWalkable,
) {
double distance = position.distanceTo(playerPosition);
// 1. Proximity Check (Matches WL_STATE.C 'MINSIGHT')
// If the player is very close, sight is automatic regardless of facing angle.
// This compensates for our lack of a noise/gunshot alert system!
if (position.distanceTo(playerPosition) < 2.0) {
return true;
}
// 1. FOV Check
// 2. FOV Check (Matches original sight angles)
double angleToPlayer = position.angleTo(playerPosition);
double diff = angle - angleToPlayer;
@@ -77,40 +80,92 @@ abstract class Enemy extends Entity {
if (diff.abs() > math.pi / 2) return false;
// 2. Map Check
Coordinate2D dir = (playerPosition - position).normalized;
double stepSize = 0.2;
// 3. Map Check (Corrected Integer Bresenham)
int currentX = position.x.toInt();
int currentY = position.y.toInt();
int targetX = playerPosition.x.toInt();
int targetY = playerPosition.y.toInt();
for (double i = 0; i < distance; i += stepSize) {
Coordinate2D checkPos = position + (dir * i);
if (!isWalkable(checkPos.x.toInt(), checkPos.y.toInt())) return false;
int dx = (targetX - currentX).abs();
int dy = -(targetY - currentY).abs();
int sx = currentX < targetX ? 1 : -1;
int sy = currentY < targetY ? 1 : -1;
int err = dx + dy;
while (true) {
if (!isWalkable(currentX, currentY)) return false;
if (currentX == targetX && currentY == targetY) break;
int e2 = 2 * err;
if (e2 >= dy) {
err += dy;
currentX += sx;
}
if (e2 <= dx) {
err += dx;
currentY += sy;
}
}
return true;
}
// The weapon asks the enemy if it is unobstructed from the shooter
bool hasLineOfSightFrom(
Coordinate2D source,
double sourceAngle,
double distance,
Coordinate2D getValidMovement(
Coordinate2D intendedMovement,
bool Function(int x, int y) isWalkable,
void Function(int x, int y) tryOpenDoor,
) {
double dirX = math.cos(sourceAngle);
double dirY = math.sin(sourceAngle);
double newX = position.x + intendedMovement.x;
double newY = position.y + intendedMovement.y;
for (double i = 0.5; i < distance; i += 0.2) {
int checkX = (source.x + dirX * i).toInt();
int checkY = (source.y + dirY * i).toInt();
if (!isWalkable(checkX, checkY)) return false;
int currentTileX = position.x.toInt();
int currentTileY = position.y.toInt();
int targetTileX = newX.toInt();
int targetTileY = newY.toInt();
bool movedX = currentTileX != targetTileX;
bool movedY = currentTileY != targetTileY;
// 1. Check Diagonal Movement
if (movedX && movedY) {
bool canMoveX = isWalkable(targetTileX, currentTileY);
bool canMoveY = isWalkable(currentTileX, targetTileY);
bool canMoveDiag = isWalkable(targetTileX, targetTileY);
if (!canMoveX || !canMoveY || !canMoveDiag) {
// Trigger doors if they are blocking the path
if (!canMoveX) tryOpenDoor(targetTileX, currentTileY);
if (!canMoveY) tryOpenDoor(currentTileX, targetTileY);
if (!canMoveDiag) tryOpenDoor(targetTileX, targetTileY);
if (canMoveX) return Coordinate2D(intendedMovement.x, 0);
if (canMoveY) return Coordinate2D(0, intendedMovement.y);
return const Coordinate2D(0, 0);
}
}
return true;
// 2. Check Cardinal Movement
if (movedX && !movedY) {
if (!isWalkable(targetTileX, currentTileY)) {
tryOpenDoor(targetTileX, currentTileY); // Try to open!
return Coordinate2D(0, intendedMovement.y);
}
}
if (movedY && !movedX) {
if (!isWalkable(currentTileX, targetTileY)) {
tryOpenDoor(currentTileX, targetTileY); // Try to open!
return Coordinate2D(intendedMovement.x, 0);
}
}
return intendedMovement;
}
// Updated Signature
({Coordinate2D movement, double newAngle}) update({
required int elapsedMs,
required Coordinate2D playerPosition,
required bool Function(int x, int y) isWalkable,
required void Function(int x, int y) tryOpenDoor, // NEW
required void Function(int damage) onDamagePlayer,
});
}