From 0647f779cd12de7e28547db95b4d348b23e5a579 Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Tue, 17 Mar 2026 23:56:21 +0100 Subject: [PATCH] Improved hud in cli Signed-off-by: Hans Kokx --- .../engine/rasterizer/ascii_rasterizer.dart | 230 +++++++++++++----- .../lib/src/engine/rasterizer/rasterizer.dart | 23 +- 2 files changed, 190 insertions(+), 63 deletions(-) diff --git a/packages/wolf_3d_dart/lib/src/engine/rasterizer/ascii_rasterizer.dart b/packages/wolf_3d_dart/lib/src/engine/rasterizer/ascii_rasterizer.dart index 7ad613c..0627619 100644 --- a/packages/wolf_3d_dart/lib/src/engine/rasterizer/ascii_rasterizer.dart +++ b/packages/wolf_3d_dart/lib/src/engine/rasterizer/ascii_rasterizer.dart @@ -60,6 +60,11 @@ class ColoredChar { } class AsciiRasterizer extends Rasterizer { + static const double _targetAspectRatio = 4 / 3; + static const int _terminalBackdropArgb = 0xFF009688; + static const int _simpleHudMinWidth = 84; + static const int _simpleHudMinRows = 7; + AsciiRasterizer({ this.activeTheme = AsciiThemes.blocks, this.isTerminal = false, @@ -79,11 +84,26 @@ class AsciiRasterizer extends Rasterizer { @override final double verticalStretch; + @override + int get projectionWidth => isTerminal + ? math.max( + 1, + math.min(width, (_terminalPixelHeight * _targetAspectRatio).floor()), + ) + : width; + + @override + int get projectionOffsetX => isTerminal ? (width - projectionWidth) ~/ 2 : 0; + @override int get projectionViewHeight => isTerminal ? viewHeight * 2 : viewHeight; int get _terminalPixelHeight => isTerminal ? height * 2 : height; + int get _viewportRightX => projectionOffsetX + projectionWidth; + + int get _terminalBackdropColor => _argbToRawColor(_terminalBackdropArgb); + // Intercept the base render call to initialize our text grid @override dynamic render(WolfEngine engine, FrameBuffer buffer) { @@ -101,18 +121,20 @@ class AsciiRasterizer extends Rasterizer { // Just grab the raw ints! final int ceilingColor = ColorPalette.vga32Bit[25]; final int floorColor = ColorPalette.vga32Bit[29]; - final int black = ColorPalette.vga32Bit[0]; + final int backdropColor = isTerminal + ? _terminalBackdropColor + : ColorPalette.vga32Bit[0]; _scenePixels = List.generate( _terminalPixelHeight, - (_) => List.filled(width, black), + (_) => List.filled(width, backdropColor), ); for (int y = 0; y < projectionViewHeight; y++) { final int color = y < projectionViewHeight / 2 ? ceilingColor : floorColor; - for (int x = 0; x < width; x++) { + for (int x = projectionOffsetX; x < _viewportRightX; x++) { _scenePixels[y][x] = color; } } @@ -217,10 +239,11 @@ class AsciiRasterizer extends Rasterizer { ); Sprite weaponSprite = engine.data.sprites[spriteIndex]; - int weaponWidth = (width * 0.5).toInt(); + int weaponWidth = (projectionWidth * 0.5).toInt(); int weaponHeight = ((projectionViewHeight * 0.8)).toInt(); - int startX = (width ~/ 2) - (weaponWidth ~/ 2); + int startX = + projectionOffsetX + (projectionWidth ~/ 2) - (weaponWidth ~/ 2); int startY = projectionViewHeight - weaponHeight + @@ -235,7 +258,9 @@ class AsciiRasterizer extends Rasterizer { if (colorByte != 255) { int sceneX = startX + dx; int drawY = startY + dy; - if (sceneX >= 0 && sceneX < width && drawY >= 0) { + if (sceneX >= projectionOffsetX && + sceneX < _viewportRightX && + drawY >= 0) { if (isTerminal && drawY < projectionViewHeight) { _scenePixels[drawY][sceneX] = ColorPalette.vga32Bit[colorByte]; } else if (!isTerminal && drawY < viewHeight) { @@ -272,7 +297,8 @@ class AsciiRasterizer extends Rasterizer { void drawHud(WolfEngine engine) { // If the terminal is at least 160 columns wide and 50 rows tall, // there are enough "pixels" to downscale the VGA image clearly. - if (width >= 160 && height >= 50) { + int hudWidth = isTerminal ? projectionWidth : width; + if (hudWidth >= 160 && height >= 50) { _drawFullVgaHud(engine); } else { _drawSimpleHud(engine); @@ -280,6 +306,13 @@ class AsciiRasterizer extends Rasterizer { } void _drawSimpleHud(WolfEngine engine) { + final int hudWidth = isTerminal ? projectionWidth : width; + final int hudRows = height - viewHeight; + if (hudWidth < _simpleHudMinWidth || hudRows < _simpleHudMinRows) { + _drawMinimalHud(engine); + return; + } + // 1. Pull Retro Colors final int vgaStatusBarBlue = ColorPalette.vga32Bit[153]; final int vgaPanelDark = ColorPalette.vga32Bit[0]; @@ -287,21 +320,46 @@ class AsciiRasterizer extends Rasterizer { final int yellow = ColorPalette.vga32Bit[11]; final int red = ColorPalette.vga32Bit[4]; - // 2. Setup Centered Layout - // The total width of our standard HUD elements is roughly 120 chars - const int hudContentWidth = 120; - final int offsetX = ((width - hudContentWidth) ~/ 2).clamp(0, width); + // Compact full simple HUD layout. + const int floorW = 10; + const int scoreW = 14; + const int livesW = 9; + const int faceW = 10; + const int healthW = 12; + const int ammoW = 10; + const int weaponW = 13; + const int gap = 1; + const int hudContentWidth = + floorW + + scoreW + + livesW + + faceW + + healthW + + ammoW + + weaponW + + (gap * 6); + + final int offsetX = + projectionOffsetX + + ((projectionWidth - hudContentWidth) ~/ 2).clamp(0, projectionWidth); + final int baseY = viewHeight + 1; // 3. Clear HUD Base if (isTerminal) { _fillTerminalRect( - 0, + projectionOffsetX, viewHeight * 2, - width, - (height - viewHeight) * 2, + projectionWidth, + hudRows * 2, vgaStatusBarBlue, ); - _fillTerminalRect(0, viewHeight * 2, width, 1, white); + _fillTerminalRect( + projectionOffsetX, + viewHeight * 2, + projectionWidth, + 1, + white, + ); } else { _fillRect( 0, @@ -335,36 +393,35 @@ class AsciiRasterizer extends Rasterizer { } } - // 5. Draw the Panels - // FLOOR - drawBorderedPanel(offsetX + 4, viewHeight + 2, 12, 5); - _writeString(offsetX + 7, viewHeight + 3, "FLOOR", white, vgaPanelDark); - _writeString( - offsetX + 9, - viewHeight + 5, - engine.activeLevel.name.split(' ').last, - white, - vgaPanelDark, - ); + // 5. Draw compact panels. + int cursorX = offsetX; - // SCORE - drawBorderedPanel(offsetX + 18, viewHeight + 2, 24, 5); - _writeString(offsetX + 27, viewHeight + 3, "SCORE", white, vgaPanelDark); + drawBorderedPanel(cursorX, baseY + 1, floorW, 4); + _writeString(cursorX + 2, baseY + 2, "FLR", white, vgaPanelDark); + String floorLabel = engine.activeLevel.name.split(' ').last; + if (floorLabel.length > 4) { + floorLabel = floorLabel.substring(floorLabel.length - 4); + } + _writeString(cursorX + 2, baseY + 3, floorLabel, white, vgaPanelDark); + cursorX += floorW + gap; + + drawBorderedPanel(cursorX, baseY + 1, scoreW, 4); + _writeString(cursorX + 4, baseY + 2, "SCORE", white, vgaPanelDark); _writeString( - offsetX + 27, - viewHeight + 5, + cursorX + 4, + baseY + 3, engine.player.score.toString().padLeft(6, '0'), white, vgaPanelDark, ); + cursorX += scoreW + gap; - // LIVES - drawBorderedPanel(offsetX + 44, viewHeight + 2, 12, 5); - _writeString(offsetX + 47, viewHeight + 3, "LIVES", white, vgaPanelDark); - _writeString(offsetX + 49, viewHeight + 5, "3", white, vgaPanelDark); + drawBorderedPanel(cursorX, baseY + 1, livesW, 4); + _writeString(cursorX + 2, baseY + 2, "LIV", white, vgaPanelDark); + _writeString(cursorX + 3, baseY + 3, "3", white, vgaPanelDark); + cursorX += livesW + gap; - // FACE (With Reactive BJ Logic) - drawBorderedPanel(offsetX + 58, viewHeight + 1, 14, 7); + drawBorderedPanel(cursorX, baseY, faceW, 5); String face = "ಠ⌣ಠ"; if (engine.player.health <= 0) { face = "x⸑x"; @@ -375,37 +432,89 @@ class AsciiRasterizer extends Rasterizer { } else if (engine.player.health <= 60) { face = "ಠ~ಠ"; } - _writeString(offsetX + 63, viewHeight + 4, face, yellow, vgaPanelDark); + _writeString(cursorX + 3, baseY + 2, face, yellow, vgaPanelDark); + cursorX += faceW + gap; - // HEALTH int healthColor = engine.player.health > 25 ? white : red; - drawBorderedPanel(offsetX + 74, viewHeight + 2, 16, 5); - _writeString(offsetX + 78, viewHeight + 3, "HEALTH", white, vgaPanelDark); + drawBorderedPanel(cursorX, baseY + 1, healthW, 4); + _writeString(cursorX + 2, baseY + 2, "HEALTH", white, vgaPanelDark); _writeString( - offsetX + 79, - viewHeight + 5, + cursorX + 3, + baseY + 3, "${engine.player.health}%", healthColor, vgaPanelDark, ); + cursorX += healthW + gap; - // AMMO - drawBorderedPanel(offsetX + 92, viewHeight + 2, 12, 5); - _writeString(offsetX + 95, viewHeight + 3, "AMMO", white, vgaPanelDark); + drawBorderedPanel(cursorX, baseY + 1, ammoW, 4); + _writeString(cursorX + 2, baseY + 2, "AMMO", white, vgaPanelDark); _writeString( - offsetX + 97, - viewHeight + 5, + cursorX + 2, + baseY + 3, "${engine.player.ammo}", white, vgaPanelDark, ); + cursorX += ammoW + gap; - // WEAPON - drawBorderedPanel(offsetX + 106, viewHeight + 2, 14, 5); + drawBorderedPanel(cursorX, baseY + 1, weaponW, 4); String weapon = engine.player.currentWeapon.type.name.spacePascalCase! .toUpperCase(); - if (weapon.length > 12) weapon = weapon.substring(0, 12); - _writeString(offsetX + 107, viewHeight + 4, weapon, white, vgaPanelDark); + if (weapon.length > weaponW - 2) { + weapon = weapon.substring(0, weaponW - 2); + } + _writeString(cursorX + 1, baseY + 3, weapon, white, vgaPanelDark); + } + + void _drawMinimalHud(WolfEngine engine) { + final int vgaStatusBarBlue = ColorPalette.vga32Bit[153]; + final int white = ColorPalette.vga32Bit[15]; + final int red = ColorPalette.vga32Bit[4]; + + final int hudRows = height - viewHeight; + if (isTerminal) { + _fillTerminalRect( + projectionOffsetX, + viewHeight * 2, + projectionWidth, + hudRows * 2, + vgaStatusBarBlue, + ); + _fillTerminalRect( + projectionOffsetX, + viewHeight * 2, + projectionWidth, + 1, + white, + ); + } else { + _fillRect(0, viewHeight, width, hudRows, ' ', vgaStatusBarBlue); + _writeString(0, viewHeight, "═" * width, white); + } + + final int healthColor = engine.player.health > 25 ? white : red; + String weapon = engine.player.currentWeapon.type.name.spacePascalCase! + .toUpperCase(); + if (weapon.length > 8) { + weapon = weapon.substring(0, 8); + } + final String hudText = + 'H:${engine.player.health}% A:${engine.player.ammo} S:${engine.player.score} W:$weapon'; + + final int lineY = viewHeight + 1; + if (lineY >= height) return; + + final int drawStartX = isTerminal ? projectionOffsetX : 0; + final int drawWidth = isTerminal ? projectionWidth : width; + final int maxTextLen = math.max(0, drawWidth - 2); + String clipped = hudText; + if (clipped.length > maxTextLen) { + clipped = clipped.substring(0, maxTextLen); + } + + final int startX = drawStartX + ((drawWidth - clipped.length) ~/ 2); + _writeString(startX, lineY, clipped, healthColor, vgaStatusBarBlue); } void _drawFullVgaHud(WolfEngine engine) { @@ -526,11 +635,13 @@ class AsciiRasterizer extends Rasterizer { int planeWidth = image.width ~/ 4; int planeSize = planeWidth * image.height; int maxDrawHeight = isTerminal ? _terminalPixelHeight : height; + int maxDrawWidth = isTerminal ? _viewportRightX : width; - double scaleX = width / 320.0; + double scaleX = (isTerminal ? projectionWidth : width) / 320.0; double scaleY = (isTerminal ? _terminalPixelHeight : height) / 200.0; - int destStartX = (startX_320 * scaleX).toInt(); + int destStartX = + (isTerminal ? projectionOffsetX : 0) + (startX_320 * scaleX).toInt(); int destStartY = (startY_200 * scaleY).toInt(); int destWidth = (image.width * scaleX).toInt(); int destHeight = (image.height * scaleY).toInt(); @@ -541,7 +652,7 @@ class AsciiRasterizer extends Rasterizer { int drawY = destStartY + dy; if (drawX >= 0 && - drawX < width && + drawX < maxDrawWidth && drawY >= 0 && drawY < maxDrawHeight) { int srcX = (dx / scaleX).toInt().clamp(0, image.width - 1); @@ -597,12 +708,19 @@ class AsciiRasterizer extends Rasterizer { void _applyDamageFlashToScene() { for (int y = 0; y < _terminalPixelHeight; y++) { - for (int x = 0; x < width; x++) { + for (int x = projectionOffsetX; x < _viewportRightX; x++) { _scenePixels[y][x] = _applyDamageFlashToColor(_scenePixels[y][x]); } } } + int _argbToRawColor(int argb) { + int r = (argb >> 16) & 0xFF; + int g = (argb >> 8) & 0xFF; + int b = argb & 0xFF; + return (0xFF000000) | (b << 16) | (g << 8) | r; + } + int _applyDamageFlashToColor(int color) { double intensity = _engine.player.damageFlash; int redBoost = (150 * intensity).toInt(); diff --git a/packages/wolf_3d_dart/lib/src/engine/rasterizer/rasterizer.dart b/packages/wolf_3d_dart/lib/src/engine/rasterizer/rasterizer.dart index ce2b94e..ce5df71 100644 --- a/packages/wolf_3d_dart/lib/src/engine/rasterizer/rasterizer.dart +++ b/packages/wolf_3d_dart/lib/src/engine/rasterizer/rasterizer.dart @@ -19,6 +19,13 @@ abstract class Rasterizer { /// Defaults to 1.0 (no squish) for standard pixel rendering. double get verticalStretch => 1.0; + /// The logical width of the projection area used for raycasting and sprites. + /// Most renderers use the full buffer width. + int get projectionWidth => width; + + /// Horizontal offset of the projection area within the output buffer. + int get projectionOffsetX => 0; + /// The logical height of the 3D projection before a renderer maps rows to output pixels. /// Most renderers use the visible view height. Terminal ASCII can override this to render /// more vertical detail and collapse it into half-block glyphs. @@ -31,7 +38,7 @@ abstract class Rasterizer { height = buffer.height; // The 3D view typically takes up the top 80% of the screen viewHeight = (height * 0.8).toInt(); - zBuffer = List.filled(width, 0.0); + zBuffer = List.filled(projectionWidth, 0.0); // 1. Setup the frame (clear screen, draw floor/ceiling) prepareFrame(engine); @@ -108,6 +115,7 @@ abstract class Rasterizer { final Player player = engine.player; final SpriteMap map = engine.currentLevel; final List wallTextures = engine.data.walls; + final int sceneWidth = projectionWidth; final int sceneHeight = projectionViewHeight; final Map doorOffsets = engine.doorManager @@ -121,8 +129,8 @@ abstract class Rasterizer { ); Coordinate2D plane = Coordinate2D(-dir.y, dir.x) * math.tan(fov / 2); - for (int x = 0; x < width; x++) { - double cameraX = 2 * x / width - 1.0; + for (int x = 0; x < sceneWidth; x++) { + double cameraX = 2 * x / sceneWidth - 1.0; Coordinate2D rayDir = dir + (plane * cameraX); int mapX = player.x.toInt(); @@ -302,7 +310,7 @@ abstract class Rasterizer { // Tell the implementation to draw this column drawWallColumn( - x, + projectionOffsetX + x, drawStart, drawEnd, columnHeight, @@ -317,6 +325,7 @@ abstract class Rasterizer { void _castSprites(WolfEngine engine) { final Player player = engine.player; final List activeSprites = List.from(engine.entities); + final int sceneWidth = projectionWidth; final int sceneHeight = projectionViewHeight; // Sort from furthest to closest (Painter's Algorithm) @@ -343,7 +352,7 @@ abstract class Rasterizer { // Only process if the sprite is in front of the camera if (transformY > 0) { - int spriteScreenX = ((width / 2) * (1 + transformX / transformY)) + int spriteScreenX = ((sceneWidth / 2) * (1 + transformX / transformY)) .toInt(); int spriteHeight = ((sceneHeight / transformY).abs() * verticalStretch) .toInt(); @@ -361,7 +370,7 @@ abstract class Rasterizer { int drawEndX = spriteWidth ~/ 2 + spriteScreenX; int clipStartX = math.max(0, drawStartX); - int clipEndX = math.min(width, drawEndX); + int clipEndX = math.min(sceneWidth, drawEndX); int safeIndex = entity.spriteIndex.clamp( 0, @@ -377,7 +386,7 @@ abstract class Rasterizer { // Tell the implementation to draw this stripe drawSpriteStripe( - stripe, + projectionOffsetX + stripe, drawStartY, drawEndY, spriteHeight,