Add shareware support and spawn correctly for difficulty levels

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-14 17:01:01 +01:00
parent 690ac1e7e6
commit 278c73a256
19 changed files with 383 additions and 238 deletions

View File

@@ -0,0 +1,28 @@
import 'dart:math' as math;
enum CardinalDirection {
east(0.0),
south(math.pi / 2),
west(math.pi),
north(3 * math.pi / 2)
;
final double radians;
const CardinalDirection(this.radians);
/// Helper to decode Wolf3D enemy directional blocks
static CardinalDirection fromEnemyIndex(int index) {
switch (index % 4) {
case 0:
return CardinalDirection.east;
case 1:
return CardinalDirection.north;
case 2:
return CardinalDirection.west;
case 3:
return CardinalDirection.south;
default:
return CardinalDirection.east;
}
}
}

1
lib/classes/sprite.dart Normal file
View File

@@ -0,0 +1 @@
typedef Sprite = List<List<int>>;

View File

@@ -2,9 +2,16 @@ import 'package:flutter/material.dart';
import 'package:wolf_dart/features/difficulty/difficulty.dart'; import 'package:wolf_dart/features/difficulty/difficulty.dart';
import 'package:wolf_dart/features/renderer/renderer.dart'; import 'package:wolf_dart/features/renderer/renderer.dart';
class DifficultyScreen extends StatelessWidget { class DifficultyScreen extends StatefulWidget {
const DifficultyScreen({super.key}); const DifficultyScreen({super.key});
@override
State<DifficultyScreen> createState() => _DifficultyScreenState();
}
class _DifficultyScreenState extends State<DifficultyScreen> {
bool isShareware = true; // Default to Shareware (WL1)
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -14,9 +21,9 @@ class DifficultyScreen extends StatelessWidget {
onPressed: () { onPressed: () {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => const WolfRenderer( builder: (_) => WolfRenderer(
difficulty: Difficulty.bringEmOn, difficulty: Difficulty.bringEmOn,
isDemo: false, isShareware: isShareware,
showSpriteGallery: true, showSpriteGallery: true,
), ),
), ),
@@ -37,35 +44,59 @@ class DifficultyScreen extends StatelessWidget {
fontFamily: 'Courier', fontFamily: 'Courier',
), ),
), ),
const SizedBox(height: 40), const SizedBox(height: 20),
// --- Version Toggle ---
Theme(
data: ThemeData(unselectedWidgetColor: Colors.grey),
child: CheckboxListTile(
title: const Text(
"Play Shareware Version (WL1)",
style: TextStyle(color: Colors.white),
),
value: isShareware,
onChanged: (bool? value) {
setState(() {
isShareware = value ?? true;
});
},
controlAffinity: ListTileControlAffinity.leading,
contentPadding: const EdgeInsets.symmetric(horizontal: 100),
),
),
const SizedBox(height: 20),
// --- Difficulty Buttons ---
ListView.builder( ListView.builder(
shrinkWrap: true, shrinkWrap: true,
itemCount: Difficulty.values.length, itemCount: Difficulty.values.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final Difficulty difficulty = Difficulty.values[index]; final Difficulty difficulty = Difficulty.values[index];
return ElevatedButton( return Padding(
style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 8.0),
backgroundColor: Colors.blueGrey[900], child: ElevatedButton(
foregroundColor: Colors.white, style: ElevatedButton.styleFrom(
minimumSize: const Size(300, 50), backgroundColor: Colors.blueGrey[900],
shape: RoundedRectangleBorder( foregroundColor: Colors.white,
borderRadius: BorderRadius.circular(4), minimumSize: const Size(300, 50),
), shape: RoundedRectangleBorder(
), borderRadius: BorderRadius.circular(4),
onPressed: () {
// Push the renderer and pass the selected difficulty
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => WolfRenderer(
difficulty: difficulty,
isDemo: false,
),
), ),
); ),
}, onPressed: () {
child: Text( Navigator.of(context).pushReplacement(
difficulty.title, MaterialPageRoute(
style: const TextStyle(fontSize: 18), builder: (_) => WolfRenderer(
difficulty: difficulty,
isShareware: isShareware,
),
),
);
},
child: Text(
difficulty.title,
style: const TextStyle(fontSize: 18),
),
), ),
); );
}, },

View File

