Eliminating side-effects

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-14 00:14:49 +01:00
parent aa686fb016
commit 0fe530e58e

View File

@@ -51,13 +51,8 @@ class _WolfRendererState extends State<WolfRenderer>
List<Entity> entities = []; List<Entity> entities = [];
// Track door animations
// Key is "X,Y" coordinate. Value is how far open it is (0.0 to 1.0)
Map<String, double> doorOffsets = {}; Map<String, double> doorOffsets = {};
Map<String, int> doorStates = {}; // 1 = opening, 2 = fully open Map<String, int> doorStates = {};
double moveStepX = 0;
double moveStepY = 0;
@override @override
void initState() { void initState() {
@@ -66,20 +61,15 @@ class _WolfRendererState extends State<WolfRenderer>
} }
Future<void> _initGame({bool demo = true}) async { Future<void> _initGame({bool demo = true}) async {
// 1. Load the entire WL1 (demo)/WL6 (retail) data
gameMap = demo ? await WolfMap.loadDemo() : await WolfMap.load(); gameMap = demo ? await WolfMap.loadDemo() : await WolfMap.load();
// 2. Extract Level 1 (E1M1)
currentLevel = gameMap.levels[0].wallGrid; currentLevel = gameMap.levels[0].wallGrid;
final Matrix<int> objectLevel = gameMap.levels[0].objectGrid; final Matrix<int> objectLevel = gameMap.levels[0].objectGrid;
// 1. SCAN FOR PLAYER SPAWN & ENTITIES
for (int y = 0; y < 64; y++) { for (int y = 0; y < 64; y++) {
for (int x = 0; x < 64; x++) { for (int x = 0; x < 64; x++) {
int objId = objectLevel[y][x]; int objId = objectLevel[y][x];
// Player Spawn
if (objId >= 19 && objId <= 22) { if (objId >= 19 && objId <= 22) {
double spawnAngle = 0.0; double spawnAngle = 0.0;
switch (objId) { switch (objId) {
@@ -117,7 +107,6 @@ class _WolfRendererState extends State<WolfRenderer>
} }
} }
// 2. CLEAN UP WALLS / PRESERVE DOORS
for (int y = 0; y < 64; y++) { for (int y = 0; y < 64; y++) {
for (int x = 0; x < 64; x++) { for (int x = 0; x < 64; x++) {
int id = currentLevel[y][x]; int id = currentLevel[y][x];
@@ -129,7 +118,6 @@ class _WolfRendererState extends State<WolfRenderer>
} }
} }
// 4. Start the game!
_bumpPlayerIfStuck(); _bumpPlayerIfStuck();
_gameLoop = createTicker(_tick)..start(); _gameLoop = createTicker(_tick)..start();
_focusNode.requestFocus(); _focusNode.requestFocus();
@@ -180,10 +168,9 @@ class _WolfRendererState extends State<WolfRenderer>
} }
bool _isWalkable(int x, int y) { bool _isWalkable(int x, int y) {
if (currentLevel[y][x] == 0) return true; // Empty space if (currentLevel[y][x] == 0) return true;
if (currentLevel[y][x] >= 90) { if (currentLevel[y][x] >= 90) {
String key = '$x,$y'; String key = '$x,$y';
// Allow the player to walk through if the door is > 70% open
if (doorOffsets[key] != null && doorOffsets[key]! > 0.7) { if (doorOffsets[key] != null && doorOffsets[key]! > 0.7) {
return true; return true;
} }
@@ -191,16 +178,19 @@ class _WolfRendererState extends State<WolfRenderer>
return false; return false;
} }
// --- ORCHESTRATOR ---
void _tick(Duration elapsed) { void _tick(Duration elapsed) {
// 1. Inputs & Intentions // 1. Process intentions and receive movement vectors
_processInputs(elapsed); final movement = _processInputs(elapsed);
// 2. State Updates // 2. Explicit State Updates
player.updateWeaponSwitch(); player.updateWeaponSwitch();
_updateDoors(); _updateDoors();
_applyMovementAndCollision(); _applyMovementAndCollision(movement.dx, movement.dy);
_updateEntities(elapsed); _updateEntities(elapsed);
_updateScreenEffects();
// Explicit reassignment from a pure(r) function
damageFlashOpacity = _calculateScreenEffects(damageFlashOpacity);
// 3. Combat // 3. Combat
if (player.updateWeapon(elapsed.inMilliseconds)) { if (player.updateWeapon(elapsed.inMilliseconds)) {
@@ -211,7 +201,6 @@ class _WolfRendererState extends State<WolfRenderer>
setState(() {}); setState(() {});
} }
// A helper method to handle getting shot
void _takeDamage(int damage) { void _takeDamage(int damage) {
player.takeDamage(damage); player.takeDamage(damage);
damageFlashOpacity = 0.5; damageFlashOpacity = 0.5;
@@ -219,16 +208,14 @@ class _WolfRendererState extends State<WolfRenderer>
void _performRaycastAttack(Duration elapsed) { void _performRaycastAttack(Duration elapsed) {
Enemy? closestEnemy; Enemy? closestEnemy;
double minDistance = 15.0; // Maximum range of the gun double minDistance = 15.0;
for (Entity entity in entities) { for (Entity entity in entities) {
if (entity is Enemy && entity.state != EntityState.dead) { if (entity is Enemy && entity.state != EntityState.dead) {
// 1. Calculate the angle from player to enemy
double dx = entity.x - player.x; double dx = entity.x - player.x;
double dy = entity.y - player.y; double dy = entity.y - player.y;
double angleToEnemy = math.atan2(dy, dx); double angleToEnemy = math.atan2(dy, dx);
// 2. Check if that angle is close to our player's aiming angle
double angleDiff = player.angle - angleToEnemy; double angleDiff = player.angle - angleToEnemy;
while (angleDiff <= -math.pi) { while (angleDiff <= -math.pi) {
angleDiff += 2 * math.pi; angleDiff += 2 * math.pi;
@@ -237,13 +224,10 @@ class _WolfRendererState extends State<WolfRenderer>
angleDiff -= 2 * math.pi; angleDiff -= 2 * math.pi;
} }
// 3. Simple bounding box check (approx 0.4 units wide)
double dist = math.sqrt(dx * dx + dy * dy); double dist = math.sqrt(dx * dx + dy * dy);
double threshold = double threshold = 0.2 / dist;
0.2 / dist; // Smaller threshold as they get further away
if (angleDiff.abs() < threshold) { if (angleDiff.abs() < threshold) {
// 4. Ensure there is no wall between you and the enemy
if (_hasLineOfSightToEnemy(entity, dist)) { if (_hasLineOfSightToEnemy(entity, dist)) {
if (dist < minDistance) { if (dist < minDistance) {
minDistance = dist; minDistance = dist;
@@ -260,7 +244,6 @@ class _WolfRendererState extends State<WolfRenderer>
elapsed.inMilliseconds, elapsed.inMilliseconds,
); );
// If the shot was fatal, reward the player
if (closestEnemy.state == EntityState.dead) { if (closestEnemy.state == EntityState.dead) {
player.score += 100; player.score += 100;
} }
@@ -279,45 +262,38 @@ class _WolfRendererState extends State<WolfRenderer>
return true; return true;
} }
void _processInputs(Duration elapsed) { // Returns movement deltas instead of modifying class variables
({double dx, double dy}) _processInputs(Duration elapsed) {
inputManager.update(); inputManager.update();
const double moveSpeed = 0.16; const double moveSpeed = 0.16;
const double turnSpeed = 0.12; const double turnSpeed = 0.12;
double dx = 0;
double dy = 0;
// Reset steps each tick
moveStepX = 0;
moveStepY = 0;
// 1. Weapon Switching
if (inputManager.requestedWeapon != null) { if (inputManager.requestedWeapon != null) {
player.requestWeaponSwitch(inputManager.requestedWeapon!); player.requestWeaponSwitch(inputManager.requestedWeapon!);
} }
// 2. Firing
if (inputManager.isFiring) { if (inputManager.isFiring) {
player.fire(elapsed.inMilliseconds); player.fire(elapsed.inMilliseconds);
} }
// 3. Movement
if (inputManager.isMovingForward) { if (inputManager.isMovingForward) {
moveStepX += math.cos(player.angle) * moveSpeed; dx += math.cos(player.angle) * moveSpeed;
moveStepY += math.sin(player.angle) * moveSpeed; dy += math.sin(player.angle) * moveSpeed;
} }
if (inputManager.isMovingBackward) { if (inputManager.isMovingBackward) {
moveStepX -= math.cos(player.angle) * moveSpeed; dx -= math.cos(player.angle) * moveSpeed;
moveStepY -= math.sin(player.angle) * moveSpeed; dy -= math.sin(player.angle) * moveSpeed;
} }
// 4. Turning
if (inputManager.isTurningLeft) player.angle -= turnSpeed; if (inputManager.isTurningLeft) player.angle -= turnSpeed;
if (inputManager.isTurningRight) player.angle += turnSpeed; if (inputManager.isTurningRight) player.angle += turnSpeed;
// Keep angle wrapped cleanly
if (player.angle < 0) player.angle += 2 * math.pi; if (player.angle < 0) player.angle += 2 * math.pi;
if (player.angle > 2 * math.pi) player.angle -= 2 * math.pi; if (player.angle > 2 * math.pi) player.angle -= 2 * math.pi;
// 5. Interaction (Doors)
if (inputManager.isInteracting) { if (inputManager.isInteracting) {
int targetX = (player.x + math.cos(player.angle)).toInt(); int targetX = (player.x + math.cos(player.angle)).toInt();
int targetY = (player.y + math.sin(player.angle)).toInt(); int targetY = (player.y + math.sin(player.angle)).toInt();
@@ -328,46 +304,41 @@ class _WolfRendererState extends State<WolfRenderer>
targetX < currentLevel[0].length) { targetX < currentLevel[0].length) {
if (currentLevel[targetY][targetX] >= 90) { if (currentLevel[targetY][targetX] >= 90) {
String key = '$targetX,$targetY'; String key = '$targetX,$targetY';
// Start the animation if it isn't already opening!
if (!doorStates.containsKey(key) || doorStates[key] == 0) { if (!doorStates.containsKey(key) || doorStates[key] == 0) {
doorStates[key] = 1; doorStates[key] = 1;
} }
} }
} }
} }
return (dx: dx, dy: dy);
} }
void _updateDoors() { void _updateDoors() {
doorStates.forEach((key, state) { doorStates.forEach((key, state) {
if (state == 1) { if (state == 1) {
// 1 = opening doorOffsets[key] = (doorOffsets[key] ?? 0.0) + 0.02;
doorOffsets[key] = (doorOffsets[key] ?? 0.0) + 0.02; // Slide speed
if (doorOffsets[key]! >= 1.0) { if (doorOffsets[key]! >= 1.0) {
doorOffsets[key] = 1.0; doorOffsets[key] = 1.0;
doorStates[key] = 2; // Mark as fully open doorStates[key] = 2;
} }
} }
}); });
} }
void _applyMovementAndCollision() { // Now receives dx and dy explicitly
void _applyMovementAndCollision(double dx, double dy) {
const double margin = 0.3; const double margin = 0.3;
// X-axis collision double newX = player.x + dx;
double newX = player.x + moveStepX; int checkX = (dx > 0) ? (newX + margin).toInt() : (newX - margin).toInt();
int checkX = (moveStepX > 0)
? (newX + margin).toInt()
: (newX - margin).toInt();
if (_isWalkable(checkX, player.y.toInt())) { if (_isWalkable(checkX, player.y.toInt())) {
player.x = newX; player.x = newX;
} }
// Y-axis collision double newY = player.y + dy;
double newY = player.y + moveStepY; int checkY = (dy > 0) ? (newY + margin).toInt() : (newY - margin).toInt();
int checkY = (moveStepY > 0)
? (newY + margin).toInt()
: (newY - margin).toInt();
if (_isWalkable(player.x.toInt(), checkY)) { if (_isWalkable(player.x.toInt(), checkY)) {
player.y = newY; player.y = newY;
@@ -385,14 +356,11 @@ class _WolfRendererState extends State<WolfRenderer>
isWalkable: _isWalkable, isWalkable: _isWalkable,
onDamagePlayer: _takeDamage, onDamagePlayer: _takeDamage,
); );
} } else if (entity is Collectible) {
// Collectible Interaction Logic
else if (entity is Collectible) {
double dx = player.x - entity.x; double dx = player.x - entity.x;
double dy = player.y - entity.y; double dy = player.y - entity.y;
double dist = math.sqrt(dx * dx + dy * dy); double dist = math.sqrt(dx * dx + dy * dy);
// If player is close enough to the item
if (dist < 0.5) { if (dist < 0.5) {
if (player.tryPickup(entity)) { if (player.tryPickup(entity)) {
itemsToRemove.add(entity); itemsToRemove.add(entity);
@@ -401,17 +369,17 @@ class _WolfRendererState extends State<WolfRenderer>
} }
} }
// Remove the items that were successfully picked up
if (itemsToRemove.isNotEmpty) { if (itemsToRemove.isNotEmpty) {
entities.removeWhere((e) => itemsToRemove.contains(e)); entities.removeWhere((e) => itemsToRemove.contains(e));
} }
} }
void _updateScreenEffects() { // Takes an input and returns a value instead of implicitly changing state
// Fade out the damage flash smoothly double _calculateScreenEffects(double currentOpacity) {
if (damageFlashOpacity > 0) { if (currentOpacity > 0) {
damageFlashOpacity = math.max(0.0, damageFlashOpacity - 0.05); return math.max(0.0, currentOpacity - 0.05);
} }
return currentOpacity;
} }
@override @override
@@ -432,7 +400,6 @@ class _WolfRendererState extends State<WolfRenderer>
onKeyEvent: (_) {}, onKeyEvent: (_) {},
child: Column( child: Column(
children: [ children: [
// Game view
Expanded( Expanded(
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
@@ -456,7 +423,6 @@ class _WolfRendererState extends State<WolfRenderer>
sprites: gameMap.sprites, sprites: gameMap.sprites,
), ),
), ),
// Weapon Viewmodel
Positioned( Positioned(
bottom: -20, bottom: -20,
left: 0, left: 0,
@@ -464,8 +430,9 @@ class _WolfRendererState extends State<WolfRenderer>
child: Center( child: Center(
child: Transform.translate( child: Transform.translate(
offset: Offset( offset: Offset(
// Bobbing math // Replaced hidden step variables with a direct intention check!
(moveStepX.abs() + moveStepY.abs()) > 0 (inputManager.isMovingForward ||
inputManager.isMovingBackward)
? math.sin( ? math.sin(
DateTime.now() DateTime.now()
.millisecondsSinceEpoch / .millisecondsSinceEpoch /
@@ -473,7 +440,6 @@ class _WolfRendererState extends State<WolfRenderer>
) * ) *
12 12
: 0, : 0,
// Y-Offset for lowering and raising
player.weaponAnimOffset, player.weaponAnimOffset,
), ),
child: SizedBox( child: SizedBox(
@@ -506,8 +472,6 @@ class _WolfRendererState extends State<WolfRenderer>
}, },
), ),
), ),
// HUD
Hud(player: player), Hud(player: player),
], ],
), ),