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 <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<dynamic> {
|
||||
static const double _targetAspectRatio = 4 / 3;
|
||||
static const int _terminalBackdropPaletteIndex = 153;
|
||||
@@ -106,7 +108,6 @@ class AsciiRasterizer extends CliRasterizer<dynamic> {
|
||||
|
||||
late List<List<ColoredChar>> _screen;
|
||||
late List<List<int>> _scenePixels;
|
||||
late WolfEngine _engine;
|
||||
|
||||
@override
|
||||
final double aspectMultiplier;
|
||||
@@ -151,8 +152,8 @@ class AsciiRasterizer extends CliRasterizer<dynamic> {
|
||||
|
||||
// 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<dynamic> {
|
||||
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<dynamic> {
|
||||
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<dynamic> {
|
||||
);
|
||||
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<dynamic> {
|
||||
}
|
||||
|
||||
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<VgaImage> 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<dynamic> {
|
||||
|
||||
@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<dynamic> {
|
||||
// --- 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<dynamic> {
|
||||
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<dynamic> {
|
||||
}
|
||||
|
||||
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<dynamic> {
|
||||
}
|
||||
|
||||
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)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<T> {
|
||||
late List<double> 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<T> {
|
||||
/// 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<T> {
|
||||
/// 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<VgaImage> 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<VgaImage> 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<VgaImage> vgaImages) {
|
||||
final int faceIndex = hudFaceVgaIndex(engine.player.health);
|
||||
if (faceIndex < vgaImages.length) {
|
||||
blitHudVgaImage(vgaImages[faceIndex], 136, 164);
|
||||
}
|
||||
}
|
||||
|
||||
void _drawHudWeaponIcon(WolfEngine engine, List<VgaImage> 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<T> {
|
||||
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,
|
||||
|
||||
@@ -31,7 +31,6 @@ class SixelRasterizer extends CliRasterizer<String> {
|
||||
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<String> {
|
||||
}
|
||||
|
||||
@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<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
);
|
||||
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<String> {
|
||||
}
|
||||
|
||||
@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<String> {
|
||||
|
||||
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<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws bitmap menu text into the indexed Sixel back buffer.
|
||||
void _drawMenuText(
|
||||
String text,
|
||||
int startX,
|
||||
@@ -466,6 +457,7 @@ class SixelRasterizer extends CliRasterizer<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws bitmap menu text centered in 320x200 menu space.
|
||||
void _drawMenuTextCentered(
|
||||
String text,
|
||||
int y,
|
||||
@@ -494,6 +486,7 @@ class SixelRasterizer extends CliRasterizer<String> {
|
||||
// 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<String> {
|
||||
// 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<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
// 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<String> {
|
||||
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<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<String> {
|
||||
}
|
||||
}
|
||||
|
||||
void _drawNumber(
|
||||
int value,
|
||||
int rightAlignX,
|
||||
int startY,
|
||||
List<VgaImage> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<FrameBuffer> {
|
||||
static const Map<String, List<String>> _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<FrameBuffer> {
|
||||
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<FrameBuffer> {
|
||||
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<FrameBuffer> {
|
||||
);
|
||||
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<FrameBuffer> {
|
||||
}
|
||||
|
||||
@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<FrameBuffer> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<FrameBuffer> {
|
||||
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<FrameBuffer> {
|
||||
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<FrameBuffer> {
|
||||
}
|
||||
}
|
||||
|
||||
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<FrameBuffer> {
|
||||
// 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<FrameBuffer> {
|
||||
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<FrameBuffer> {
|
||||
}
|
||||
}
|
||||
|
||||
void _drawNumber(
|
||||
int value,
|
||||
int rightAlignX,
|
||||
int startY,
|
||||
List<VgaImage> 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();
|
||||
|
||||
Reference in New Issue
Block a user