Re-added the sprite screen. Made some adjustments to enemy AI.

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-15 18:08:00 +01:00
parent 2892984e4e
commit 25c08dfe99
12 changed files with 163 additions and 117 deletions

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart'; import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
import 'package:wolf_3d_flutter/wolf_3d.dart'; import 'package:wolf_3d_flutter/wolf_3d.dart';
import 'package:wolf_dart/screens/difficulty_screen.dart'; import 'package:wolf_dart/screens/difficulty_screen.dart';
import 'package:wolf_dart/screens/sprite_gallery.dart';
class EpisodeScreen extends StatefulWidget { class EpisodeScreen extends StatefulWidget {
const EpisodeScreen({super.key}); const EpisodeScreen({super.key});
@@ -31,7 +32,19 @@ class _EpisodeScreenState extends State<EpisodeScreen> {
final List<Episode> episodes = Wolf3d.I.activeGame.episodes; final List<Episode> episodes = Wolf3d.I.activeGame.episodes;
return Scaffold( return Scaffold(
backgroundColor: Colors.black, backgroundColor: Colors.teal,
floatingActionButton: IconButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return SpriteGallery(sprites: Wolf3d.I.sprites);
},
),
);
},
icon: Icon(Icons.bug_report),
),
body: Center( body: Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,

View File

@@ -18,16 +18,24 @@ class SpriteGallery extends StatelessWidget {
backgroundColor: Colors.black, backgroundColor: Colors.black,
body: GridView.builder( body: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 8, // 8 sprites per row crossAxisCount: 8,
), ),
itemCount: sprites.length, itemCount: sprites.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
// --- Check which enemy owns this sprite --- // --- Check which enemy owns this sprite ---
String label = "Idx: $index"; String label = "Sprite Index: $index";
for (final enemy in EnemyType.values) { for (final enemy in EnemyType.values) {
if (enemy.claimsSpriteIndex(index)) { if (enemy.claimsSpriteIndex(index)) {
final EnemyAnimation? animation = enemy.getAnimationFromSprite(
index,
);
// Appends the enum name (e.g., "guard", "dog") // Appends the enum name (e.g., "guard", "dog")
label += "\n${enemy.name}"; label += "\n${enemy.name}";
// Appends the animation name
if (animation != null) {
label += "\n${animation.name}";
}
break; break;
} }
} }

View File

@@ -1,9 +1,8 @@
enum Difficulty { enum Difficulty {
canIPlayDaddy(0, "Can I play, Daddy?"), canIPlayDaddy(0, "Can I play, Daddy?"),
dontHurtMe(1, "Don't hurt me."), dontHurtMe(0, "Don't hurt me."),
bringEmOn(2, "Bring em' on!"), bringEmOn(1, "Bring em' on!"),
iAmDeathIncarnate(3, "I am Death incarnate!"), iAmDeathIncarnate(2, "I am Death incarnate!");
;
final String title; final String title;
final int level; final int level;

View File

@@ -94,27 +94,6 @@ abstract class MapObject {
static const int deadGuard = 124; // Decorative only in WL1 static const int deadGuard = 124; // Decorative only in WL1
static const int deadAardwolf = 125; // 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) {
// 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;
}
static double getAngle(int id) { static double getAngle(int id) {
// Player spawn // Player spawn
switch (id) { switch (id) {
@@ -140,23 +119,34 @@ abstract class MapObject {
return CardinalDirection.fromEnemyIndex(directionIndex).radians; return CardinalDirection.fromEnemyIndex(directionIndex).radians;
} }
static bool shouldSpawn(int id, Difficulty selectedDifficulty) { /// Only handles the "Is this ID allowed on this difficulty?" math.
EnemyType? type = EnemyType.fromMapId(id); /// Does NOT decide IF an object is an enemy or decoration.
static bool isDifficultyAllowed(int objId, Difficulty difficulty) {
if (objId == 124) return true;
// If it's not a standard enemy (it's a decoration, boss, or player), spawn it int? requiredTier;
if (type == null) return true;
bool isStaticId = id >= (type.staticId - 2) && id <= type.staticId; // Static Tier Math (IDs 23-54)
int offset = isStaticId ? (id - type.staticId) : (id - type.patrolId); if (objId >= 23 && objId <= 54) {
int normalizedOffset = offset >= 18 ? offset - 18 : offset; requiredTier = (objId - 23) % 3;
}
// Patrol Tier Math (IDs 108-197)
else if (objId >= 108 && objId <= 197) {
int offsetInType = (objId - 108) % 18;
requiredTier = offsetInType ~/ 4;
}
return switch (normalizedOffset) { if (requiredTier == null) {
< 4 => true, // Spawns on all difficulties // Default to allowed if no tier logic exists
< 8 => selectedDifficulty.level >= Difficulty.bringEmOn.level, // Normal return true;
< 16 => }
selectedDifficulty.level >=
Difficulty.iAmDeathIncarnate.level, // Hard & Ambush int currentTier = switch (difficulty) {
_ => true, // Dead bodies (decorations) Difficulty.canIPlayDaddy || Difficulty.dontHurtMe => 0,
Difficulty.bringEmOn => 1,
Difficulty.iAmDeathIncarnate => 2,
}; };
return requiredTier == currentTier;
} }
} }

View File

@@ -102,8 +102,6 @@ class WolfEngine {
for (int x = 0; x < 64; x++) { for (int x = 0; x < 64; x++) {
int objId = objectLevel[y][x]; int objId = objectLevel[y][x];
if (!MapObject.shouldSpawn(objId, difficulty)) continue;
if (objId >= MapObject.playerNorth && objId <= MapObject.playerWest) { if (objId >= MapObject.playerNorth && objId <= MapObject.playerWest) {
double spawnAngle = 0.0; double spawnAngle = 0.0;
if (objId == MapObject.playerNorth) { if (objId == MapObject.playerNorth) {

View File

@@ -0,0 +1,24 @@
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
import 'package:wolf_3d_entities/wolf_3d_entities.dart';
class DeadAardwolf extends Decorative {
static const int sprite = 96;
DeadAardwolf({required super.x, required super.y})
: super(spriteIndex: sprite, state: EntityState.staticObj, mapId: 125);
/// This is the self-spawning logic we discussed.
/// It only claims the ID 124.
static DeadAardwolf? trySpawn(
int objId,
double x,
double y,
Difficulty difficulty, {
bool isSharewareMode = false,
}) {
if (objId == 125) {
return DeadAardwolf(x: x, y: y);
}
return null;
}
}

View File

@@ -0,0 +1,30 @@
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
import 'package:wolf_3d_entities/wolf_3d_entities.dart';
class DeadGuard extends Decorative {
/// The sprite index in VSWAP for the final "dead" frame of a Guard.
static const int deadGuardSprite = 95;
DeadGuard({required super.x, required super.y})
: super(
spriteIndex: deadGuardSprite,
state: EntityState.staticObj,
// We set mapId to 124 so we can identify it if needed
mapId: 124,
);
/// This is the self-spawning logic we discussed.
/// It only claims the ID 124.
static DeadGuard? trySpawn(
int objId,
double x,
double y,
Difficulty difficulty, {
bool isSharewareMode = false,
}) {
if (objId == 124) {
return DeadGuard(x: x, y: y);
}
return null;
}
}

View File

@@ -42,6 +42,9 @@ class Decorative extends Entity {
Difficulty difficulty, { Difficulty difficulty, {
bool isSharewareMode = false, bool isSharewareMode = false,
}) { }) {
// 2. Standard props (Table, Lamp, etc) use the tiered check
if (!MapObject.isDifficultyAllowed(objId, difficulty)) return null;
if (isDecoration(objId)) { if (isDecoration(objId)) {
return Decorative( return Decorative(
x: x, x: x,

View File

@@ -11,31 +11,11 @@ import 'package:wolf_3d_entities/src/entity.dart';
enum EnemyAnimation { idle, walking, attacking, pain, dying, dead } enum EnemyAnimation { idle, walking, attacking, pain, dying, dead }
enum EnemyType { enum EnemyType {
guard( guard(staticId: 23, patrolId: 108, spriteBaseIdx: 50),
staticId: 108, officer(staticId: 26, patrolId: 126, spriteBaseIdx: 238),
patrolId: 124, ss(staticId: 29, patrolId: 144, spriteBaseIdx: 138),
spriteBaseIdx: 50, dog(staticId: 32, patrolId: 162, spriteBaseIdx: 99),
), // Update spriteBaseIdx to your actual value mutant(staticId: 35, patrolId: 180, spriteBaseIdx: 187);
dog(
staticId: 144,
patrolId: 160,
spriteBaseIdx: 99,
), // Update spriteBaseIdx to your actual value
ss(
staticId: 176,
patrolId: 192,
spriteBaseIdx: 138,
), // Retained from your snippet
mutant(
staticId: 238,
patrolId: 254,
spriteBaseIdx: 185,
), // Update spriteBaseIdx to your actual value
officer(
staticId: 270,
patrolId: 286,
spriteBaseIdx: 226,
); // Update spriteBaseIdx to your actual value
final int staticId; final int staticId;
final int patrolId; final int patrolId;
@@ -47,19 +27,18 @@ enum EnemyType {
required this.spriteBaseIdx, required this.spriteBaseIdx,
}); });
/// Wolfenstein 3D allocates blocks of 16 IDs per enemy type for standing and patrolling /// Checks if the ID belongs to this enemy type range
/// (4 directions x 4 difficulty levels = 16 IDs)
bool claimsMapId(int id) { bool claimsMapId(int id) {
bool isStatic = id >= staticId && id < staticId + 16; // Static enemies span 3 IDs (Easy, Medium, Hard)
bool isPatrol = id >= patrolId && id < patrolId + 16; bool isStatic = id >= staticId && id < staticId + 3;
// Patrolling enemies span 18 IDs per type
bool isPatrol = id >= patrolId && id < patrolId + 18;
return isStatic || isPatrol; return isStatic || isPatrol;
} }
static EnemyType? fromMapId(int id) { static EnemyType? fromMapId(int id) {
for (final type in EnemyType.values) { for (final type in EnemyType.values) {
if (type.claimsMapId(id)) { if (type.claimsMapId(id)) return type;
return type;
}
} }
return null; return null;
} }
@@ -200,6 +179,7 @@ abstract class Enemy extends Entity {
int damage = 10; int damage = 10;
bool isDying = false; bool isDying = false;
bool hasDroppedItem = false; bool hasDroppedItem = false;
bool isAlerted = false;
// Replaces ob->temp2 for reaction delays // Replaces ob->temp2 for reaction delays
int reactionTimeMs = 0; int reactionTimeMs = 0;
@@ -210,6 +190,8 @@ abstract class Enemy extends Entity {
health -= amount; health -= amount;
lastActionTime = currentTime; lastActionTime = currentTime;
isAlerted = true;
if (health <= 0) { if (health <= 0) {
state = EntityState.dead; state = EntityState.dead;
isDying = true; isDying = true;
@@ -227,15 +209,19 @@ abstract class Enemy extends Entity {
int baseReactionMs = 200, int baseReactionMs = 200,
int reactionVarianceMs = 600, int reactionVarianceMs = 600,
}) { }) {
if (state == EntityState.idle && if (!isAlerted && hasLineOfSight(playerPosition, isWalkable)) {
hasLineOfSight(playerPosition, isWalkable)) {
if (reactionTimeMs == 0) { if (reactionTimeMs == 0) {
reactionTimeMs = reactionTimeMs =
elapsedMs + elapsedMs +
baseReactionMs + baseReactionMs +
math.Random().nextInt(reactionVarianceMs); math.Random().nextInt(reactionVarianceMs);
} else if (elapsedMs >= reactionTimeMs) { } else if (elapsedMs >= reactionTimeMs) {
state = EntityState.patrolling; isAlerted = true;
if (state == EntityState.idle) {
state = EntityState.patrolling;
}
lastActionTime = elapsedMs; lastActionTime = elapsedMs;
reactionTimeMs = 0; reactionTimeMs = 0;
} }
@@ -363,16 +349,20 @@ abstract class Enemy extends Entity {
Difficulty difficulty, { Difficulty difficulty, {
bool isSharewareMode = false, bool isSharewareMode = false,
}) { }) {
// 1. Check Difficulty & Compatibility // ID 124 (dead guard) and 125 (dead aardwolf) fall inside Guard's patrol
if (!MapObject.shouldSpawn(objId, difficulty)) return null; // range (108125) but are decorative bodies, not live actors.
if (objId == MapObject.deadGuard || objId == MapObject.deadAardwolf) {
return null;
}
// Explicitly ignore "Dead Guard" Map ID so Decorative.trySpawn can handle it if (objId >= MapObject.playerNorth && objId <= MapObject.playerWest) {
// In Wolf3D, 124 is the dead guard. return null;
if (objId == 124) return null; }
// If the checkbox is checked, block non-Shareware enemies // 2. I use the utility to check if this enemy is allowed on this difficulty
if (isSharewareMode && !MapObject.isSharewareCompatible(objId)) return null; if (!MapObject.isDifficultyAllowed(objId, difficulty)) return null;
// 3. I check if I even know what this enemy is
final type = EnemyType.fromMapId(objId); final type = EnemyType.fromMapId(objId);
if (type == null) return null; if (type == null) return null;

View File

@@ -37,6 +37,10 @@ class Guard extends Enemy {
double distance = position.distanceTo(playerPosition); double distance = position.distanceTo(playerPosition);
double angleToPlayer = position.angleTo(playerPosition); double angleToPlayer = position.angleTo(playerPosition);
if (isAlerted && state != EntityState.dead) {
newAngle = angleToPlayer;
}
if (state != EntityState.idle && state != EntityState.dead) { if (state != EntityState.idle && state != EntityState.dead) {
newAngle = angleToPlayer; newAngle = angleToPlayer;
} }
@@ -67,9 +71,11 @@ class Guard extends Enemy {
); );
if (state == EntityState.patrolling) { if (state == EntityState.patrolling) {
if (distance > 0.8) { if (!isAlerted || distance > 0.8) {
double moveX = math.cos(angleToPlayer) * speed; double currentMoveAngle = isAlerted ? angleToPlayer : angle;
double moveY = math.sin(angleToPlayer) * speed; double moveX = math.cos(currentMoveAngle) * speed;
double moveY = math.sin(currentMoveAngle) * speed;
movement = getValidMovement( movement = getValidMovement(
Coordinate2D(moveX, moveY), Coordinate2D(moveX, moveY),
isWalkable, isWalkable,
@@ -77,7 +83,7 @@ class Guard extends Enemy {
); );
} }
if (distance < 6.0 && elapsedMs - lastActionTime > 1500) { if (isAlerted && distance < 6.0 && elapsedMs - lastActionTime > 1500) {
if (hasLineOfSight(playerPosition, isWalkable)) { if (hasLineOfSight(playerPosition, isWalkable)) {
state = EntityState.attacking; state = EntityState.attacking;
lastActionTime = elapsedMs; lastActionTime = elapsedMs;

View File

@@ -1,5 +1,7 @@
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart'; import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
import 'package:wolf_3d_entities/src/entities/collectible.dart'; import 'package:wolf_3d_entities/src/entities/collectible.dart';
import 'package:wolf_3d_entities/src/entities/decorations/dead_aardwolf.dart';
import 'package:wolf_3d_entities/src/entities/decorations/dead_guard.dart';
import 'package:wolf_3d_entities/src/entities/decorative.dart'; import 'package:wolf_3d_entities/src/entities/decorative.dart';
import 'package:wolf_3d_entities/src/entities/enemies/bosses/hans_grosse.dart'; import 'package:wolf_3d_entities/src/entities/enemies/bosses/hans_grosse.dart';
import 'package:wolf_3d_entities/src/entities/enemies/enemy.dart'; import 'package:wolf_3d_entities/src/entities/enemies/enemy.dart';
@@ -16,15 +18,21 @@ typedef EntitySpawner =
abstract class EntityRegistry { abstract class EntityRegistry {
static final List<EntitySpawner> _spawners = [ static final List<EntitySpawner> _spawners = [
// Special
DeadGuard.trySpawn,
DeadAardwolf.trySpawn,
// Bosses // Bosses
HansGrosse.trySpawn, HansGrosse.trySpawn,
// Decorations
Decorative.trySpawn,
// Enemies need to try to spawn first // Enemies need to try to spawn first
Enemy.spawn, Enemy.spawn,
// Everything else // Collectables
Collectible.trySpawn, Collectible.trySpawn,
Decorative.trySpawn,
]; ];
static Entity? spawn( static Entity? spawn(
@@ -35,12 +43,6 @@ abstract class EntityRegistry {
int maxSprites, { int maxSprites, {
bool isSharewareMode = false, bool isSharewareMode = false,
}) { }) {
// 1. Difficulty check before even looking for a spawner
if (!MapObject.shouldSpawn(objId, difficulty)) return null;
// If the checkbox is checked, block non-Shareware enemies
if (isSharewareMode && !MapObject.isSharewareCompatible(objId)) return null;
if (objId == 0) return null; if (objId == 0) return null;
for (final spawner in _spawners) { for (final spawner in _spawners) {
@@ -51,23 +53,8 @@ abstract class EntityRegistry {
difficulty, difficulty,
isSharewareMode: isSharewareMode, isSharewareMode: isSharewareMode,
); );
if (entity != null) return entity;
final EnemyType? type = EnemyType.fromMapId(objId);
if (type != null) {
print("Spawning ${type.name} enemy");
}
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;
return null; // No class claimed this Map ID
} }
} }

View File

@@ -96,8 +96,6 @@ class _WolfRendererState extends State<WolfRenderer>
for (int x = 0; x < 64; x++) { for (int x = 0; x < 64; x++) {
int objId = objectLevel[y][x]; int objId = objectLevel[y][x];
if (!MapObject.shouldSpawn(objId, widget.difficulty)) continue;
if (objId >= MapObject.playerNorth && objId <= MapObject.playerWest) { if (objId >= MapObject.playerNorth && objId <= MapObject.playerWest) {
double spawnAngle = 0.0; double spawnAngle = 0.0;
if (objId == MapObject.playerNorth) { if (objId == MapObject.playerNorth) {