@@ -3,15 +3,22 @@ import 'dart:math' as math;
|
|||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
||||||
|
|
||||||
|
/// Orchestrates the state and interaction of all doors within a level.
|
||||||
|
///
|
||||||
|
/// This manager maintains a map of [Door] objects and ensures that when a
|
||||||
|
/// door's state changes (e.g., starts closing), the appropriate global
|
||||||
|
/// game events (like sound effects) are triggered.
|
||||||
class DoorManager {
|
class DoorManager {
|
||||||
// Key is '$x,$y'
|
/// A lookup table for doors, keyed by their grid coordinates: "$x,$y".
|
||||||
final Map<String, Door> doors = {};
|
final Map<String, Door> doors = {};
|
||||||
|
|
||||||
// Callback to play sounds without tightly coupling to the audio engine
|
/// Callback used to trigger sound effects without tight coupling
|
||||||
|
/// to a specific audio engine implementation.
|
||||||
final void Function(int sfxId) onPlaySound;
|
final void Function(int sfxId) onPlaySound;
|
||||||
|
|
||||||
DoorManager({required this.onPlaySound});
|
DoorManager({required this.onPlaySound});
|
||||||
|
|
||||||
|
/// Scans the [wallGrid] for tile IDs >= 90 and initializes [Door] instances.
|
||||||
void initDoors(SpriteMap wallGrid) {
|
void initDoors(SpriteMap wallGrid) {
|
||||||
doors.clear();
|
doors.clear();
|
||||||
for (int y = 0; y < wallGrid.length; y++) {
|
for (int y = 0; y < wallGrid.length; y++) {
|
||||||
@@ -24,17 +31,19 @@ class DoorManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates all managed doors and plays the "close" sound if a door
|
||||||
|
/// naturally begins its closing cycle.
|
||||||
void update(Duration elapsed) {
|
void update(Duration elapsed) {
|
||||||
for (final door in doors.values) {
|
for (final door in doors.values) {
|
||||||
final newState = door.update(elapsed.inMilliseconds);
|
final newState = door.update(elapsed.inMilliseconds);
|
||||||
|
|
||||||
// The Manager decides: "If a door just started closing, play the close sound."
|
|
||||||
if (newState == DoorState.closing) {
|
if (newState == DoorState.closing) {
|
||||||
onPlaySound(WolfSound.closeDoor);
|
onPlaySound(WolfSound.closeDoor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handles player-initiated interaction with doors based on facing direction.
|
||||||
void handleInteraction(double playerX, double playerY, double playerAngle) {
|
void handleInteraction(double playerX, double playerY, double playerAngle) {
|
||||||
int targetX = (playerX + math.cos(playerAngle)).toInt();
|
int targetX = (playerX + math.cos(playerAngle)).toInt();
|
||||||
int targetY = (playerY + math.sin(playerAngle)).toInt();
|
int targetY = (playerY + math.sin(playerAngle)).toInt();
|
||||||
@@ -42,14 +51,15 @@ class DoorManager {
|
|||||||
String key = '$targetX,$targetY';
|
String key = '$targetX,$targetY';
|
||||||
if (doors.containsKey(key)) {
|
if (doors.containsKey(key)) {
|
||||||
if (doors[key]!.interact()) {
|
if (doors[key]!.interact()) {
|
||||||
// The Manager decides: "Player successfully opened a door, play the sound."
|
|
||||||
onPlaySound(WolfSound.openDoor);
|
onPlaySound(WolfSound.openDoor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Attempted by AI entities to open a door blocking their path.
|
||||||
void tryOpenDoor(int x, int y) {
|
void tryOpenDoor(int x, int y) {
|
||||||
String key = '$x,$y';
|
String key = '$x,$y';
|
||||||
|
// AI only interacts if the door is currently fully closed (offset == 0).
|
||||||
if (doors.containsKey(key) && doors[key]!.offset == 0.0) {
|
if (doors.containsKey(key) && doors[key]!.offset == 0.0) {
|
||||||
if (doors[key]!.interact()) {
|
if (doors[key]!.interact()) {
|
||||||
onPlaySound(WolfSound.openDoor);
|
onPlaySound(WolfSound.openDoor);
|
||||||
@@ -68,12 +78,14 @@ class DoorManager {
|
|||||||
return offsets;
|
return offsets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if the door at [x], [y] is sufficiently open for
|
||||||
|
/// an entity (player or enemy) to walk through.
|
||||||
bool isDoorOpenEnough(int x, int y) {
|
bool isDoorOpenEnough(int x, int y) {
|
||||||
String key = '$x,$y';
|
String key = '$x,$y';
|
||||||
if (doors.containsKey(key)) {
|
if (doors.containsKey(key)) {
|
||||||
// 0.7 offset means 70% open, similar to the original engine's check
|
// 0.7 (70% open) is the standard collision threshold.
|
||||||
return doors[key]!.offset > 0.7;
|
return doors[key]!.offset > 0.7;
|
||||||
}
|
}
|
||||||
return false; // Not a door we manage
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:math' as math;
|
|||||||
|
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
|
||||||
|
/// Represents a secret wall that slides when triggered by the player.
|
||||||
class Pushwall {
|
class Pushwall {
|
||||||
int x;
|
int x;
|
||||||
int y;
|
int y;
|
||||||
@@ -14,16 +15,21 @@ class Pushwall {
|
|||||||
Pushwall(this.x, this.y, this.mapId);
|
Pushwall(this.x, this.y, this.mapId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Manages the detection and real-time movement of secret pushwalls.
|
||||||
|
///
|
||||||
|
/// Only one pushwall can be active (moving) at any given time.
|
||||||
class PushwallManager {
|
class PushwallManager {
|
||||||
final Map<String, Pushwall> pushwalls = {};
|
final Map<String, Pushwall> pushwalls = {};
|
||||||
Pushwall? activePushwall;
|
Pushwall? activePushwall;
|
||||||
|
|
||||||
|
/// Populates the lookup table using the level's object grid.
|
||||||
void initPushwalls(SpriteMap wallGrid, SpriteMap objectGrid) {
|
void initPushwalls(SpriteMap wallGrid, SpriteMap objectGrid) {
|
||||||
pushwalls.clear();
|
pushwalls.clear();
|
||||||
activePushwall = null;
|
activePushwall = null;
|
||||||
|
|
||||||
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++) {
|
||||||
|
// Pushwalls are identified by a specific trigger ID in the object layer.
|
||||||
if (objectGrid[y][x] == MapObject.pushwallTrigger) {
|
if (objectGrid[y][x] == MapObject.pushwallTrigger) {
|
||||||
pushwalls['$x,$y'] = Pushwall(x, y, wallGrid[y][x]);
|
pushwalls['$x,$y'] = Pushwall(x, y, wallGrid[y][x]);
|
||||||
}
|
}
|
||||||
@@ -31,16 +37,16 @@ class PushwallManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Logic for sliding the wall and updating the physical [wallGrid] for collisions.
|
||||||
void update(Duration elapsed, SpriteMap wallGrid) {
|
void update(Duration elapsed, SpriteMap wallGrid) {
|
||||||
if (activePushwall == null) return;
|
if (activePushwall == null) return;
|
||||||
final pw = activePushwall!;
|
final pw = activePushwall!;
|
||||||
|
|
||||||
// Original logic: 1/128 tile per tick.
|
// Movement speed matches the original DOS executable (roughly 0.54 tiles/sec).
|
||||||
// At 70 ticks/sec, that is roughly 0.54 tiles per second.
|
|
||||||
const double originalSpeed = 0.546875;
|
const double originalSpeed = 0.546875;
|
||||||
pw.offset += (elapsed.inMilliseconds / 1000.0) * originalSpeed;
|
pw.offset += (elapsed.inMilliseconds / 1000.0) * originalSpeed;
|
||||||
|
|
||||||
// Once it crosses a full tile boundary, we update the collision grid!
|
// Handle the transition between grid tiles.
|
||||||
if (pw.offset >= 1.0) {
|
if (pw.offset >= 1.0) {
|
||||||
pw.offset -= 1.0;
|
pw.offset -= 1.0;
|
||||||
pw.tilesMoved++;
|
pw.tilesMoved++;
|
||||||
@@ -48,17 +54,19 @@ class PushwallManager {
|
|||||||
int nextX = pw.x + pw.dirX;
|
int nextX = pw.x + pw.dirX;
|
||||||
int nextY = pw.y + pw.dirY;
|
int nextY = pw.y + pw.dirY;
|
||||||
|
|
||||||
// Move the solid block in the physical grid
|
// Update structural grid: the wall is now "solid" in the new tile.
|
||||||
wallGrid[nextY][nextX] = pw.mapId;
|
wallGrid[nextY][nextX] = pw.mapId;
|
||||||
wallGrid[pw.y][pw.x] = 0; // Clear the old space so the player can walk in
|
|
||||||
|
|
||||||
// Update the dictionary key
|
// The previous tile is now walkable.
|
||||||
|
wallGrid[pw.y][pw.x] = 0;
|
||||||
|
|
||||||
|
// Update lookup keys
|
||||||
pushwalls.remove('${pw.x},${pw.y}');
|
pushwalls.remove('${pw.x},${pw.y}');
|
||||||
pw.x = nextX;
|
pw.x = nextX;
|
||||||
pw.y = nextY;
|
pw.y = nextY;
|
||||||
pushwalls['${pw.x},${pw.y}'] = pw;
|
pushwalls['${pw.x},${pw.y}'] = pw;
|
||||||
|
|
||||||
// Check if we should keep sliding
|
// Determine if movement is blocked by the world boundary or another solid tile.
|
||||||
bool blocked = false;
|
bool blocked = false;
|
||||||
int checkX = pw.x + pw.dirX;
|
int checkX = pw.x + pw.dirX;
|
||||||
int checkY = pw.y + pw.dirY;
|
int checkY = pw.y + pw.dirY;
|
||||||
@@ -69,10 +77,10 @@ class PushwallManager {
|
|||||||
checkY >= wallGrid.length) {
|
checkY >= wallGrid.length) {
|
||||||
blocked = true;
|
blocked = true;
|
||||||
} else if (wallGrid[checkY][checkX] != 0) {
|
} else if (wallGrid[checkY][checkX] != 0) {
|
||||||
blocked = true; // Blocked by another wall or a door
|
blocked = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Standard Wolf3D pushwalls move exactly 2 tiles (or 1 if blocked)
|
// Secret walls stop after moving 2 tiles or hitting an obstruction.
|
||||||
if (pw.tilesMoved >= 2 || blocked) {
|
if (pw.tilesMoved >= 2 || blocked) {
|
||||||
activePushwall = null;
|
activePushwall = null;
|
||||||
pw.offset = 0.0;
|
pw.offset = 0.0;
|
||||||
@@ -80,13 +88,13 @@ class PushwallManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Triggers a pushwall based on player proximity and facing direction.
|
||||||
void handleInteraction(
|
void handleInteraction(
|
||||||
double playerX,
|
double playerX,
|
||||||
double playerY,
|
double playerY,
|
||||||
double playerAngle,
|
double playerAngle,
|
||||||
SpriteMap wallGrid,
|
SpriteMap wallGrid,
|
||||||
) {
|
) {
|
||||||
// Only one pushwall can move at a time in the original engine!
|
|
||||||
if (activePushwall != null) return;
|
if (activePushwall != null) return;
|
||||||
|
|
||||||
int targetX = (playerX + math.cos(playerAngle)).toInt();
|
int targetX = (playerX + math.cos(playerAngle)).toInt();
|
||||||
@@ -96,7 +104,7 @@ class PushwallManager {
|
|||||||
if (pushwalls.containsKey(key)) {
|
if (pushwalls.containsKey(key)) {
|
||||||
final pw = pushwalls[key]!;
|
final pw = pushwalls[key]!;
|
||||||
|
|
||||||
// Determine the push direction based on the player's relative position
|
// Determine push direction (X or Y) based on which axis the player is closer to.
|
||||||
double dx = (targetX + 0.5) - playerX;
|
double dx = (targetX + 0.5) - playerX;
|
||||||
double dy = (targetY + 0.5) - playerY;
|
double dy = (targetY + 0.5) - playerY;
|
||||||
|
|
||||||
@@ -108,10 +116,9 @@ class PushwallManager {
|
|||||||
pw.dirY = dy > 0 ? 1 : -1;
|
pw.dirY = dy > 0 ? 1 : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the tile behind the wall is empty before starting the push
|
// Only start the push if the space immediately behind the wall is empty.
|
||||||
int checkX = targetX + pw.dirX;
|
int checkX = targetX + pw.dirX;
|
||||||
int checkY = targetY + pw.dirY;
|
int checkY = targetY + pw.dirY;
|
||||||
|
|
||||||
if (wallGrid[checkY][checkX] == 0) {
|
if (wallGrid[checkY][checkX] == 0) {
|
||||||
activePushwall = pw;
|
activePushwall = pw;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user