From 895a9976043e05d992f480d6af10ec31733a49f1 Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Fri, 13 Mar 2026 23:27:40 +0100 Subject: [PATCH] Improve rendering Signed-off-by: Hans Kokx --- .../entities/enemies/brown_guard.dart | 8 +- lib/features/input/input_manager.dart | 59 +++++++++ lib/features/player/player.dart | 120 ++++++++++++++---- lib/features/renderer/raycast_painter.dart | 48 +++---- lib/features/renderer/renderer.dart | 85 +++++++------ 5 files changed, 227 insertions(+), 93 deletions(-) create mode 100644 lib/features/input/input_manager.dart diff --git a/lib/features/entities/enemies/brown_guard.dart b/lib/features/entities/enemies/brown_guard.dart index ff1dfbd..4290755 100644 --- a/lib/features/entities/enemies/brown_guard.dart +++ b/lib/features/entities/enemies/brown_guard.dart @@ -78,8 +78,12 @@ class BrownGuard extends Enemy { } double diff = angle - angleToPlayer; - 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; if (octant < 0) octant += 8; diff --git a/lib/features/input/input_manager.dart b/lib/features/input/input_manager.dart new file mode 100644 index 0000000..def362a --- /dev/null +++ b/lib/features/input/input_manager.dart @@ -0,0 +1,59 @@ +import 'package:flutter/services.dart'; + +class InputManager { + Set _previousKeys = {}; + + bool isMovingForward = false; + bool isMovingBackward = false; + bool isTurningLeft = false; + bool isTurningRight = false; + + // Discrete (triggers once per press) + bool isInteracting = false; + + // Continuous + bool isFiring = false; + + int? requestedWeaponIndex; + + void update() { + final pressedKeys = HardwareKeyboard.instance.logicalKeysPressed; + + // Calculate all keys that were pressed exactly on this frame + final newlyPressedKeys = pressedKeys.difference(_previousKeys); + + // * Movement + isMovingForward = pressedKeys.contains(LogicalKeyboardKey.keyW); + isMovingBackward = pressedKeys.contains(LogicalKeyboardKey.keyS); + isTurningLeft = pressedKeys.contains(LogicalKeyboardKey.keyA); + isTurningRight = pressedKeys.contains(LogicalKeyboardKey.keyD); + + // * Interaction (Space) + // Much simpler now using the newlyPressedKeys set + isInteracting = newlyPressedKeys.contains(LogicalKeyboardKey.space); + + // * Firing (Left Control) + // - Keeping this continuous for machine guns + isFiring = + pressedKeys.contains(LogicalKeyboardKey.controlLeft) && + !pressedKeys.contains(LogicalKeyboardKey.space); + + // * Manual Weapon Switching + requestedWeaponIndex = null; + + // Iterate through newly pressed keys and switch on them + for (final LogicalKeyboardKey key in newlyPressedKeys) { + switch (key) { + case LogicalKeyboardKey.digit1: + requestedWeaponIndex = 0; // Knife + case LogicalKeyboardKey.digit2: + requestedWeaponIndex = 1; // Pistol + case LogicalKeyboardKey.digit3: + requestedWeaponIndex = 2; // Machine Gun + } + } + + // * Save state for next tick + _previousKeys = Set.from(pressedKeys); + } +} diff --git a/lib/features/player/player.dart b/lib/features/player/player.dart index e7882bd..4d34440 100644 --- a/lib/features/player/player.dart +++ b/lib/features/player/player.dart @@ -7,6 +7,8 @@ import 'package:wolf_dart/features/weapon/weapons/knife.dart'; import 'package:wolf_dart/features/weapon/weapons/machine_gun.dart'; import 'package:wolf_dart/features/weapon/weapons/pistol.dart'; +enum WeaponSwitchState { idle, lowering, raising } + class Player { // Spatial double x; @@ -24,9 +26,21 @@ class Player { bool hasMachineGun = false; bool hasChainGun = false; - // Weapon + // Weapon System late Weapon currentWeapon; - final List availableWeapons = []; + int currentWeaponIndex = 1; // Starts with Pistol (Index 1) + + // Fixed indices: 0 = Knife, 1 = Pistol, 2 = Machine Gun, 3 = Chain Gun + final List availableWeapons = [null, null, null, null]; + + WeaponSwitchState switchState = WeaponSwitchState.idle; + int? pendingWeaponIndex; + + // 0.0 is resting, 500.0 is fully off-screen + double weaponAnimOffset = 0.0; + + // How fast the weapon drops/raises per tick + final double switchSpeed = 30.0; Player({ required this.x, @@ -34,18 +48,53 @@ class Player { required this.angle, }) { // Start with Knife and Pistol - availableWeapons.add(Knife()); - availableWeapons.add(Pistol()); - currentWeapon = availableWeapons[1]; + availableWeapons[0] = Knife(); + availableWeapons[1] = Pistol(); + currentWeapon = availableWeapons[1]!; } // Helper getter to interface with the RaycasterPainter LinearCoordinates get position => (x: x, y: y); - // Helper methods to keep state manipulation safe + // --- Weapon Switching & Animation Logic --- + + void updateWeaponSwitch() { + if (switchState == WeaponSwitchState.lowering) { + weaponAnimOffset += switchSpeed; + if (weaponAnimOffset >= 500.0) { + weaponAnimOffset = 500.0; + currentWeaponIndex = pendingWeaponIndex!; + currentWeapon = availableWeapons[currentWeaponIndex]!; + switchState = WeaponSwitchState.raising; + } + } else if (switchState == WeaponSwitchState.raising) { + weaponAnimOffset -= switchSpeed; + if (weaponAnimOffset <= 0) { + weaponAnimOffset = 0.0; + switchState = WeaponSwitchState.idle; + } + } + } + + void requestWeaponSwitch(int index) { + // Prevent switching if animating, firing, picking the same gun, or if slot is empty + if (switchState != WeaponSwitchState.idle) return; + if (currentWeapon.state != WeaponState.idle) return; + if (index == currentWeaponIndex) return; + if (index < 0 || index >= availableWeapons.length) return; + if (availableWeapons[index] == null) return; + + // Don't switch to a firearm if out of ammo + if (index > 0 && ammo <= 0) return; + + pendingWeaponIndex = index; + switchState = WeaponSwitchState.lowering; + } + + // --- Health & Damage --- + void takeDamage(int damage) { health = math.max(0, health - damage); - if (health <= 0) { print("YOU DIED!"); } else { @@ -55,24 +104,22 @@ class Player { void heal(int amount) { final int newHealth = math.min(100, health + amount); - if (health < 100) { print("Feelin' better. ($newHealth)"); } - health = newHealth; } void addAmmo(int amount) { final int newAmmo = math.min(99, ammo + amount); - if (ammo < 99) { print("Hell yeah. ($newAmmo)"); } - ammo = newAmmo; } + // --- Interaction & Firing --- + bool tryPickup(Collectible item) { bool pickedUp = false; @@ -86,12 +133,18 @@ class Player { case CollectibleType.ammo: if (ammo >= 99) return false; + + int previousAmmo = ammo; addAmmo(8); + + // Auto-switch back to Pistol if holding Knife and just got ammo + if (currentWeaponIndex == 0 && previousAmmo <= 0) { + requestWeaponSwitch(1); + } pickedUp = true; break; case CollectibleType.treasure: - // Score values for Cross (52), Chalice (53), Chest (54), Crown (55) if (item.mapId == 52) score += 100; if (item.mapId == 53) score += 500; if (item.mapId == 54) score += 1000; @@ -105,9 +158,23 @@ class Player { break; case CollectibleType.weapon: - if (item.mapId == 50) hasMachineGun = true; - if (item.mapId == 51) hasChainGun = true; - pickedUp = true; + if (item.mapId == 50) { + if (!hasMachineGun) { + hasMachineGun = true; + availableWeapons[2] = MachineGun(); + } + requestWeaponSwitch(2); + pickedUp = true; + } + // Assuming mapId 51 is Chain Gun for later + if (item.mapId == 51) { + if (!hasChainGun) { + hasChainGun = true; + // availableWeapons[3] = ChainGun(); // Uncomment when you add the class + } + requestWeaponSwitch(3); + pickedUp = true; + } break; case CollectibleType.key: @@ -120,10 +187,18 @@ class Player { } void fire(int currentTime) { - // Only spend ammo if the weapon isn't a knife + if (switchState != WeaponSwitchState.idle) { + return; // No shooting while switching + } + bool shotFired = currentWeapon.fire(currentTime, currentAmmo: ammo); - if (shotFired && currentWeapon is! Knife) { + if (shotFired && currentWeaponIndex > 0) { + // If it's a gun ammo--; + if (ammo <= 0) { + // Auto-switch to knife when out of bullets + requestWeaponSwitch(0); + } } } @@ -132,8 +207,6 @@ class Player { int oldFrame = currentWeapon.frameIndex; currentWeapon.update(currentTime); - // In your Pistol (Indices 212-215), Index 213 is the flash. - // This translates to frameIndex == 1 in our fireFrames list. if (currentWeapon.state == WeaponState.firing && oldFrame == 0 && currentWeapon.frameIndex == 1) { @@ -141,13 +214,4 @@ class Player { } return false; } - - // Logic to switch weapons (e.g., picking up the Machine Gun) - void equipBestWeapon() { - if (hasChainGun) { - /* set chain gun */ - } else if (hasMachineGun) { - currentWeapon = MachineGun(); - } - } } diff --git a/lib/features/renderer/raycast_painter.dart b/lib/features/renderer/raycast_painter.dart index fe1ec9c..3b60802 100644 --- a/lib/features/renderer/raycast_painter.dart +++ b/lib/features/renderer/raycast_painter.dart @@ -27,27 +27,29 @@ class RaycasterPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { + // Disable anti-aliasing for the background to prevent edge bleeding + final Paint bgPaint = Paint()..isAntiAlias = false; + // 1. Draw Ceiling & Floor canvas.drawRect( Rect.fromLTWH(0, 0, size.width, size.height / 2), - Paint()..color = Colors.blueGrey[900]!, + bgPaint..color = Colors.blueGrey[900]!, ); canvas.drawRect( Rect.fromLTWH(0, size.height / 2, size.width, size.height / 2), - Paint()..color = Colors.brown[900]!, + bgPaint..color = Colors.brown[900]!, ); // --- OPTIMIZATION: Lock to Retro Resolution --- const int renderWidth = 320; - // Calculate how wide each column should be on the actual screen double columnWidth = size.width / renderWidth; - // Create a single Paint object to reuse (massive performance boost) - // Add 0.5 to strokeWidth to prevent anti-aliasing seams between columns - final Paint columnPaint = Paint()..strokeWidth = columnWidth + 0.5; + // CRITICAL FIX: Disable anti-aliasing so edges remain perfectly sharp + final Paint columnPaint = Paint() + ..isAntiAlias = false + ..strokeWidth = columnWidth + 0.5; - // The 1D Z-Buffer locked to our render width List zBuffer = List.filled(renderWidth, 0.0); double dirX = math.cos(player.angle); @@ -153,12 +155,11 @@ class RaycasterPainter extends CustomPainter { } wallX -= wallX.floor(); - // Pass the scaled drawX instead of the raw loop index double drawX = x * columnWidth; _drawTexturedColumn( canvas, - drawX, // <-- Updated + drawX, perpWallDist, wallX, side, @@ -166,7 +167,7 @@ class RaycasterPainter extends CustomPainter { hitWallId, textures, doorOffset, - columnPaint, // <-- Pass the reusable Paint object + columnPaint, ); } @@ -189,34 +190,24 @@ class RaycasterPainter extends CustomPainter { double transformY = invDet * (-planeY * spriteX + planeX * spriteY); if (transformY > 0) { - // Map sprite X to our 320 renderWidth int spriteScreenX = ((renderWidth / 2) * (1 + transformX / transformY)) .toInt(); - - // Calculate height in REAL screen pixels int spriteHeight = (size.height / transformY).abs().toInt(); - - // Calculate width in COLUMNS (320 space) to maintain the square aspect ratio int spriteColumnWidth = (spriteHeight / columnWidth).toInt(); - // Use the new column width for start/end points int drawStartX = -spriteColumnWidth ~/ 2 + spriteScreenX; int drawEndX = spriteColumnWidth ~/ 2 + spriteScreenX; - // Clip to screen boundaries int clipStartX = math.max(0, drawStartX); int clipEndX = math.min(renderWidth - 1, drawEndX); for (int stripe = clipStartX; stripe < clipEndX; stripe++) { - // THE Z-BUFFER CHECK! if (transformY < zBuffer[stripe]) { - // Map the texture X using the new column width! double texXDouble = (stripe - drawStartX) * 64 / spriteColumnWidth; int texX = texXDouble.toInt().clamp(0, 63); double startY = (size.height / 2) - (spriteHeight / 2); double stepY = spriteHeight / 64.0; - double drawX = stripe * columnWidth; int safeIndex = entity.spriteIndex.clamp(0, sprites.length - 1); @@ -226,7 +217,9 @@ class RaycasterPainter extends CustomPainter { int colorByte = spritePixels[texX][ty]; if (colorByte != 255) { - double endY = startY + stepY; + // ADDED 0.5 OVERLAP TO PREVENT VERTICAL SEAMS + double endY = startY + stepY + 0.5; + if (endY > 0 && startY < size.height) { columnPaint.color = ColorPalette.vga[colorByte]; canvas.drawLine( @@ -246,7 +239,7 @@ class RaycasterPainter extends CustomPainter { void _drawTexturedColumn( Canvas canvas, - double drawX, // <-- Receive scaled draw position + double drawX, double distance, double wallX, int side, @@ -254,7 +247,7 @@ class RaycasterPainter extends CustomPainter { int hitWallId, List> textures, double doorOffset, - Paint paint, // <-- Receive reused Paint object + Paint paint, ) { if (distance <= 0.01) distance = 0.01; @@ -281,10 +274,10 @@ class RaycasterPainter extends CustomPainter { for (int ty = 0; ty < 64; ty++) { int colorByte = textures[texNum][texX][ty]; - // Update the color of our shared paint object paint.color = ColorPalette.vga[colorByte]; - double endY = startY + stepY; + // ADDED 0.5 OVERLAP TO PREVENT VERTICAL SEAMS + double endY = startY + stepY + 0.5; if (endY > 0 && startY < size.height) { canvas.drawLine( @@ -300,7 +293,8 @@ class RaycasterPainter extends CustomPainter { @override bool shouldRepaint(RaycasterPainter oldDelegate) { - return oldDelegate.player != player || - oldDelegate.player.angle != player.angle; + // Because the Player object instance remains the same, a pure equality check fails. + // Given that your Ticker loop calls setState every frame, returning true is safest. + return true; } } diff --git a/lib/features/renderer/renderer.dart b/lib/features/renderer/renderer.dart index 9980d03..a737b35 100644 --- a/lib/features/renderer/renderer.dart +++ b/lib/features/renderer/renderer.dart @@ -2,7 +2,6 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'package:flutter/services.dart'; import 'package:wolf_dart/classes/linear_coordinates.dart'; import 'package:wolf_dart/classes/matrix.dart'; import 'package:wolf_dart/features/difficulty/difficulty.dart'; @@ -10,6 +9,7 @@ import 'package:wolf_dart/features/entities/collectible.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_registry.dart'; +import 'package:wolf_dart/features/input/input_manager.dart'; import 'package:wolf_dart/features/map/wolf_map.dart'; import 'package:wolf_dart/features/player/player.dart'; import 'package:wolf_dart/features/renderer/raycast_painter.dart'; @@ -35,6 +35,7 @@ class WolfRenderer extends StatefulWidget { class _WolfRendererState extends State with SingleTickerProviderStateMixin { + final InputManager inputManager = InputManager(); late Ticker _gameLoop; final FocusNode _focusNode = FocusNode(); late WolfMap gameMap; @@ -45,7 +46,7 @@ class _WolfRendererState extends State late Player player; bool _isLoading = true; - bool _spaceWasPressed = false; + final bool _spaceWasPressed = false; double damageFlashOpacity = 0.0; @@ -192,6 +193,9 @@ class _WolfRendererState extends State } void _tick(Duration elapsed) { + // * Process all inputs, first. + inputManager.update(); + const double moveSpeed = 0.12; const double turnSpeed = 0.08; @@ -199,6 +203,34 @@ class _WolfRendererState extends State moveStepX = 0; moveStepY = 0; + // Handle Manual Weapon Switching + if (inputManager.requestedWeaponIndex != null) { + player.requestWeaponSwitch(inputManager.requestedWeaponIndex!); + } + + // Handle Movement using InputManager + if (inputManager.isMovingForward) { + moveStepX += math.cos(player.angle) * moveSpeed; + moveStepY += math.sin(player.angle) * moveSpeed; + } + if (inputManager.isMovingBackward) { + moveStepX -= math.cos(player.angle) * moveSpeed; + moveStepY -= math.sin(player.angle) * moveSpeed; + } + if (inputManager.isTurningLeft) { + 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 > 2 * math.pi) player.angle -= 2 * math.pi; + + // UPDATE WEAPON ANIMATION (Lowering/Raising) + player.updateWeaponSwitch(); + // 1. ANIMATE DOORS doorStates.forEach((key, state) { if (state == 1) { @@ -211,25 +243,6 @@ class _WolfRendererState extends State } }); - final pressedKeys = HardwareKeyboard.instance.logicalKeysPressed; - - if (pressedKeys.contains(LogicalKeyboardKey.keyW)) { - moveStepX += math.cos(player.angle) * moveSpeed; - moveStepY += math.sin(player.angle) * moveSpeed; - } - if (pressedKeys.contains(LogicalKeyboardKey.keyS)) { - moveStepX -= math.cos(player.angle) * moveSpeed; - moveStepY -= math.sin(player.angle) * moveSpeed; - } - if (pressedKeys.contains(LogicalKeyboardKey.keyA)) { - player.angle -= turnSpeed; - } - if (pressedKeys.contains(LogicalKeyboardKey.keyD)) { - player.angle += turnSpeed; - } - if (player.angle < 0) player.angle += 2 * math.pi; - if (player.angle > 2 * math.pi) player.angle -= 2 * math.pi; - // 2. UPDATED WALL COLLISION const double margin = 0.3; double newX = player.x + moveStepX; @@ -250,10 +263,8 @@ class _WolfRendererState extends State player.y = newY; } - // 3. UPDATED DOOR INTERACTION - bool isSpacePressed = pressedKeys.contains(LogicalKeyboardKey.space); - - if (isSpacePressed && !_spaceWasPressed) { + // 3. UPDATED DOOR INTERACTION (Using InputManager) + if (inputManager.isInteracting) { int targetX = (player.x + math.cos(player.angle)).toInt(); int targetY = (player.y + math.sin(player.angle)).toInt(); @@ -270,7 +281,6 @@ class _WolfRendererState extends State } } } - _spaceWasPressed = isSpacePressed; // --- 4. UPDATE ENTITY LOGIC --- List itemsToRemove = []; // Collect items to delete after the loop @@ -284,7 +294,7 @@ class _WolfRendererState extends State onDamagePlayer: _takeDamage, ); } - // NEW: Add Collectible Interaction Logic + // Collectible Interaction Logic else if (entity is Collectible) { double dx = player.x - entity.x; double dy = player.y - entity.y; @@ -306,17 +316,15 @@ class _WolfRendererState extends State }); } - // 5. Weapon - // Update weapon animation and check for flash frame + // 5. Weapon Raycast & Firing bool shouldCheckHit = player.updateWeapon(elapsed.inMilliseconds); if (shouldCheckHit) { _performRaycastAttack(elapsed); } - // Input to trigger firing - if (pressedKeys.contains(LogicalKeyboardKey.controlLeft) && - !_spaceWasPressed) { + // Input to trigger firing (Using InputManager) + if (inputManager.isFiring) { player.fire(elapsed.inMilliseconds); } @@ -347,8 +355,12 @@ class _WolfRendererState extends State // 2. Check if that angle is close to our player's aiming angle double angleDiff = player.angle - angleToEnemy; - while (angleDiff <= -math.pi) angleDiff += 2 * math.pi; - while (angleDiff > math.pi) angleDiff -= 2 * math.pi; + while (angleDiff <= -math.pi) { + angleDiff += 2 * math.pi; + } + while (angleDiff > math.pi) { + angleDiff -= 2 * math.pi; + } // 3. Simple bounding box check (approx 0.4 units wide) double dist = math.sqrt(dx * dx + dy * dy); @@ -442,8 +454,7 @@ class _WolfRendererState extends State child: Center( child: Transform.translate( offset: Offset( - 0, - // Bobbing math: only moves if velocity is > 0 + // Bobbing math (moveStepX.abs() + moveStepY.abs()) > 0 ? math.sin( DateTime.now() @@ -452,6 +463,8 @@ class _WolfRendererState extends State ) * 12 : 0, + // Y-Offset for lowering and raising + player.weaponAnimOffset, ), child: SizedBox( width: 500,