Adding more enemy types

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-14 19:19:45 +01:00
parent 278c73a256
commit 9915aec674
12 changed files with 638 additions and 104 deletions

View File

@@ -20,15 +20,17 @@ class Dog extends Enemy {
state: EntityState.idle,
);
static Dog? trySpawn(int objId, double x, double y, Difficulty difficulty) {
// The renderer already checked difficulty, so we just check the ID block!
if (objId >= MapObject.dogStart && objId <= MapObject.dogStart + 17) {
static Dog? trySpawn(int objId, double x, double y, Difficulty _) {
// Dogs span 216 to 251.
if (objId >= MapObject.dogStart && objId <= MapObject.dogStart + 35) {
bool isPatrolling = objId >= MapObject.dogStart + 18;
return Dog(
x: x,
y: y,
angle: MapObject.getAngle(objId),
mapId: objId,
);
)..state = isPatrolling ? EntityState.patrolling : EntityState.idle;
}
return null;
}
@@ -61,8 +63,12 @@ class Dog extends Enemy {
double diff = angleToPlayer - newAngle;
while (diff <= -math.pi) diff += 2 * math.pi;
while (diff > math.pi) diff -= 2 * math.pi;
while (diff <= -math.pi) {
diff += 2 * math.pi;
}
while (diff > math.pi) {
diff -= 2 * math.pi;
}
int octant = ((diff + (math.pi / 8)) / (math.pi / 4)).floor() % 8;
if (octant < 0) octant += 8;
@@ -92,13 +98,13 @@ class Dog extends Enemy {
spriteIndex = 107 + (walkFrame * 8) + octant;
if (distance < 1.0 && elapsedMs - lastActionTime > 1000) {
state = EntityState.shooting;
state = EntityState.attacking;
lastActionTime = elapsedMs;
_hasBittenThisCycle = false;
}
break;
case EntityState.shooting:
case EntityState.attacking:
int timeAttacking = elapsedMs - lastActionTime;
if (timeAttacking < 200) {
spriteIndex = 139;

View File

@@ -38,23 +38,6 @@ abstract class Enemy extends Entity {
}
}
static double getInitialAngle(int objId) {
int normalizedId = (objId - 108) % 36;
int direction = normalizedId % 4;
switch (direction) {
case 0:
return 0.0;
case 1:
return 3 * math.pi / 2;
case 2:
return math.pi;
case 3:
return math.pi / 2;
default:
return 0.0;
}
}
void checkWakeUp({
required int elapsedMs,
required Coordinate2D playerPosition,
@@ -187,7 +170,7 @@ abstract class Enemy extends Entity {
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 x, int y) tryOpenDoor,
required void Function(int damage) onDamagePlayer,
});
}

View File

@@ -6,35 +6,35 @@ import 'package:wolf_dart/features/entities/enemies/enemy.dart';
import 'package:wolf_dart/features/entities/entity.dart';
import 'package:wolf_dart/features/entities/map_objects.dart';
class BrownGuard extends Enemy {
class Guard extends Enemy {
static const double speed = 0.03;
bool _hasFiredThisCycle = false;
BrownGuard({
Guard({
required super.x,
required super.y,
required super.angle,
required super.mapId,
}) : super(
// Default front-facing idle
spriteIndex: 50,
state: EntityState.idle,
);
static BrownGuard? trySpawn(
int objId,
double x,
double y,
Difficulty difficulty,
) {
// Use the range constants we defined in MapObject!
if (objId >= MapObject.guardStart && objId <= MapObject.guardStart + 17) {
return BrownGuard(
static Guard? trySpawn(int objId, double x, double y, Difficulty difficulty) {
// Guards span 108 to 143. (124 and 125 are decorative dead bodies).
if (objId >= MapObject.guardStart &&
objId <= MapObject.guardStart + 35 &&
objId != 124 &&
objId != 125) {
// If the ID is in the second half of the block, it's a Patrolling guard
bool isPatrolling = objId >= MapObject.guardStart + 18;
return Guard(
x: x,
y: y,
angle: MapObject.getAngle(objId),
mapId: objId,
);
)..state = isPatrolling ? EntityState.patrolling : EntityState.idle;
}
return null;
}
@@ -66,8 +66,12 @@ class BrownGuard extends Enemy {
// Octant logic (Directional sprites)
double diff = angleToPlayer - newAngle;
while (diff <= -math.pi) diff += 2 * math.pi;
while (diff > math.pi) diff -= 2 * math.pi;
while (diff <= -math.pi) {
diff += 2 * math.pi;
}
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,14 +100,14 @@ class BrownGuard extends Enemy {
if (distance < 6.0 && elapsedMs - lastActionTime > 1500) {
if (hasLineOfSight(playerPosition, isWalkable)) {
state = EntityState.shooting;
state = EntityState.attacking;
lastActionTime = elapsedMs;
_hasFiredThisCycle = false;
}
}
break;
case EntityState.shooting:
case EntityState.attacking:
int timeShooting = elapsedMs - lastActionTime;
if (timeShooting < 150) {
spriteIndex = 96; // Aiming

View File

@@ -0,0 +1,156 @@
import 'dart:math' as math;
import 'package:wolf_dart/classes/coordinate_2d.dart';
import 'package:wolf_dart/features/difficulty/difficulty.dart';
import 'package:wolf_dart/features/entities/enemies/enemy.dart';
import 'package:wolf_dart/features/entities/entity.dart';
import 'package:wolf_dart/features/entities/map_objects.dart';
class Mutant extends Enemy {
static const double speed = 0.045;
static const int _baseSprite = 187;
bool _hasFiredThisCycle = false;
Mutant({
required super.x,
required super.y,
required super.angle,
required super.mapId,
}) : super(
spriteIndex: _baseSprite,
state: EntityState.idle,
) {
health = 45;
damage = 10;
}
static Mutant? trySpawn(
int objId,
double x,
double y,
Difficulty difficulty,
) {
// Mutants span 252 to 287
if (objId >= MapObject.mutantStart && objId <= MapObject.mutantStart + 35) {
bool isPatrolling = objId >= MapObject.mutantStart + 18;
return Mutant(
x: x,
y: y,
angle: MapObject.getAngle(objId),
mapId: objId,
)..state = isPatrolling ? EntityState.patrolling : EntityState.idle;
}
return null;
}
@override
({Coordinate2D movement, double newAngle}) update({
required int elapsedMs,
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;
// Mutants don't make wake-up noises in the original game!
checkWakeUp(
elapsedMs: elapsedMs,
playerPosition: playerPosition,
isWalkable: isWalkable,
);
double distance = position.distanceTo(playerPosition);
double angleToPlayer = position.angleTo(playerPosition);
if (state != EntityState.idle && state != EntityState.dead) {
newAngle = angleToPlayer;
}
double diff = angleToPlayer - newAngle;
while (diff <= -math.pi) {
diff += 2 * math.pi;
}
while (diff > math.pi) {
diff -= 2 * math.pi;
}
int octant = ((diff + (math.pi / 8)) / (math.pi / 4)).floor() % 8;
if (octant < 0) octant += 8;
switch (state) {
case EntityState.idle:
spriteIndex = _baseSprite + octant;
break;
case EntityState.patrolling:
if (distance > 0.8) {
double moveX = math.cos(angleToPlayer) * speed;
double moveY = math.sin(angleToPlayer) * speed;
movement = getValidMovement(
Coordinate2D(moveX, moveY),
isWalkable,
tryOpenDoor,
);
}
int walkFrame = (elapsedMs ~/ 150) % 4;
spriteIndex = (_baseSprite + 8) + (walkFrame * 8) + octant;
if (distance < 6.0 && elapsedMs - lastActionTime > 1000) {
if (hasLineOfSight(playerPosition, isWalkable)) {
state = EntityState.attacking;
lastActionTime = elapsedMs;
_hasFiredThisCycle = false;
}
}
break;
case EntityState.attacking:
int timeShooting = elapsedMs - lastActionTime;
if (timeShooting < 150) {
spriteIndex = _baseSprite + 46; // Aiming
} else if (timeShooting < 300) {
spriteIndex = _baseSprite + 47; // Firing
if (!_hasFiredThisCycle) {
onDamagePlayer(damage);
_hasFiredThisCycle = true;
}
} else if (timeShooting < 450) {
spriteIndex = _baseSprite + 48; // Recoil
} else {
state = EntityState.patrolling;
lastActionTime = elapsedMs;
}
break;
case EntityState.pain:
spriteIndex = _baseSprite + 44;
if (elapsedMs - lastActionTime > 250) {
state = EntityState.patrolling;
lastActionTime = elapsedMs;
}
break;
case EntityState.dead:
if (isDying) {
int deathFrame = (elapsedMs - lastActionTime) ~/ 150;
if (deathFrame < 4) {
spriteIndex = (_baseSprite + 40) + deathFrame;
} else {
spriteIndex = _baseSprite + 45;
isDying = false;
}
} else {
spriteIndex = _baseSprite + 45;
}
break;
default:
break;
}
return (movement: movement, newAngle: newAngle);
}
}

View File

@@ -0,0 +1,156 @@
import 'dart:math' as math;
import 'package:wolf_dart/classes/coordinate_2d.dart';
import 'package:wolf_dart/features/difficulty/difficulty.dart';
import 'package:wolf_dart/features/entities/enemies/enemy.dart';
import 'package:wolf_dart/features/entities/entity.dart';
import 'package:wolf_dart/features/entities/map_objects.dart';
class Officer extends Enemy {
static const double speed = 0.055;
static const int _baseSprite = 50;
bool _hasFiredThisCycle = false;
Officer({
required super.x,
required super.y,
required super.angle,
required super.mapId,
}) : super(
spriteIndex: _baseSprite,
state: EntityState.idle,
) {
health = 50;
damage = 15;
}
static Officer? trySpawn(
int objId,
double x,
double y,
Difficulty difficulty,
) {
if (objId >= MapObject.officerStart &&
objId <= MapObject.officerStart + 35) {
bool isPatrolling = objId >= MapObject.officerStart + 18;
return Officer(
x: x,
y: y,
angle: MapObject.getAngle(objId),
mapId: objId,
)..state = isPatrolling ? EntityState.patrolling : EntityState.idle;
}
return null;
}
@override
({Coordinate2D movement, double newAngle}) update({
required int elapsedMs,
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;
checkWakeUp(
elapsedMs: elapsedMs,
playerPosition: playerPosition,
isWalkable: isWalkable,
);
double distance = position.distanceTo(playerPosition);
double angleToPlayer = position.angleTo(playerPosition);
if (state != EntityState.idle && state != EntityState.dead) {
newAngle = angleToPlayer;
}
double diff = angleToPlayer - newAngle;
while (diff <= -math.pi) {
diff += 2 * math.pi;
}
while (diff > math.pi) {
diff -= 2 * math.pi;
}
int octant = ((diff + (math.pi / 8)) / (math.pi / 4)).floor() % 8;
if (octant < 0) octant += 8;
switch (state) {
case EntityState.idle:
spriteIndex = _baseSprite + octant;
break;
case EntityState.patrolling:
if (distance > 0.8) {
double moveX = math.cos(angleToPlayer) * speed;
double moveY = math.sin(angleToPlayer) * speed;
movement = getValidMovement(
Coordinate2D(moveX, moveY),
isWalkable,
tryOpenDoor,
);
}
int walkFrame = (elapsedMs ~/ 150) % 4;
spriteIndex = (_baseSprite + 8) + (walkFrame * 8) + octant;
if (distance < 6.0 && elapsedMs - lastActionTime > 1000) {
if (hasLineOfSight(playerPosition, isWalkable)) {
state = EntityState.attacking;
lastActionTime = elapsedMs;
_hasFiredThisCycle = false;
}
}
break;
case EntityState.attacking:
int timeShooting = elapsedMs - lastActionTime;
if (timeShooting < 150) {
spriteIndex = _baseSprite + 46; // Aiming
} else if (timeShooting < 300) {
spriteIndex = _baseSprite + 47; // Firing
if (!_hasFiredThisCycle) {
onDamagePlayer(damage);
_hasFiredThisCycle = true;
}
} else if (timeShooting < 450) {
spriteIndex = _baseSprite + 48; // Recoil
} else {
state = EntityState.patrolling;
lastActionTime = elapsedMs;
}
break;
case EntityState.pain:
spriteIndex = _baseSprite + 44;
if (elapsedMs - lastActionTime > 250) {
state = EntityState.patrolling;
lastActionTime = elapsedMs;
}
break;
case EntityState.dead:
if (isDying) {
int deathFrame = (elapsedMs - lastActionTime) ~/ 150;
if (deathFrame < 4) {
spriteIndex = (_baseSprite + 40) + deathFrame;
} else {
spriteIndex = _baseSprite + 45;
isDying = false;
}
} else {
spriteIndex = _baseSprite + 45;
}
break;
default:
break;
}
return (movement: movement, newAngle: newAngle);
}
}

View File

@@ -0,0 +1,151 @@
import 'dart:math' as math;
import 'package:wolf_dart/classes/coordinate_2d.dart';
import 'package:wolf_dart/features/difficulty/difficulty.dart';
import 'package:wolf_dart/features/entities/enemies/enemy.dart';
import 'package:wolf_dart/features/entities/entity.dart';
import 'package:wolf_dart/features/entities/map_objects.dart';
class SS extends Enemy {
static const double speed = 0.04;
static const int _baseSprite = 138;
bool _hasFiredThisCycle = false;
SS({
required super.x,
required super.y,
required super.angle,
required super.mapId,
}) : super(
spriteIndex: _baseSprite,
state: EntityState.idle,
) {
health = 100;
damage = 20;
}
static SS? trySpawn(int objId, double x, double y, Difficulty difficulty) {
if (objId >= MapObject.ssStart && objId <= MapObject.ssStart + 35) {
bool isPatrolling = objId >= MapObject.ssStart + 18;
return SS(
x: x,
y: y,
angle: MapObject.getAngle(objId),
mapId: objId,
)..state = isPatrolling ? EntityState.patrolling : EntityState.idle;
}
return null;
}
@override
({Coordinate2D movement, double newAngle}) update({
required int elapsedMs,
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;
checkWakeUp(
elapsedMs: elapsedMs,
playerPosition: playerPosition,
isWalkable: isWalkable,
);
double distance = position.distanceTo(playerPosition);
double angleToPlayer = position.angleTo(playerPosition);
if (state != EntityState.idle && state != EntityState.dead) {
newAngle = angleToPlayer;
}
double diff = angleToPlayer - newAngle;
while (diff <= -math.pi) {
diff += 2 * math.pi;
}
while (diff > math.pi) {
diff -= 2 * math.pi;
}
int octant = ((diff + (math.pi / 8)) / (math.pi / 4)).floor() % 8;
if (octant < 0) octant += 8;
switch (state) {
case EntityState.idle:
spriteIndex = _baseSprite + octant;
break;
case EntityState.patrolling:
if (distance > 0.8) {
double moveX = math.cos(angleToPlayer) * speed;
double moveY = math.sin(angleToPlayer) * speed;
movement = getValidMovement(
Coordinate2D(moveX, moveY),
isWalkable,
tryOpenDoor,
);
}
int walkFrame = (elapsedMs ~/ 150) % 4;
spriteIndex = (_baseSprite + 8) + (walkFrame * 8) + octant;
if (distance < 6.0 && elapsedMs - lastActionTime > 1500) {
if (hasLineOfSight(playerPosition, isWalkable)) {
state = EntityState.attacking;
lastActionTime = elapsedMs;
_hasFiredThisCycle = false;
}
}
break;
case EntityState.attacking:
// SS machine gun fires much faster than a standard pistol!
int timeShooting = elapsedMs - lastActionTime;
if (timeShooting < 100) {
spriteIndex = _baseSprite + 46; // Aiming
} else if (timeShooting < 200) {
spriteIndex = _baseSprite + 47; // Firing
if (!_hasFiredThisCycle) {
onDamagePlayer(damage);
_hasFiredThisCycle = true;
}
} else if (timeShooting < 300) {
spriteIndex = _baseSprite + 48; // Recoil
} else {
state = EntityState.patrolling;
lastActionTime = elapsedMs;
}
break;
case EntityState.pain:
spriteIndex = _baseSprite + 44;
if (elapsedMs - lastActionTime > 250) {
state = EntityState.patrolling;
lastActionTime = elapsedMs;
}
break;
case EntityState.dead:
if (isDying) {
int deathFrame = (elapsedMs - lastActionTime) ~/ 150;
if (deathFrame < 4) {
spriteIndex = (_baseSprite + 40) + deathFrame;
} else {
spriteIndex = _baseSprite + 45;
isDying = false;
}
} else {
spriteIndex = _baseSprite + 45;
}
break;
default:
break;
}
return (movement: movement, newAngle: newAngle);
}
}