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_flutter/wolf_3d.dart';
import 'package:wolf_dart/screens/difficulty_screen.dart';
import 'package:wolf_dart/screens/sprite_gallery.dart';
class EpisodeScreen extends StatefulWidget {
const EpisodeScreen({super.key});
@@ -31,7 +32,19 @@ class _EpisodeScreenState extends State<EpisodeScreen> {
final List<Episode> episodes = Wolf3d.I.activeGame.episodes;
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(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,

View File

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

View File

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

View File

@@ -94,27 +94,6 @@ abstract class MapObject {
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) {
// 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) {
// Player spawn
switch (id) {
@@ -140,23 +119,34 @@ abstract class MapObject {
return CardinalDirection.fromEnemyIndex(directionIndex).radians;
}
static bool shouldSpawn(int id, Difficulty selectedDifficulty) {
EnemyType? type = EnemyType.fromMapId(id);
/// Only handles the "Is this ID allowed on this difficulty?" math.
/// 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
if (type == null) return true;
int? requiredTier;
bool isStaticId = id >= (type.staticId - 2) && id <= type.staticId;
int offset = isStaticId ? (id - type.staticId) : (id - type.patrolId);
int normalizedOffset = offset >= 18 ? offset - 18 : offset;
// 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;
}
return switch (normalizedOffset) {
< 4 => true, // Spawns on all difficulties
< 8 => selectedDifficulty.level >= Difficulty.bringEmOn.level, // Normal
< 16 =>
selectedDifficulty.level >=
Difficulty.iAmDeathIncarnate.level, // Hard & Ambush
_ => true, // Dead bodies (decorations)
if (requiredTier == null) {
// Default to allowed if no tier logic exists
return true;
}
int currentTier = switch (difficulty) {
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++) {
int objId = objectLevel[y][x];
if (!MapObject.shouldSpawn(objId, difficulty)) continue;
if (objId >= MapObject.playerNorth && objId <= MapObject.playerWest) {
double spawnAngle = 0.0;
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, {
bool isSharewareMode = false,
}) {
// 2. Standard props (Table, Lamp, etc) use the tiered check
if (!MapObject.isDifficultyAllowed(objId, difficulty)) return null;
if (isDecoration(objId)) {
return Decorative(
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 EnemyType {
guard(
staticId: 108,
patrolId: 124,
spriteBaseIdx: 50,
), // Update spriteBaseIdx to your actual value
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
guard(staticId: 23, patrolId: 108, spriteBaseIdx: 50),
officer(staticId: 26, patrolId: 126, spriteBaseIdx: 238),
ss(staticId: 29, patrolId: 144, spriteBaseIdx: 138),
dog(staticId: 32, patrolId: 162, spriteBaseIdx: 99),
mutant(staticId: 35, patrolId: 180, spriteBaseIdx: 187);
final int staticId;
final int patrolId;
@@ -47,19 +27,18 @@ enum EnemyType {
required this.spriteBaseIdx,
});
/// Wolfenstein 3D allocates blocks of 16 IDs per enemy type for standing and patrolling
/// (4 directions x 4 difficulty levels = 16 IDs)
/// Checks if the ID belongs to this enemy type range
bool claimsMapId(int id) {
bool isStatic = id >= staticId && id < staticId + 16;
bool isPatrol = id >= patrolId && id < patrolId + 16;
// Static enemies span 3 IDs (Easy, Medium, Hard)
bool isStatic = id >= staticId && id < staticId + 3;
// Patrolling enemies span 18 IDs per type
bool isPatrol = id >= patrolId && id < patrolId + 18;
return isStatic || isPatrol;
}
static EnemyType? fromMapId(int id) {
for (final type in EnemyType.values) {
if (type.claimsMapId(id)) {
return type;
}
if (type.claimsMapId(id)) return type;
}
return null;
}
@@ -200,6 +179,7 @@ abstract class Enemy extends Entity {
int damage = 10;
bool isDying = false;
bool hasDroppedItem = false;
bool isAlerted = false;
// Replaces ob->temp2 for reaction delays
int reactionTimeMs = 0;
@@ -210,6 +190,8 @@ abstract class Enemy extends Entity {
health -= amount;
lastActionTime = currentTime;
isAlerted = true;
if (health <= 0) {
state = EntityState.dead;
isDying = true;
@@ -227,15 +209,19 @@ abstract class Enemy extends Entity {
int baseReactionMs = 200,
int reactionVarianceMs = 600,
}) {
if (state == EntityState.idle &&
hasLineOfSight(playerPosition, isWalkable)) {
if (!isAlerted && hasLineOfSight(playerPosition, isWalkable)) {
if (reactionTimeMs == 0) {
reactionTimeMs =
elapsedMs +
baseReactionMs +
math.Random().nextInt(reactionVarianceMs);
} else if (elapsedMs >= reactionTimeMs) {
isAlerted = true;
if (state == EntityState.idle) {
state = EntityState.patrolling;
}
lastActionTime = elapsedMs;
reactionTimeMs = 0;
}
@@ -363,16 +349,20 @@ abstract class Enemy extends Entity {
Difficulty difficulty, {
bool isSharewareMode = false,
}) {
// 1. Check Difficulty & Compatibility
if (!MapObject.shouldSpawn(objId, difficulty)) return null;
// ID 124 (dead guard) and 125 (dead aardwolf) fall inside Guard's patrol
// 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
// In Wolf3D, 124 is the dead guard.
if (objId == 124) return null;
if (objId >= MapObject.playerNorth && objId <= MapObject.playerWest) {
return null;
}
// If the checkbox is checked, block non-Shareware enemies
if (isSharewareMode && !MapObject.isSharewareCompatible(objId)) return null;
// 2. I use the utility to check if this enemy is allowed on this difficulty
if (!MapObject.isDifficultyAllowed(objId, difficulty)) return null;
// 3. I check if I even know what this enemy is
final type = EnemyType.fromMapId(objId);
if (type == null) return null;

View File

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

View File

@@ -1,5 +1,7 @@
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/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/enemies/bosses/hans_grosse.dart';
import 'package:wolf_3d_entities/src/entities/enemies/enemy.dart';
@@ -16,15 +18,21 @@ typedef EntitySpawner =
abstract class EntityRegistry {
static final List<EntitySpawner> _spawners = [
// Special
DeadGuard.trySpawn,
DeadAardwolf.trySpawn,
// Bosses
HansGrosse.trySpawn,
// Decorations
Decorative.trySpawn,
// Enemies need to try to spawn first
Enemy.spawn,
// Everything else
// Collectables
Collectible.trySpawn,
Decorative.trySpawn,
];
static Entity? spawn(
@@ -35,12 +43,6 @@ abstract class EntityRegistry {
int maxSprites, {
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;
for (final spawner in _spawners) {
@@ -51,23 +53,8 @@ abstract class EntityRegistry {
difficulty,
isSharewareMode: isSharewareMode,
);
final EnemyType? type = EnemyType.fromMapId(objId);
if (type != null) {
print("Spawning ${type.name} enemy");
if (entity != null) return entity;
}
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
return null;
}
}

View File

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