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:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user