From 25c08dfe999dbf409be3ad7e55cfea88d8156f51 Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Sun, 15 Mar 2026 18:08:00 +0100 Subject: [PATCH] Re-added the sprite screen. Made some adjustments to enemy AI. Signed-off-by: Hans Kokx --- lib/screens/episode_screen.dart | 15 +++- lib/screens/sprite_gallery.dart | 12 ++- .../lib/src/difficulty.dart | 7 +- .../lib/src/map_objects.dart | 60 +++++++-------- .../lib/src/wolf_3d_engine_base.dart | 2 - .../entities/decorations/dead_aardwolf.dart | 24 ++++++ .../src/entities/decorations/dead_guard.dart | 30 ++++++++ .../lib/src/entities/decorative.dart | 3 + .../lib/src/entities/enemies/enemy.dart | 74 ++++++++----------- .../lib/src/entities/enemies/guard.dart | 14 +++- .../lib/src/entity_registry.dart | 37 +++------- .../lib/wolf_3d_renderer.dart | 2 - 12 files changed, 163 insertions(+), 117 deletions(-) create mode 100644 packages/wolf_3d_entities/lib/src/entities/decorations/dead_aardwolf.dart create mode 100644 packages/wolf_3d_entities/lib/src/entities/decorations/dead_guard.dart diff --git a/lib/screens/episode_screen.dart b/lib/screens/episode_screen.dart index 3fe4f33..ae93d2e 100644 --- a/lib/screens/episode_screen.dart +++ b/lib/screens/episode_screen.dart @@ -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 { final List 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, diff --git a/lib/screens/sprite_gallery.dart b/lib/screens/sprite_gallery.dart index 251a265..8746121 100644 --- a/lib/screens/sprite_gallery.dart +++ b/lib/screens/sprite_gallery.dart @@ -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; } } diff --git a/packages/wolf_3d_data_types/lib/src/difficulty.dart b/packages/wolf_3d_data_types/lib/src/difficulty.dart index ab5dd90..7a95eae 100644 --- a/packages/wolf_3d_data_types/lib/src/difficulty.dart +++ b/packages/wolf_3d_data_types/lib/src/difficulty.dart @@ -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; diff --git a/packages/wolf_3d_data_types/lib/src/map_objects.dart b/packages/wolf_3d_data_types/lib/src/map_objects.dart index 0ea6c18..0a6923f 100644 --- a/packages/wolf_3d_data_types/lib/src/map_objects.dart +++ b/packages/wolf_3d_data_types/lib/src/map_objects.dart @@ -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; } } diff --git a/packages/wolf_3d_engine/lib/src/wolf_3d_engine_base.dart b/packages/wolf_3d_engine/lib/src/wolf_3d_engine_base.dart index ce92524..2360e37 100644 --- a/packages/wolf_3d_engine/lib/src/wolf_3d_engine_base.dart +++ b/packages/wolf_3d_engine/lib/src/wolf_3d_engine_base.dart @@ -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) { diff --git a/packages/wolf_3d_entities/lib/src/entities/decorations/dead_aardwolf.dart b/packages/wolf_3d_entities/lib/src/entities/decorations/dead_aardwolf.dart new file mode 100644 index 0000000..0b12c90 --- /dev/null +++ b/packages/wolf_3d_entities/lib/src/entities/decorations/dead_aardwolf.dart @@ -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; + } +} diff --git a/packages/wolf_3d_entities/lib/src/entities/decorations/dead_guard.dart b/packages/wolf_3d_entities/lib/src/entities/decorations/dead_guard.dart new file mode 100644 index 0000000..3a0f6d2 --- /dev/null +++ b/packages/wolf_3d_entities/lib/src/entities/decorations/dead_guard.dart @@ -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; + } +} diff --git a/packages/wolf_3d_entities/lib/src/entities/decorative.dart b/packages/wolf_3d_entities/lib/src/entities/decorative.dart index d75f698..d338739 100644 --- a/packages/wolf_3d_entities/lib/src/entities/decorative.dart +++ b/packages/wolf_3d_entities/lib/src/entities/decorative.dart @@ -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, diff --git a/packages/wolf_3d_entities/lib/src/entities/enemies/enemy.dart b/packages/wolf_3d_entities/lib/src/entities/enemies/enemy.dart index ffc5cd6..1e10b6b 100644 --- a/packages/wolf_3d_entities/lib/src/entities/enemies/enemy.dart +++ b/packages/wolf_3d_entities/lib/src/entities/enemies/enemy.dart @@ -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) { - state = EntityState.patrolling; + 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 (108–125) 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; diff --git a/packages/wolf_3d_entities/lib/src/entities/enemies/guard.dart b/packages/wolf_3d_entities/lib/src/entities/enemies/guard.dart index 37c46df..ee6f0fb 100644 --- a/packages/wolf_3d_entities/lib/src/entities/enemies/guard.dart +++ b/packages/wolf_3d_entities/lib/src/entities/enemies/guard.dart @@ -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; diff --git a/packages/wolf_3d_entities/lib/src/entity_registry.dart b/packages/wolf_3d_entities/lib/src/entity_registry.dart index 68e4ce4..5c4aadf 100644 --- a/packages/wolf_3d_entities/lib/src/entity_registry.dart +++ b/packages/wolf_3d_entities/lib/src/entity_registry.dart @@ -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 _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) { - // 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! - } + if (entity != null) return entity; } - print("No class claimed this Map ID > objId $objId"); - return null; // No class claimed this Map ID + return null; } } diff --git a/packages/wolf_3d_renderer/lib/wolf_3d_renderer.dart b/packages/wolf_3d_renderer/lib/wolf_3d_renderer.dart index 061936e..813f1b8 100644 --- a/packages/wolf_3d_renderer/lib/wolf_3d_renderer.dart +++ b/packages/wolf_3d_renderer/lib/wolf_3d_renderer.dart @@ -96,8 +96,6 @@ class _WolfRendererState extends State 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) {