Fixed HUD background rendering
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -22,12 +22,16 @@ class SoftwareRasterizer {
|
|||||||
|
|
||||||
// NEW: Apply the full-screen damage tint last
|
// NEW: Apply the full-screen damage tint last
|
||||||
_drawDamageFlash(engine, buffer);
|
_drawDamageFlash(engine, buffer);
|
||||||
|
|
||||||
|
_drawHud(engine, buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _clearScreen(FrameBuffer 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(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<double> zBuffer) {
|
void _castWalls(WolfEngine engine, FrameBuffer buffer, List<double> zBuffer) {
|
||||||
@@ -235,15 +239,16 @@ class SoftwareRasterizer {
|
|||||||
FrameBuffer buffer,
|
FrameBuffer buffer,
|
||||||
double playerAngle,
|
double playerAngle,
|
||||||
) {
|
) {
|
||||||
|
const int viewHeight = 160;
|
||||||
if (distance <= 0.01) distance = 0.01;
|
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;
|
if (drawStart < 0) drawStart = 0;
|
||||||
|
|
||||||
int drawEnd = lineHeight ~/ 2 + buffer.height ~/ 2;
|
int drawEnd = lineHeight ~/ 2 + viewHeight ~/ 2;
|
||||||
if (drawEnd >= buffer.height) drawEnd = buffer.height - 1;
|
if (drawEnd >= viewHeight) drawEnd = viewHeight - 1;
|
||||||
|
|
||||||
int texNum;
|
int texNum;
|
||||||
if (hitWallId >= 90) {
|
if (hitWallId >= 90) {
|
||||||
@@ -258,7 +263,7 @@ class SoftwareRasterizer {
|
|||||||
if (side == 1 && math.sin(playerAngle) < 0) texX = 63 - texX;
|
if (side == 1 && math.sin(playerAngle) < 0) texX = 63 - texX;
|
||||||
|
|
||||||
double step = 64.0 / lineHeight;
|
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];
|
Sprite texture = textures[texNum];
|
||||||
|
|
||||||
@@ -278,6 +283,7 @@ class SoftwareRasterizer {
|
|||||||
FrameBuffer buffer,
|
FrameBuffer buffer,
|
||||||
List<double> zBuffer,
|
List<double> zBuffer,
|
||||||
) {
|
) {
|
||||||
|
const int viewHeight = 160;
|
||||||
final Player player = engine.player;
|
final Player player = engine.player;
|
||||||
final List<Entity> activeSprites = List.from(engine.entities);
|
final List<Entity> activeSprites = List.from(engine.entities);
|
||||||
|
|
||||||
@@ -306,16 +312,16 @@ class SoftwareRasterizer {
|
|||||||
if (transformY > 0) {
|
if (transformY > 0) {
|
||||||
int spriteScreenX = ((buffer.width / 2) * (1 + transformX / transformY))
|
int spriteScreenX = ((buffer.width / 2) * (1 + transformX / transformY))
|
||||||
.toInt();
|
.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
|
// In 1x1 buffer pixels, the width of the sprite is equal to its height
|
||||||
int spriteWidth = spriteHeight;
|
int spriteWidth = spriteHeight;
|
||||||
|
|
||||||
int drawStartY = -spriteHeight ~/ 2 + buffer.height ~/ 2;
|
int drawStartY = -spriteHeight ~/ 2 + viewHeight ~/ 2;
|
||||||
if (drawStartY < 0) drawStartY = 0;
|
if (drawStartY < 0) drawStartY = 0;
|
||||||
|
|
||||||
int drawEndY = spriteHeight ~/ 2 + buffer.height ~/ 2;
|
int drawEndY = spriteHeight ~/ 2 + viewHeight ~/ 2;
|
||||||
if (drawEndY >= buffer.height) drawEndY = buffer.height - 1;
|
if (drawEndY >= buffer.height) drawEndY = viewHeight - 1;
|
||||||
|
|
||||||
int drawStartX = -spriteWidth ~/ 2 + spriteScreenX;
|
int drawStartX = -spriteWidth ~/ 2 + spriteScreenX;
|
||||||
int drawEndX = spriteWidth ~/ 2 + spriteScreenX;
|
int drawEndX = spriteWidth ~/ 2 + spriteScreenX;
|
||||||
@@ -336,7 +342,7 @@ class SoftwareRasterizer {
|
|||||||
|
|
||||||
double step = 64.0 / spriteHeight;
|
double step = 64.0 / spriteHeight;
|
||||||
double texPos =
|
double texPos =
|
||||||
(drawStartY - buffer.height / 2 + spriteHeight / 2) * step;
|
(drawStartY - viewHeight / 2 + spriteHeight / 2) * step;
|
||||||
|
|
||||||
for (int y = drawStartY; y < drawEndY; y++) {
|
for (int y = drawStartY; y < drawEndY; y++) {
|
||||||
int texY = texPos.toInt() & 63;
|
int texY = texPos.toInt() & 63;
|
||||||
@@ -357,6 +363,7 @@ class SoftwareRasterizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _drawWeapon(WolfEngine engine, FrameBuffer buffer) {
|
void _drawWeapon(WolfEngine engine, FrameBuffer buffer) {
|
||||||
|
const int viewHeight = 160;
|
||||||
int spriteIndex = engine.player.currentWeapon.getCurrentSpriteIndex(
|
int spriteIndex = engine.player.currentWeapon.getCurrentSpriteIndex(
|
||||||
engine.data.sprites.length,
|
engine.data.sprites.length,
|
||||||
);
|
);
|
||||||
@@ -371,7 +378,7 @@ class SoftwareRasterizer {
|
|||||||
|
|
||||||
// Kept the grounding to the bottom of the screen
|
// Kept the grounding to the bottom of the screen
|
||||||
int startY =
|
int startY =
|
||||||
buffer.height - weaponHeight + (engine.player.weaponAnimOffset ~/ 2);
|
viewHeight - weaponHeight + (engine.player.weaponAnimOffset ~/ 2);
|
||||||
|
|
||||||
for (int x = 0; x < 64; x++) {
|
for (int x = 0; x < 64; x++) {
|
||||||
for (int y = 0; y < 64; y++) {
|
for (int y = 0; y < 64; y++) {
|
||||||
@@ -420,4 +427,82 @@ class SoftwareRasterizer {
|
|||||||
buffer.pixels[i] = (a << 24) | (b << 16) | (g << 8) | r;
|
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<VgaImage> 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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,6 @@ import 'package:flutter/scheduler.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';
|
||||||
import 'package:wolf_3d_input/wolf_3d_input.dart';
|
import 'package:wolf_3d_input/wolf_3d_input.dart';
|
||||||
import 'package:wolf_3d_renderer/hud.dart';
|
|
||||||
|
|
||||||
class WolfRenderer extends StatefulWidget {
|
class WolfRenderer extends StatefulWidget {
|
||||||
const WolfRenderer(
|
const WolfRenderer(
|
||||||
@@ -123,7 +122,7 @@ class _WolfRendererState extends State<WolfRenderer>
|
|||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
return Center(
|
return Center(
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: 16 / 10,
|
aspectRatio: 320 / 200,
|
||||||
child: CustomPaint(
|
child: CustomPaint(
|
||||||
size: Size(constraints.maxWidth, constraints.maxHeight),
|
size: Size(constraints.maxWidth, constraints.maxHeight),
|
||||||
painter: BufferPainter(_renderedFrame),
|
painter: BufferPainter(_renderedFrame),
|
||||||
@@ -133,7 +132,6 @@ class _WolfRendererState extends State<WolfRenderer>
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Hud(player: engine.player),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user