From f7ca65ab6e499f67e84515ca18fd7db9168eba52 Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Mon, 16 Mar 2026 10:42:09 +0100 Subject: [PATCH] Abstracted more functionality into the base rasterizer Signed-off-by: Hans Kokx --- .../wolf_3d_engine/lib/src/player/player.dart | 32 +- .../wolf_3d_engine/lib/src/rasterizer.dart | 358 +++++++++- .../lib/src/software_rasterizer.dart | 621 +++++------------- .../lib/src/wolf_3d_engine_base.dart | 9 +- .../lib/ascii_rasterizer.dart | 483 ++++++++------ 5 files changed, 836 insertions(+), 667 deletions(-) diff --git a/packages/wolf_3d_engine/lib/src/player/player.dart b/packages/wolf_3d_engine/lib/src/player/player.dart index cefee40..1b09b63 100644 --- a/packages/wolf_3d_engine/lib/src/player/player.dart +++ b/packages/wolf_3d_engine/lib/src/player/player.dart @@ -16,6 +16,10 @@ class Player { int ammo = 8; int score = 0; + // Damage flash + double damageFlash = 0.0; // 0.0 is none, 1.0 is maximum red + final double damageFlashFadeSpeed = 0.05; // How fast it fades per tick + // Inventory bool hasGoldKey = false; bool hasSilverKey = false; @@ -40,17 +44,25 @@ class Player { // How fast the weapon drops/raises per tick final double switchSpeed = 30.0; - Player({ - required this.x, - required this.y, - required this.angle, - }) { + Player({required this.x, required this.y, required this.angle}) { currentWeapon = weapons[WeaponType.pistol]!; } // Helper getter to interface with the RaycasterPainter Coordinate2D get position => Coordinate2D(x, y); + // --- General Update --- + + void tick(Duration elapsed) { + // Fade the damage flash over time + if (damageFlash > 0.0) { + // Assuming 60fps, we fade it out + damageFlash = math.max(0.0, damageFlash - damageFlashFadeSpeed); + } + + updateWeaponSwitch(); + } + // --- Weapon Switching & Animation Logic --- void updateWeaponSwitch() { @@ -95,6 +107,11 @@ class Player { void takeDamage(int damage) { health = math.max(0, health - damage); + + // Spike the damage flash based on how much damage was taken + // A 10 damage hit gives a 0.5 flash, a 20 damage hit maxes it out at 1.0 + damageFlash = math.min(1.0, damageFlash + (damage * 0.05)); + if (health <= 0) { print("YOU DIED!"); } else { @@ -184,10 +201,7 @@ class Player { if (switchState != WeaponSwitchState.idle) return; // We pass the isFiring state to handle automatic vs semi-auto behavior - bool shotFired = currentWeapon.fire( - currentTime, - currentAmmo: ammo, - ); + bool shotFired = currentWeapon.fire(currentTime, currentAmmo: ammo); if (shotFired && currentWeapon.type != WeaponType.knife) { ammo--; diff --git a/packages/wolf_3d_engine/lib/src/rasterizer.dart b/packages/wolf_3d_engine/lib/src/rasterizer.dart index 2c407a1..edf129a 100644 --- a/packages/wolf_3d_engine/lib/src/rasterizer.dart +++ b/packages/wolf_3d_engine/lib/src/rasterizer.dart @@ -1,6 +1,362 @@ +import 'dart:math' as math; + import 'package:wolf_3d_data_types/wolf_3d_data_types.dart'; import 'package:wolf_3d_engine/wolf_3d_engine.dart'; +import 'package:wolf_3d_entities/wolf_3d_entities.dart'; abstract class Rasterizer { - dynamic render(WolfEngine engine, FrameBuffer buffer); + late List zBuffer; + late int width; + late int height; + late int viewHeight; + + /// A multiplier to adjust the width of sprites. + /// Pixel renderers usually keep this at 1.0. + /// ASCII renderers can override this (e.g., 0.6) to account for tall characters. + double get aspectMultiplier => 1.0; + + /// The main entry point called by the game loop. + /// Orchestrates the mathematical rendering pipeline. + dynamic render(WolfEngine engine, FrameBuffer buffer) { + width = buffer.width; + height = buffer.height; + // The 3D view typically takes up the top 80% of the screen + viewHeight = (height * 0.8).toInt(); + zBuffer = List.filled(width, 0.0); + + // 1. Setup the frame (clear screen, draw floor/ceiling) + prepareFrame(engine); + + // 2. Do the heavy math for Raycasting Walls + _castWalls(engine); + + // 3. Do the heavy math for Projecting Sprites + _castSprites(engine); + + // 4. Draw 2D Overlays + drawWeapon(engine); + drawHud(engine); + + // 5. Finalize and return the frame data (Buffer or String/List) + return finalizeFrame(); + } + + // =========================================================================== + // ABSTRACT METHODS (Implemented by the child renderers) + // =========================================================================== + + /// Initialize buffers, clear the screen, and draw the floor/ceiling. + void prepareFrame(WolfEngine engine); + + /// Draw a single vertical column of a wall. + void drawWallColumn( + int x, + int drawStart, + int drawEnd, + int columnHeight, + Sprite texture, + int texX, + double perpWallDist, + int side, + ); + + /// Draw a single vertical stripe of a sprite (enemy/item). + void drawSpriteStripe( + int stripeX, + int drawStartY, + int drawEndY, + int spriteHeight, + Sprite texture, + int texX, + double transformY, + ); + + /// Draw the player's weapon overlay at the bottom of the 3D view. + void drawWeapon(WolfEngine engine); + + /// Draw the 2D status bar at the bottom 20% of the screen. + void drawHud(WolfEngine engine); + + /// Return the finished frame (e.g., the FrameBuffer itself, or an ASCII list). + dynamic finalizeFrame(); + + // =========================================================================== + // CORE ENGINE MATH (Shared across all renderers) + // =========================================================================== + + void _castWalls(WolfEngine engine) { + final Player player = engine.player; + final SpriteMap map = engine.currentLevel; + final List wallTextures = engine.data.walls; + + final Map doorOffsets = engine.doorManager + .getOffsetsForRenderer(); + final Pushwall? activePushwall = engine.pushwallManager.activePushwall; + + final double fov = math.pi / 3; + Coordinate2D dir = Coordinate2D( + math.cos(player.angle), + math.sin(player.angle), + ); + Coordinate2D plane = Coordinate2D(-dir.y, dir.x) * math.tan(fov / 2); + + for (int x = 0; x < width; x++) { + double cameraX = 2 * x / width - 1.0; + Coordinate2D rayDir = dir + (plane * cameraX); + + int mapX = player.x.toInt(); + int mapY = player.y.toInt(); + + double deltaDistX = (rayDir.x == 0) ? 1e30 : (1.0 / rayDir.x).abs(); + double deltaDistY = (rayDir.y == 0) ? 1e30 : (1.0 / rayDir.y).abs(); + + double sideDistX, sideDistY, perpWallDist = 0.0; + int stepX, stepY, side = 0, hitWallId = 0; + bool hit = false, hitOutOfBounds = false, customDistCalculated = false; + double textureOffset = 0.0; + Set ignoredDoors = {}; + + if (rayDir.x < 0) { + stepX = -1; + sideDistX = (player.x - mapX) * deltaDistX; + } else { + stepX = 1; + sideDistX = (mapX + 1.0 - player.x) * deltaDistX; + } + if (rayDir.y < 0) { + stepY = -1; + sideDistY = (player.y - mapY) * deltaDistY; + } else { + stepY = 1; + sideDistY = (mapY + 1.0 - player.y) * deltaDistY; + } + + // DDA Loop + while (!hit) { + if (sideDistX < sideDistY) { + sideDistX += deltaDistX; + mapX += stepX; + side = 0; + } else { + sideDistY += deltaDistY; + mapY += stepY; + side = 1; + } + + if (mapY < 0 || + mapY >= map.length || + mapX < 0 || + mapX >= map[0].length) { + hit = true; + hitOutOfBounds = true; + } else if (map[mapY][mapX] > 0) { + String mapKey = '$mapX,$mapY'; + + // DOOR LOGIC + if (map[mapY][mapX] >= 90 && !ignoredDoors.contains(mapKey)) { + double currentOffset = doorOffsets[mapKey] ?? 0.0; + if (currentOffset > 0.0) { + double perpWallDistTemp = (side == 0) + ? (sideDistX - deltaDistX) + : (sideDistY - deltaDistY); + double wallXTemp = (side == 0) + ? player.y + perpWallDistTemp * rayDir.y + : player.x + perpWallDistTemp * rayDir.x; + wallXTemp -= wallXTemp.floor(); + if (wallXTemp < currentOffset) { + ignoredDoors.add(mapKey); + continue; // Ray passes through the open part of the door + } + } + hit = true; + hitWallId = map[mapY][mapX]; + textureOffset = currentOffset; + } + // PUSHWALL LOGIC + else if (activePushwall != null && + mapX == activePushwall.x && + mapY == activePushwall.y) { + hit = true; + hitWallId = map[mapY][mapX]; + + double pOffset = activePushwall.offset; + int pDirX = activePushwall.dirX; + int pDirY = activePushwall.dirY; + + perpWallDist = (side == 0) + ? (sideDistX - deltaDistX) + : (sideDistY - deltaDistY); + + if (side == 0 && pDirX != 0) { + if (pDirX == stepX) { + double intersect = perpWallDist + pOffset * deltaDistX; + if (intersect < sideDistY) { + perpWallDist = intersect; + } else { + side = 1; + perpWallDist = sideDistY - deltaDistY; + } + } else { + perpWallDist -= (1.0 - pOffset) * deltaDistX; + } + } else if (side == 1 && pDirY != 0) { + if (pDirY == stepY) { + double intersect = perpWallDist + pOffset * deltaDistY; + if (intersect < sideDistX) { + perpWallDist = intersect; + } else { + side = 0; + perpWallDist = sideDistX - deltaDistX; + } + } else { + perpWallDist -= (1.0 - pOffset) * deltaDistY; + } + } else { + double wallFraction = (side == 0) + ? player.y + perpWallDist * rayDir.y + : player.x + perpWallDist * rayDir.x; + wallFraction -= wallFraction.floor(); + if (side == 0) { + if (pDirY == 1 && wallFraction < pOffset) hit = false; + if (pDirY == -1 && wallFraction > (1.0 - pOffset)) hit = false; + if (hit) textureOffset = pOffset * pDirY; + } else { + if (pDirX == 1 && wallFraction < pOffset) hit = false; + if (pDirX == -1 && wallFraction > (1.0 - pOffset)) hit = false; + if (hit) textureOffset = pOffset * pDirX; + } + } + if (!hit) continue; + customDistCalculated = true; + } else { + hit = true; + hitWallId = map[mapY][mapX]; + } + } + } + + if (hitOutOfBounds) continue; + + if (!customDistCalculated) { + perpWallDist = (side == 0) + ? (sideDistX - deltaDistX) + : (sideDistY - deltaDistY); + } + if (perpWallDist < 0.1) perpWallDist = 0.1; + + // Save for sprite depth checks + zBuffer[x] = perpWallDist; + + // Calculate Texture X Coordinate + double wallX = (side == 0) + ? player.y + perpWallDist * rayDir.y + : player.x + perpWallDist * rayDir.x; + wallX -= wallX.floor(); + + int texNum; + if (hitWallId >= 90) { + texNum = 98.clamp(0, wallTextures.length - 1); + } else { + texNum = ((hitWallId - 1) * 2).clamp(0, wallTextures.length - 2); + if (side == 1) texNum += 1; + } + Sprite texture = wallTextures[texNum]; + + // Texture flipping for specific orientations + int texX = (((wallX - textureOffset) % 1.0) * 64).toInt().clamp(0, 63); + if (side == 0 && math.cos(player.angle) > 0) texX = 63 - texX; + if (side == 1 && math.sin(player.angle) < 0) texX = 63 - texX; + + // Calculate drawing dimensions + int columnHeight = (viewHeight / perpWallDist).toInt(); + int drawStart = (-columnHeight ~/ 2 + viewHeight ~/ 2).clamp( + 0, + viewHeight, + ); + int drawEnd = (columnHeight ~/ 2 + viewHeight ~/ 2).clamp(0, viewHeight); + + // Tell the implementation to draw this column + drawWallColumn( + x, + drawStart, + drawEnd, + columnHeight, + texture, + texX, + perpWallDist, + side, + ); + } + } + + void _castSprites(WolfEngine engine) { + final Player player = engine.player; + final List activeSprites = List.from(engine.entities); + + // Sort from furthest to closest (Painter's Algorithm) + activeSprites.sort((a, b) { + double distA = player.position.distanceTo(a.position); + double distB = player.position.distanceTo(b.position); + return distB.compareTo(distA); + }); + + Coordinate2D dir = Coordinate2D( + math.cos(player.angle), + math.sin(player.angle), + ); + Coordinate2D plane = + Coordinate2D(-dir.y, dir.x) * math.tan((math.pi / 3) / 2); + + for (Entity entity in activeSprites) { + Coordinate2D spritePos = entity.position - player.position; + + double invDet = 1.0 / (plane.x * dir.y - dir.x * plane.y); + double transformX = invDet * (dir.y * spritePos.x - dir.x * spritePos.y); + double transformY = + invDet * (-plane.y * spritePos.x + plane.x * spritePos.y); + + // Only process if the sprite is in front of the camera + if (transformY > 0) { + int spriteScreenX = ((width / 2) * (1 + transformX / transformY)) + .toInt(); + int spriteHeight = (viewHeight / transformY).abs().toInt(); + + // Scale width based on the aspectMultiplier (useful for ASCII) + int spriteWidth = (spriteHeight * aspectMultiplier).toInt(); + + int drawStartY = -spriteHeight ~/ 2 + viewHeight ~/ 2; + int drawEndY = spriteHeight ~/ 2 + viewHeight ~/ 2; + int drawStartX = -spriteWidth ~/ 2 + spriteScreenX; + int drawEndX = spriteWidth ~/ 2 + spriteScreenX; + + int clipStartX = math.max(0, drawStartX); + int clipEndX = math.min(width - 1, drawEndX); + + int safeIndex = entity.spriteIndex.clamp( + 0, + engine.data.sprites.length - 1, + ); + Sprite texture = engine.data.sprites[safeIndex]; + + // Loop through the visible vertical stripes + for (int stripe = clipStartX; stripe < clipEndX; stripe++) { + // Check the Z-Buffer to see if a wall is in front of this stripe + if (transformY < zBuffer[stripe]) { + int texX = ((stripe - drawStartX) * 64 ~/ spriteWidth).clamp(0, 63); + + // Tell the implementation to draw this stripe + drawSpriteStripe( + stripe, + drawStartY, + drawEndY, + spriteHeight, + texture, + texX, + transformY, + ); + } + } + } + } + } } diff --git a/packages/wolf_3d_engine/lib/src/software_rasterizer.dart b/packages/wolf_3d_engine/lib/src/software_rasterizer.dart index 50d62ca..99450f3 100644 --- a/packages/wolf_3d_engine/lib/src/software_rasterizer.dart +++ b/packages/wolf_3d_engine/lib/src/software_rasterizer.dart @@ -2,430 +2,187 @@ import 'dart:math' as math; import 'package:wolf_3d_data_types/wolf_3d_data_types.dart'; import 'package:wolf_3d_engine/wolf_3d_engine.dart'; -import 'package:wolf_3d_entities/wolf_3d_entities.dart'; -class SoftwareRasterizer implements Rasterizer { - // Pre-calculated VGA colors for ceiling and floor - final int ceilingColor = ColorPalette.vga32Bit[25]; - final int floorColor = ColorPalette.vga32Bit[29]; +class SoftwareRasterizer extends Rasterizer { + late FrameBuffer _buffer; + late WolfEngine _engine; + + // Intercept the base render call to store our references + @override + dynamic render(WolfEngine engine, FrameBuffer buffer) { + _engine = engine; + _buffer = buffer; + return super.render(engine, buffer); + } @override - void render(WolfEngine engine, FrameBuffer buffer) { - _clearScreen(buffer); + void prepareFrame(WolfEngine engine) { + // Top half is ceiling color (25), bottom half is floor color (29) + int ceilingColor = ColorPalette.vga32Bit[25]; + int floorColor = ColorPalette.vga32Bit[29]; - List zBuffer = List.filled(buffer.width, 0.0); - - _castWalls(engine, buffer, zBuffer); - _castSprites(engine, buffer, zBuffer); - - // NEW: Draw the weapon on top of the 3D world - _drawWeapon(engine, buffer); - - // NEW: Apply the full-screen damage tint last - _drawDamageFlash(engine, buffer); - - _drawHud(engine, buffer); - } - - void _clearScreen(FrameBuffer buffer) { - const int viewHeight = 160; - int halfScreen = (buffer.width * viewHeight) ~/ 2; - // Only clear the top 160 rows! - buffer.pixels.fillRange(0, halfScreen, ceilingColor); - buffer.pixels.fillRange(halfScreen, buffer.width * viewHeight, floorColor); - } - - void _castWalls(WolfEngine engine, FrameBuffer buffer, List zBuffer) { - final double fov = math.pi / 3; - final Player player = engine.player; - final SpriteMap map = engine.currentLevel; - - // Fetch dynamic states from the managers - final Map doorOffsets = engine.doorManager - .getOffsetsForRenderer(); - final Pushwall? activePushwall = engine.pushwallManager.activePushwall; - - Coordinate2D dir = Coordinate2D( - math.cos(player.angle), - math.sin(player.angle), - ); - Coordinate2D plane = Coordinate2D(-dir.y, dir.x) * math.tan(fov / 2); - - for (int x = 0; x < buffer.width; x++) { - double cameraX = 2 * x / buffer.width - 1.0; - Coordinate2D rayDir = dir + (plane * cameraX); - - int mapX = player.x.toInt(); - int mapY = player.y.toInt(); - - double sideDistX; - double sideDistY; - double deltaDistX = (rayDir.x == 0) ? 1e30 : (1.0 / rayDir.x).abs(); - double deltaDistY = (rayDir.y == 0) ? 1e30 : (1.0 / rayDir.y).abs(); - double perpWallDist = 0.0; - - int stepX; - int stepY; - bool hit = false; - bool hitOutOfBounds = false; - int side = 0; - int hitWallId = 0; - - double textureOffset = 0.0; - bool customDistCalculated = false; - Set ignoredDoors = {}; - - if (rayDir.x < 0) { - stepX = -1; - sideDistX = (player.x - mapX) * deltaDistX; - } else { - stepX = 1; - sideDistX = (mapX + 1.0 - player.x) * deltaDistX; + for (int y = 0; y < viewHeight; y++) { + int color = (y < viewHeight / 2) ? ceilingColor : floorColor; + for (int x = 0; x < width; x++) { + _buffer.pixels[y * width + x] = color; } - if (rayDir.y < 0) { - stepY = -1; - sideDistY = (player.y - mapY) * deltaDistY; - } else { - stepY = 1; - sideDistY = (mapY + 1.0 - player.y) * deltaDistY; - } - - // --- DDA LOOP --- - while (!hit) { - if (sideDistX < sideDistY) { - sideDistX += deltaDistX; - mapX += stepX; - side = 0; - } else { - sideDistY += deltaDistY; - mapY += stepY; - side = 1; - } - - if (mapY < 0 || - mapY >= map.length || - mapX < 0 || - mapX >= map[0].length) { - hit = true; - hitOutOfBounds = true; - } else if (map[mapY][mapX] > 0) { - String mapKey = '$mapX,$mapY'; - - // DOOR LOGIC - if (map[mapY][mapX] >= 90 && !ignoredDoors.contains(mapKey)) { - double currentOffset = doorOffsets[mapKey] ?? 0.0; - if (currentOffset > 0.0) { - double perpWallDistTemp = (side == 0) - ? (sideDistX - deltaDistX) - : (sideDistY - deltaDistY); - double wallXTemp = (side == 0) - ? player.y + perpWallDistTemp * rayDir.y - : player.x + perpWallDistTemp * rayDir.x; - wallXTemp -= wallXTemp.floor(); - if (wallXTemp < currentOffset) { - ignoredDoors.add(mapKey); - continue; - } - } - hit = true; - hitWallId = map[mapY][mapX]; - textureOffset = currentOffset; - } - // PUSHWALL LOGIC - else if (activePushwall != null && - mapX == activePushwall.x && - mapY == activePushwall.y) { - hit = true; - hitWallId = map[mapY][mapX]; - - double pOffset = activePushwall.offset; - int pDirX = activePushwall.dirX; - int pDirY = activePushwall.dirY; - - perpWallDist = (side == 0) - ? (sideDistX - deltaDistX) - : (sideDistY - deltaDistY); - - if (side == 0 && pDirX != 0) { - if (pDirX == stepX) { - double intersect = perpWallDist + pOffset * deltaDistX; - if (intersect < sideDistY) { - perpWallDist = intersect; - } else { - side = 1; - perpWallDist = sideDistY - deltaDistY; - } - } else { - perpWallDist -= (1.0 - pOffset) * deltaDistX; - } - } else if (side == 1 && pDirY != 0) { - if (pDirY == stepY) { - double intersect = perpWallDist + pOffset * deltaDistY; - if (intersect < sideDistX) { - perpWallDist = intersect; - } else { - side = 0; - perpWallDist = sideDistX - deltaDistX; - } - } else { - perpWallDist -= (1.0 - pOffset) * deltaDistY; - } - } else { - double wallFraction = (side == 0) - ? player.y + perpWallDist * rayDir.y - : player.x + perpWallDist * rayDir.x; - wallFraction -= wallFraction.floor(); - - if (side == 0) { - if (pDirY == 1 && wallFraction < pOffset) hit = false; - if (pDirY == -1 && wallFraction > (1.0 - pOffset)) hit = false; - if (hit) textureOffset = pOffset * pDirY; - } else { - if (pDirX == 1 && wallFraction < pOffset) hit = false; - if (pDirX == -1 && wallFraction > (1.0 - pOffset)) hit = false; - if (hit) textureOffset = pOffset * pDirX; - } - } - if (!hit) continue; - customDistCalculated = true; - } - // STANDARD WALL - else { - hit = true; - hitWallId = map[mapY][mapX]; - } - } - } - - if (hitOutOfBounds) continue; - - if (!customDistCalculated) { - if (side == 0) { - perpWallDist = (sideDistX - deltaDistX); - } else { - perpWallDist = (sideDistY - deltaDistY); - } - } - - // Log the distance so sprites know if they are occluded - zBuffer[x] = perpWallDist; - - double wallX = (side == 0) - ? player.y + perpWallDist * rayDir.y - : player.x + perpWallDist * rayDir.x; - wallX -= wallX.floor(); - - _drawTexturedColumn( - x, - perpWallDist, - wallX, - side, - hitWallId, - engine.data.walls, // Wall textures - textureOffset, - buffer, - player.angle, - ); } } - void _drawTexturedColumn( + @override + void drawWallColumn( int x, - double distance, - double wallX, + int drawStart, + int drawEnd, + int columnHeight, + Sprite texture, + int texX, + double perpWallDist, int side, - int hitWallId, - List textures, - double textureOffset, - FrameBuffer buffer, - double playerAngle, ) { - const int viewHeight = 160; - if (distance <= 0.01) distance = 0.01; - - int lineHeight = (viewHeight / distance).toInt(); - - int drawStart = -lineHeight ~/ 2 + viewHeight ~/ 2; - if (drawStart < 0) drawStart = 0; - - int drawEnd = lineHeight ~/ 2 + viewHeight ~/ 2; - if (drawEnd >= viewHeight) drawEnd = viewHeight - 1; - - int texNum; - if (hitWallId >= 90) { - texNum = 98.clamp(0, textures.length - 1); - } else { - texNum = ((hitWallId - 1) * 2).clamp(0, textures.length - 2); - if (side == 1) texNum += 1; - } - - int texX = (((wallX - textureOffset) % 1.0) * 64).toInt().clamp(0, 63); - if (side == 0 && math.cos(playerAngle) > 0) texX = 63 - texX; - if (side == 1 && math.sin(playerAngle) < 0) texX = 63 - texX; - - double step = 64.0 / lineHeight; - double texPos = (drawStart - viewHeight / 2 + lineHeight / 2) * step; - - Sprite texture = textures[texNum]; - for (int y = drawStart; y < drawEnd; y++) { - int texY = texPos.toInt() & 63; - texPos += step; + // Calculate which Y pixel of the texture to sample + double relativeY = + (y - (-columnHeight ~/ 2 + viewHeight ~/ 2)) / columnHeight; + int texY = (relativeY * 64).toInt().clamp(0, 63); int colorByte = texture.pixels[texX * 64 + texY]; - int color32 = ColorPalette.vga32Bit[colorByte]; + int pixelColor = ColorPalette.vga32Bit[colorByte]; - buffer.pixels[y * buffer.width + x] = color32; + // Darken Y-side walls for faux directional lighting + if (side == 1) { + pixelColor = _shadeColor(pixelColor); + } + + _buffer.pixels[y * width + x] = pixelColor; } } - void _castSprites( - WolfEngine engine, - FrameBuffer buffer, - List zBuffer, + @override + void drawSpriteStripe( + int stripeX, + int drawStartY, + int drawEndY, + int spriteHeight, + Sprite texture, + int texX, + double transformY, ) { - const int viewHeight = 160; - final Player player = engine.player; - final List activeSprites = List.from(engine.entities); + for ( + int y = math.max(0, drawStartY); + y < math.min(viewHeight, drawEndY); + y++ + ) { + double relativeY = (y - drawStartY) / spriteHeight; + int texY = (relativeY * 64).toInt().clamp(0, 63); - // Sort entities from furthest to closest (Painter's Algorithm) - activeSprites.sort((a, b) { - double distA = player.position.distanceTo(a.position); - double distB = player.position.distanceTo(b.position); - return distB.compareTo(distA); - }); + int colorByte = texture.pixels[texX * 64 + texY]; - Coordinate2D dir = Coordinate2D( - math.cos(player.angle), - math.sin(player.angle), - ); - Coordinate2D plane = - Coordinate2D(-dir.y, dir.x) * math.tan((math.pi / 3) / 2); - - for (Entity entity in activeSprites) { - Coordinate2D spritePos = entity.position - player.position; - - double invDet = 1.0 / (plane.x * dir.y - dir.x * plane.y); - double transformX = invDet * (dir.y * spritePos.x - dir.x * spritePos.y); - double transformY = - invDet * (-plane.y * spritePos.x + plane.x * spritePos.y); - - if (transformY > 0) { - int spriteScreenX = ((buffer.width / 2) * (1 + transformX / transformY)) - .toInt(); - int spriteHeight = (viewHeight / transformY).abs().toInt(); - - // In 1x1 buffer pixels, the width of the sprite is equal to its height - int spriteWidth = spriteHeight; - - int drawStartY = -spriteHeight ~/ 2 + viewHeight ~/ 2; - if (drawStartY < 0) drawStartY = 0; - - int drawEndY = spriteHeight ~/ 2 + viewHeight ~/ 2; - if (drawEndY >= buffer.height) drawEndY = viewHeight - 1; - - int drawStartX = -spriteWidth ~/ 2 + spriteScreenX; - int drawEndX = spriteWidth ~/ 2 + spriteScreenX; - - int clipStartX = math.max(0, drawStartX); - int clipEndX = math.min(buffer.width - 1, drawEndX); - - int safeIndex = entity.spriteIndex.clamp( - 0, - engine.data.sprites.length - 1, - ); - Sprite spritePixels = engine.data.sprites[safeIndex]; - - for (int stripe = clipStartX; stripe < clipEndX; stripe++) { - // Z-Buffer Check! Only draw the vertical stripe if it's in front of the wall - if (transformY < zBuffer[stripe]) { - int texX = ((stripe - drawStartX) * 64 ~/ spriteWidth).clamp(0, 63); - - double step = 64.0 / spriteHeight; - double texPos = - (drawStartY - viewHeight / 2 + spriteHeight / 2) * step; - - for (int y = drawStartY; y < drawEndY; y++) { - int texY = texPos.toInt() & 63; - texPos += step; - - int colorByte = spritePixels.pixels[texX * 64 + texY]; - - if (colorByte != 255) { - // 255 is transparent - buffer.pixels[y * buffer.width + stripe] = - ColorPalette.vga32Bit[colorByte]; - } - } - } - } + // 255 is the "transparent" color index in VGA Wolfenstein + if (colorByte != 255) { + _buffer.pixels[y * width + stripeX] = ColorPalette.vga32Bit[colorByte]; } } } - void _drawWeapon(WolfEngine engine, FrameBuffer buffer) { - const int viewHeight = 160; + @override + void drawWeapon(WolfEngine engine) { int spriteIndex = engine.player.currentWeapon.getCurrentSpriteIndex( engine.data.sprites.length, ); Sprite weaponSprite = engine.data.sprites[spriteIndex]; - // Dropped the scale from 4 to 2 (a 50% reduction in size) - const int scale = 2; - const int weaponWidth = 64 * scale; - const int weaponHeight = 64 * scale; + int weaponWidth = (width * 0.5).toInt(); + int weaponHeight = (viewHeight * 0.8).toInt(); - int startX = (buffer.width ~/ 2) - (weaponWidth ~/ 2); - - // Kept the grounding to the bottom of the screen + int startX = (width ~/ 2) - (weaponWidth ~/ 2); int startY = - viewHeight - weaponHeight + (engine.player.weaponAnimOffset ~/ 2); + viewHeight - weaponHeight + (engine.player.weaponAnimOffset ~/ 4); - for (int x = 0; x < 64; x++) { - for (int y = 0; y < 64; y++) { - int colorByte = weaponSprite.pixels[x * 64 + y]; + for (int dy = 0; dy < weaponHeight; dy++) { + for (int dx = 0; dx < weaponWidth; dx++) { + int texX = (dx * 64 ~/ weaponWidth).clamp(0, 63); + int texY = (dy * 64 ~/ weaponHeight).clamp(0, 63); + int colorByte = weaponSprite.pixels[texX * 64 + texY]; if (colorByte != 255) { - int color32 = ColorPalette.vga32Bit[colorByte]; - - for (int sx = 0; sx < scale; sx++) { - for (int sy = 0; sy < scale; sy++) { - int drawX = startX + (x * scale) + sx; - int drawY = startY + (y * scale) + sy; - - if (drawX >= 0 && - drawX < buffer.width && - drawY >= 0 && - drawY < buffer.height) { - buffer.pixels[drawY * buffer.width + drawX] = color32; - } - } + int drawX = startX + dx; + int drawY = startY + dy; + if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < viewHeight) { + _buffer.pixels[drawY * width + drawX] = + ColorPalette.vga32Bit[colorByte]; } } } } } - void _drawDamageFlash(WolfEngine engine, FrameBuffer buffer) { - if (engine.damageFlashOpacity <= 0) return; + @override + void drawHud(WolfEngine engine) { + int statusBarIndex = engine.data.vgaImages.indexWhere( + (img) => img.width == 320 && img.height == 40, + ); + if (statusBarIndex == -1) return; - int alpha = (engine.damageFlashOpacity * 256).toInt().clamp(0, 256); - int invAlpha = 256 - alpha; + // 1. Draw Background + _blitVgaImage(engine.data.vgaImages[statusBarIndex], 0, 160); - for (int i = 0; i < buffer.pixels.length; i++) { - int color = buffer.pixels[i]; + // 2. Draw Stats (100% mathematically accurate right-aligned coordinates) + _drawNumber(1, 32, 176, engine.data.vgaImages); // Floor + _drawNumber(engine.player.score, 96, 176, engine.data.vgaImages); // Score + _drawNumber(3, 120, 176, engine.data.vgaImages); // Lives + _drawNumber( + engine.player.health, + 192, + 176, + engine.data.vgaImages, + ); // Health + _drawNumber(engine.player.ammo, 232, 176, engine.data.vgaImages); // Ammo - int r = color & 0xFF; - int g = (color >> 8) & 0xFF; - int b = (color >> 16) & 0xFF; - int a = (color >> 24) & 0xFF; + // 3. Draw BJ's Face & Current Weapon + _drawFace(engine); + _drawWeaponIcon(engine); + } - // Blend with Red - r = ((r * invAlpha) + (255 * alpha)) >> 8; - g = (g * invAlpha) >> 8; - b = (b * invAlpha) >> 8; + @override + FrameBuffer finalizeFrame() { + // If the player took damage, overlay a red tint across the 3D view + if (_engine.player.damageFlash > 0) { + _applyDamageFlash(); + } + return _buffer; // Return the fully painted pixel array + } - buffer.pixels[i] = (a << 24) | (b << 16) | (g << 8) | r; + // =========================================================================== + // PRIVATE HELPER METHODS + // =========================================================================== + + /// Maps the planar VGA image data directly to 32-bit pixels. + /// (Assuming a 1:1 scale, which is standard for the 320x200 software renderer). + void _blitVgaImage(VgaImage image, int startX, int startY) { + int planeWidth = image.width ~/ 4; + int planeSize = planeWidth * image.height; + + for (int dy = 0; dy < image.height; dy++) { + for (int dx = 0; dx < image.width; dx++) { + int drawX = startX + dx; + int drawY = startY + dy; + + if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < height) { + int srcX = dx.clamp(0, image.width - 1); + int srcY = dy.clamp(0, image.height - 1); + + int plane = srcX % 4; + int sx = srcX ~/ 4; + int index = (plane * planeSize) + (srcY * planeWidth) + sx; + + int colorByte = image.pixels[index]; + if (colorByte != 255) { + _buffer.pixels[drawY * width + drawX] = + ColorPalette.vga32Bit[colorByte]; + } + } + } } } @@ -433,26 +190,22 @@ class SoftwareRasterizer implements Rasterizer { int value, int rightAlignX, int startY, - FrameBuffer buffer, List vgaImages, ) { - // The yellow 8x16 HUD digits start exactly at 96 const int zeroIndex = 96; String numStr = value.toString(); - int currentX = rightAlignX - (numStr.length * 8); for (int i = 0; i < numStr.length; i++) { int digit = int.parse(numStr[i]); - if (zeroIndex + digit < vgaImages.length) { - _blitVgaImage(vgaImages[zeroIndex + digit], currentX, startY, buffer); + _blitVgaImage(vgaImages[zeroIndex + digit], currentX, startY); } currentX += 8; } } - void _drawFace(WolfEngine engine, FrameBuffer buffer) { + void _drawFace(WolfEngine engine) { int health = engine.player.health; int faceIndex; @@ -464,12 +217,11 @@ class SoftwareRasterizer implements Rasterizer { } if (faceIndex < engine.data.vgaImages.length) { - // Exactly X=136. This will perfectly smother the background face. - _blitVgaImage(engine.data.vgaImages[faceIndex], 136, 164, buffer); + _blitVgaImage(engine.data.vgaImages[faceIndex], 136, 164); } } - void _drawWeaponIcon(WolfEngine engine, FrameBuffer buffer) { + void _drawWeaponIcon(WolfEngine engine) { int weaponIndex = 89; // Default to Pistol if (engine.player.hasChainGun) { @@ -479,84 +231,41 @@ class SoftwareRasterizer implements Rasterizer { } if (weaponIndex < engine.data.vgaImages.length) { - // Exactly X=256 - _blitVgaImage(engine.data.vgaImages[weaponIndex], 256, 164, buffer); + _blitVgaImage(engine.data.vgaImages[weaponIndex], 256, 164); } } - void _drawHud(WolfEngine engine, FrameBuffer buffer) { - int statusBarIndex = engine.data.vgaImages.indexWhere( - (img) => img.width == 320 && img.height == 40, - ); - - if (statusBarIndex == -1) return; - - // 1. Draw Background - _blitVgaImage(engine.data.vgaImages[statusBarIndex], 0, 160, buffer); - - // 2. Draw Stats (100% mathematically accurate right-aligned coordinates) - _drawNumber(1, 32, 176, buffer, engine.data.vgaImages); // Floor - _drawNumber( - engine.player.score, - 96, - 176, - buffer, - engine.data.vgaImages, - ); // Score - _drawNumber(3, 120, 176, buffer, engine.data.vgaImages); // Lives - _drawNumber( - engine.player.health, - 192, - 176, - buffer, - engine.data.vgaImages, - ); // Health - _drawNumber( - engine.player.ammo, - 232, - 176, - buffer, - engine.data.vgaImages, - ); // Ammo - - // 3. Draw BJ's Face & Current Weapon - _drawFace(engine, buffer); - _drawWeaponIcon(engine, buffer); + /// Darkens a 32-bit 0xAABBGGRR color by roughly 30% without touching Alpha + int _shadeColor(int color) { + int r = (color & 0xFF) * 7 ~/ 10; + int g = ((color >> 8) & 0xFF) * 7 ~/ 10; + int b = ((color >> 16) & 0xFF) * 7 ~/ 10; + return (0xFF000000) | (b << 16) | (g << 8) | r; } - void _blitVgaImage( - VgaImage image, - int startX, - int startY, - FrameBuffer buffer, - ) { - // Wolfenstein 3D VGA images are stored in "Mode Y" Planar format. - // We must de-interleave the 4 planes to draw them correctly! - int planeWidth = image.width ~/ 4; - int planeSize = planeWidth * image.height; + /// Tints the top 80% of the screen red based on player.damageFlash intensity + void _applyDamageFlash() { + // Grab the intensity (0.0 to 1.0) + double intensity = _engine.player.damageFlash; - for (int y = 0; y < image.height; y++) { - for (int x = 0; x < image.width; x++) { - int drawX = startX + x; - int drawY = startY + y; + // Calculate how much to boost red and drop green/blue + int redBoost = (150 * intensity).toInt(); + double colorDrop = 1.0 - (0.5 * intensity); - if (drawX >= 0 && - drawX < buffer.width && - drawY >= 0 && - drawY < buffer.height) { - // Planar to Linear coordinate conversion - int plane = x % 4; - int sx = x ~/ 4; - int index = (plane * planeSize) + (y * planeWidth) + sx; + for (int y = 0; y < viewHeight; y++) { + for (int x = 0; x < width; x++) { + int index = y * width + x; + int color = _buffer.pixels[index]; - int colorByte = image.pixels[index]; + int r = color & 0xFF; + int g = (color >> 8) & 0xFF; + int b = (color >> 16) & 0xFF; - if (colorByte != 255) { - // 255 is transparent - buffer.pixels[drawY * buffer.width + drawX] = - ColorPalette.vga32Bit[colorByte]; - } - } + r = (r + redBoost).clamp(0, 255); + g = (g * colorDrop).toInt(); + b = (b * colorDrop).toInt(); + + _buffer.pixels[index] = (0xFF000000) | (b << 16) | (g << 8) | r; } } } diff --git a/packages/wolf_3d_engine/lib/src/wolf_3d_engine_base.dart b/packages/wolf_3d_engine/lib/src/wolf_3d_engine_base.dart index 7a158c2..6c9e1b7 100644 --- a/packages/wolf_3d_engine/lib/src/wolf_3d_engine_base.dart +++ b/packages/wolf_3d_engine/lib/src/wolf_3d_engine_base.dart @@ -39,7 +39,6 @@ class WolfEngine { int _currentLevelIndex = 0; int? _returnLevelIndex; - double damageFlashOpacity = 0.0; bool isInitialized = false; void init() { @@ -57,8 +56,8 @@ class WolfEngine { doorManager.update(elapsed); pushwallManager.update(elapsed, currentLevel); + player.tick(elapsed); - player.updateWeaponSwitch(); player.angle += inputResult.dAngle; if (player.angle < 0) player.angle += 2 * math.pi; @@ -74,10 +73,6 @@ class WolfEngine { _updateEntities(elapsed); - if (damageFlashOpacity > 0) { - damageFlashOpacity = math.max(0.0, damageFlashOpacity - 0.05); - } - player.updateWeapon( currentTime: elapsed.inMilliseconds, entities: entities, @@ -87,7 +82,6 @@ class WolfEngine { void _loadLevel() { entities.clear(); - damageFlashOpacity = 0.0; final episode = data.episodes[_currentEpisodeIndex]; activeLevel = episode.levels[_currentLevelIndex]; @@ -279,7 +273,6 @@ class WolfEngine { tryOpenDoor: doorManager.tryOpenDoor, onDamagePlayer: (int damage) { player.takeDamage(damage); - damageFlashOpacity = 0.5; }, ); diff --git a/packages/wolf_3d_renderer/lib/ascii_rasterizer.dart b/packages/wolf_3d_renderer/lib/ascii_rasterizer.dart index c5c7d63..de5fcc6 100644 --- a/packages/wolf_3d_renderer/lib/ascii_rasterizer.dart +++ b/packages/wolf_3d_renderer/lib/ascii_rasterizer.dart @@ -3,7 +3,6 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:wolf_3d_data_types/wolf_3d_data_types.dart'; import 'package:wolf_3d_engine/wolf_3d_engine.dart'; -import 'package:wolf_3d_entities/wolf_3d_entities.dart'; class ColoredChar { final String char; @@ -11,231 +10,329 @@ class ColoredChar { ColoredChar(this.char, this.color); } -class AsciiRasterizer { +class AsciiRasterizer extends Rasterizer { static const String _charset = "@%#*+=-:. "; - // NEW: Helper to safely convert and artificially boost your raw memory colors + late List> _screen; + late WolfEngine _engine; + + // Terminal characters are usually twice as tall as they are wide. + // We override the base multiplier to squish sprites horizontally. + @override + double get aspectMultiplier => 0.6; + + // --- HELPER: Color Conversion --- Color _vgaToColor(int vgaColor, {double brightnessBoost = 2.0}) { int r = vgaColor & 0xFF; int g = (vgaColor >> 8) & 0xFF; int b = (vgaColor >> 16) & 0xFF; - // Apply the boost and clamp to 255 to prevent color overflow r = (r * brightnessBoost).toInt().clamp(0, 255); g = (g * brightnessBoost).toInt().clamp(0, 255); b = (b * brightnessBoost).toInt().clamp(0, 255); - // Force Alpha to 255 (fully opaque) return Color.fromARGB(255, r, g, b); } - List> render(WolfEngine engine, FrameBuffer framebuffer) { - final int width = framebuffer.width; - final int height = framebuffer.height; + // Intercept the base render call to initialize our text grid + @override + dynamic render(WolfEngine engine, FrameBuffer buffer) { + _engine = engine; + _screen = List.generate( + buffer.height, + (_) => List.filled(buffer.width, ColoredChar(' ', Colors.black)), + ); + return super.render(engine, buffer); + } - // Grab ceiling and floor colors from the original palette + @override + void prepareFrame(WolfEngine engine) { final Color ceilingColor = _vgaToColor(ColorPalette.vga32Bit[25]); final Color floorColor = _vgaToColor(ColorPalette.vga32Bit[29]); - final List> screen = List.generate( - height, - (_) => List.filled(width, ColoredChar(' ', ceilingColor)), - ); - - final List zBuffer = List.filled(width, 0.0); - - final Player player = engine.player; - final SpriteMap map = engine.currentLevel; - final List wallTextures = engine.data.walls; - - final double fov = math.pi / 3; - Coordinate2D dir = Coordinate2D( - math.cos(player.angle), - math.sin(player.angle), - ); - Coordinate2D plane = Coordinate2D(-dir.y, dir.x) * math.tan(fov / 2); - - // 1. CAST WALLS - for (int x = 0; x < width; x++) { - double cameraX = 2 * x / width - 1.0; - Coordinate2D rayDir = dir + (plane * cameraX); - - int mapX = player.x.toInt(); - int mapY = player.y.toInt(); - - double deltaDistX = (rayDir.x == 0) ? 1e30 : (1.0 / rayDir.x).abs(); - double deltaDistY = (rayDir.y == 0) ? 1e30 : (1.0 / rayDir.y).abs(); - - double sideDistX, sideDistY; - int stepX, stepY, side = 0, hitWallId = 0; - bool hit = false; - - if (rayDir.x < 0) { - stepX = -1; - sideDistX = (player.x - mapX) * deltaDistX; - } else { - stepX = 1; - sideDistX = (mapX + 1.0 - player.x) * deltaDistX; - } - if (rayDir.y < 0) { - stepY = -1; - sideDistY = (player.y - mapY) * deltaDistY; - } else { - stepY = 1; - sideDistY = (mapY + 1.0 - player.y) * deltaDistY; - } - - while (!hit) { - if (sideDistX < sideDistY) { - sideDistX += deltaDistX; - mapX += stepX; - side = 0; - } else { - sideDistY += deltaDistY; - mapY += stepY; - side = 1; - } - if (mapY < 0 || - mapY >= map.length || - mapX < 0 || - mapX >= map[0].length) { - break; - } - if (map[mapY][mapX] > 0) { - hit = true; - hitWallId = map[mapY][mapX]; - } - } - - double perpWallDist = (side == 0) - ? (sideDistX - deltaDistX) - : (sideDistY - deltaDistY); - if (perpWallDist < 0.1) perpWallDist = 0.1; - - zBuffer[x] = perpWallDist; - - double wallX = (side == 0) - ? player.y + perpWallDist * rayDir.y - : player.x + perpWallDist * rayDir.x; - wallX -= wallX.floor(); - int texX = (wallX * 64).toInt().clamp(0, 63); - - int texNum = ((hitWallId - 1) * 2).clamp(0, wallTextures.length - 2); - if (side == 1) texNum += 1; - Sprite texture = wallTextures[texNum]; - - int columnHeight = (height / perpWallDist).toInt(); - int drawStart = (-columnHeight ~/ 2 + height ~/ 2).clamp(0, height); - int drawEnd = (columnHeight ~/ 2 + height ~/ 2).clamp(0, height); - - double brightness = (1.5 / (perpWallDist + 1.0)).clamp(0.0, 1.0); - String wallChar = - _charset[((1.0 - brightness) * (_charset.length - 1)).toInt().clamp( - 0, - _charset.length - 1, - )]; - - for (int y = 0; y < height; y++) { - if (y >= drawStart && y < drawEnd) { - double relativeY = (y - drawStart) / (drawEnd - drawStart); - int texY = (relativeY * 64).toInt().clamp(0, 63); - - int colorByte = texture.pixels[texX * 64 + texY]; - - // Use our new color conversion! - Color pixelColor = _vgaToColor(ColorPalette.vga32Bit[colorByte]); - - // Optional: slightly darken the Y-side walls for a faux-lighting effect - // if (side == 1) { - // pixelColor = Color.fromARGB( - // 255, - // (pixelColor.r * 0.7).toInt(), - // (pixelColor.g * 0.7).toInt(), - // (pixelColor.b * 0.7).toInt(), - // ); - // } - - screen[y][x] = ColoredChar(wallChar, pixelColor); - } else if (y >= drawEnd) { - // Floor - screen[y][x] = ColoredChar('.', floorColor); - } else { - // Ceiling - screen[y][x] = ColoredChar(' ', ceilingColor); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + if (y < viewHeight / 2) { + _screen[y][x] = ColoredChar(' ', ceilingColor); + } else if (y < viewHeight) { + _screen[y][x] = ColoredChar('.', floorColor); } } } + } - // 2. CAST SPRITES (Enemies/Items) - final List activeSprites = List.from(engine.entities); - activeSprites.sort((a, b) { - double distA = player.position.distanceTo(a.position); - double distB = player.position.distanceTo(b.position); - return distB.compareTo(distA); - }); - - for (Entity entity in activeSprites) { - Coordinate2D spritePos = entity.position - player.position; - - double invDet = 1.0 / (plane.x * dir.y - dir.x * plane.y); - double transformX = invDet * (dir.y * spritePos.x - dir.x * spritePos.y); - double transformY = - invDet * (-plane.y * spritePos.x + plane.x * spritePos.y); - - if (transformY > 0) { - int spriteScreenX = ((width / 2) * (1 + transformX / transformY)) - .toInt(); - int spriteHeight = (height / transformY).abs().toInt(); - int spriteWidth = (spriteHeight * (width / height) * 0.6).toInt(); - - int drawStartY = -spriteHeight ~/ 2 + height ~/ 2; - int drawEndY = spriteHeight ~/ 2 + height ~/ 2; - int drawStartX = -spriteWidth ~/ 2 + spriteScreenX; - int drawEndX = spriteWidth ~/ 2 + spriteScreenX; - - int clipStartX = math.max(0, drawStartX); - int clipEndX = math.min(width - 1, drawEndX); - - int safeIndex = entity.spriteIndex.clamp( + @override + void drawWallColumn( + int x, + int drawStart, + int drawEnd, + int columnHeight, + Sprite texture, + int texX, + double perpWallDist, + int side, + ) { + double brightness = (1.5 / (perpWallDist + 1.0)).clamp(0.0, 1.0); + String wallChar = + _charset[((1.0 - brightness) * (_charset.length - 1)).toInt().clamp( 0, - engine.data.sprites.length - 1, + _charset.length - 1, + )]; + + for (int y = drawStart; y < drawEnd; y++) { + double relativeY = + (y - (-columnHeight ~/ 2 + viewHeight ~/ 2)) / columnHeight; + int texY = (relativeY * 64).toInt().clamp(0, 63); + + int colorByte = texture.pixels[texX * 64 + texY]; + Color pixelColor = _vgaToColor(ColorPalette.vga32Bit[colorByte]); + + // Faux directional lighting + if (side == 1) { + pixelColor = Color.fromARGB( + 255, + (pixelColor.r * 0.9).toInt().clamp(0, 255), + (pixelColor.g * 0.9).toInt().clamp(0, 255), + (pixelColor.b * 0.9).toInt().clamp(0, 255), ); - Sprite spritePixels = engine.data.sprites[safeIndex]; + } - double brightness = (1.5 / (transformY + 1.0)).clamp(0.0, 1.0); - String spriteChar = - _charset[((1.0 - brightness) * (_charset.length - 1)).toInt().clamp( - 0, - _charset.length - 1, - )]; + _screen[y][x] = ColoredChar(wallChar, pixelColor); + } + } - for (int stripe = clipStartX; stripe < clipEndX; stripe++) { - if (transformY < zBuffer[stripe]) { - int texX = ((stripe - drawStartX) * 64 ~/ spriteWidth).clamp(0, 63); + @override + void drawSpriteStripe( + int stripeX, + int drawStartY, + int drawEndY, + int spriteHeight, + Sprite texture, + int texX, + double transformY, + ) { + double brightness = (1.5 / (transformY + 1.0)).clamp(0.0, 1.0); + String spriteChar = + _charset[((1.0 - brightness) * (_charset.length - 1)).toInt().clamp( + 0, + _charset.length - 1, + )]; - for ( - int y = math.max(0, drawStartY); - y < math.min(height, drawEndY); - y++ - ) { - double relativeY = (y - drawStartY) / (drawEndY - drawStartY); - int texY = (relativeY * 64).toInt().clamp(0, 63); + for ( + int y = math.max(0, drawStartY); + y < math.min(viewHeight, drawEndY); + y++ + ) { + double relativeY = (y - drawStartY) / spriteHeight; + int texY = (relativeY * 64).toInt().clamp(0, 63); - int colorByte = spritePixels.pixels[texX * 64 + texY]; + int colorByte = texture.pixels[texX * 64 + texY]; + if (colorByte != 255) { + _screen[y][stripeX] = ColoredChar( + spriteChar, + _vgaToColor(ColorPalette.vga32Bit[colorByte]), + ); + } + } + } - if (colorByte != 255) { - // Apply the safe color conversion here as well - Color pixelColor = _vgaToColor( - ColorPalette.vga32Bit[colorByte], - ); - screen[y][stripe] = ColoredChar(spriteChar, pixelColor); - } - } + @override + void drawWeapon(WolfEngine engine) { + int spriteIndex = engine.player.currentWeapon.getCurrentSpriteIndex( + engine.data.sprites.length, + ); + Sprite weaponSprite = engine.data.sprites[spriteIndex]; + + int weaponWidth = (width * 0.5).toInt(); + int weaponHeight = (viewHeight * 0.8).toInt(); + + int startX = (width ~/ 2) - (weaponWidth ~/ 2); + int startY = + viewHeight - weaponHeight + (engine.player.weaponAnimOffset ~/ 4); + + for (int dy = 0; dy < weaponHeight; dy++) { + for (int dx = 0; dx < weaponWidth; dx++) { + int texX = (dx * 64 ~/ weaponWidth).clamp(0, 63); + int texY = (dy * 64 ~/ weaponHeight).clamp(0, 63); + + int colorByte = weaponSprite.pixels[texX * 64 + texY]; + if (colorByte != 255) { + int drawX = startX + dx; + int drawY = startY + dy; + if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < viewHeight) { + _screen[drawY][drawX] = ColoredChar( + '@', + _vgaToColor(ColorPalette.vga32Bit[colorByte]), + ); } } } } + } - return screen; + @override + void drawHud(WolfEngine engine) { + int statusBarIndex = engine.data.vgaImages.indexWhere( + (img) => img.width == 320 && img.height == 40, + ); + if (statusBarIndex == -1) return; + + // 1. Draw Background + _blitVgaImageAscii(engine.data.vgaImages[statusBarIndex], 0, 160); + + // 2. Draw Stats + _drawNumberAscii(1, 32, 176, engine.data.vgaImages); // Floor + _drawNumberAscii( + engine.player.score, + 96, + 176, + engine.data.vgaImages, + ); // Score + _drawNumberAscii(3, 120, 176, engine.data.vgaImages); // Lives + _drawNumberAscii( + engine.player.health, + 192, + 176, + engine.data.vgaImages, + ); // Health + _drawNumberAscii( + engine.player.ammo, + 232, + 176, + engine.data.vgaImages, + ); // Ammo + + // 3. Draw BJ's Face & Current Weapon + _drawFaceAscii(engine); + _drawWeaponIconAscii(engine); + } + + void _drawNumberAscii( + int value, + int rightAlignX, + int startY, + List vgaImages, + ) { + const int zeroIndex = 96; + String numStr = value.toString(); + int currentX = rightAlignX - (numStr.length * 8); + + for (int i = 0; i < numStr.length; i++) { + int digit = int.parse(numStr[i]); + if (zeroIndex + digit < vgaImages.length) { + _blitVgaImageAscii(vgaImages[zeroIndex + digit], currentX, startY); + } + currentX += 8; + } + } + + void _drawFaceAscii(WolfEngine engine) { + int health = engine.player.health; + int faceIndex; + + if (health <= 0) { + faceIndex = 127; + } else { + int healthTier = ((100 - health) ~/ 16).clamp(0, 6); + faceIndex = 106 + (healthTier * 3); + } + + if (faceIndex < engine.data.vgaImages.length) { + _blitVgaImageAscii(engine.data.vgaImages[faceIndex], 136, 164); + } + } + + void _drawWeaponIconAscii(WolfEngine engine) { + int weaponIndex = 89; + if (engine.player.hasChainGun) { + weaponIndex = 91; + } else if (engine.player.hasMachineGun) { + weaponIndex = 90; + } + + if (weaponIndex < engine.data.vgaImages.length) { + _blitVgaImageAscii(engine.data.vgaImages[weaponIndex], 256, 164); + } + } + + @override + dynamic finalizeFrame() { + if (_engine.player.damageFlash > 0.0) { + _applyDamageFlash(); + } + return _screen; + } + + // --- PRIVATE HUD DRAWING HELPERS --- + + void _blitVgaImageAscii(VgaImage image, int startX_320, int startY_200) { + int planeWidth = image.width ~/ 4; + int planeSize = planeWidth * image.height; + + double scaleX = width / 320.0; + double scaleY = height / 200.0; + + int destStartX = (startX_320 * scaleX).toInt(); + int destStartY = (startY_200 * scaleY).toInt(); + int destWidth = (image.width * scaleX).toInt(); + int destHeight = (image.height * scaleY).toInt(); + + for (int dy = 0; dy < destHeight; dy++) { + for (int dx = 0; dx < destWidth; dx++) { + int drawX = destStartX + dx; + int drawY = destStartY + dy; + + if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < height) { + int srcX = (dx / scaleX).toInt().clamp(0, image.width - 1); + int srcY = (dy / scaleY).toInt().clamp(0, image.height - 1); + + int plane = srcX % 4; + int sx = srcX ~/ 4; + int index = (plane * planeSize) + (srcY * planeWidth) + sx; + + int colorByte = image.pixels[index]; + if (colorByte != 255) { + // Using '█' for UI to make it look solid + _screen[drawY][drawX] = ColoredChar( + '█', + _vgaToColor( + ColorPalette.vga32Bit[colorByte], + brightnessBoost: 1.5, + ), + ); + } + } + } + } + } + + // --- DAMAGE FLASH --- + + void _applyDamageFlash() { + double intensity = _engine.player.damageFlash; + int redBoost = (150 * intensity).toInt(); + double colorDrop = 1.0 - (0.5 * intensity); + + for (int y = 0; y < viewHeight; y++) { + for (int x = 0; x < width; x++) { + Color c = _screen[y][x].color; + + int r = ((c.r * 255).round().clamp(0, 255) + redBoost).clamp(0, 255); + int g = ((c.g * 255).round().clamp(0, 255) * colorDrop).toInt().clamp( + 0, + 255, + ); + int b = ((c.b * 255).round().clamp(0, 255) * colorDrop).toInt().clamp( + 0, + 255, + ); + + // Replace the existing character with a red-tinted version + _screen[y][x] = ColoredChar( + _screen[y][x].char, + Color.fromARGB(255, r, g, b), + ); + } + } } }