Draw better CLI HUD

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-16 16:59:47 +01:00
parent 400720a56e
commit cd56366366
3 changed files with 95 additions and 39 deletions

View File

@@ -41,10 +41,7 @@ void main() async {
final input = CliInput(); final input = CliInput();
final cliAudio = CliSilentAudio(); final cliAudio = CliSilentAudio();
final rasterizer = AsciiRasterizer( final rasterizer = AsciiRasterizer();
aspectMultiplier: 1.0,
verticalStretch: 2.0,
);
FrameBuffer buffer = FrameBuffer( FrameBuffer buffer = FrameBuffer(
stdout.terminalColumns, stdout.terminalColumns,

View File

@@ -1,5 +1,6 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:arcane_helper_utils/arcane_helper_utils.dart';
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart'; import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
import 'package:wolf_3d_engine/wolf_3d_engine.dart'; import 'package:wolf_3d_engine/wolf_3d_engine.dart';
@@ -222,53 +223,97 @@ class AsciiRasterizer extends Rasterizer {
} }
void _drawSimpleHud(WolfEngine engine) { void _drawSimpleHud(WolfEngine engine) {
// 1. Clear the HUD area so stray wall characters don't bleed in // 1. Pull Retro Colors
for (int y = viewHeight; y < height; y++) { final int vgaStatusBarBlue = ColorPalette.vga32Bit[153];
for (int x = 0; x < width; x++) { final int vgaPanelDark = ColorPalette.vga32Bit[0];
_screen[y][x] = ColoredChar(' ', 0xFF000000); final int white = ColorPalette.vga32Bit[15];
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);
// 3. Clear HUD Base
_fillRect(0, viewHeight, width, height - viewHeight, ' ', vgaStatusBarBlue);
_writeString(0, viewHeight, "" * width, white);
// 4. Panel Drawing Helper
void drawBorderedPanel(int startX, int startY, int w, int h) {
_fillRect(startX, startY, w, h, ' ', vgaPanelDark);
// Horizontal lines
_writeString(startX, startY, "${"" * (w - 2)}", white);
_writeString(startX, startY + h - 1, "${"" * (w - 2)}", white);
// Vertical sides
for (int i = 1; i < h - 1; i++) {
_writeString(startX, startY + i, "", white);
_writeString(startX + w - 1, startY + i, "", white);
} }
} }
// 2. Draw a decorative border to separate the 3D view from the HUD // 5. Draw the Panels
int borderColor = 0xFF555555; // Dark Gray // FLOOR
for (int x = 0; x < width; x++) { drawBorderedPanel(offsetX + 4, viewHeight + 2, 12, 5);
_screen[viewHeight][x] = ColoredChar('=', borderColor); _writeString(offsetX + 7, viewHeight + 3, "FLOOR", white);
} _writeString(
offsetX + 9,
viewHeight + 5,
engine.activeLevel.name.split(' ').last,
white,
);
// 3. Define some clean terminal colors // SCORE
int white = 0xFFFFFFFF; drawBorderedPanel(offsetX + 18, viewHeight + 2, 24, 5);
int yellow = 0xFFFFFF55; _writeString(offsetX + 27, viewHeight + 3, "SCORE", white);
int red = 0xFFFF5555; _writeString(
offsetX + 27,
viewHeight + 5,
engine.player.score.toString().padLeft(6, '0'),
white,
);
// Turn health text red if dying // LIVES
int healthColor = engine.player.health > 25 ? white : red; drawBorderedPanel(offsetX + 44, viewHeight + 2, 12, 5);
_writeString(offsetX + 47, viewHeight + 3, "LIVES", white);
_writeString(offsetX + 49, viewHeight + 5, "3", white);
// Format the strings nicely // FACE (With Reactive BJ Logic)
String score = "SCORE: ${engine.player.score.toString().padLeft(6, '0')}"; drawBorderedPanel(offsetX + 58, viewHeight + 1, 14, 7);
String health = "HEALTH: ${engine.player.health}%";
String ammo = "AMMO: ${engine.player.ammo}";
int textY = viewHeight + 4; // Center it vertically in the HUD area
// 4. Print the stats evenly spaced across the bottom
_writeString(6, textY, "FLOOR 1", white);
_writeString(22, textY, score, white);
_writeString(44, textY, "LIVES: 3", white);
// 5. Reactive ASCII Face
String face = " :-)"; String face = " :-)";
if (engine.player.health <= 0) { if (engine.player.health <= 0) {
face = " X-x"; face = " X-x";
} else if (engine.player.damageFlash > 0.1) {
face = " :-O"; // Mouth open in pain!
} else if (engine.player.health <= 25) { } else if (engine.player.health <= 25) {
face = " :-("; face = " :-(";
} else if (engine.player.health <= 60) { } else if (engine.player.health <= 60) {
face = " :-|"; face = " :-|";
} }
_writeString(offsetX + 63, viewHeight + 4, face, yellow);
_writeString(62, textY, "[$face]", yellow); // HEALTH
int healthColor = engine.player.health > 25 ? white : red;
drawBorderedPanel(offsetX + 74, viewHeight + 2, 16, 5);
_writeString(offsetX + 78, viewHeight + 3, "HEALTH", white);
_writeString(
offsetX + 79,
viewHeight + 5,
"${engine.player.health}%",
healthColor,
);
_writeString(78, textY, health, healthColor); // AMMO
_writeString(100, textY, ammo, white); drawBorderedPanel(offsetX + 92, viewHeight + 2, 12, 5);
_writeString(offsetX + 95, viewHeight + 3, "AMMO", white);
_writeString(offsetX + 97, viewHeight + 5, "${engine.player.ammo}", white);
// WEAPON
drawBorderedPanel(offsetX + 106, viewHeight + 2, 14, 5);
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);
} }
void _drawFullVgaHud(WolfEngine engine) { void _drawFullVgaHud(WolfEngine engine) {
@@ -355,6 +400,19 @@ class AsciiRasterizer extends Rasterizer {
} }
} }
/// Helper to fill a rectangular area with a specific char and background color
void _fillRect(int startX, int startY, int w, int h, String char, int color) {
for (int dy = 0; dy < h; dy++) {
for (int dx = 0; dx < w; dx++) {
int x = startX + dx;
int y = startY + dy;
if (x >= 0 && x < width && y >= 0 && y < height) {
_screen[y][x] = ColoredChar(char, color);
}
}
}
}
@override @override
dynamic finalizeFrame() { dynamic finalizeFrame() {
if (_engine.player.damageFlash > 0.0) { if (_engine.player.damageFlash > 0.0) {

View File

@@ -13,6 +13,7 @@ dependencies:
wolf_3d_entities: any wolf_3d_entities: any
wolf_3d_data: any wolf_3d_data: any
wolf_3d_input: any wolf_3d_input: any
arcane_helper_utils: ^1.4.7
dev_dependencies: dev_dependencies:
lints: ^6.0.0 lints: ^6.0.0