diff --git a/packages/wolf_3d_engine/lib/src/rasterizer.dart b/packages/wolf_3d_engine/lib/src/rasterizer.dart index 2da1861..2c407a1 100644 --- a/packages/wolf_3d_engine/lib/src/rasterizer.dart +++ b/packages/wolf_3d_engine/lib/src/rasterizer.dart @@ -1,508 +1,6 @@ -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 { - // Pre-calculated VGA colors for ceiling and floor - final int ceilingColor = ColorPalette.vga32Bit[25]; - final int floorColor = ColorPalette.vga32Bit[29]; - - void render(WolfEngine engine, FrameBuffer buffer) { - _clearScreen(buffer); - - 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; - } - 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( - int x, - double distance, - double wallX, - 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; - - int colorByte = texture.pixels[texX * 64 + texY]; - int color32 = ColorPalette.vga32Bit[colorByte]; - - buffer.pixels[y * buffer.width + x] = color32; - } - } - - void _castSprites( - WolfEngine engine, - FrameBuffer buffer, - List zBuffer, - ) { - const int viewHeight = 160; - final Player player = engine.player; - final List activeSprites = List.from(engine.entities); - - // 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); - }); - - 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]; - } - } - } - } - } - } - } - - void _drawWeapon(WolfEngine engine, FrameBuffer buffer) { - const int viewHeight = 160; - 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 startX = (buffer.width ~/ 2) - (weaponWidth ~/ 2); - - // Kept the grounding to the bottom of the screen - int startY = - viewHeight - weaponHeight + (engine.player.weaponAnimOffset ~/ 2); - - for (int x = 0; x < 64; x++) { - for (int y = 0; y < 64; y++) { - int colorByte = weaponSprite.pixels[x * 64 + y]; - - 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; - } - } - } - } - } - } - } - - void _drawDamageFlash(WolfEngine engine, FrameBuffer buffer) { - if (engine.damageFlashOpacity <= 0) return; - - int alpha = (engine.damageFlashOpacity * 256).toInt().clamp(0, 256); - int invAlpha = 256 - alpha; - - for (int i = 0; i < buffer.pixels.length; i++) { - int color = buffer.pixels[i]; - - int r = color & 0xFF; - int g = (color >> 8) & 0xFF; - int b = (color >> 16) & 0xFF; - int a = (color >> 24) & 0xFF; - - // Blend with Red - r = ((r * invAlpha) + (255 * alpha)) >> 8; - g = (g * invAlpha) >> 8; - b = (b * invAlpha) >> 8; - - buffer.pixels[i] = (a << 24) | (b << 16) | (g << 8) | r; - } - } - - void _drawHud(WolfEngine engine, FrameBuffer buffer) { - // Clever trick: Find the only 320x40 graphic in the VGA chunks! - int statusBarIndex = engine.data.vgaImages.indexWhere( - (img) => img.width == 320 && img.height == 40, - ); - - if (statusBarIndex == -1) return; // Safety check if it fails to find it - - VgaImage statusBar = engine.data.vgaImages[statusBarIndex]; - - // Draw the background status bar at Y=160 - _blitVgaImage(statusBar, 0, 160, buffer); - - // --- We will add the digits and face here next --- - } - - void _drawNumber( - int value, - int rightAlignX, - int startY, - FrameBuffer buffer, - List vgaImages, - int zeroIndex, // The VGA index of the '0' digit - ) { - String numStr = value.toString(); - - // Original Wolf3D status bar digits are exactly 8 pixels wide - // We calculate the starting X by moving left based on how many digits there are - int currentX = rightAlignX - (numStr.length * 8); - - for (int i = 0; i < numStr.length; i++) { - int digit = int.parse(numStr[i]); - - // Because digits 0-9 are stored sequentially, we can just add the - // actual number to the base 'zeroIndex' to get the right graphic! - _blitVgaImage(vgaImages[zeroIndex + digit], currentX, startY, buffer); - - currentX += 8; // Move right for the next digit - } - } - - 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; - - for (int y = 0; y < image.height; y++) { - for (int x = 0; x < image.width; x++) { - int drawX = startX + x; - int drawY = startY + y; - - 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; - - int colorByte = image.pixels[index]; - - if (colorByte != 255) { - // 255 is transparent - buffer.pixels[drawY * buffer.width + drawX] = - ColorPalette.vga32Bit[colorByte]; - } - } - } - } - } +abstract class Rasterizer { + dynamic render(WolfEngine engine, FrameBuffer buffer); } diff --git a/packages/wolf_3d_engine/lib/src/software_rasterizer.dart b/packages/wolf_3d_engine/lib/src/software_rasterizer.dart new file mode 100644 index 0000000..b568a0e --- /dev/null +++ b/packages/wolf_3d_engine/lib/src/software_rasterizer.dart @@ -0,0 +1,561 @@ +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]; + + @override + void render(WolfEngine engine, FrameBuffer buffer) { + _clearScreen(buffer); + + 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; + } + 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( + int x, + double distance, + double wallX, + 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; + + int colorByte = texture.pixels[texX * 64 + texY]; + int color32 = ColorPalette.vga32Bit[colorByte]; + + buffer.pixels[y * buffer.width + x] = color32; + } + } + + void _castSprites( + WolfEngine engine, + FrameBuffer buffer, + List zBuffer, + ) { + const int viewHeight = 160; + final Player player = engine.player; + final List activeSprites = List.from(engine.entities); + + // 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); + }); + + 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]; + } + } + } + } + } + } + } + + void _drawWeapon(WolfEngine engine, FrameBuffer buffer) { + const int viewHeight = 160; + 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 startX = (buffer.width ~/ 2) - (weaponWidth ~/ 2); + + // Kept the grounding to the bottom of the screen + int startY = + viewHeight - weaponHeight + (engine.player.weaponAnimOffset ~/ 2); + + for (int x = 0; x < 64; x++) { + for (int y = 0; y < 64; y++) { + int colorByte = weaponSprite.pixels[x * 64 + y]; + + 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; + } + } + } + } + } + } + } + + void _drawDamageFlash(WolfEngine engine, FrameBuffer buffer) { + if (engine.damageFlashOpacity <= 0) return; + + int alpha = (engine.damageFlashOpacity * 256).toInt().clamp(0, 256); + int invAlpha = 256 - alpha; + + for (int i = 0; i < buffer.pixels.length; i++) { + int color = buffer.pixels[i]; + + int r = color & 0xFF; + int g = (color >> 8) & 0xFF; + int b = (color >> 16) & 0xFF; + int a = (color >> 24) & 0xFF; + + // Blend with Red + r = ((r * invAlpha) + (255 * alpha)) >> 8; + g = (g * invAlpha) >> 8; + b = (b * invAlpha) >> 8; + + buffer.pixels[i] = (a << 24) | (b << 16) | (g << 8) | r; + } + } + + void _drawNumber( + 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); + } + currentX += 8; + } + } + + void _drawFace(WolfEngine engine, FrameBuffer buffer) { + int health = engine.player.health; + int faceIndex; + + if (health <= 0) { + faceIndex = 127; // Dead face + } else { + int healthTier = ((100 - health) ~/ 16).clamp(0, 6); + + // Base face is 106 (100% health, looking forward). + faceIndex = 106 + (healthTier * 3); + } + + if (faceIndex < engine.data.vgaImages.length) { + _blitVgaImage(engine.data.vgaImages[faceIndex], 142, 164, buffer); + } + } + + void _drawWeaponIcon(WolfEngine engine, FrameBuffer buffer) { + int weaponIndex = 89; // Default to Pistol (Index 89) + + if (engine.player.hasChainGun) { + weaponIndex = 91; + } else if (engine.player.hasMachineGun) { + weaponIndex = 90; + } + + if (weaponIndex < engine.data.vgaImages.length) { + // The weapon box starts at X=272. Let's center it vertically at 164. + _blitVgaImage(engine.data.vgaImages[weaponIndex], 272, 164, buffer); + } + } + + 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 (Right-aligned to fit inside the dark blue boxes) + // (Using a hardcoded '1' for the floor since your engine hides currentLevelIndex) + _drawNumber(1, 40, 176, buffer, engine.data.vgaImages); // Floor + _drawNumber( + engine.player.score, + 104, + 176, + buffer, + engine.data.vgaImages, + ); // Score + _drawNumber(3, 136, 176, buffer, engine.data.vgaImages); // Lives + _drawNumber( + engine.player.health, + 216, + 176, + buffer, + engine.data.vgaImages, + ); // Health + + // Pulled the Ammo X coordinate back to 264 so it doesn't bleed into the weapon! + _drawNumber(engine.player.ammo, 264, 176, buffer, engine.data.vgaImages); + + // 3. Draw BJ's Face & Current Weapon + _drawFace(engine, buffer); + _drawWeaponIcon(engine, buffer); + } + + 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; + + for (int y = 0; y < image.height; y++) { + for (int x = 0; x < image.width; x++) { + int drawX = startX + x; + int drawY = startY + y; + + 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; + + int colorByte = image.pixels[index]; + + if (colorByte != 255) { + // 255 is transparent + buffer.pixels[drawY * buffer.width + drawX] = + ColorPalette.vga32Bit[colorByte]; + } + } + } + } + } +} diff --git a/packages/wolf_3d_engine/lib/wolf_3d_engine.dart b/packages/wolf_3d_engine/lib/wolf_3d_engine.dart index 156deda..ec9c694 100644 --- a/packages/wolf_3d_engine/lib/wolf_3d_engine.dart +++ b/packages/wolf_3d_engine/lib/wolf_3d_engine.dart @@ -3,10 +3,12 @@ /// More dartdocs go here. library; +export 'src/ascii_rasterizer.dart'; export 'src/engine_audio.dart'; export 'src/engine_input.dart'; export 'src/managers/door_manager.dart'; export 'src/managers/pushwall_manager.dart'; export 'src/player/player.dart'; export 'src/rasterizer.dart'; +export 'src/software_rasterizer.dart'; export 'src/wolf_3d_engine_base.dart';