From d93f4671632a3fcbfcf68e63dc25d094c0c8122a Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Wed, 18 Mar 2026 17:25:20 +0100 Subject: [PATCH] Refactor rasterizer classes to centralize HUD drawing and pixel decoding - Introduced `decodePixel` method in `VgaImage` to handle pixel mapping for planar VGA images. - Updated `AsciiRasterizer`, `SixelRasterizer`, and `SoftwareRasterizer` to utilize the new `decodePixel` method for improved clarity and reduced code duplication. - Centralized HUD drawing logic in the `Rasterizer` base class with `drawStandardVgaHud` method, allowing subclasses to easily implement HUD rendering. - Removed redundant HUD drawing methods from individual rasterizers, streamlining the codebase. - Enhanced documentation for clarity on methods and their purposes. Signed-off-by: Hans Kokx --- .../lib/src/data_types/image.dart | 14 ++ .../lib/src/rasterizer/ascii_rasterizer.dart | 149 ++------------- .../lib/src/rasterizer/rasterizer.dart | 118 ++++++++++++ .../lib/src/rasterizer/sixel_rasterizer.dart | 134 ++++---------- .../src/rasterizer/software_rasterizer.dart | 175 ++++-------------- 5 files changed, 217 insertions(+), 373 deletions(-) diff --git a/packages/wolf_3d_dart/lib/src/data_types/image.dart b/packages/wolf_3d_dart/lib/src/data_types/image.dart index c75cc9f..8388cfd 100644 --- a/packages/wolf_3d_dart/lib/src/data_types/image.dart +++ b/packages/wolf_3d_dart/lib/src/data_types/image.dart @@ -24,4 +24,18 @@ class VgaImage { required this.height, required this.pixels, }); + + /// Returns the palette index at `[srcX, srcY]` for planar VGA image data. + /// + /// Callers are expected to provide coordinates already clamped to the image + /// bounds. The Wolf3D VGA format stores image bytes in 4 interleaved planes; + /// this helper centralizes that mapping so rasterizers do not duplicate it. + int decodePixel(int srcX, int srcY) { + final int planeWidth = width ~/ 4; + final int planeSize = planeWidth * height; + final int plane = srcX % 4; + final int sx = srcX ~/ 4; + final int index = (plane * planeSize) + (srcY * planeWidth) + sx; + return pixels[index]; + } } diff --git a/packages/wolf_3d_dart/lib/src/rasterizer/ascii_rasterizer.dart b/packages/wolf_3d_dart/lib/src/rasterizer/ascii_rasterizer.dart index 78c041c..c97787d 100644 --- a/packages/wolf_3d_dart/lib/src/rasterizer/ascii_rasterizer.dart +++ b/packages/wolf_3d_dart/lib/src/rasterizer/ascii_rasterizer.dart @@ -78,6 +78,8 @@ enum AsciiRasterizerMode { terminalGrid, } +/// Text-mode rasterizer that can render to ANSI escape output or a Flutter +/// grid model of colored characters. class AsciiRasterizer extends CliRasterizer { static const double _targetAspectRatio = 4 / 3; static const int _terminalBackdropPaletteIndex = 153; @@ -106,7 +108,6 @@ class AsciiRasterizer extends CliRasterizer { late List> _screen; late List> _scenePixels; - late WolfEngine _engine; @override final double aspectMultiplier; @@ -151,8 +152,8 @@ class AsciiRasterizer extends CliRasterizer { // Intercept the base render call to initialize our text grid @override + /// Initializes the character grid before running the shared render pipeline. dynamic render(WolfEngine engine) { - _engine = engine; _screen = List.generate( engine.frameBuffer.height, (_) => List.filled( @@ -213,9 +214,7 @@ class AsciiRasterizer extends CliRasterizer { double brightness = calculateDepthBrightness(perpWallDist); for (int y = drawStart; y < drawEnd; y++) { - double relativeY = - (y - (-columnHeight ~/ 2 + projectionViewHeight ~/ 2)) / columnHeight; - int texY = (relativeY * 64).toInt().clamp(0, 63); + int texY = wallTexY(y, columnHeight); int colorByte = texture.pixels[texX * 64 + texY]; int pixelColor = ColorPalette.vga32Bit[colorByte]; // Raw int @@ -251,8 +250,7 @@ class AsciiRasterizer extends CliRasterizer { y < math.min(projectionViewHeight, drawEndY); y++ ) { - double relativeY = (y - drawStartY) / spriteHeight; - int texY = (relativeY * 64).toInt().clamp(0, 63); + int texY = spriteTexY(y, drawStartY, spriteHeight); int colorByte = texture.pixels[texX * 64 + texY]; if (colorByte != 255) { @@ -285,12 +283,11 @@ class AsciiRasterizer extends CliRasterizer { ); Sprite weaponSprite = engine.data.sprites[spriteIndex]; - int weaponWidth = (projectionWidth * 0.5).toInt(); - int weaponHeight = ((projectionViewHeight * 0.8)).toInt(); - - int startX = - projectionOffsetX + (projectionWidth ~/ 2) - (weaponWidth ~/ 2); - int startY = + final bounds = weaponScreenBounds(engine); + final int weaponWidth = bounds.weaponWidth; + final int weaponHeight = (projectionViewHeight * 0.8).toInt(); + final int startX = bounds.startX; + final int startY = projectionViewHeight - weaponHeight + (engine.player.weaponAnimOffset * (_usesTerminalLayout ? 2 : 1) ~/ 4); @@ -925,87 +922,13 @@ class AsciiRasterizer extends CliRasterizer { } void _drawFullVgaHud(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); + drawStandardVgaHud(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 + /// Blits a VGA image into ASCII HUD space (mapped/scaled from 320x200). + void blitHudVgaImage(VgaImage image, int startX320, int startY200) { + _blitVgaImageAscii(image, startX320, startY200); } /// Helper to fill a rectangular area with a specific char and background color @@ -1023,7 +946,7 @@ class AsciiRasterizer extends CliRasterizer { @override dynamic finalizeFrame() { - if (_engine.difficulty != null && _engine.player.damageFlash > 0.0) { + if (engine.difficulty != null && engine.player.damageFlash > 0.0) { if (_usesTerminalLayout) { _applyDamageFlashToScene(); } else { @@ -1040,8 +963,6 @@ class AsciiRasterizer extends CliRasterizer { // --- PRIVATE HUD DRAWING HELPERS --- void _blitVgaImageAscii(VgaImage image, int startX_320, int startY_200) { - int planeWidth = image.width ~/ 4; - int planeSize = planeWidth * image.height; int maxDrawHeight = _usesTerminalLayout ? _terminalPixelHeight : height; int maxDrawWidth = _usesTerminalLayout ? _viewportRightX : width; @@ -1068,11 +989,7 @@ class AsciiRasterizer extends CliRasterizer { 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]; + int colorByte = image.decodePixel(srcX, srcY); if (colorByte != 255) { if (_usesTerminalLayout) { _scenePixels[drawY][drawX] = ColorPalette.vga32Bit[colorByte]; @@ -1151,7 +1068,7 @@ class AsciiRasterizer extends CliRasterizer { } int _applyDamageFlashToColor(int color) { - double intensity = _engine.player.damageFlash; + double intensity = engine.player.damageFlash; int redBoost = (150 * intensity).toInt(); double colorDrop = 1.0 - (0.5 * intensity); @@ -1253,34 +1170,6 @@ class AsciiRasterizer extends CliRasterizer { } int _rgbToPaletteColor(int rgb) { - return ColorPalette.vga32Bit[_rgbToPaletteIndex(rgb)]; - } - - int _rgbToPaletteIndex(int rgb) { - final int targetR = (rgb >> 16) & 0xFF; - final int targetG = (rgb >> 8) & 0xFF; - final int targetB = rgb & 0xFF; - - int bestIndex = 0; - int bestDistance = 1 << 30; - - for (int i = 0; i < 256; i++) { - final int color = ColorPalette.vga32Bit[i]; - final int r = color & 0xFF; - final int g = (color >> 8) & 0xFF; - final int b = (color >> 16) & 0xFF; - - final int dr = targetR - r; - final int dg = targetG - g; - final int db = targetB - b; - final int distance = (dr * dr) + (dg * dg) + (db * db); - - if (distance < bestDistance) { - bestDistance = distance; - bestIndex = i; - } - } - - return bestIndex; + return ColorPalette.vga32Bit[ColorPalette.findClosestPaletteIndex(rgb)]; } } diff --git a/packages/wolf_3d_dart/lib/src/rasterizer/rasterizer.dart b/packages/wolf_3d_dart/lib/src/rasterizer/rasterizer.dart index 2f5afc2..d24a306 100644 --- a/packages/wolf_3d_dart/lib/src/rasterizer/rasterizer.dart +++ b/packages/wolf_3d_dart/lib/src/rasterizer/rasterizer.dart @@ -4,12 +4,20 @@ import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; import 'package:wolf_3d_dart/wolf_3d_engine.dart'; import 'package:wolf_3d_dart/wolf_3d_entities.dart'; +/// Shared rendering pipeline and math utilities for all Wolf3D rasterizers. +/// +/// Subclasses implement draw primitives for their output target (software +/// framebuffer, ANSI text, Sixel, etc), while this base class coordinates +/// raycasting, sprite projection, and common HUD calculations. abstract class Rasterizer { late List zBuffer; late int width; late int height; late int viewHeight; + /// The current engine instance; set at the start of every [render] call. + late WolfEngine engine; + /// 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. @@ -41,6 +49,7 @@ abstract class Rasterizer { /// The main entry point called by the game loop. /// Orchestrates the mathematical rendering pipeline. T render(WolfEngine engine) { + this.engine = engine; width = engine.frameBuffer.width; height = engine.frameBuffer.height; // The 3D view typically takes up the top 80% of the screen @@ -113,6 +122,68 @@ abstract class Rasterizer { /// Default implementation is a no-op for renderers that don't support menus. void drawMenu(WolfEngine engine) {} + /// Plots a VGA image into this renderer's HUD coordinate space. + /// + /// Coordinates are in the original 320x200 HUD space. Renderers that support + /// shared HUD composition should override this. + void blitHudVgaImage(VgaImage image, int startX320, int startY200) {} + + /// Shared Wolf3D VGA HUD sequence used by software/sixel/ASCII-full-HUD. + /// + /// Coordinates are intentionally in original 320x200 HUD space so each + /// renderer can scale/map them consistently via [blitHudVgaImage]. + void drawStandardVgaHud(WolfEngine engine) { + final List vgaImages = engine.data.vgaImages; + final int statusBarIndex = vgaImages.indexWhere( + (img) => img.width == 320 && img.height == 40, + ); + if (statusBarIndex == -1) return; + + blitHudVgaImage(vgaImages[statusBarIndex], 0, 160); + _drawHudNumber(vgaImages, 1, 32, 176); + _drawHudNumber(vgaImages, engine.player.score, 96, 176); + _drawHudNumber(vgaImages, 3, 120, 176); + _drawHudNumber(vgaImages, engine.player.health, 192, 176); + _drawHudNumber(vgaImages, engine.player.ammo, 232, 176); + _drawHudFace(engine, vgaImages); + _drawHudWeaponIcon(engine, vgaImages); + } + + void _drawHudNumber( + List vgaImages, + int value, + int rightAlignX, + int startY, + ) { + // HUD numbers are rendered with fixed-width VGA glyphs (8 px advance). + const int zeroIndex = 96; + final String numStr = value.toString(); + int currentX = rightAlignX - (numStr.length * 8); + + for (int i = 0; i < numStr.length; i++) { + final int digit = int.parse(numStr[i]); + final int imageIndex = zeroIndex + digit; + if (imageIndex < vgaImages.length) { + blitHudVgaImage(vgaImages[imageIndex], currentX, startY); + } + currentX += 8; + } + } + + void _drawHudFace(WolfEngine engine, List vgaImages) { + final int faceIndex = hudFaceVgaIndex(engine.player.health); + if (faceIndex < vgaImages.length) { + blitHudVgaImage(vgaImages[faceIndex], 136, 164); + } + } + + void _drawHudWeaponIcon(WolfEngine engine, List vgaImages) { + final int weaponIndex = hudWeaponVgaIndex(engine); + if (weaponIndex < vgaImages.length) { + blitHudVgaImage(vgaImages[weaponIndex], 256, 164); + } + } + // =========================================================================== // SHARED LIGHTING MATH // =========================================================================== @@ -124,6 +195,53 @@ abstract class Rasterizer { return (10.0 / (distance + 2.0)).clamp(0.0, 1.0); } + // =========================================================================== + // SHARED PROJECTION MATH + // =========================================================================== + + /// Returns the texture Y coordinate for the given screen row inside a wall + /// column. Works for both pixel and terminal renderers. + int wallTexY(int y, int columnHeight) { + final double relativeY = + (y - (-columnHeight ~/ 2 + viewHeight ~/ 2)) / columnHeight; + return (relativeY * 64).toInt().clamp(0, 63); + } + + /// Returns the texture Y coordinate for the given screen row inside a sprite + /// stripe. + int spriteTexY(int y, int drawStartY, int spriteHeight) { + final double relativeY = (y - drawStartY) / spriteHeight; + return (relativeY * 64).toInt().clamp(0, 63); + } + + /// Returns the screen-space bounds for the player's weapon overlay. + /// + /// [weaponWidth] and [weaponHeight] are in pixels; [startX]/[startY] are + /// the top-left draw origin. Uses [projectionWidth] so that renderers + /// with a narrower projection area (e.g. ASCII terminal) are handled + /// correctly. + ({int weaponWidth, int weaponHeight, int startX, int startY}) + weaponScreenBounds(WolfEngine engine) { + final int ww = (projectionWidth * 0.5).toInt(); + final int wh = (viewHeight * 0.8).toInt(); + final int sx = projectionOffsetX + (projectionWidth ~/ 2) - (ww ~/ 2); + final int sy = viewHeight - wh + (engine.player.weaponAnimOffset ~/ 4); + return (weaponWidth: ww, weaponHeight: wh, startX: sx, startY: sy); + } + + /// Returns the VGA image index for BJ's face sprite based on player health. + int hudFaceVgaIndex(int health) { + if (health <= 0) return 127; + return 106 + (((100 - health) ~/ 16).clamp(0, 6) * 3); + } + + /// Returns the VGA image index for the current weapon icon in the HUD. + int hudWeaponVgaIndex(WolfEngine engine) { + if (engine.player.hasChainGun) return 91; + if (engine.player.hasMachineGun) return 90; + return 89; + } + ({double distance, int side, int hitWallId, double wallX})? _intersectActivePushwall( Player player, diff --git a/packages/wolf_3d_dart/lib/src/rasterizer/sixel_rasterizer.dart b/packages/wolf_3d_dart/lib/src/rasterizer/sixel_rasterizer.dart index f5f8a00..402fa7f 100644 --- a/packages/wolf_3d_dart/lib/src/rasterizer/sixel_rasterizer.dart +++ b/packages/wolf_3d_dart/lib/src/rasterizer/sixel_rasterizer.dart @@ -31,7 +31,6 @@ class SixelRasterizer extends CliRasterizer { static const String _terminalTealBackground = '\x1b[48;2;0;150;136m'; late Uint8List _screen; - late WolfEngine _engine; int _offsetColumns = 0; int _offsetRows = 0; int _outputWidth = 1; @@ -205,8 +204,9 @@ class SixelRasterizer extends CliRasterizer { } @override + /// Renders using a temporary scaled framebuffer sized for Sixel output, + /// then restores the original terminal-sized framebuffer. String render(WolfEngine engine) { - _engine = engine; final FrameBuffer originalBuffer = engine.frameBuffer; final FrameBuffer scaledBuffer = _createScaledBuffer(originalBuffer); @@ -244,9 +244,7 @@ class SixelRasterizer extends CliRasterizer { int side, ) { for (int y = drawStart; y < drawEnd; y++) { - double relativeY = - (y - (-columnHeight ~/ 2 + viewHeight ~/ 2)) / columnHeight; - int texY = (relativeY * 64).toInt().clamp(0, 63); + int texY = wallTexY(y, columnHeight); int colorByte = texture.pixels[texX * 64 + texY]; _screen[y * width + x] = colorByte; @@ -268,8 +266,7 @@ class SixelRasterizer extends CliRasterizer { y < math.min(viewHeight, drawEndY); y++ ) { - double relativeY = (y - drawStartY) / spriteHeight; - int texY = (relativeY * 64).toInt().clamp(0, 63); + int texY = spriteTexY(y, drawStartY, spriteHeight); int colorByte = texture.pixels[texX * 64 + texY]; @@ -287,12 +284,11 @@ class SixelRasterizer extends CliRasterizer { ); 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); + final bounds = weaponScreenBounds(engine); + final int weaponWidth = bounds.weaponWidth; + final int weaponHeight = bounds.weaponHeight; + final int startX = bounds.startX; + final int startY = bounds.startY; for (int dy = 0; dy < weaponHeight; dy++) { for (int dx = 0; dx < weaponWidth; dx++) { @@ -312,22 +308,15 @@ class SixelRasterizer extends CliRasterizer { } @override + /// Delegates the canonical Wolf3D VGA HUD draw sequence to the base class. void drawHud(WolfEngine engine) { - int statusBarIndex = engine.data.vgaImages.indexWhere( - (img) => img.width == 320 && img.height == 40, - ); - if (statusBarIndex == -1) return; + drawStandardVgaHud(engine); + } - _blitVgaImage(engine.data.vgaImages[statusBarIndex], 0, 160); - - _drawNumber(1, 32, 176, engine.data.vgaImages); - _drawNumber(engine.player.score, 96, 176, engine.data.vgaImages); - _drawNumber(3, 120, 176, engine.data.vgaImages); - _drawNumber(engine.player.health, 192, 176, engine.data.vgaImages); - _drawNumber(engine.player.ammo, 232, 176, engine.data.vgaImages); - - _drawFace(engine); - _drawWeaponIcon(engine); + @override + /// Blits a VGA image into the Sixel index buffer HUD space (320x200). + void blitHudVgaImage(VgaImage image, int startX320, int startY200) { + _blitVgaImage(image, startX320, startY200); } @override @@ -402,6 +391,7 @@ class SixelRasterizer extends CliRasterizer { int get _menuOptionScale => width < 220 ? 1 : 1; + /// Draws a compact fallback menu layout for narrow render targets. void _drawCompactMenu( int selectedDifficultyIndex, int headingIndex, @@ -427,6 +417,7 @@ class SixelRasterizer extends CliRasterizer { } } + /// Draws bitmap menu text into the indexed Sixel back buffer. void _drawMenuText( String text, int startX, @@ -466,6 +457,7 @@ class SixelRasterizer extends CliRasterizer { } } + /// Draws bitmap menu text centered in 320x200 menu space. void _drawMenuTextCentered( String text, int y, @@ -494,6 +486,7 @@ class SixelRasterizer extends CliRasterizer { // UI FALLBACK // =========================================================================== + /// Returns an ANSI fallback UI when Sixel capability is unavailable. String _renderNotSupportedMessage() { int cols = 80; int rows = 24; @@ -542,13 +535,14 @@ class SixelRasterizer extends CliRasterizer { // SIXEL ENCODER // =========================================================================== + /// Encodes the current indexed frame buffer as a Sixel image stream. String toSixelString() { StringBuffer sb = StringBuffer(); sb.write('\x1bPq'); - double damageIntensity = _engine.difficulty == null + double damageIntensity = engine.difficulty == null ? 0.0 - : _engine.player.damageFlash; + : engine.player.damageFlash; int redBoost = (150 * damageIntensity).toInt(); double colorDrop = 1.0 - (0.5 * damageIntensity); @@ -621,6 +615,8 @@ class SixelRasterizer extends CliRasterizer { return sb.toString(); } + /// Samples the render buffer at output-space coordinates using nearest-neighbor + /// mapping from Sixel output pixels to source pixels. int _sampleScaledPixel(int outX, int outY) { final int srcX = ((((outX + 0.5) * width) / _outputWidth) - 0.5) .round() @@ -637,6 +633,7 @@ class SixelRasterizer extends CliRasterizer { return _screen[srcY * width + srcX]; } + /// Emits run-length encoded Sixel data for one repeated column mask value. void _writeSixelRle(StringBuffer sb, int value, int runLength) { String char = String.fromCharCode(value + 63); if (runLength > 3) { @@ -650,9 +647,8 @@ class SixelRasterizer extends CliRasterizer { // PRIVATE HUD HELPERS // =========================================================================== + /// Blits a VGA image into the Sixel back buffer with 320x200 scaling. void _blitVgaImage(VgaImage image, int startX, int startY) { - int planeWidth = image.width ~/ 4; - int planeSize = planeWidth * image.height; final double scaleX = width / 320.0; final double scaleY = height / 200.0; @@ -670,11 +666,7 @@ class SixelRasterizer extends CliRasterizer { 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]; + int colorByte = image.decodePixel(srcX, srcY); if (colorByte != 255) { _screen[drawY * width + drawX] = colorByte; } @@ -683,6 +675,7 @@ class SixelRasterizer extends CliRasterizer { } } + /// Fills a rectangle in 320x200 virtual space into the scaled Sixel buffer. void _fillRect320(int startX, int startY, int w, int h, int colorIndex) { final double scaleX = width / 320.0; final double scaleY = height / 200.0; @@ -703,73 +696,8 @@ class SixelRasterizer extends CliRasterizer { } } - 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 = (health <= 0) - ? 127 - : 106 + (((100 - health) ~/ 16).clamp(0, 6) * 3); - if (faceIndex < engine.data.vgaImages.length) { - _blitVgaImage(engine.data.vgaImages[faceIndex], 136, 164); - } - } - - void _drawWeaponIcon(WolfEngine engine) { - int weaponIndex = 89; - 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); - } - } - + /// Maps an RGB color to the nearest VGA palette index. int _rgbToPaletteIndex(int rgb) { - final int targetR = (rgb >> 16) & 0xFF; - final int targetG = (rgb >> 8) & 0xFF; - final int targetB = rgb & 0xFF; - - int bestIndex = 0; - int bestDistance = 1 << 30; - - for (int i = 0; i < 256; i++) { - final int color = ColorPalette.vga32Bit[i]; - final int r = color & 0xFF; - final int g = (color >> 8) & 0xFF; - final int b = (color >> 16) & 0xFF; - - final int dr = targetR - r; - final int dg = targetG - g; - final int db = targetB - b; - final int dist = (dr * dr) + (dg * dg) + (db * db); - - if (dist < bestDistance) { - bestDistance = dist; - bestIndex = i; - } - } - - return bestIndex; + return ColorPalette.findClosestPaletteIndex(rgb); } } diff --git a/packages/wolf_3d_dart/lib/src/rasterizer/software_rasterizer.dart b/packages/wolf_3d_dart/lib/src/rasterizer/software_rasterizer.dart index 5345a62..559ae7b 100644 --- a/packages/wolf_3d_dart/lib/src/rasterizer/software_rasterizer.dart +++ b/packages/wolf_3d_dart/lib/src/rasterizer/software_rasterizer.dart @@ -1,54 +1,21 @@ import 'dart:math' as math; +import 'package:wolf_3d_dart/src/rasterizer/menu_font.dart'; import 'package:wolf_3d_dart/src/rasterizer/rasterizer.dart'; import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; import 'package:wolf_3d_dart/wolf_3d_engine.dart'; import 'package:wolf_3d_dart/wolf_3d_menu.dart'; +/// Pixel-accurate software rasterizer that writes directly into [FrameBuffer]. +/// +/// This is the canonical "modern framebuffer" implementation and serves as a +/// visual reference for terminal renderers. class SoftwareRasterizer extends Rasterizer { - static const Map> _menuFont = { - 'A': ['01110', '10001', '10001', '11111', '10001', '10001', '10001'], - 'B': ['11110', '10001', '10001', '11110', '10001', '10001', '11110'], - 'C': ['01110', '10001', '10000', '10000', '10000', '10001', '01110'], - 'D': ['11110', '10001', '10001', '10001', '10001', '10001', '11110'], - 'E': ['11111', '10000', '10000', '11110', '10000', '10000', '11111'], - 'F': ['11111', '10000', '10000', '11110', '10000', '10000', '10000'], - 'G': ['01110', '10001', '10000', '10111', '10001', '10001', '01111'], - 'H': ['10001', '10001', '10001', '11111', '10001', '10001', '10001'], - 'I': ['11111', '00100', '00100', '00100', '00100', '00100', '11111'], - 'K': ['10001', '10010', '10100', '11000', '10100', '10010', '10001'], - 'L': ['10000', '10000', '10000', '10000', '10000', '10000', '11111'], - 'M': ['10001', '11011', '10101', '10101', '10001', '10001', '10001'], - 'N': ['10001', '10001', '11001', '10101', '10011', '10001', '10001'], - 'O': ['01110', '10001', '10001', '10001', '10001', '10001', '01110'], - 'P': ['11110', '10001', '10001', '11110', '10000', '10000', '10000'], - 'R': ['11110', '10001', '10001', '11110', '10100', '10010', '10001'], - 'S': ['01111', '10000', '10000', '01110', '00001', '00001', '11110'], - 'T': ['11111', '00100', '00100', '00100', '00100', '00100', '00100'], - 'U': ['10001', '10001', '10001', '10001', '10001', '10001', '01110'], - 'W': ['10001', '10001', '10001', '10101', '10101', '11011', '10001'], - 'Y': ['10001', '10001', '01010', '00100', '00100', '00100', '00100'], - '?': ['01110', '10001', '00001', '00010', '00100', '00000', '00100'], - '!': ['00100', '00100', '00100', '00100', '00100', '00000', '00100'], - ',': ['00000', '00000', '00000', '00000', '00110', '00100', '01000'], - '.': ['00000', '00000', '00000', '00000', '00000', '00110', '00110'], - "'": ['00100', '00100', '00100', '00000', '00000', '00000', '00000'], - ' ': ['00000', '00000', '00000', '00000', '00000', '00000', '00000'], - }; - late FrameBuffer _buffer; - late WolfEngine _engine; - - // Intercept the base render call to store our references - @override - FrameBuffer render(WolfEngine engine) { - _engine = engine; - _buffer = engine.frameBuffer; - return super.render(engine); - } @override void prepareFrame(WolfEngine engine) { + _buffer = engine.frameBuffer; // Top half is ceiling color (25), bottom half is floor color (29) int ceilingColor = ColorPalette.vga32Bit[25]; int floorColor = ColorPalette.vga32Bit[29]; @@ -73,10 +40,7 @@ class SoftwareRasterizer extends Rasterizer { 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 texY = wallTexY(y, columnHeight); int colorByte = texture.pixels[texX * 64 + texY]; int pixelColor = ColorPalette.vga32Bit[colorByte]; @@ -105,8 +69,7 @@ class SoftwareRasterizer extends Rasterizer { y < math.min(viewHeight, drawEndY); y++ ) { - double relativeY = (y - drawStartY) / spriteHeight; - int texY = (relativeY * 64).toInt().clamp(0, 63); + int texY = spriteTexY(y, drawStartY, spriteHeight); int colorByte = texture.pixels[texX * 64 + texY]; @@ -124,12 +87,11 @@ class SoftwareRasterizer extends Rasterizer { ); 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); + final bounds = weaponScreenBounds(engine); + final int weaponWidth = bounds.weaponWidth; + final int weaponHeight = bounds.weaponHeight; + final int startX = bounds.startX; + final int startY = bounds.startY; for (int dy = 0; dy < weaponHeight; dy++) { for (int dx = 0; dx < weaponWidth; dx++) { @@ -150,30 +112,15 @@ class SoftwareRasterizer extends Rasterizer { } @override + /// Delegates the canonical Wolf3D VGA HUD draw sequence to the base class. void drawHud(WolfEngine engine) { - int statusBarIndex = engine.data.vgaImages.indexWhere( - (img) => img.width == 320 && img.height == 40, - ); - if (statusBarIndex == -1) return; + drawStandardVgaHud(engine); + } - // 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 + /// Blits a VGA image into the software framebuffer HUD space (320x200). + void blitHudVgaImage(VgaImage image, int startX320, int startY200) { + _blitVgaImage(image, startX320, startY200); } @override @@ -245,6 +192,8 @@ class SoftwareRasterizer extends Rasterizer { } } + /// Converts an `RRGGBB` menu color into the framebuffer's packed channel + /// order (`0xAABBGGRR`) used throughout this renderer. int _rgbToFrameColor(int rgb) { final int r = (rgb >> 16) & 0xFF; final int g = (rgb >> 8) & 0xFF; @@ -254,6 +203,7 @@ class SoftwareRasterizer extends Rasterizer { return (0xFF000000) | (b << 16) | (g << 8) | r; } + /// Draws bitmap menu text directly into the framebuffer. void _drawMenuText( String text, int startX, @@ -264,7 +214,7 @@ class SoftwareRasterizer extends Rasterizer { int x = startX; for (final rune in text.runes) { final char = String.fromCharCode(rune).toUpperCase(); - final pattern = _menuFont[char] ?? _menuFont[' ']!; + final pattern = WolfMenuFont.glyphFor(char); for (int row = 0; row < pattern.length; row++) { final bits = pattern[row]; @@ -282,25 +232,26 @@ class SoftwareRasterizer extends Rasterizer { } } - x += (6 * scale); + x += WolfMenuFont.glyphAdvance(char, scale); } } + /// Draws bitmap menu text centered in the current framebuffer width. void _drawMenuTextCentered( String text, int y, int color, { int scale = 1, }) { - final textWidth = text.length * 6 * scale; - final x = ((width - textWidth) ~/ 2).clamp(0, width - 1); + final int textWidth = WolfMenuFont.measureTextWidth(text, scale); + final int x = ((width - textWidth) ~/ 2).clamp(0, width - 1); _drawMenuText(text, x, y, color, scale: scale); } @override FrameBuffer finalizeFrame() { // If the player took damage, overlay a red tint across the 3D view - if (_engine.difficulty != null && _engine.player.damageFlash > 0) { + if (engine.difficulty != null && engine.player.damageFlash > 0) { _applyDamageFlash(); } return _buffer; // Return the fully painted pixel array @@ -310,12 +261,10 @@ class SoftwareRasterizer extends Rasterizer { // 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). + /// Maps planar VGA image data directly to 32-bit framebuffer pixels. + /// + /// This renderer assumes a 1:1 mapping with the canonical 320x200 layout. 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; @@ -324,12 +273,7 @@ class SoftwareRasterizer extends Rasterizer { 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]; + int colorByte = image.decodePixel(srcX, srcY); if (colorByte != 255) { _buffer.pixels[drawY * width + drawX] = ColorPalette.vga32Bit[colorByte]; @@ -339,59 +283,10 @@ class SoftwareRasterizer extends Rasterizer { } } - 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 + /// Tints the 3D view red based on `player.damageFlash` intensity. void _applyDamageFlash() { // Grab the intensity (0.0 to 1.0) - double intensity = _engine.player.damageFlash; + double intensity = engine.player.damageFlash; // Calculate how much to boost red and drop green/blue int redBoost = (150 * intensity).toInt();