Add shareware support and spawn correctly for difficulty levels
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
28
lib/classes/cardinal_direction.dart
Normal file
28
lib/classes/cardinal_direction.dart
Normal 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
1
lib/classes/sprite.dart
Normal file
@@ -0,0 +1 @@
|
|||||||
|
typedef Sprite = List<List<int>>;
|
||||||
@@ -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),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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++) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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++) {
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user