Slowly fixing enemies

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-15 19:36:37 +01:00
parent 0eebf8e4fa
commit 301218a01b
5 changed files with 35 additions and 72 deletions

View File

@@ -1,32 +1,25 @@
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
class EnemyMapData {
final int baseStaticId;
final int basePatrolId;
final int baseId;
const EnemyMapData({required this.baseStaticId, required this.basePatrolId});
const EnemyMapData(this.baseId);
/// True if this ID belongs to this enemy type at all
bool claimsId(int id) {
bool isStatic = id >= baseStaticId && id <= baseStaticId + 2;
// Patrol blocks contain 12 directional IDs and up to 6 ambush/special IDs
bool isPatrolOrAmbush = id >= basePatrolId && id < basePatrolId + 18;
return isStatic || isPatrolOrAmbush;
}
/// True if the ID falls anywhere within this enemy's 36-ID block
bool claimsId(int id) => id >= baseId && id < baseId + 36;
/// Exact check: Is this a static enemy on this specific difficulty?
bool isStaticForDifficulty(int id, Difficulty difficulty) {
return id == baseStaticId + difficulty.level;
int start = baseId + (difficulty.level * 4);
return id >= start && id < start + 4;
}
/// Exact check: Is this a patrolling enemy on this specific difficulty?
bool isPatrolForDifficulty(int id, Difficulty difficulty) {
int startId = basePatrolId + (difficulty.level * 4);
return id >= startId && id < startId + 4; // Spans 4 directions
int start = baseId + 12 + (difficulty.level * 4);
return id >= start && id < start + 4;
}
/// Exact check: Is this an ambush enemy on this specific difficulty?
bool isAmbushForDifficulty(int id, Difficulty difficulty) {
return id == basePatrolId + 12 + difficulty.level;
int start = baseId + 24 + (difficulty.level * 4);
return id >= start && id < start + 4;
}
}

View File

