Add shareware support and spawn correctly for difficulty levels

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-14 17:01:01 +01:00
parent 690ac1e7e6
commit 278c73a256
19 changed files with 383 additions and 238 deletions

View File

@@ -1,3 +1,4 @@
import 'package:wolf_dart/features/difficulty/difficulty.dart';
import 'package:wolf_dart/features/entities/entity.dart';
enum CollectibleType { ammo, health, treasure, weapon, key }
@@ -31,7 +32,7 @@ class Collectible extends Entity {
int objId,
double x,
double y,
int difficultyLevel,
Difficulty _,
) {
if (isCollectible(objId)) {
return Collectible(

View File

@@ -1,3 +1,4 @@
import 'package:wolf_dart/features/difficulty/difficulty.dart';
import 'package:wolf_dart/features/entities/entity.dart';
class Decorative extends Entity {
@@ -32,7 +33,7 @@ class Decorative extends Entity {
int objId,
double x,
double y,
int difficultyLevel,
Difficulty _,
) {
if (isDecoration(objId)) {
return Decorative(

View File

@@ -1,12 +1,13 @@
import 'dart:math' as math;
import 'package:wolf_dart/classes/sprite.dart';
import 'package:wolf_dart/features/map/door.dart';
class DoorManager {
// Key is '$x,$y'
final Map<String, Door> doors = {};
void initDoors(List<List<int>> wallGrid) {
void initDoors(Sprite wallGrid) {
doors.clear();
for (int y = 0; y < wallGrid.length; y++) {
for (int x = 0; x < wallGrid[y].length; x++) {

View File

@@ -1,8 +1,10 @@
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 BrownGuard extends Enemy {
static const double speed = 0.03;
@@ -23,33 +25,18 @@ class BrownGuard extends Enemy {
int objId,
double x,
double y,
int difficultyLevel,
Difficulty difficulty,
) {
bool canSpawn = false;
switch (difficultyLevel) {
case 0:
canSpawn = objId >= 108 && objId <= 115;
break;
case 1:
canSpawn = objId >= 144 && objId <= 151;
break;
case 2:
canSpawn = objId >= 180 && objId <= 187;
break;
case 3:
canSpawn = objId >= 216 && objId <= 223;
break;
}
if (canSpawn) {
// Use the range constants we defined in MapObject!
if (objId >= MapObject.guardStart && objId <= MapObject.guardStart + 17) {
return BrownGuard(
x: x,
y: y,
angle: Enemy.getInitialAngle(objId),
angle: MapObject.getAngle(objId),
mapId: objId,
);
}
return null; // Not a Brown Guard!
return null;
}
@override
@@ -63,20 +50,11 @@ class BrownGuard extends Enemy {
Coordinate2D movement = const Coordinate2D(0, 0);
double newAngle = angle;
// 1. Wake up logic (Matches SightPlayer & FirstSighting)
if (state == EntityState.idle &&
hasLineOfSight(playerPosition, isWalkable)) {
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
}
}
checkWakeUp(
elapsedMs: elapsedMs,
playerPosition: playerPosition,
isWalkable: isWalkable,
);
double distance = position.distanceTo(playerPosition);
double angleToPlayer = position.angleTo(playerPosition);
@@ -85,14 +63,11 @@ class BrownGuard extends Enemy {
newAngle = angleToPlayer;
}
// Octant logic remains the same
double diff = newAngle - angleToPlayer;
while (diff <= -math.pi) {
diff += 2 * math.pi;
}
while (diff > math.pi) {
diff -= 2 * math.pi;
}
// Octant logic (Directional sprites)
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;
@@ -105,13 +80,10 @@ class BrownGuard extends Enemy {
case EntityState.patrolling:
if (distance > 0.8) {
// 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,
@@ -119,12 +91,9 @@ class BrownGuard extends Enemy {
);
}
// Animation fix: Update the sprite so he actually turns and walks!
int walkFrame = (elapsedMs ~/ 150) % 4;
spriteIndex = 58 + (walkFrame * 8) + octant;
// 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;
@@ -132,20 +101,20 @@ class BrownGuard extends Enemy {
_hasFiredThisCycle = false;
}
}
break; // Fallthrough fix: Don't forget the break!
break;
case EntityState.shooting:
int timeShooting = elapsedMs - lastActionTime;
if (timeShooting < 150) {
spriteIndex = 96;
spriteIndex = 96; // Aiming
} else if (timeShooting < 300) {
spriteIndex = 97;
spriteIndex = 97; // Firing
if (!_hasFiredThisCycle) {
onDamagePlayer(10); // DAMAGING PLAYER
onDamagePlayer(10);
_hasFiredThisCycle = true;
}
} else if (timeShooting < 450) {
spriteIndex = 98;
spriteIndex = 98; // Recoil
} else {
state = EntityState.patrolling;
lastActionTime = elapsedMs;
@@ -153,7 +122,8 @@ class BrownGuard extends Enemy {
break;
case EntityState.pain:
spriteIndex = 94;
spriteIndex = 94; // Ouch frame
// Stay in pain for a brief moment, then resume attacking
if (elapsedMs - lastActionTime > 250) {
state = EntityState.patrolling;
lastActionTime = elapsedMs;
@@ -164,9 +134,10 @@ class BrownGuard extends Enemy {
if (isDying) {
int deathFrame = (elapsedMs - lastActionTime) ~/ 150;
if (deathFrame < 4) {
spriteIndex = 90 + deathFrame - 1;
// FIX: Removed the buggy "- 1"
spriteIndex = 90 + deathFrame;
} else {
spriteIndex = 95;
spriteIndex = 95; // Final dead frame
isDying = false;
}
} else {

View File

@@ -1,11 +1,13 @@
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'; // NEW
class Dog extends Enemy {
static const double speed = 0.05; // Dogs are much faster than guards!
static const double speed = 0.05;
bool _hasBittenThisCycle = false;
Dog({
@@ -14,32 +16,17 @@ class Dog extends Enemy {
required super.angle,
required super.mapId,
}) : super(
spriteIndex: 99, // Dogs start at index 99 in VSWAP
spriteIndex: 99,
state: EntityState.idle,
);
static Dog? trySpawn(int objId, double x, double y, int difficultyLevel) {
bool canSpawn = false;
switch (difficultyLevel) {
case 0:
canSpawn = objId >= 116 && objId <= 119;
break;
case 1:
canSpawn = objId >= 152 && objId <= 155;
break;
case 2:
canSpawn = objId >= 188 && objId <= 191;
break;
case 3:
canSpawn = objId >= 224 && objId <= 227;
break;
}
if (canSpawn) {
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) {
return Dog(
x: x,
y: y,
angle: Enemy.getInitialAngle(objId),
angle: MapObject.getAngle(objId),
mapId: objId,
);
}
@@ -51,23 +38,19 @@ 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 x, int y) tryOpenDoor,
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)) {
if (reactionTimeMs == 0) {
reactionTimeMs = elapsedMs + 100 + math.Random().nextInt(200);
} else if (elapsedMs >= reactionTimeMs) {
state = EntityState.patrolling;
lastActionTime = elapsedMs;
reactionTimeMs = 0;
}
}
checkWakeUp(
elapsedMs: elapsedMs,
playerPosition: playerPosition,
isWalkable: isWalkable,
baseReactionMs: 100,
reactionVarianceMs: 200,
);
double distance = position.distanceTo(playerPosition);
double angleToPlayer = position.angleTo(playerPosition);
@@ -76,13 +59,10 @@ class Dog extends Enemy {
newAngle = angleToPlayer;
}
double diff = newAngle - angleToPlayer;
while (diff <= -math.pi) {
diff += 2 * math.pi;
}
while (diff > math.pi) {
diff -= 2 * math.pi;
}
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;
@@ -95,15 +75,12 @@ class Dog extends Enemy {
case EntityState.patrolling:
if (distance > 0.8) {
double deltaX = playerPosition.x - position.x;
double deltaY = playerPosition.y - position.y;
double moveX = deltaX > 0 ? speed : (deltaX < 0 ? -speed : 0);
double moveY = deltaY > 0 ? speed : (deltaY < 0 ? -speed : 0);
// UPGRADED: Smooth vector movement instead of grid-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,
@@ -135,6 +112,22 @@ class Dog extends Enemy {
}
break;
// Make sure dogs have a death state so they don't stay standing!
case EntityState.dead:
if (isDying) {
int deathFrame = (elapsedMs - lastActionTime) ~/ 100;
if (deathFrame < 4) {
spriteIndex =
140 + deathFrame; // Dog death frames usually start here
} else {
spriteIndex = 143; // Dead dog on floor
isDying = false;
}
} else {
spriteIndex = 143;
}
break;
default:
break;
}

View File

@@ -55,6 +55,28 @@ abstract class Enemy extends Entity {
}
}
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,
@@ -63,7 +85,7 @@ abstract class Enemy extends Entity {
// 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) {
if (position.distanceTo(playerPosition) < 1.2) {
return true;
}

View File

@@ -1,15 +1,17 @@
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/entity.dart';
import 'package:wolf_dart/features/entities/map_objects.dart';
typedef EntitySpawner =
Entity? Function(
int objId,
double x,
double y,
int difficultyLevel,
Difficulty difficulty,
);
abstract class EntityRegistry {
@@ -24,11 +26,14 @@ abstract class EntityRegistry {
int objId,
double x,
double y,
int difficultyLevel,
Difficulty difficulty,
int maxSprites,
) {
// 1. Difficulty check before even looking for a spawner
if (!MapObject.shouldSpawn(objId, difficulty)) return null;
for (final spawner in _spawners) {
Entity? entity = spawner(objId, x, y, difficultyLevel);
Entity? entity = spawner(objId, x, y, difficulty);
if (entity != null) {
// Safety bounds check for the VSWAP array

View File

@@ -1,4 +1,7 @@
abstract class MapObjectId {
import 'package:wolf_dart/classes/cardinal_direction.dart';
import 'package:wolf_dart/features/difficulty/difficulty.dart';
abstract class MapObject {
// --- Player Spawns ---
static const int playerNorth = 19;
static const int playerEast = 20;
@@ -12,7 +15,7 @@ abstract class MapObjectId {
static const int floorLamp = 26;
static const int chandelier = 27;
static const int hangingSkeleton = 28;
static const int dogFood = 29; // Also used as decoration
static const int dogFoodDecoration = 29;
static const int whiteColumn = 30;
static const int pottedPlant = 31;
static const int blueSkeleton = 32;
@@ -25,12 +28,12 @@ abstract class MapObjectId {
static const int emptyCage = 39;
static const int cageWithSkeleton = 40;
static const int bones = 41;
static const int goldenKeybowl = 42; // Decorative only
static const int goldenKeyBowl = 42;
// --- Collectibles & Items ---
// --- Collectibles ---
static const int goldKey = 43;
static const int silverKey = 44;
static const int bed = 45; // Bed is usually non-collectible but in this range
static const int bed = 45;
static const int basket = 46;
static const int food = 47;
static const int medkit = 48;
@@ -43,62 +46,110 @@ abstract class MapObjectId {
static const int crown = 55;
static const int extraLife = 56;
// --- More Decorations ---
static const int bloodPool = 57;
// --- Environmental ---
static const int bloodPoolSmall = 57;
static const int barrel = 58;
static const int well = 59;
static const int emptyWell = 60;
static const int wellFull = 59;
static const int wellEmpty = 60;
static const int bloodPoolLarge = 61;
static const int flag = 62;
static const int callanard = 63; // Aardwolf sign / hidden message
static const int aardwolfSign = 63;
static const int bonesAndSkull = 64;
static const int wallHanging = 65;
static const int stove = 66;
static const int spearRack = 67;
static const int vines = 68;
// --- Special Objects ---
static const int secretDoor = 98; // Often used for the Pushwall trigger
static const int elevatorToSecretLevel = 99;
static const int exitTrigger = 100;
// --- Logic & Triggers ---
static const int pushwallTrigger = 98;
static const int secretExitTrigger = 99;
static const int normalExitTrigger = 100;
// --- Enemies (Spawn Points) ---
// Guards
static const int guardEasyNorth = 108;
static const int guardEasyEast = 109;
static const int guardEasySouth = 110;
static const int guardEasyWest = 111;
// --- 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+
// SS Guards
static const int ssEasyNorth = 124;
static const int ssEasyEast = 125;
static const int ssEasySouth = 126;
static const int ssEasyWest = 127;
// Dogs
static const int dogEasyNorth = 140;
static const int dogEasyEast = 141;
static const int dogEasySouth = 142;
static const int dogEasyWest = 143;
// Mutants
static const int mutantEasyNorth = 156;
static const int mutantEasyEast = 157;
static const int mutantEasySouth = 158;
static const int mutantEasyWest = 159;
// Officers
static const int officerEasyNorth = 172;
static const int officerEasyEast = 173;
static const int officerEasySouth = 174;
static const int officerEasyWest = 175;
// Bosses (Single spawn points)
// Bosses (Shared between WL1 and WL6)
static const int bossHansGrosse = 214;
// WL6 Exclusive Bosses
static const int bossDrSchabbs = 215;
static const int bossFakeHitler = 216;
static const int bossMechaHitler = 217;
static const int bossOttoGiftmacher = 218;
static const int bossGretelGrosse = 219;
static const int bossGeneralFettgesicht = 220;
static const int bossTransGrosse = 216;
static const int bossUbermutant = 217;
static const int bossDeathKnight = 218;
static const int bossMechaHitler = 219;
static const int bossHitlerGhost = 220;
static const int bossGretelGrosse = 221;
static const int bossGiftmacher = 222;
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;
/// 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;
}
/// 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;
case playerEast:
return CardinalDirection.east.radians;
case playerSouth:
return CardinalDirection.south.radians;
case playerWest:
return CardinalDirection.west.radians;
}
if (id < 108 || id > 213) return 0.0;
// Enemy directions are mapped in groups of 4
return CardinalDirection.fromEnemyIndex(id - 108).radians;
}
}

View File

@@ -25,7 +25,7 @@ class PushwallManager {
for (int y = 0; y < objectGrid.length; y++) {
for (int x = 0; x < objectGrid[y].length; x++) {
if (objectGrid[y][x] == MapObjectId.secretDoor) {
if (objectGrid[y][x] == MapObject.pushwallTrigger) {
pushwalls['$x,$y'] = Pushwall(x, y, wallGrid[y][x]);
}
}