Adding more enemy types
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -12,9 +12,13 @@ class Decorative extends Entity {
|
||||
|
||||
// Checks if the Map ID belongs to a standard decoration
|
||||
static bool isDecoration(int objId) {
|
||||
if (objId == 124) return true; // Dead guard
|
||||
// ID 124 is a dead guard in WL1, but an SS guard in WL6.
|
||||
// However, for spawning purposes, if the SS trySpawn fails,
|
||||
// we only want to treat it as a decoration if it's not a live actor.
|
||||
if (objId == 124 || objId == 125) return true;
|
||||
|
||||
if (objId >= 23 && objId <= 70) {
|
||||
// Exclude the collectibles!
|
||||
// Exclude collectibles defined in MapObject
|
||||
if ((objId >= 43 && objId <= 44) || (objId >= 47 && objId <= 56)) {
|
||||
return false;
|
||||
}
|
||||
@@ -23,9 +27,11 @@ class Decorative extends Entity {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Safely calculates the VSWAP sprite index for standard decorations
|
||||
static int getSpriteIndex(int objId) {
|
||||
if (objId == 124) return 95; // Dead guard
|
||||
if (objId == 124) return 95; // Dead guard sprite index
|
||||
if (objId == 125) return 96; // Dead Aardwolf/Other body
|
||||
|
||||
// Standard decorations are typically offset by 21 in the VSWAP
|
||||
return objId - 21;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
156
lib/features/entities/enemies/mutant.dart
Normal file
156
lib/features/entities/enemies/mutant.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
156
lib/features/entities/enemies/officer.dart
Normal file
156
lib/features/entities/enemies/officer.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
151
lib/features/entities/enemies/ss.dart
Normal file
151
lib/features/entities/enemies/ss.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:wolf_dart/classes/coordinate_2d.dart';
|
||||
|
||||
enum EntityState { staticObj, idle, patrolling, shooting, pain, dead }
|
||||
enum EntityState { staticObj, idle, patrolling, attacking, pain, dead }
|
||||
|
||||
abstract class Entity<T> {
|
||||
double x;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import 'package:wolf_dart/features/difficulty/difficulty.dart';
|
||||
import 'package:wolf_dart/features/entities/collectible.dart';
|
||||
import 'package:wolf_dart/features/entities/decorative.dart';
|
||||
import 'package:wolf_dart/features/entities/enemies/brown_guard.dart';
|
||||
import 'package:wolf_dart/features/entities/enemies/dog.dart';
|
||||
import 'package:wolf_dart/features/entities/enemies/guard.dart';
|
||||
import 'package:wolf_dart/features/entities/enemies/mutant.dart';
|
||||
import 'package:wolf_dart/features/entities/enemies/officer.dart';
|
||||
import 'package:wolf_dart/features/entities/enemies/ss.dart';
|
||||
import 'package:wolf_dart/features/entities/entity.dart';
|
||||
import 'package:wolf_dart/features/entities/map_objects.dart';
|
||||
|
||||
@@ -16,8 +19,14 @@ typedef EntitySpawner =
|
||||
|
||||
abstract class EntityRegistry {
|
||||
static final List<EntitySpawner> _spawners = [
|
||||
BrownGuard.trySpawn,
|
||||
// Enemies need to try to spawn first
|
||||
Guard.trySpawn,
|
||||
Officer.trySpawn,
|
||||
SS.trySpawn,
|
||||
Mutant.trySpawn,
|
||||
Dog.trySpawn,
|
||||
|
||||
// Everything else
|
||||
Collectible.trySpawn,
|
||||
Decorative.trySpawn,
|
||||
];
|
||||
@@ -32,17 +41,22 @@ abstract class EntityRegistry {
|
||||
// 1. Difficulty check before even looking for a spawner
|
||||
if (!MapObject.shouldSpawn(objId, difficulty)) return null;
|
||||
|
||||
if (objId == 0) return null;
|
||||
|
||||
for (final spawner in _spawners) {
|
||||
Entity? entity = spawner(objId, x, y, difficulty);
|
||||
|
||||
if (entity != null) {
|
||||
// Safety bounds check for the VSWAP array
|
||||
if (entity.spriteIndex >= 0 && entity.spriteIndex < maxSprites) {
|
||||
print("Spawned entity with objId $objId");
|
||||
return entity;
|
||||
}
|
||||
print("VSWAP doesn't have this sprite! objId $objId");
|
||||
return null; // VSWAP doesn't have this sprite!
|
||||
}
|
||||
}
|
||||
print("No class claimed this Map ID > objId $objId");
|
||||
return null; // No class claimed this Map ID
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,13 +65,6 @@ abstract class MapObject {
|
||||
static const int secretExitTrigger = 99;
|
||||
static const int normalExitTrigger = 100;
|
||||
|
||||
// --- Enemy Base IDs (Easy, North) ---
|
||||
static const int _guardBase = 108;
|
||||
static const int _officerBase = 126; // WL6 only
|
||||
static const int _ssBase = 144; // WL6 only
|
||||
static const int _dogBase = 162;
|
||||
static const int _mutantBase = 180; // Episode 2+
|
||||
|
||||
// Bosses (Shared between WL1 and WL6)
|
||||
static const int bossHansGrosse = 214;
|
||||
|
||||
@@ -87,55 +80,38 @@ abstract class MapObject {
|
||||
static const int bossFettgesicht = 223;
|
||||
|
||||
// --- Enemy Range Constants ---
|
||||
static const int guardStart = 108;
|
||||
static const int officerStart = 126;
|
||||
static const int ssStart = 144;
|
||||
static const int dogStart = 162;
|
||||
static const int mutantStart = 180;
|
||||
static const int guardStart = 108; // 108-143
|
||||
static const int officerStart = 144; // 144-179 (WL6)
|
||||
static const int ssStart = 180; // 180-215 (WL6)
|
||||
static const int dogStart = 216; // 216-251
|
||||
static const int mutantStart = 252; // 252-287 (WL6)
|
||||
|
||||
// --- Missing Decorative Bodies ---
|
||||
static const int deadGuard = 124; // Decorative only in WL1
|
||||
static const int deadAardwolf = 125; // Decorative only in WL1
|
||||
|
||||
/// Returns true if the object ID exists in the Shareware version.
|
||||
/// Returns true if the object ID exists in the Shareware version.
|
||||
static bool isSharewareCompatible(int id) {
|
||||
// WL1 only had Guards (108-125), Dogs (162-179), and Hans Grosse (214)
|
||||
if (id >= 126 && id < 162) return false; // No Officers or SS
|
||||
if (id >= 180 && id < 214) return false; // No Mutants
|
||||
if (id > 214) return false; // No other bosses
|
||||
return true;
|
||||
// Standard Decorations & Collectibles
|
||||
if (id <= vines) return true;
|
||||
|
||||
// Logic Triggers (Exits/Pushwalls)
|
||||
if (id >= pushwallTrigger && id <= normalExitTrigger) return true;
|
||||
|
||||
// Guards (108-143 includes dead bodies at 124/125)
|
||||
if (id >= guardStart && id < officerStart) return true;
|
||||
|
||||
// Dogs (216-251) - These ARE in Shareware!
|
||||
if (id >= dogStart && id < mutantStart) return true;
|
||||
|
||||
// Episode 1 Boss
|
||||
if (id == bossHansGrosse) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Resolves which enemy type a map ID belongs to.
|
||||
static String getEnemyType(int id) {
|
||||
if (id >= 108 && id <= 125) return "Guard";
|
||||
if (id >= 126 && id <= 143) return "Officer";
|
||||
if (id >= 144 && id <= 161) return "SS";
|
||||
if (id >= 162 && id <= 179) return "Dog";
|
||||
if (id >= 180 && id <= 197) return "Mutant";
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
/// Checks if an object should be spawned based on chosen difficulty.
|
||||
static bool shouldSpawn(int id, Difficulty selectedDifficulty) {
|
||||
if (id < 108 || id > 213) return true; // Items/Players/Bosses always spawn
|
||||
|
||||
// Enemy blocks are 18 IDs wide (e.g., 108-125 for Guards)
|
||||
int relativeId = (id - 108) % 18;
|
||||
|
||||
// 0-3: Easy, 4-7: Medium, 8-11: Hard
|
||||
if (relativeId < 4) return true; // Easy spawns on everything
|
||||
if (relativeId < 8) {
|
||||
return selectedDifficulty != Difficulty.canIPlayDaddy; // Medium/Hard
|
||||
}
|
||||
if (relativeId < 12) {
|
||||
return selectedDifficulty == Difficulty.iAmDeathIncarnate; // Hard only
|
||||
}
|
||||
|
||||
// 12-15 are typically "Ambush" versions of the Easy/Medium/Hard guards
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Determines the spawn orientation of an enemy or player.
|
||||
/// Determines the spawn orientation of an enemy or player.
|
||||
static double getAngle(int id) {
|
||||
// Player spawn angles
|
||||
switch (id) {
|
||||
case playerNorth:
|
||||
return CardinalDirection.north.radians;
|
||||
@@ -147,9 +123,52 @@ abstract class MapObject {
|
||||
return CardinalDirection.west.radians;
|
||||
}
|
||||
|
||||
if (id < 108 || id > 213) return 0.0;
|
||||
// FIX: Expand the boundary to include ALL enemies (Dogs and Mutants)
|
||||
if (id < guardStart || id > (mutantStart + 35)) return 0.0;
|
||||
|
||||
// Enemy directions are mapped in groups of 4
|
||||
return CardinalDirection.fromEnemyIndex(id - 108).radians;
|
||||
int baseId;
|
||||
if (id >= mutantStart) {
|
||||
baseId = mutantStart;
|
||||
} else if (id >= dogStart) {
|
||||
baseId = dogStart;
|
||||
} else if (id >= ssStart) {
|
||||
baseId = ssStart;
|
||||
} else if (id >= officerStart) {
|
||||
baseId = officerStart;
|
||||
} else {
|
||||
baseId = guardStart;
|
||||
}
|
||||
|
||||
// FIX: Normalize patrolling enemies back to the standing block, THEN get the 4-way angle
|
||||
int directionIndex = ((id - baseId) % 18) % 4;
|
||||
return CardinalDirection.fromEnemyIndex(directionIndex).radians;
|
||||
}
|
||||
|
||||
static bool shouldSpawn(int id, Difficulty selectedDifficulty) {
|
||||
// FIX: Expand the boundary so Dogs and Mutants aren't bypassing difficulty checks
|
||||
if (id < guardStart || id > (mutantStart + 35)) return true;
|
||||
|
||||
int baseId;
|
||||
if (id >= mutantStart) {
|
||||
baseId = mutantStart;
|
||||
} else if (id >= dogStart) {
|
||||
baseId = dogStart;
|
||||
} else if (id >= ssStart) {
|
||||
baseId = ssStart;
|
||||
} else if (id >= officerStart) {
|
||||
baseId = officerStart;
|
||||
} else {
|
||||
baseId = guardStart;
|
||||
}
|
||||
|
||||
int relativeId = (id - baseId) % 18;
|
||||
|
||||
return switch (relativeId) {
|
||||
< 4 => true,
|
||||
< 8 => selectedDifficulty.level >= Difficulty.dontHurtMe.level,
|
||||
< 12 => selectedDifficulty.level >= Difficulty.bringEmOn.level,
|
||||
< 16 => selectedDifficulty.level >= Difficulty.iAmDeathIncarnate.level,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:wolf_dart/classes/matrix.dart';
|
||||
import 'package:wolf_dart/features/map/vswap_parser.dart';
|
||||
import 'package:wolf_dart/features/map/wolf_level.dart';
|
||||
import 'package:wolf_dart/features/map/wolf_map_parser.dart';
|
||||
import 'package:wolf_dart/vswap_parser.dart';
|
||||
|
||||
class WolfMap {
|
||||
/// The fully parsed and decompressed levels from the game files.
|
||||
|
||||
@@ -101,4 +101,43 @@ class VswapParser {
|
||||
}
|
||||
return sprites;
|
||||
}
|
||||
|
||||
/// Extracts digitized sound effects (PCM Audio) from VSWAP.WL1
|
||||
static List<Uint8List> parseSounds(ByteData vswap) {
|
||||
int chunks = vswap.getUint16(0, Endian.little);
|
||||
int soundStart = vswap.getUint16(4, Endian.little);
|
||||
|
||||
List<int> offsets = [];
|
||||
List<int> lengths = [];
|
||||
|
||||
// Offsets are 32-bit integers starting at byte 6
|
||||
for (int i = 0; i < chunks; i++) {
|
||||
offsets.add(vswap.getUint32(6 + (i * 4), Endian.little));
|
||||
}
|
||||
|
||||
// Lengths are 16-bit integers immediately following the offset array
|
||||
int lengthStart = 6 + (chunks * 4);
|
||||
for (int i = 0; i < chunks; i++) {
|
||||
lengths.add(vswap.getUint16(lengthStart + (i * 2), Endian.little));
|
||||
}
|
||||
|
||||
List<Uint8List> sounds = [];
|
||||
|
||||
// Sounds start after the sprites and go to the end of the chunks
|
||||
for (int i = soundStart; i < chunks; i++) {
|
||||
int offset = offsets[i];
|
||||
int length = lengths[i];
|
||||
|
||||
if (offset == 0 || length == 0) {
|
||||
sounds.add(Uint8List(0)); // Empty placeholder
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract the raw 8-bit PCM audio bytes
|
||||
final soundData = vswap.buffer.asUint8List(offset, length);
|
||||
sounds.add(soundData);
|
||||
}
|
||||
|
||||
return sounds;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user