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'; 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 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]; 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; } } } @override void drawWallColumn( int x, int drawStart, int drawEnd, int columnHeight, Sprite texture, int texX, double perpWallDist, int side, ) { for (int y = drawStart; y < drawEnd; y++) { // 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 pixelColor = ColorPalette.vga32Bit[colorByte]; // Darken Y-side walls for faux directional lighting if (side == 1) { pixelColor = shadeColor(pixelColor); } _buffer.pixels[y * width + x] = pixelColor; } } @override void drawSpriteStripe( int stripeX, int drawStartY, int drawEndY, int spriteHeight, Sprite texture, int texX, double transformY, ) { 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 = texture.pixels[texX * 64 + texY]; // 255 is the "transparent" color index in VGA Wolfenstein if (colorByte != 255) { _buffer.pixels[y * width + stripeX] = ColorPalette.vga32Bit[colorByte]; } } } @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) { _buffer.pixels[drawY * width + drawX] = ColorPalette.vga32Bit[colorByte]; } } } } } @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 _blitVgaImage(engine.data.vgaImages[statusBarIndex], 0, 160); // 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 // 3. Draw BJ's Face & Current Weapon _drawFace(engine); _drawWeaponIcon(engine); } @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 } // =========================================================================== // 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]; } } } } } void _drawNumber( 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) { _blitVgaImage(vgaImages[zeroIndex + digit], currentX, startY); } currentX += 8; } } void _drawFace(WolfEngine engine) { int health = engine.player.health; int faceIndex; if (health <= 0) { faceIndex = 127; // Dead face } else { int healthTier = ((100 - health) ~/ 16).clamp(0, 6); faceIndex = 106 + (healthTier * 3); } if (faceIndex < engine.data.vgaImages.length) { _blitVgaImage(engine.data.vgaImages[faceIndex], 136, 164); } } void _drawWeaponIcon(WolfEngine engine) { int weaponIndex = 89; // Default to Pistol if (engine.player.hasChainGun) { weaponIndex = 91; } else if (engine.player.hasMachineGun) { weaponIndex = 90; } if (weaponIndex < engine.data.vgaImages.length) { _blitVgaImage(engine.data.vgaImages[weaponIndex], 256, 164); } } /// 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; // Calculate how much to boost red and drop green/blue 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++) { int index = y * width + x; int color = _buffer.pixels[index]; int r = color & 0xFF; int g = (color >> 8) & 0xFF; int b = (color >> 16) & 0xFF; r = (r + redBoost).clamp(0, 255); g = (g * colorDrop).toInt(); b = (b * colorDrop).toInt(); _buffer.pixels[index] = (0xFF000000) | (b << 16) | (g << 8) | r; } } } }