@@ -1,3 +1,4 @@
import 'package:wolf_dart/features/difficulty/difficulty.dart';
import 'package:wolf_dart/features/entities/entity.dart'; import 'package:wolf_dart/features/entities/entity.dart';
enum CollectibleType { ammo, health, treasure, weapon, key } enum CollectibleType { ammo, health, treasure, weapon, key }
@@ -31,7 +32,7 @@ class Collectible extends Entity {
int objId, int objId,
double x, double x,
double y, double y,
int difficultyLevel, Difficulty _,
) { ) {
if (isCollectible(objId)) { if (isCollectible(objId)) {
return Collectible( return Collectible(

View File

@@ -1,3 +1,4 @@
import 'package:wolf_dart/features/difficulty/difficulty.dart';
import 'package:wolf_dart/features/entities/entity.dart'; import 'package:wolf_dart/features/entities/entity.dart';
class Decorative extends Entity { class Decorative extends Entity {
@@ -32,7 +33,7 @@ class Decorative extends Entity {
int objId, int objId,
double x, double x,
double y, double y,
int difficultyLevel, Difficulty _,
) { ) {
if (isDecoration(objId)) { if (isDecoration(objId)) {
return Decorative( return Decorative(

View File

@@ -1,12 +1,13 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:wolf_dart/classes/sprite.dart';
import 'package:wolf_dart/features/map/door.dart'; import 'package:wolf_dart/features/map/door.dart';
class DoorManager { class DoorManager {
// Key is '$x,$y' // Key is '$x,$y'
final Map<String, Door> doors = {}; final Map<String, Door> doors = {};
void initDoors(List<List<int>> wallGrid) { void initDoors(Sprite wallGrid) {
doors.clear(); doors.clear();
for (int y = 0; y < wallGrid.length; y++) { for (int y = 0; y < wallGrid.length; y++) {
for (int x = 0; x < wallGrid[y].length; x++) { for (int x = 0; x < wallGrid[y].length; x++) {

View File

@@ -1,8 +1,10 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:wolf_dart/classes/coordinate_2d.dart'; import 'package:wolf_dart/classes/coordinate_2d.dart';
import 'package:wolf_dart/features/difficulty/difficulty.dart';
import 'package:wolf_dart/features/entities/enemies/enemy.dart'; import 'package:wolf_dart/features/entities/enemies/enemy.dart';
import 'package:wolf_dart/features/entities/entity.dart'; import 'package:wolf_dart/features/entities/entity.dart';
import 'package:wolf_dart/features/entities/map_objects.dart';
class BrownGuard extends Enemy { class BrownGuard extends Enemy {
static const double speed = 0.03; static const double speed = 0.03;
@@ -23,33 +25,18 @@ class BrownGuard extends Enemy {
int objId, int objId,
double x, double x,
double y, double y,
int difficultyLevel, Difficulty difficulty,
) { ) {
bool canSpawn = false; // Use the range constants we defined in MapObject!
switch (difficultyLevel) { if (objId >= MapObject.guardStart && objId <= MapObject.guardStart + 17) {
case 0:
canSpawn = objId >= 108 && objId <= 115;
break;
case 1:
canSpawn = objId >= 144 && objId <= 151;
break;
case 2:
canSpawn = objId >= 180 && objId <= 187;
break;
case 3:
canSpawn = objId >= 216 && objId <= 223;
break;
}
if (canSpawn) {
return BrownGuard( return BrownGuard(
x: x, x: x,
y: y, y: y,
angle: Enemy.getInitialAngle(objId), angle: MapObject.getAngle(objId),
mapId: objId, mapId: objId,
); );
} }
return null; // Not a Brown Guard! return null;
} }
@override @override
@@ -63,20 +50,11 @@ class BrownGuard extends Enemy {
Coordinate2D movement = const Coordinate2D(0, 0); Coordinate2D movement = const Coordinate2D(0, 0);
double newAngle = angle; double newAngle = angle;
// 1. Wake up logic (Matches SightPlayer & FirstSighting) checkWakeUp(
if (state == EntityState.idle && elapsedMs: elapsedMs,
hasLineOfSight(playerPosition, isWalkable)) { playerPosition: playerPosition,
if (reactionTimeMs == 0) { isWalkable: isWalkable,
// Init reaction delay: ~1 to 4 tics in C (1 tic = ~14ms, but plays out longer in engine ticks). );
// Let's approximate human-feeling reaction time: 200ms - 800ms
reactionTimeMs = elapsedMs + 200 + math.Random().nextInt(600);
} else if (elapsedMs >= reactionTimeMs) {
state =
EntityState.patrolling; // Equivalent to FirstSighting chase frame
lastActionTime = elapsedMs;
reactionTimeMs = 0; // Reset
}
}
double distance = position.distanceTo(playerPosition); double distance = position.distanceTo(playerPosition);
double angleToPlayer = position.angleTo(playerPosition); double angleToPlayer = position.angleTo(playerPosition);
@@ -85,14 +63,11 @@ class BrownGuard extends Enemy {
newAngle = angleToPlayer; newAngle = angleToPlayer;
} }
// Octant logic remains the same // Octant logic (Directional sprites)
double diff = newAngle - angleToPlayer; double diff = angleToPlayer - newAngle;
while (diff <= -math.pi) {
diff += 2 * math.pi; while (diff <= -math.pi) diff += 2 * math.pi;
} while (diff > math.pi) diff -= 2 * math.pi;
while (diff > math.pi) {
diff -= 2 * math.pi;
}
int octant = ((diff + (math.pi / 8)) / (math.pi / 4)).floor() % 8; int octant = ((diff + (math.pi / 8)) / (math.pi / 4)).floor() % 8;
if (octant < 0) octant += 8; if (octant < 0) octant += 8;
@@ -105,13 +80,10 @@ class BrownGuard extends Enemy {
case EntityState.patrolling: case EntityState.patrolling:
if (distance > 0.8) { if (distance > 0.8) {
// Jitter fix: Use continuous vector movement instead of single-axis snapping
double moveX = math.cos(angleToPlayer) * speed; double moveX = math.cos(angleToPlayer) * speed;
double moveY = math.sin(angleToPlayer) * speed; double moveY = math.sin(angleToPlayer) * speed;
Coordinate2D intendedMovement = Coordinate2D(moveX, moveY); Coordinate2D intendedMovement = Coordinate2D(moveX, moveY);
// Pass tryOpenDoor down!
movement = getValidMovement( movement = getValidMovement(
intendedMovement, intendedMovement,
isWalkable, isWalkable,
@@ -119,12 +91,9 @@ class BrownGuard extends Enemy {
); );
} }
// Animation fix: Update the sprite so he actually turns and walks!
int walkFrame = (elapsedMs ~/ 150) % 4; int walkFrame = (elapsedMs ~/ 150) % 4;
spriteIndex = 58 + (walkFrame * 8) + octant; spriteIndex = 58 + (walkFrame * 8) + octant;
// Shooting fix: Give him permission to stop and shoot you
// (1500ms delay between shots)
if (distance < 6.0 && elapsedMs - lastActionTime > 1500) { if (distance < 6.0 && elapsedMs - lastActionTime > 1500) {
if (hasLineOfSight(playerPosition, isWalkable)) { if (hasLineOfSight(playerPosition, isWalkable)) {
state = EntityState.shooting; state = EntityState.shooting;
@@ -132,20 +101,20 @@ class BrownGuard extends Enemy {
_hasFiredThisCycle = false; _hasFiredThisCycle = false;
} }
} }
break; // Fallthrough fix: Don't forget the break! break;
case EntityState.shooting: case EntityState.shooting:
int timeShooting = elapsedMs - lastActionTime; int timeShooting = elapsedMs - lastActionTime;
if (timeShooting < 150) { if (timeShooting < 150) {
spriteIndex = 96; spriteIndex = 96; // Aiming
} else if (timeShooting < 300) { } else if (timeShooting < 300) {
spriteIndex = 97; spriteIndex = 97; // Firing
if (!_hasFiredThisCycle) { if (!_hasFiredThisCycle) {
onDamagePlayer(10); // DAMAGING PLAYER onDamagePlayer(10);
_hasFiredThisCycle = true; _hasFiredThisCycle = true;
} }
} else if (timeShooting < 450) { } else if (timeShooting < 450) {
spriteIndex = 98; spriteIndex = 98; // Recoil
} else { } else {
state = EntityState.patrolling; state = EntityState.patrolling;
lastActionTime = elapsedMs; lastActionTime = elapsedMs;
@@ -153,7 +122,8 @@ class BrownGuard extends Enemy {
break; break;
case EntityState.pain: case EntityState.pain:
spriteIndex = 94; spriteIndex = 94; // Ouch frame
// Stay in pain for a brief moment, then resume attacking
if (elapsedMs - lastActionTime > 250) { if (elapsedMs - lastActionTime > 250) {
state = EntityState.patrolling; state = EntityState.patrolling;
lastActionTime = elapsedMs; lastActionTime = elapsedMs;
@@ -164,9 +134,10 @@ class BrownGuard extends Enemy {
if (isDying) { if (isDying) {
int deathFrame = (elapsedMs - lastActionTime) ~/ 150; int deathFrame = (elapsedMs - lastActionTime) ~/ 150;
if (deathFrame < 4) { if (deathFrame < 4) {
spriteIndex = 90 + deathFrame - 1; // FIX: Removed the buggy "- 1"
spriteIndex = 90 + deathFrame;
} else { } else {
spriteIndex = 95; spriteIndex = 95; // Final dead frame
isDying = false; isDying = false;
} }
} else { } else {

View File

@@ -1,11 +1,13 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:wolf_dart/classes/coordinate_2d.dart'; import 'package:wolf_dart/classes/coordinate_2d.dart';
import 'package:wolf_dart/features/difficulty/difficulty.dart';
import 'package:wolf_dart/features/entities/enemies/enemy.dart'; import 'package:wolf_dart/features/entities/enemies/enemy.dart';
import 'package:wolf_dart/features/entities/entity.dart'; import 'package:wolf_dart/features/entities/entity.dart';
import 'package:wolf_dart/features/entities/map_objects.dart'; // NEW
class Dog extends Enemy { class Dog extends Enemy {
static const double speed = 0.05; // Dogs are much faster than guards! static const double speed = 0.05;
bool _hasBittenThisCycle = false; bool _hasBittenThisCycle = false;
Dog({ Dog({
@@ -14,32 +16,17 @@ class Dog extends Enemy {
required super.angle, required super.angle,
required super.mapId, required super.mapId,
}) : super( }) : super(
spriteIndex: 99, // Dogs start at index 99 in VSWAP spriteIndex: 99,
state: EntityState.idle, state: EntityState.idle,
); );
static Dog? trySpawn(int objId, double x, double y, int difficultyLevel) { static Dog? trySpawn(int objId, double x, double y, Difficulty difficulty) {
bool canSpawn = false; // The renderer already checked difficulty, so we just check the ID block!
switch (difficultyLevel) { if (objId >= MapObject.dogStart && objId <= MapObject.dogStart + 17) {
case 0:
canSpawn = objId >= 116 && objId <= 119;
break;
case 1:
canSpawn = objId >= 152 && objId <= 155;
break;
case 2:
canSpawn = objId >= 188 && objId <= 191;
break;
case 3:
canSpawn = objId >= 224 && objId <= 227;
break;
}
if (canSpawn) {
return Dog( return Dog(
x: x, x: x,
y: y, y: y,
angle: Enemy.getInitialAngle(objId), angle: MapObject.getAngle(objId),
mapId: objId, mapId: objId,
); );
} }
@@ -51,23 +38,19 @@ class Dog extends Enemy {
required int elapsedMs, required int elapsedMs,
required Coordinate2D playerPosition, required Coordinate2D playerPosition,
required bool Function(int x, int y) isWalkable, required bool Function(int x, int y) isWalkable,
required void Function(int x, int y) tryOpenDoor, // NEW required void Function(int x, int y) tryOpenDoor,
required void Function(int damage) onDamagePlayer, required void Function(int damage) onDamagePlayer,
}) { }) {
Coordinate2D movement = const Coordinate2D(0, 0); Coordinate2D movement = const Coordinate2D(0, 0);
double newAngle = angle; double newAngle = angle;
// 1. Wake up logic checkWakeUp(
if (state == EntityState.idle && elapsedMs: elapsedMs,
hasLineOfSight(playerPosition, isWalkable)) { playerPosition: playerPosition,
if (reactionTimeMs == 0) { isWalkable: isWalkable,
reactionTimeMs = elapsedMs + 100 + math.Random().nextInt(200); baseReactionMs: 100,
} else if (elapsedMs >= reactionTimeMs) { reactionVarianceMs: 200,
state = EntityState.patrolling; );
lastActionTime = elapsedMs;
reactionTimeMs = 0;
}
}
double distance = position.distanceTo(playerPosition); double distance = position.distanceTo(playerPosition);
double angleToPlayer = position.angleTo(playerPosition); double angleToPlayer = position.angleTo(playerPosition);
@@ -76,13 +59,10 @@ class Dog extends Enemy {
newAngle = angleToPlayer; newAngle = angleToPlayer;
} }
double diff = newAngle - angleToPlayer; double diff = angleToPlayer - newAngle;
while (diff <= -math.pi) {
diff += 2 * math.pi; while (diff <= -math.pi) diff += 2 * math.pi;
} while (diff > math.pi) diff -= 2 * math.pi;
while (diff > math.pi) {
diff -= 2 * math.pi;
}
int octant = ((diff + (math.pi / 8)) / (math.pi / 4)).floor() % 8; int octant = ((diff + (math.pi / 8)) / (math.pi / 4)).floor() % 8;
if (octant < 0) octant += 8; if (octant < 0) octant += 8;
@@ -95,15 +75,12 @@ class Dog extends Enemy {
case EntityState.patrolling: case EntityState.patrolling:
if (distance > 0.8) { if (distance > 0.8) {
double deltaX = playerPosition.x - position.x; // UPGRADED: Smooth vector movement instead of grid-snapping
double deltaY = playerPosition.y - position.y; double moveX = math.cos(angleToPlayer) * speed;
double moveY = math.sin(angleToPlayer) * speed;
double moveX = deltaX > 0 ? speed : (deltaX < 0 ? -speed : 0);
double moveY = deltaY > 0 ? speed : (deltaY < 0 ? -speed : 0);
Coordinate2D intendedMovement = Coordinate2D(moveX, moveY); Coordinate2D intendedMovement = Coordinate2D(moveX, moveY);
// Pass tryOpenDoor down!
movement = getValidMovement( movement = getValidMovement(
intendedMovement, intendedMovement,
isWalkable, isWalkable,
@@ -135,6 +112,22 @@ class Dog extends Enemy {
} }
break; break;
// Make sure dogs have a death state so they don't stay standing!
case EntityState.dead:
if (isDying) {
int deathFrame = (elapsedMs - lastActionTime) ~/ 100;
if (deathFrame < 4) {
spriteIndex =
140 + deathFrame; // Dog death frames usually start here
} else {
spriteIndex = 143; // Dead dog on floor
isDying = false;
}
} else {
spriteIndex = 143;
}
break;
default: default:
break; break;
} }

View File

@@ -55,6 +55,28 @@ abstract class Enemy extends Entity {
} }
} }
void checkWakeUp({
required int elapsedMs,
required Coordinate2D playerPosition,
required bool Function(int x, int y) isWalkable,
int baseReactionMs = 200,
int reactionVarianceMs = 600,
}) {
if (state == EntityState.idle &&
hasLineOfSight(playerPosition, isWalkable)) {
if (reactionTimeMs == 0) {
reactionTimeMs =
elapsedMs +
baseReactionMs +
math.Random().nextInt(reactionVarianceMs);
} else if (elapsedMs >= reactionTimeMs) {
state = EntityState.patrolling;
lastActionTime = elapsedMs;
reactionTimeMs = 0;
}
}
}
// Matches WL_STATE.C's 'CheckLine' using canonical Integer DDA traversal // Matches WL_STATE.C's 'CheckLine' using canonical Integer DDA traversal
bool hasLineOfSight( bool hasLineOfSight(
Coordinate2D playerPosition, Coordinate2D playerPosition,
@@ -63,7 +85,7 @@ abstract class Enemy extends Entity {
// 1. Proximity Check (Matches WL_STATE.C 'MINSIGHT') // 1. Proximity Check (Matches WL_STATE.C 'MINSIGHT')
// If the player is very close, sight is automatic regardless of facing angle. // If the player is very close, sight is automatic regardless of facing angle.
// This compensates for our lack of a noise/gunshot alert system! // This compensates for our lack of a noise/gunshot alert system!
if (position.distanceTo(playerPosition) < 2.0) { if (position.distanceTo(playerPosition) < 1.2) {
return true; return true;
} }

View File

@@ -1,15 +1,17 @@
import 'package:wolf_dart/features/difficulty/difficulty.dart';
import 'package:wolf_dart/features/entities/collectible.dart'; import 'package:wolf_dart/features/entities/collectible.dart';
import 'package:wolf_dart/features/entities/decorative.dart'; import 'package:wolf_dart/features/entities/decorative.dart';
import 'package:wolf_dart/features/entities/enemies/brown_guard.dart'; import 'package:wolf_dart/features/entities/enemies/brown_guard.dart';
import 'package:wolf_dart/features/entities/enemies/dog.dart'; import 'package:wolf_dart/features/entities/enemies/dog.dart';
import 'package:wolf_dart/features/entities/entity.dart'; import 'package:wolf_dart/features/entities/entity.dart';
import 'package:wolf_dart/features/entities/map_objects.dart';
typedef EntitySpawner = typedef EntitySpawner =
Entity? Function( Entity? Function(
int objId, int objId,
double x, double x,
double y, double y,
int difficultyLevel, Difficulty difficulty,
); );
abstract class EntityRegistry { abstract class EntityRegistry {
@@ -24,11 +26,14 @@ abstract class EntityRegistry {
int objId, int objId,
double x, double x,
double y, double y,
int difficultyLevel, Difficulty difficulty,
int maxSprites, int maxSprites,
) { ) {
// 1. Difficulty check before even looking for a spawner
if (!MapObject.shouldSpawn(objId, difficulty)) return null;
for (final spawner in _spawners) { for (final spawner in _spawners) {
Entity? entity = spawner(objId, x, y, difficultyLevel); Entity? entity = spawner(objId, x, y, difficulty);
if (entity != null) { if (entity != null) {
// Safety bounds check for the VSWAP array // Safety bounds check for the VSWAP array

View File

@@ -1,4 +1,7 @@
abstract class MapObjectId { import 'package:wolf_dart/classes/cardinal_direction.dart';
import 'package:wolf_dart/features/difficulty/difficulty.dart';
abstract class MapObject {
// --- Player Spawns --- // --- Player Spawns ---
static const int playerNorth = 19; static const int playerNorth = 19;
static const int playerEast = 20; static const int playerEast = 20;
@@ -12,7 +15,7 @@ abstract class MapObjectId {
static const int floorLamp = 26; static const int floorLamp = 26;
static const int chandelier = 27; static const int chandelier = 27;
static const int hangingSkeleton = 28; static const int hangingSkeleton = 28;
static const int dogFood = 29; // Also used as decoration static const int dogFoodDecoration = 29;
static const int whiteColumn = 30; static const int whiteColumn = 30;
static const int pottedPlant = 31; static const int pottedPlant = 31;
static const int blueSkeleton = 32; static const int blueSkeleton = 32;
@@ -25,12 +28,12 @@ abstract class MapObjectId {
static const int emptyCage = 39; static const int emptyCage = 39;
static const int cageWithSkeleton = 40; static const int cageWithSkeleton = 40;
static const int bones = 41; static const int bones = 41;
static const int goldenKeybowl = 42; // Decorative only static const int goldenKeyBowl = 42;
// --- Collectibles & Items --- // --- Collectibles ---
static const int goldKey = 43; static const int goldKey = 43;
static const int silverKey = 44; static const int silverKey = 44;
static const int bed = 45; // Bed is usually non-collectible but in this range static const int bed = 45;
static const int basket = 46; static const int basket = 46;
static const int food = 47; static const int food = 47;
static const int medkit = 48; static const int medkit = 48;
@@ -43,62 +46,110 @@ abstract class MapObjectId {
static const int crown = 55; static const int crown = 55;
static const int extraLife = 56; static const int extraLife = 56;
// --- More Decorations --- // --- Environmental ---
static const int bloodPool = 57; static const int bloodPoolSmall = 57;
static const int barrel = 58; static const int barrel = 58;
static const int well = 59; static const int wellFull = 59;
static const int emptyWell = 60; static const int wellEmpty = 60;
static const int bloodPoolLarge = 61; static const int bloodPoolLarge = 61;
static const int flag = 62; static const int flag = 62;
static const int callanard = 63; // Aardwolf sign / hidden message static const int aardwolfSign = 63;
static const int bonesAndSkull = 64; static const int bonesAndSkull = 64;
static const int wallHanging = 65; static const int wallHanging = 65;
static const int stove = 66; static const int stove = 66;
static const int spearRack = 67; static const int spearRack = 67;
static const int vines = 68; static const int vines = 68;
// --- Special Objects --- // --- Logic & Triggers ---
static const int secretDoor = 98; // Often used for the Pushwall trigger static const int pushwallTrigger = 98;
static const int elevatorToSecretLevel = 99; static const int secretExitTrigger = 99;
static const int exitTrigger = 100; static const int normalExitTrigger = 100;
// --- Enemies (Spawn Points) --- // --- Enemy Base IDs (Easy, North) ---
// Guards static const int _guardBase = 108;
static const int guardEasyNorth = 108; static const int _officerBase = 126; // WL6 only
static const int guardEasyEast = 109; static const int _ssBase = 144; // WL6 only
static const int guardEasySouth = 110; static const int _dogBase = 162;
static const int guardEasyWest = 111; static const int _mutantBase = 180; // Episode 2+
// SS Guards // Bosses (Shared between WL1 and WL6)
static const int ssEasyNorth = 124;
static const int ssEasyEast = 125;
static const int ssEasySouth = 126;
static const int ssEasyWest = 127;
// Dogs
static const int dogEasyNorth = 140;
static const int dogEasyEast = 141;
static const int dogEasySouth = 142;
static const int dogEasyWest = 143;
// Mutants
static const int mutantEasyNorth = 156;
static const int mutantEasyEast = 157;
static const int mutantEasySouth = 158;
static const int mutantEasyWest = 159;
// Officers
static const int officerEasyNorth = 172;
static const int officerEasyEast = 173;
static const int officerEasySouth = 174;
static const int officerEasyWest = 175;
// Bosses (Single spawn points)
static const int bossHansGrosse = 214; static const int bossHansGrosse = 214;
// WL6 Exclusive Bosses
static const int bossDrSchabbs = 215; static const int bossDrSchabbs = 215;
static const int bossFakeHitler = 216; static const int bossTransGrosse = 216;
static const int bossMechaHitler = 217; static const int bossUbermutant = 217;
static const int bossOttoGiftmacher = 218; static const int bossDeathKnight = 218;
static const int bossGretelGrosse = 219; static const int bossMechaHitler = 219;
static const int bossGeneralFettgesicht = 220; static const int bossHitlerGhost = 220;
static const int bossGretelGrosse = 221;
static const int bossGiftmacher = 222;
static const int bossFettgesicht = 223;
// --- Enemy Range Constants ---
static const int guardStart = 108;
static const int officerStart = 126;
static const int ssStart = 144;
static const int dogStart = 162;
static const int mutantStart = 180;
/// Returns true if the object ID exists in the Shareware version.
static bool isSharewareCompatible(int id) {
// WL1 only had Guards (108-125), Dogs (162-179), and Hans Grosse (214)
if (id >= 126 && id < 162) return false; // No Officers or SS
if (id >= 180 && id < 214) return false; // No Mutants
if (id > 214) return false; // No other bosses
return true;
}
/// Resolves which enemy type a map ID belongs to.
static String getEnemyType(int id) {
if (id >= 108 && id <= 125) return "Guard";
if (id >= 126 && id <= 143) return "Officer";
if (id >= 144 && id <= 161) return "SS";
if (id >= 162 && id <= 179) return "Dog";
if (id >= 180 && id <= 197) return "Mutant";
return "Unknown";
}
/// Checks if an object should be spawned based on chosen difficulty.
static bool shouldSpawn(int id, Difficulty selectedDifficulty) {
if (id < 108 || id > 213) return true; // Items/Players/Bosses always spawn
// Enemy blocks are 18 IDs wide (e.g., 108-125 for Guards)
int relativeId = (id - 108) % 18;
// 0-3: Easy, 4-7: Medium, 8-11: Hard
if (relativeId < 4) return true; // Easy spawns on everything
if (relativeId < 8) {
return selectedDifficulty != Difficulty.canIPlayDaddy; // Medium/Hard
}
if (relativeId < 12) {
return selectedDifficulty == Difficulty.iAmDeathIncarnate; // Hard only
}
// 12-15 are typically "Ambush" versions of the Easy/Medium/Hard guards
return true;
}
/// Determines the spawn orientation of an enemy or player.
/// Determines the spawn orientation of an enemy or player.
static double getAngle(int id) {
// Player spawn angles
switch (id) {
case playerNorth:
return CardinalDirection.north.radians;
case playerEast:
return CardinalDirection.east.radians;
case playerSouth:
return CardinalDirection.south.radians;
case playerWest:
return CardinalDirection.west.radians;
}
if (id < 108 || id > 213) return 0.0;
// Enemy directions are mapped in groups of 4
return CardinalDirection.fromEnemyIndex(id - 108).radians;
}
} }

View File

@@ -25,7 +25,7 @@ class PushwallManager {
for (int y = 0; y < objectGrid.length; y++) { for (int y = 0; y < objectGrid.length; y++) {
for (int x = 0; x < objectGrid[y].length; x++) { for (int x = 0; x < objectGrid[y].length; x++) {
if (objectGrid[y][x] == MapObjectId.secretDoor) { if (objectGrid[y][x] == MapObject.pushwallTrigger) {
pushwalls['$x,$y'] = Pushwall(x, y, wallGrid[y][x]); pushwalls['$x,$y'] = Pushwall(x, y, wallGrid[y][x]);
} }
} }

View File

@@ -1,6 +1,7 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:wolf_dart/classes/matrix.dart'; import 'package:wolf_dart/classes/matrix.dart';
import 'package:wolf_dart/classes/sprite.dart';
class VswapParser { class VswapParser {
/// Extracts the 64x64 wall textures from VSWAP.WL1 /// Extracts the 64x64 wall textures from VSWAP.WL1
@@ -17,7 +18,7 @@ class VswapParser {
} }
// 3. Extract the Wall Textures // 3. Extract the Wall Textures
List<List<List<int>>> textures = []; List<Sprite> textures = [];
// Walls are chunks 0 through (spriteStart - 1) // Walls are chunks 0 through (spriteStart - 1)
for (int i = 0; i < spriteStart; i++) { for (int i = 0; i < spriteStart; i++) {
@@ -26,7 +27,7 @@ class VswapParser {
// Walls are always exactly 64x64 pixels (4096 bytes) // Walls are always exactly 64x64 pixels (4096 bytes)
// Note: Wolf3D stores pixels in COLUMN-MAJOR order (Top to bottom, then left to right) // Note: Wolf3D stores pixels in COLUMN-MAJOR order (Top to bottom, then left to right)
List<List<int>> texture = List.generate(64, (_) => List.filled(64, 0)); Sprite texture = List.generate(64, (_) => List.filled(64, 0));
for (int x = 0; x < 64; x++) { for (int x = 0; x < 64; x++) {
for (int y = 0; y < 64; y++) { for (int y = 0; y < 64; y++) {

View File

@@ -18,14 +18,18 @@ class WolfMap {
); );
/// Asynchronously loads the map files and parses them into a new WolfMap instance. /// Asynchronously loads the map files and parses them into a new WolfMap instance.
static Future<WolfMap> loadDemo() async { static Future<WolfMap> loadShareware() async {
// 1. Load the binary data // 1. Load the binary data
final mapHead = await rootBundle.load("assets/MAPHEAD.WL1"); final mapHead = await rootBundle.load("assets/MAPHEAD.WL1");
final gameMaps = await rootBundle.load("assets/GAMEMAPS.WL1"); final gameMaps = await rootBundle.load("assets/GAMEMAPS.WL1");
final vswap = await rootBundle.load("assets/VSWAP.WL1"); final vswap = await rootBundle.load("assets/VSWAP.WL1");
// 2. Parse the data using the parser we just built // 2. Parse the data using the parser we just built
final parsedLevels = WolfMapParser.parseMaps(mapHead, gameMaps); final parsedLevels = WolfMapParser.parseMaps(
mapHead,
gameMaps,
isShareware: true,
);
final parsedTextures = VswapParser.parseWalls(vswap); final parsedTextures = VswapParser.parseWalls(vswap);
final parsedSprites = VswapParser.parseSprites(vswap); final parsedSprites = VswapParser.parseSprites(vswap);
@@ -38,7 +42,7 @@ class WolfMap {
} }
/// Asynchronously loads the map files and parses them into a new WolfMap instance. /// Asynchronously loads the map files and parses them into a new WolfMap instance.
static Future<WolfMap> load() async { static Future<WolfMap> loadRetail() async {
// 1. Load the binary data // 1. Load the binary data
final mapHead = await rootBundle.load("assets/MAPHEAD.WL6"); final mapHead = await rootBundle.load("assets/MAPHEAD.WL6");
final gameMaps = await rootBundle.load("assets/GAMEMAPS.WL6"); final gameMaps = await rootBundle.load("assets/GAMEMAPS.WL6");

View File

@@ -2,11 +2,16 @@ import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:wolf_dart/classes/matrix.dart'; import 'package:wolf_dart/classes/matrix.dart';
import 'package:wolf_dart/features/entities/map_objects.dart';
import 'package:wolf_dart/features/map/wolf_level.dart'; import 'package:wolf_dart/features/map/wolf_level.dart';
abstract class WolfMapParser { abstract class WolfMapParser {
/// Parses MAPHEAD and GAMEMAPS to extract the raw level data. /// Parses MAPHEAD and GAMEMAPS to extract the raw level data.
static List<WolfLevel> parseMaps(ByteData mapHead, ByteData gameMaps) { static List<WolfLevel> parseMaps(
ByteData mapHead,
ByteData gameMaps, {
bool isShareware = false,
}) {
List<WolfLevel> levels = []; List<WolfLevel> levels = [];
// 1. READ MAPHEAD // 1. READ MAPHEAD
@@ -66,6 +71,21 @@ abstract class WolfMapParser {
Uint16List carmackExpandedObjects = _expandCarmack(compressedObjectData); Uint16List carmackExpandedObjects = _expandCarmack(compressedObjectData);
List<int> flatObjectGrid = _expandRlew(carmackExpandedObjects, rlewTag); List<int> flatObjectGrid = _expandRlew(carmackExpandedObjects, rlewTag);
for (int i = 0; i < flatObjectGrid.length; i++) {
int id = flatObjectGrid[i];
// Handle the 'secret' pushwalls (Logic check)
if (id == MapObject.pushwallTrigger) {
// In Wolf3D, ID 98 means the wall at this same index in Plane 0 is pushable.
// You might want to mark this in your engine state.
}
// Filter out invalid IDs for Shareware to prevent crashes
if (isShareware && !MapObject.isSharewareCompatible(id)) {
flatObjectGrid[i] = 0; // Turn unknown objects into empty space
}
}
Matrix<int> wallGrid = []; Matrix<int> wallGrid = [];
Matrix<int> objectGrid = []; // NEW Matrix<int> objectGrid = []; // NEW

View File

@@ -132,7 +132,7 @@ class Player {
switch (item.type) { switch (item.type) {
case CollectibleType.health: case CollectibleType.health:
if (health >= 100) return false; if (health >= 100) return false;
heal(item.mapId == MapObjectId.dogFood ? 4 : 25); heal(item.mapId == MapObject.dogFoodDecoration ? 4 : 25);
pickedUp = true; pickedUp = true;
break; break;
@@ -147,11 +147,11 @@ class Player {
break; break;
case CollectibleType.treasure: case CollectibleType.treasure:
if (item.mapId == MapObjectId.cross) score += 100; if (item.mapId == MapObject.cross) score += 100;
if (item.mapId == MapObjectId.chalice) score += 500; if (item.mapId == MapObject.chalice) score += 500;
if (item.mapId == MapObjectId.chest) score += 1000; if (item.mapId == MapObject.chest) score += 1000;
if (item.mapId == MapObjectId.crown) score += 5000; if (item.mapId == MapObject.crown) score += 5000;
if (item.mapId == MapObjectId.extraLife) { if (item.mapId == MapObject.extraLife) {
heal(100); heal(100);
addAmmo(25); addAmmo(25);
} }
@@ -159,7 +159,7 @@ class Player {
break; break;
case CollectibleType.weapon: case CollectibleType.weapon:
if (item.mapId == MapObjectId.machineGun) { if (item.mapId == MapObject.machineGun) {
if (weapons[WeaponType.machineGun] == null) { if (weapons[WeaponType.machineGun] == null) {
weapons[WeaponType.machineGun] = MachineGun(); weapons[WeaponType.machineGun] = MachineGun();
hasMachineGun = true; hasMachineGun = true;
@@ -168,7 +168,7 @@ class Player {
requestWeaponSwitch(WeaponType.machineGun); requestWeaponSwitch(WeaponType.machineGun);
pickedUp = true; pickedUp = true;
} }
if (item.mapId == MapObjectId.chainGun) { if (item.mapId == MapObject.chainGun) {
if (weapons[WeaponType.chainGun] == null) { if (weapons[WeaponType.chainGun] == null) {
weapons[WeaponType.chainGun] = ChainGun(); weapons[WeaponType.chainGun] = ChainGun();
hasChainGun = true; hasChainGun = true;
@@ -180,8 +180,8 @@ class Player {
break; break;
case CollectibleType.key: case CollectibleType.key:
if (item.mapId == MapObjectId.goldKey) hasGoldKey = true; if (item.mapId == MapObject.goldKey) hasGoldKey = true;
if (item.mapId == MapObjectId.silverKey) hasSilverKey = true; if (item.mapId == MapObject.silverKey) hasSilverKey = true;
pickedUp = true; pickedUp = true;
break; break;
} }

View File

@@ -23,14 +23,14 @@ import 'package:wolf_dart/sprite_gallery.dart';
class WolfRenderer extends StatefulWidget { class WolfRenderer extends StatefulWidget {
const WolfRenderer({ const WolfRenderer({
super.key, super.key,
required this.difficulty, this.difficulty = Difficulty.bringEmOn,
this.showSpriteGallery = false, this.showSpriteGallery = false,
this.isDemo = true, this.isShareware = true,
}); });
final Difficulty difficulty; final Difficulty difficulty;
final bool showSpriteGallery; final bool showSpriteGallery;
final bool isDemo; final bool isShareware;
@override @override
State<WolfRenderer> createState() => _WolfRendererState(); State<WolfRenderer> createState() => _WolfRendererState();
@@ -60,11 +60,13 @@ class _WolfRendererState extends State<WolfRenderer>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_initGame(demo: widget.isDemo); _initGame(widget.isShareware);
} }
Future<void> _initGame({bool demo = true}) async { Future<void> _initGame(bool isShareware) async {
gameMap = demo ? await WolfMap.loadDemo() : await WolfMap.load(); gameMap = isShareware
? await WolfMap.loadShareware()
: await WolfMap.loadRetail();
currentLevel = gameMap.levels[0].wallGrid; currentLevel = gameMap.levels[0].wallGrid;
doorManager.initDoors(currentLevel); doorManager.initDoors(currentLevel);
@@ -77,30 +79,35 @@ 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 (objId >= MapObjectId.playerNorth && if (!MapObject.shouldSpawn(objId, widget.difficulty)) continue;
objId <= MapObjectId.playerWest) {
if (objId >= MapObject.playerNorth && objId <= MapObject.playerWest) {
double spawnAngle = 0.0; double spawnAngle = 0.0;
switch (objId) { switch (objId) {
case MapObjectId.playerNorth: case MapObject.playerNorth:
spawnAngle = 3 * math.pi / 2; spawnAngle = 3 * math.pi / 2;
break; break;
case MapObjectId.playerEast: case MapObject.playerEast:
spawnAngle = 0.0; spawnAngle = 0.0;
break; break;
case MapObjectId.playerSouth: case MapObject.playerSouth:
spawnAngle = math.pi / 2; spawnAngle = math.pi / 2;
break; break;
case MapObjectId.playerWest: case MapObject.playerWest:
spawnAngle = math.pi; spawnAngle = math.pi;
break; break;
} }
player = Player(x: x + 0.5, y: y + 0.5, angle: spawnAngle); player = Player(
x: x + 0.5,
y: y + 0.5,
angle: spawnAngle,
);
} else { } else {
Entity? newEntity = EntityRegistry.spawn( Entity? newEntity = EntityRegistry.spawn(
objId, objId,
x + 0.5, x + 0.5,
y + 0.5, y + 0.5,
widget.difficulty.level, widget.difficulty,
gameMap.sprites.length, gameMap.sprites.length,
); );
@@ -342,10 +349,10 @@ class _WolfRendererState extends State<WolfRenderer>
// Map ID 44 is usually the Ammo Clip in the Object Grid/Registry // Map ID 44 is usually the Ammo Clip in the Object Grid/Registry
Entity? droppedAmmo = EntityRegistry.spawn( Entity? droppedAmmo = EntityRegistry.spawn(
MapObjectId.ammoClip, MapObject.ammoClip,
entity.x, entity.x,
entity.y, entity.y,
widget.difficulty.level, widget.difficulty,
gameMap.sprites.length, gameMap.sprites.length,
); );
@@ -436,28 +443,17 @@ class _WolfRendererState extends State<WolfRenderer>
right: 0, right: 0,
child: Center( child: Center(
child: Transform.translate( child: Transform.translate(
offset: Offset( offset: Offset(0, player.weaponAnimOffset),
// Replaced hidden step variables with a direct intention check!
(inputManager.isMovingForward ||
inputManager.isMovingBackward)
? math.sin(
DateTime.now()
.millisecondsSinceEpoch /
100,
) *
12
: 0,
player.weaponAnimOffset,
),
child: SizedBox( child: SizedBox(
width: 500, width: 500,
height: 500, height: 500,
child: CustomPaint( child: CustomPaint(
painter: WeaponPainter( painter: WeaponPainter(
sprite: sprite:
gameMap.sprites[player gameMap.sprites[player.currentWeapon
.currentWeapon .getCurrentSpriteIndex(
.currentSprite], gameMap.sprites.length,
)],
), ),
), ),
), ),

View File

@@ -1,8 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:wolf_dart/classes/sprite.dart';
import 'package:wolf_dart/features/renderer/color_palette.dart'; import 'package:wolf_dart/features/renderer/color_palette.dart';
class WeaponPainter extends CustomPainter { class WeaponPainter extends CustomPainter {
final List<List<int>> sprite; final Sprite? sprite;
// Initialize a reusable Paint object and disable anti-aliasing to keep the // Initialize a reusable Paint object and disable anti-aliasing to keep the
// pixels perfectly sharp and chunky. // pixels perfectly sharp and chunky.
@@ -14,6 +15,8 @@ class WeaponPainter extends CustomPainter {
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
if (sprite == null) return;
// Calculate width and height separately in case the container isn't a // Calculate width and height separately in case the container isn't a
// perfect square // perfect square
double pixelWidth = size.width / 64; double pixelWidth = size.width / 64;
@@ -21,7 +24,7 @@ class WeaponPainter extends CustomPainter {
for (int x = 0; x < 64; x++) { for (int x = 0; x < 64; x++) {
for (int y = 0; y < 64; y++) { for (int y = 0; y < 64; y++) {
int colorByte = sprite[x][y]; int colorByte = sprite![x][y];
if (colorByte != 255) { if (colorByte != 255) {
// 255 is our transparent magenta // 255 is our transparent magenta

View File

@@ -30,8 +30,24 @@ abstract class Weapon {
this.isAutomatic = true, this.isAutomatic = true,
}); });
int get currentSprite => int getCurrentSpriteIndex(int maxSprites) {
state == WeaponState.idle ? idleSprite : fireFrames[frameIndex]; int baseSprite = state == WeaponState.idle
? idleSprite
: fireFrames[frameIndex];
// Retail VSWAP typically has exactly 436 sprites (indices 0 to 435).
// The 20 weapon sprites are ALWAYS placed at the very end of the sprite block.
// This dynamically aligns the base index to the end of any VSWAP file!
int dynamicOffset = 436 - maxSprites;
int calculatedIndex = baseSprite - dynamicOffset;
// Safety check!
if (calculatedIndex < 0 || calculatedIndex >= maxSprites) {
print("WARNING: Weapon sprite index $calculatedIndex out of bounds!");
return 0;
}
return calculatedIndex;
}
void releaseTrigger() { void releaseTrigger() {
_triggerReleased = true; _triggerReleased = true;