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

@@ -38,14 +38,10 @@ class SpriteGallery extends StatelessWidget {
} }
// Append the Map IDs for level editing reference // Append the Map IDs for level editing reference
int staticBase = enemy.mapData.baseStaticId; int staticBase = enemy.mapData.baseId;
int patrolBase = enemy.mapData.basePatrolId;
label += label +=
"\nStat: $staticBase (E), ${staticBase + 1} (M), ${staticBase + 2} (H)"; "\nStat: $staticBase (E), ${staticBase + 1} (M), ${staticBase + 2} (H)";
label += "\nPat: $patrolBase-${patrolBase + 3}(E)";
label += "\nPat: ${patrolBase + 4}-${patrolBase + 7}(M)";
label += "\nPat: ${patrolBase + 8}-${patrolBase + 11}(H)";
break; break;
} }
} }

View File

@@ -1,32 +1,25 @@
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart'; import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
class EnemyMapData { class EnemyMapData {
final int baseStaticId; final int baseId;
final int basePatrolId;
const EnemyMapData({required this.baseStaticId, required this.basePatrolId}); const EnemyMapData(this.baseId);
/// True if this ID belongs to this enemy type at all /// True if the ID falls anywhere within this enemy's 36-ID block
bool claimsId(int id) { bool claimsId(int id) => id >= baseId && id < baseId + 36;
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;
}
/// Exact check: Is this a static enemy on this specific difficulty?
bool isStaticForDifficulty(int id, Difficulty 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) { bool isPatrolForDifficulty(int id, Difficulty difficulty) {
int startId = basePatrolId + (difficulty.level * 4); int start = baseId + 12 + (difficulty.level * 4);
return id >= startId && id < startId + 4; // Spans 4 directions return id >= start && id < start + 4;
} }
/// Exact check: Is this an ambush enemy on this specific difficulty?
bool isAmbushForDifficulty(int id, Difficulty 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 const int deadAardwolf = 125; // Decorative only in WL1
static double getAngle(int id) { static double getAngle(int id) {
// Player spawn
switch (id) { switch (id) {
case playerNorth: case playerNorth:
return CardinalDirection.north.radians; return CardinalDirection.north.radians;
@@ -107,47 +106,29 @@ abstract class MapObject {
return CardinalDirection.west.radians; return CardinalDirection.west.radians;
} }
// Boss check
if (id == bossHansGrosse) return 0.0; if (id == bossHansGrosse) return 0.0;
final EnemyType? type = EnemyType.fromMapId(id); final EnemyType? type = EnemyType.fromMapId(id);
if (type == null) return 0.0; if (type == null) return 0.0;
// Because the map data strictly groups patrol directions in blocks of 4 // Because all enemies are in blocks of 4, modulo 4 gets the exact angle
// (East=0, North=1, West=2, South=3), a simple modulo 4 against the base ID return CardinalDirection.fromEnemyIndex(id % 4).radians;
// gives us the exact cardinal direction!
int directionIndex = (id - type.mapData.basePatrolId) % 4;
return CardinalDirection.fromEnemyIndex(directionIndex).radians;
} }
/// Only handles the "Is this ID allowed on this difficulty?" math. /// Determines if an object should be spawned on the current difficulty.
/// Does NOT decide IF an object is an enemy or decoration.
static bool isDifficultyAllowed(int objId, Difficulty 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; // 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
// Static Tier Math (IDs 23-54) // enemy does not belong on this difficulty.
if (objId >= 23 && objId <= 54) { if (EnemyType.fromMapId(objId) != null) {
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
return true; return true;
} }
int currentTier = switch (difficulty) { // 3. All non-enemy map objects (keys, ammo, puddles, plants)
Difficulty.canIPlayDaddy || Difficulty.dontHurtMe => 0, // are NOT difficulty-tiered. They always spawn.
Difficulty.bringEmOn => 1, return true;
Difficulty.iAmDeathIncarnate => 2,
};
return requiredTier == currentTier;
} }
} }

View File

@@ -186,7 +186,6 @@ abstract class Enemy extends Entity {
required void Function(int damage) onDamagePlayer, required void Function(int damage) onDamagePlayer,
}); });
/// Centralized factory to handle all enemy spawning logic
static Enemy? spawn( static Enemy? spawn(
int objId, int objId,
double x, double x,
@@ -194,6 +193,7 @@ abstract class Enemy extends Entity {
Difficulty difficulty, { Difficulty difficulty, {
bool isSharewareMode = false, bool isSharewareMode = false,
}) { }) {
// 124 (Dead Guard) famously overwrote a patrol ID in the original engine!
if (objId == MapObject.deadGuard || objId == MapObject.deadAardwolf) { if (objId == MapObject.deadGuard || objId == MapObject.deadAardwolf) {
return null; return null;
} }
@@ -206,23 +206,20 @@ abstract class Enemy extends Entity {
final mapData = type.mapData; final mapData = type.mapData;
// 1. Validate Difficulty and Determine State // ALL enemies have explicit directional angles!
double spawnAngle = CardinalDirection.fromEnemyIndex(objId).radians;
EntityState spawnState; EntityState spawnState;
double spawnAngle = 0.0;
if (mapData.isPatrolForDifficulty(objId, difficulty)) { if (mapData.isPatrolForDifficulty(objId, difficulty)) {
spawnState = EntityState.patrolling; spawnState = EntityState.patrolling;
spawnAngle = CardinalDirection.fromEnemyIndex(objId).radians;
} else if (mapData.isStaticForDifficulty(objId, difficulty)) { } else if (mapData.isStaticForDifficulty(objId, difficulty)) {
spawnState = EntityState.idle; // Faces the player dynamically later spawnState = EntityState.idle;
} else if (mapData.isAmbushForDifficulty(objId, difficulty)) { } else if (mapData.isAmbushForDifficulty(objId, difficulty)) {
spawnState = EntityState.ambush; // Stays perfectly still until alerted spawnState = EntityState.ambush;
} else { } else {
// If the ID belongs to this enemy but NOT this difficulty, skip spawning! return null; // ID belongs to this enemy, but not on this difficulty
return null;
} }
// 2. Return the instance
return switch (type) { return switch (type) {
EnemyType.guard => Guard(x: x, y: y, angle: spawnAngle, mapId: objId), EnemyType.guard => Guard(x: x, y: y, angle: spawnAngle, mapId: objId),
EnemyType.dog => Dog(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 { enum EnemyType {
guard( guard(
mapData: EnemyMapData(baseStaticId: 23, basePatrolId: 108), mapData: EnemyMapData(108),
animations: EnemyAnimationMap( animations: EnemyAnimationMap(
idle: SpriteFrameRange(50, 57), idle: SpriteFrameRange(50, 57),
walking: SpriteFrameRange(58, 89), walking: SpriteFrameRange(58, 89),
@@ -56,22 +56,18 @@ enum EnemyType {
), ),
), ),
dog( dog(
mapData: EnemyMapData(baseStaticId: 26, basePatrolId: 126), mapData: EnemyMapData(216),
// Translated from your old offset logic so the game doesn't break
animations: EnemyAnimationMap( animations: EnemyAnimationMap(
idle: SpriteFrameRange(99, 106), idle: SpriteFrameRange(99, 106),
walking: SpriteFrameRange(107, 130), walking: SpriteFrameRange(107, 130),
attacking: SpriteFrameRange(131, 133), attacking: SpriteFrameRange(131, 133),
pain: SpriteFrameRange( pain: SpriteFrameRange(131, 131),
131,
131,
), // Dog shared pain/attack frame in old code
dying: SpriteFrameRange(134, 136), dying: SpriteFrameRange(134, 136),
dead: SpriteFrameRange(137, 137), dead: SpriteFrameRange(137, 137),
), ),
), ),
ss( ss(
mapData: EnemyMapData(baseStaticId: 29, basePatrolId: 144), mapData: EnemyMapData(180),
animations: EnemyAnimationMap( animations: EnemyAnimationMap(
idle: SpriteFrameRange(138, 145), idle: SpriteFrameRange(138, 145),
walking: SpriteFrameRange(146, 177), walking: SpriteFrameRange(146, 177),
@@ -82,7 +78,7 @@ enum EnemyType {
), ),
), ),
mutant( mutant(
mapData: EnemyMapData(baseStaticId: 32, basePatrolId: 162), mapData: EnemyMapData(252),
animations: EnemyAnimationMap( animations: EnemyAnimationMap(
idle: SpriteFrameRange(187, 194), idle: SpriteFrameRange(187, 194),
walking: SpriteFrameRange(195, 226), walking: SpriteFrameRange(195, 226),
@@ -93,7 +89,7 @@ enum EnemyType {
), ),
), ),
officer( officer(
mapData: EnemyMapData(baseStaticId: 35, basePatrolId: 180), mapData: EnemyMapData(144),
animations: EnemyAnimationMap( animations: EnemyAnimationMap(
idle: SpriteFrameRange(238, 245), idle: SpriteFrameRange(238, 245),
walking: SpriteFrameRange(246, 277), walking: SpriteFrameRange(246, 277),