Files
wolf_dart/lib/features/entities/enemies/enemy.dart
2026-03-14 21:06:31 +01:00

220 lines
6.3 KiB
Dart

import 'dart:math' as math;
import 'package:wolf_dart/classes/coordinate_2d.dart';
import 'package:wolf_dart/features/entities/entity.dart';
enum EnemyType {
guard(mapBaseId: 108, spriteBaseIdx: 50),
dog(mapBaseId: 216, spriteBaseIdx: 99),
ss(mapBaseId: 180, spriteBaseIdx: 138),
mutant(mapBaseId: 252, spriteBaseIdx: 187),
officer(mapBaseId: 144, spriteBaseIdx: 238),
;
final int mapBaseId;
final int spriteBaseIdx;
const EnemyType({
required this.mapBaseId,
required this.spriteBaseIdx,
});
/// Helper to check if a specific TED5 Map ID belongs to this enemy
bool claimsMapId(int id) => id >= mapBaseId && id <= mapBaseId + 35;
/// Helper to find which EnemyType a given Map ID belongs to
static EnemyType? fromMapId(int id) {
for (final type in EnemyType.values) {
if (type.claimsMapId(id)) return type;
}
return null;
}
bool claimsSpriteIndex(int index) {
return switch (this) {
// Walk, Action, & Death: 50-98
EnemyType.guard => index >= 50 && index <= 98,
// Walk, Action, & Death: 99-137
EnemyType.dog => index >= 99 && index <= 137,
// Walk, Action, & Death: 138-186
EnemyType.ss => index >= 138 && index <= 186,
// Walk, Action, & Death: 187-237
EnemyType.mutant => index >= 187 && index <= 237,
// Walk, Action, & Death: 238-287
EnemyType.officer => index >= 238 && index <= 287,
};
}
}
abstract class Enemy extends Entity {
Enemy({
required super.x,
required super.y,
required super.spriteIndex,
super.angle,
super.state,
super.mapId,
super.lastActionTime,
});
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;
lastActionTime = currentTime;
if (health <= 0) {
state = EntityState.dead;
isDying = true;
} else if (math.Random().nextDouble() < 0.5) {
state = EntityState.pain;
} else {
state = EntityState.patrolling;
}
}
void checkWakeUp({
required int elapsedMs,
required Coordinate2D playerPosition,
required bool Function(int x, int y) isWalkable,
int baseReactionMs = 200,
int reactionVarianceMs = 600,
}) {
if (state == EntityState.idle &&
hasLineOfSight(playerPosition, isWalkable)) {
if (reactionTimeMs == 0) {
reactionTimeMs =
elapsedMs +
baseReactionMs +
math.Random().nextInt(reactionVarianceMs);
} else if (elapsedMs >= reactionTimeMs) {
state = EntityState.patrolling;
lastActionTime = elapsedMs;
reactionTimeMs = 0;
}
}
}
// Matches WL_STATE.C's 'CheckLine' using canonical Integer DDA traversal
bool hasLineOfSight(
Coordinate2D playerPosition,
bool Function(int x, int y) isWalkable,
) {
// 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) < 1.2) {
return true;
}
// 2. FOV Check (Matches original sight angles)
double angleToPlayer = position.angleTo(playerPosition);
double diff = angle - angleToPlayer;
while (diff <= -math.pi) {
diff += 2 * math.pi;
}
while (diff > math.pi) {
diff -= 2 * math.pi;
}
if (diff.abs() > math.pi / 2) return false;
// 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();
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;
}
Coordinate2D getValidMovement(
Coordinate2D intendedMovement,
bool Function(int x, int y) isWalkable,
void Function(int x, int y) tryOpenDoor,
) {
double newX = position.x + intendedMovement.x;
double newY = position.y + intendedMovement.y;
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);
}
}
// 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,
required void Function(int damage) onDamagePlayer,
});
}