From e9e56eac9af9f5f6ec4a4ab9debe3623136c3dd3 Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Mon, 16 Mar 2026 00:22:54 +0100 Subject: [PATCH] Fixed HUD background rendering Signed-off-by: Hans Kokx --- .../wolf_3d_engine/lib/src/rasterizer.dart | 111 ++++++++++++++++-- packages/wolf_3d_renderer/lib/hud.dart | 91 -------------- .../wolf_3d_renderer/lib/weapon_painter.dart | 53 --------- .../lib/wolf_3d_renderer.dart | 4 +- 4 files changed, 99 insertions(+), 160 deletions(-) delete mode 100644 packages/wolf_3d_renderer/lib/hud.dart delete mode 100644 packages/wolf_3d_renderer/lib/weapon_painter.dart diff --git a/packages/wolf_3d_engine/lib/src/rasterizer.dart b/packages/wolf_3d_engine/lib/src/rasterizer.dart index 290b777..2da1861 100644 --- a/packages/wolf_3d_engine/lib/src/rasterizer.dart +++ b/packages/wolf_3d_engine/lib/src/rasterizer.dart @@ -22,12 +22,16 @@ class SoftwareRasterizer { // NEW: Apply the full-screen damage tint last _drawDamageFlash(engine, buffer); + + _drawHud(engine, buffer); } void _clearScreen(FrameBuffer buffer) { - int halfScreen = (buffer.width * buffer.height) ~/ 2; + const int viewHeight = 160; + int halfScreen = (buffer.width * viewHeight) ~/ 2; + // Only clear the top 160 rows! buffer.pixels.fillRange(0, halfScreen, ceilingColor); - buffer.pixels.fillRange(halfScreen, buffer.pixels.length, floorColor); + buffer.pixels.fillRange(halfScreen, buffer.width * viewHeight, floorColor); } void _castWalls(WolfEngine engine, FrameBuffer buffer, List zBuffer) { @@ -235,15 +239,16 @@ class SoftwareRasterizer { FrameBuffer buffer, double playerAngle, ) { + const int viewHeight = 160; if (distance <= 0.01) distance = 0.01; - int lineHeight = (buffer.height / distance).toInt(); + int lineHeight = (viewHeight / distance).toInt(); - int drawStart = -lineHeight ~/ 2 + buffer.height ~/ 2; + int drawStart = -lineHeight ~/ 2 + viewHeight ~/ 2; if (drawStart < 0) drawStart = 0; - int drawEnd = lineHeight ~/ 2 + buffer.height ~/ 2; - if (drawEnd >= buffer.height) drawEnd = buffer.height - 1; + int drawEnd = lineHeight ~/ 2 + viewHeight ~/ 2; + if (drawEnd >= viewHeight) drawEnd = viewHeight - 1; int texNum; if (hitWallId >= 90) { @@ -258,7 +263,7 @@ class SoftwareRasterizer { if (side == 1 && math.sin(playerAngle) < 0) texX = 63 - texX; double step = 64.0 / lineHeight; - double texPos = (drawStart - buffer.height / 2 + lineHeight / 2) * step; + double texPos = (drawStart - viewHeight / 2 + lineHeight / 2) * step; Sprite texture = textures[texNum]; @@ -278,6 +283,7 @@ class SoftwareRasterizer { FrameBuffer buffer, List zBuffer, ) { + const int viewHeight = 160; final Player player = engine.player; final List activeSprites = List.from(engine.entities); @@ -306,16 +312,16 @@ class SoftwareRasterizer { if (transformY > 0) { int spriteScreenX = ((buffer.width / 2) * (1 + transformX / transformY)) .toInt(); - int spriteHeight = (buffer.height / transformY).abs().toInt(); + int spriteHeight = (viewHeight / transformY).abs().toInt(); // In 1x1 buffer pixels, the width of the sprite is equal to its height int spriteWidth = spriteHeight; - int drawStartY = -spriteHeight ~/ 2 + buffer.height ~/ 2; + int drawStartY = -spriteHeight ~/ 2 + viewHeight ~/ 2; if (drawStartY < 0) drawStartY = 0; - int drawEndY = spriteHeight ~/ 2 + buffer.height ~/ 2; - if (drawEndY >= buffer.height) drawEndY = buffer.height - 1; + int drawEndY = spriteHeight ~/ 2 + viewHeight ~/ 2; + if (drawEndY >= buffer.height) drawEndY = viewHeight - 1; int drawStartX = -spriteWidth ~/ 2 + spriteScreenX; int drawEndX = spriteWidth ~/ 2 + spriteScreenX; @@ -336,7 +342,7 @@ class SoftwareRasterizer { double step = 64.0 / spriteHeight; double texPos = - (drawStartY - buffer.height / 2 + spriteHeight / 2) * step; + (drawStartY - viewHeight / 2 + spriteHeight / 2) * step; for (int y = drawStartY; y < drawEndY; y++) { int texY = texPos.toInt() & 63; @@ -357,6 +363,7 @@ class SoftwareRasterizer { } void _drawWeapon(WolfEngine engine, FrameBuffer buffer) { + const int viewHeight = 160; int spriteIndex = engine.player.currentWeapon.getCurrentSpriteIndex( engine.data.sprites.length, ); @@ -371,7 +378,7 @@ class SoftwareRasterizer { // Kept the grounding to the bottom of the screen int startY = - buffer.height - weaponHeight + (engine.player.weaponAnimOffset ~/ 2); + viewHeight - weaponHeight + (engine.player.weaponAnimOffset ~/ 2); for (int x = 0; x < 64; x++) { for (int y = 0; y < 64; y++) { @@ -420,4 +427,82 @@ class SoftwareRasterizer { buffer.pixels[i] = (a << 24) | (b << 16) | (g << 8) | r; } } + + void _drawHud(WolfEngine engine, FrameBuffer buffer) { + // Clever trick: Find the only 320x40 graphic in the VGA chunks! + int statusBarIndex = engine.data.vgaImages.indexWhere( + (img) => img.width == 320 && img.height == 40, + ); + + if (statusBarIndex == -1) return; // Safety check if it fails to find it + + VgaImage statusBar = engine.data.vgaImages[statusBarIndex]; + + // Draw the background status bar at Y=160 + _blitVgaImage(statusBar, 0, 160, buffer); + + // --- We will add the digits and face here next --- + } + + void _drawNumber( + int value, + int rightAlignX, + int startY, + FrameBuffer buffer, + List vgaImages, + int zeroIndex, // The VGA index of the '0' digit + ) { + String numStr = value.toString(); + + // Original Wolf3D status bar digits are exactly 8 pixels wide + // We calculate the starting X by moving left based on how many digits there are + int currentX = rightAlignX - (numStr.length * 8); + + for (int i = 0; i < numStr.length; i++) { + int digit = int.parse(numStr[i]); + + // Because digits 0-9 are stored sequentially, we can just add the + // actual number to the base 'zeroIndex' to get the right graphic! + _blitVgaImage(vgaImages[zeroIndex + digit], currentX, startY, buffer); + + currentX += 8; // Move right for the next digit + } + } + + void _blitVgaImage( + VgaImage image, + int startX, + int startY, + FrameBuffer buffer, + ) { + // Wolfenstein 3D VGA images are stored in "Mode Y" Planar format. + // We must de-interleave the 4 planes to draw them correctly! + int planeWidth = image.width ~/ 4; + int planeSize = planeWidth * image.height; + + for (int y = 0; y < image.height; y++) { + for (int x = 0; x < image.width; x++) { + int drawX = startX + x; + int drawY = startY + y; + + if (drawX >= 0 && + drawX < buffer.width && + drawY >= 0 && + drawY < buffer.height) { + // Planar to Linear coordinate conversion + int plane = x % 4; + int sx = x ~/ 4; + int index = (plane * planeSize) + (y * planeWidth) + sx; + + int colorByte = image.pixels[index]; + + if (colorByte != 255) { + // 255 is transparent + buffer.pixels[drawY * buffer.width + drawX] = + ColorPalette.vga32Bit[colorByte]; + } + } + } + } + } } diff --git a/packages/wolf_3d_renderer/lib/hud.dart b/packages/wolf_3d_renderer/lib/hud.dart deleted file mode 100644 index 38e2b16..0000000 --- a/packages/wolf_3d_renderer/lib/hud.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:wolf_3d_engine/wolf_3d_engine.dart'; - -class Hud extends StatelessWidget { - final Player player; - - const Hud({super.key, required this.player}); - - @override - Widget build(BuildContext context) { - // We'll give the HUD a fixed height relative to the screen - return Container( - height: 100, - color: const Color(0xFF323232), // Classic dark grey status bar - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _buildStatColumn("FLOOR", "1"), - _buildStatColumn("SCORE", "${player.score}"), - _buildStatColumn("LIVES", "3"), - _buildFace(), - _buildStatColumn( - "HEALTH", - "${player.health}%", - color: _getHealthColor(), - ), - _buildStatColumn("AMMO", "${player.ammo}"), - _buildWeaponIcon(), - ], - ), - ); - } - - Widget _buildStatColumn( - String label, - String value, { - Color color = Colors.white, - }) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - label, - style: const TextStyle( - color: Colors.red, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - Text( - value, - style: TextStyle(color: color, fontSize: 20, fontFamily: 'monospace'), - ), - ], - ); - } - - Widget _buildFace() { - // For now, we'll use a simple icon. Later we can map VSWAP indices - // for BJ Blazkowicz's face based on health percentage. - return Container( - width: 60, - height: 80, - decoration: BoxDecoration( - color: Colors.blueGrey[800], - border: Border.all(color: Colors.grey, width: 2), - ), - child: const Icon(Icons.face, color: Colors.white, size: 40), - ); - } - - Widget _buildWeaponIcon() { - IconData weaponIcon = Icons.horizontal_rule; // Default Knife/Pistol - if (player.hasChainGun) weaponIcon = Icons.reorder; - if (player.hasMachineGun) weaponIcon = Icons.view_headline; - - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text("WEAPON", style: TextStyle(color: Colors.red, fontSize: 10)), - Icon(weaponIcon, color: Colors.white), - ], - ); - } - - Color _getHealthColor() { - if (player.health > 50) return Colors.white; - if (player.health > 25) return Colors.orange; - return Colors.red; - } -} diff --git a/packages/wolf_3d_renderer/lib/weapon_painter.dart b/packages/wolf_3d_renderer/lib/weapon_painter.dart deleted file mode 100644 index bf7d66a..0000000 --- a/packages/wolf_3d_renderer/lib/weapon_painter.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:wolf_3d_data_types/wolf_3d_data_types.dart'; - -class WeaponPainter extends CustomPainter { - final Sprite? sprite; - - // Initialize a reusable Paint object and disable anti-aliasing to keep the - // pixels perfectly sharp and chunky. - final Paint _paint = Paint() - ..isAntiAlias = false - ..style = PaintingStyle.fill; - - WeaponPainter({required this.sprite}); - - @override - void paint(Canvas canvas, Size size) { - if (sprite == null) return; - - // Calculate width and height separately in case the container isn't a - // perfect square - double pixelWidth = size.width / 64; - double pixelHeight = size.height / 64; - - for (int x = 0; x < 64; x++) { - for (int y = 0; y < 64; y++) { - int colorByte = sprite!.pixels[x * 64 + y]; - - if (colorByte != 255) { - // 255 is our transparent magenta - _paint.color = Color(ColorPalette.vga32Bit[colorByte]); - - canvas.drawRect( - Rect.fromLTWH( - x * pixelWidth, - y * pixelHeight, - // Add a tiny 0.5 overlap to completely eliminate visual seams - pixelWidth + 0.5, - pixelHeight + 0.5, - ), - _paint, - ); - } - } - } - } - - @override - bool shouldRepaint(covariant WeaponPainter oldDelegate) { - // ONLY repaint if the actual animation frame (sprite) has changed! - // This saves massive amounts of CPU when the player is just walking around. - return oldDelegate.sprite != sprite; - } -} diff --git a/packages/wolf_3d_renderer/lib/wolf_3d_renderer.dart b/packages/wolf_3d_renderer/lib/wolf_3d_renderer.dart index 416863e..a5b34ae 100644 --- a/packages/wolf_3d_renderer/lib/wolf_3d_renderer.dart +++ b/packages/wolf_3d_renderer/lib/wolf_3d_renderer.dart @@ -5,7 +5,6 @@ import 'package:flutter/scheduler.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_input/wolf_3d_input.dart'; -import 'package:wolf_3d_renderer/hud.dart'; class WolfRenderer extends StatefulWidget { const WolfRenderer( @@ -123,7 +122,7 @@ class _WolfRendererState extends State builder: (context, constraints) { return Center( child: AspectRatio( - aspectRatio: 16 / 10, + aspectRatio: 320 / 200, child: CustomPaint( size: Size(constraints.maxWidth, constraints.maxHeight), painter: BufferPainter(_renderedFrame), @@ -133,7 +132,6 @@ class _WolfRendererState extends State }, ), ), - Hud(player: engine.player), ], ), ),