@@ -95,7 +95,6 @@ abstract class MapObject {
static const int deadAardwolf = 125; // Decorative only in WL1
static double getAngle(int id) {
// Player spawn
switch (id) {
case playerNorth:
return CardinalDirection.north.radians;
@@ -107,47 +106,29 @@ abstract class MapObject {
return CardinalDirection.west.radians;
}
// Boss check
if (id == bossHansGrosse) return 0.0;
final EnemyType? type = EnemyType.fromMapId(id);
if (type == null) return 0.0;
// Because the map data strictly groups patrol directions in blocks of 4
// (East=0, North=1, West=2, South=3), a simple modulo 4 against the base ID
// gives us the exact cardinal direction!
int directionIndex = (id - type.mapData.basePatrolId) % 4;
return CardinalDirection.fromEnemyIndex(directionIndex).radians;
// Because all enemies are in blocks of 4, modulo 4 gets the exact angle
return CardinalDirection.fromEnemyIndex(id % 4).radians;
}
/// Only handles the "Is this ID allowed on this difficulty?" math.
/// Does NOT decide IF an object is an enemy or decoration.
/// Determines if an object should be spawned on the current difficulty.
static bool isDifficultyAllowed(int objId, Difficulty difficulty) {
if (objId == 124) return true;
// 1. Dead bodies always spawn
if (objId == deadGuard || objId == deadAardwolf) return true;
int? requiredTier;
// Static Tier Math (IDs 23-54)
if (objId >= 23 && objId <= 54) {
requiredTier = (objId - 23) % 3;
}
// Patrol Tier Math (IDs 108-197)
else if (objId >= 108 && objId <= 197) {
int offsetInType = (objId - 108) % 18;
requiredTier = offsetInType ~/ 4;
}
if (requiredTier == null) {
// Default to allowed if no tier logic exists
// 2. If it's an enemy, we return true to let it pass through to the
// Enemy.spawn factory. The factory will safely return null if the
// enemy does not belong on this difficulty.
if (EnemyType.fromMapId(objId) != null) {
return true;
}
int currentTier = switch (difficulty) {
Difficulty.canIPlayDaddy || Difficulty.dontHurtMe => 0,
Difficulty.bringEmOn => 1,
Difficulty.iAmDeathIncarnate => 2,
};
return requiredTier == currentTier;
// 3. All non-enemy map objects (keys, ammo, puddles, plants)
// are NOT difficulty-tiered. They always spawn.
return true;
}
}

View File

@@ -186,7 +186,6 @@ abstract class Enemy extends Entity {
required void Function(int damage) onDamagePlayer,
});
/// Centralized factory to handle all enemy spawning logic
static Enemy? spawn(
int objId,
double x,
@@ -194,6 +193,7 @@ abstract class Enemy extends Entity {
Difficulty difficulty, {
bool isSharewareMode = false,
}) {
// 124 (Dead Guard) famously overwrote a patrol ID in the original engine!
if (objId == MapObject.deadGuard || objId == MapObject.deadAardwolf) {
return null;
}
@@ -206,23 +206,20 @@ abstract class Enemy extends Entity {
final mapData = type.mapData;
// 1. Validate Difficulty and Determine State
// ALL enemies have explicit directional angles!
double spawnAngle = CardinalDirection.fromEnemyIndex(objId).radians;
EntityState spawnState;
double spawnAngle = 0.0;
if (mapData.isPatrolForDifficulty(objId, difficulty)) {
spawnState = EntityState.patrolling;
spawnAngle = CardinalDirection.fromEnemyIndex(objId).radians;
} else if (mapData.isStaticForDifficulty(objId, difficulty)) {
spawnState = EntityState.idle; // Faces the player dynamically later
spawnState = EntityState.idle;
} else if (mapData.isAmbushForDifficulty(objId, difficulty)) {
spawnState = EntityState.ambush; // Stays perfectly still until alerted
spawnState = EntityState.ambush;
} else {
// If the ID belongs to this enemy but NOT this difficulty, skip spawning!
return null;
return null; // ID belongs to this enemy, but not on this difficulty
}
// 2. Return the instance
return switch (type) {
EnemyType.guard => Guard(x: x, y: y, angle: spawnAngle, mapId: objId),
EnemyType.dog => Dog(x: x, y: y, angle: spawnAngle, mapId: objId),

View File

@@ -45,7 +45,7 @@ class EnemyAnimationMap {
enum EnemyType {
guard(
mapData: EnemyMapData(baseStaticId: 23, basePatrolId: 108),
mapData: EnemyMapData(108),
animations: EnemyAnimationMap(
idle: SpriteFrameRange(50, 57),
walking: SpriteFrameRange(58, 89),
@@ -56,22 +56,18 @@ enum EnemyType {
),
),
dog(
mapData: EnemyMapData(baseStaticId: 26, basePatrolId: 126),
// Translated from your old offset logic so the game doesn't break
mapData: EnemyMapData(216),
animations: EnemyAnimationMap(
idle: SpriteFrameRange(99, 106),
walking: SpriteFrameRange(107, 130),
attacking: SpriteFrameRange(131, 133),
pain: SpriteFrameRange(
131,
131,
), // Dog shared pain/attack frame in old code
pain: SpriteFrameRange(131, 131),
dying: SpriteFrameRange(134, 136),
dead: SpriteFrameRange(137, 137),
),
),
ss(
mapData: EnemyMapData(baseStaticId: 29, basePatrolId: 144),
mapData: EnemyMapData(180),
animations: EnemyAnimationMap(
idle: SpriteFrameRange(138, 145),
walking: SpriteFrameRange(146, 177),
@@ -82,7 +78,7 @@ enum EnemyType {
),
),
mutant(
mapData: EnemyMapData(baseStaticId: 32, basePatrolId: 162),
mapData: EnemyMapData(252),
animations: EnemyAnimationMap(
idle: SpriteFrameRange(187, 194),
walking: SpriteFrameRange(195, 226),
@@ -93,7 +89,7 @@ enum EnemyType {
),
),
officer(
mapData: EnemyMapData(baseStaticId: 35, basePatrolId: 180),
mapData: EnemyMapData(144),
animations: EnemyAnimationMap(
idle: SpriteFrameRange(238, 245),
walking: SpriteFrameRange(246, 277),