Incremental improvements to HUD
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -1,508 +1,6 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
|
||||
import 'package:wolf_3d_engine/wolf_3d_engine.dart';
|
||||
import 'package:wolf_3d_entities/wolf_3d_entities.dart';
|
||||
|
||||
class SoftwareRasterizer {
|
||||
// Pre-calculated VGA colors for ceiling and floor
|
||||
final int ceilingColor = ColorPalette.vga32Bit[25];
|
||||
final int floorColor = ColorPalette.vga32Bit[29];
|
||||
|
||||
void render(WolfEngine engine, FrameBuffer buffer) {
|
||||
_clearScreen(buffer);
|
||||
|
||||
List<double> zBuffer = List.filled(buffer.width, 0.0);
|
||||
|
||||
_castWalls(engine, buffer, zBuffer);
|
||||
_castSprites(engine, buffer, zBuffer);
|
||||
|
||||
// NEW: Draw the weapon on top of the 3D world
|
||||
_drawWeapon(engine, buffer);
|
||||
|
||||
// NEW: Apply the full-screen damage tint last
|
||||
_drawDamageFlash(engine, buffer);
|
||||
|
||||
_drawHud(engine, buffer);
|
||||
}
|
||||
|
||||
void _clearScreen(FrameBuffer buffer) {
|
||||
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.width * viewHeight, floorColor);
|
||||
}
|
||||
|
||||
void _castWalls(WolfEngine engine, FrameBuffer buffer, List<double> zBuffer) {
|
||||
final double fov = math.pi / 3;
|
||||
final Player player = engine.player;
|
||||
final SpriteMap map = engine.currentLevel;
|
||||
|
||||
// Fetch dynamic states from the managers
|
||||
final Map<String, double> doorOffsets = engine.doorManager
|
||||
.getOffsetsForRenderer();
|
||||
final Pushwall? activePushwall = engine.pushwallManager.activePushwall;
|
||||
|
||||
Coordinate2D dir = Coordinate2D(
|
||||
math.cos(player.angle),
|
||||
math.sin(player.angle),
|
||||
);
|
||||
Coordinate2D plane = Coordinate2D(-dir.y, dir.x) * math.tan(fov / 2);
|
||||
|
||||
for (int x = 0; x < buffer.width; x++) {
|
||||
double cameraX = 2 * x / buffer.width - 1.0;
|
||||
Coordinate2D rayDir = dir + (plane * cameraX);
|
||||
|
||||
int mapX = player.x.toInt();
|
||||
int mapY = player.y.toInt();
|
||||
|
||||
double sideDistX;
|
||||
double sideDistY;
|
||||
double deltaDistX = (rayDir.x == 0) ? 1e30 : (1.0 / rayDir.x).abs();
|
||||
double deltaDistY = (rayDir.y == 0) ? 1e30 : (1.0 / rayDir.y).abs();
|
||||
double perpWallDist = 0.0;
|
||||
|
||||
int stepX;
|
||||
int stepY;
|
||||
bool hit = false;
|
||||
bool hitOutOfBounds = false;
|
||||
int side = 0;
|
||||
int hitWallId = 0;
|
||||
|
||||
double textureOffset = 0.0;
|
||||
bool customDistCalculated = false;
|
||||
Set<String> ignoredDoors = {};
|
||||
|
||||
if (rayDir.x < 0) {
|
||||
stepX = -1;
|
||||
sideDistX = (player.x - mapX) * deltaDistX;
|
||||
} else {
|
||||
stepX = 1;
|
||||
sideDistX = (mapX + 1.0 - player.x) * deltaDistX;
|
||||
}
|
||||
if (rayDir.y < 0) {
|
||||
stepY = -1;
|
||||
sideDistY = (player.y - mapY) * deltaDistY;
|
||||
} else {
|
||||
stepY = 1;
|
||||
sideDistY = (mapY + 1.0 - player.y) * deltaDistY;
|
||||
}
|
||||
|
||||
// --- DDA LOOP ---
|
||||
while (!hit) {
|
||||
if (sideDistX < sideDistY) {
|
||||
sideDistX += deltaDistX;
|
||||
mapX += stepX;
|
||||
side = 0;
|
||||
} else {
|
||||
sideDistY += deltaDistY;
|
||||
mapY += stepY;
|
||||
side = 1;
|
||||
}
|
||||
|
||||
if (mapY < 0 ||
|
||||
mapY >= map.length ||
|
||||
mapX < 0 ||
|
||||
mapX >= map[0].length) {
|
||||
hit = true;
|
||||
hitOutOfBounds = true;
|
||||
} else if (map[mapY][mapX] > 0) {
|
||||
String mapKey = '$mapX,$mapY';
|
||||
|
||||
// DOOR LOGIC
|
||||
if (map[mapY][mapX] >= 90 && !ignoredDoors.contains(mapKey)) {
|
||||
double currentOffset = doorOffsets[mapKey] ?? 0.0;
|
||||
if (currentOffset > 0.0) {
|
||||
double perpWallDistTemp = (side == 0)
|
||||
? (sideDistX - deltaDistX)
|
||||
: (sideDistY - deltaDistY);
|
||||
double wallXTemp = (side == 0)
|
||||
? player.y + perpWallDistTemp * rayDir.y
|
||||
: player.x + perpWallDistTemp * rayDir.x;
|
||||
wallXTemp -= wallXTemp.floor();
|
||||
if (wallXTemp < currentOffset) {
|
||||
ignoredDoors.add(mapKey);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
hit = true;
|
||||
hitWallId = map[mapY][mapX];
|
||||
textureOffset = currentOffset;
|
||||
}
|
||||
// PUSHWALL LOGIC
|
||||
else if (activePushwall != null &&
|
||||
mapX == activePushwall.x &&
|
||||
mapY == activePushwall.y) {
|
||||
hit = true;
|
||||
hitWallId = map[mapY][mapX];
|
||||
|
||||
double pOffset = activePushwall.offset;
|
||||
int pDirX = activePushwall.dirX;
|
||||
int pDirY = activePushwall.dirY;
|
||||
|
||||
perpWallDist = (side == 0)
|
||||
? (sideDistX - deltaDistX)
|
||||
: (sideDistY - deltaDistY);
|
||||
|
||||
if (side == 0 && pDirX != 0) {
|
||||
if (pDirX == stepX) {
|
||||
double intersect = perpWallDist + pOffset * deltaDistX;
|
||||
if (intersect < sideDistY) {
|
||||
perpWallDist = intersect;
|
||||
} else {
|
||||
side = 1;
|
||||
perpWallDist = sideDistY - deltaDistY;
|
||||
}
|
||||
} else {
|
||||
perpWallDist -= (1.0 - pOffset) * deltaDistX;
|
||||
}
|
||||
} else if (side == 1 && pDirY != 0) {
|
||||
if (pDirY == stepY) {
|
||||
double intersect = perpWallDist + pOffset * deltaDistY;
|
||||
if (intersect < sideDistX) {
|
||||
perpWallDist = intersect;
|
||||
} else {
|
||||
side = 0;
|
||||
perpWallDist = sideDistX - deltaDistX;
|
||||
}
|
||||
} else {
|
||||
perpWallDist -= (1.0 - pOffset) * deltaDistY;
|
||||
}
|
||||
} else {
|
||||
double wallFraction = (side == 0)
|
||||
? player.y + perpWallDist * rayDir.y
|
||||
: player.x + perpWallDist * rayDir.x;
|
||||
wallFraction -= wallFraction.floor();
|
||||
|
||||
if (side == 0) {
|
||||
if (pDirY == 1 && wallFraction < pOffset) hit = false;
|
||||
if (pDirY == -1 && wallFraction > (1.0 - pOffset)) hit = false;
|
||||
if (hit) textureOffset = pOffset * pDirY;
|
||||
} else {
|
||||
if (pDirX == 1 && wallFraction < pOffset) hit = false;
|
||||
if (pDirX == -1 && wallFraction > (1.0 - pOffset)) hit = false;
|
||||
if (hit) textureOffset = pOffset * pDirX;
|
||||
}
|
||||
}
|
||||
if (!hit) continue;
|
||||
customDistCalculated = true;
|
||||
}
|
||||
// STANDARD WALL
|
||||
else {
|
||||
hit = true;
|
||||
hitWallId = map[mapY][mapX];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hitOutOfBounds) continue;
|
||||
|
||||
if (!customDistCalculated) {
|
||||
if (side == 0) {
|
||||
perpWallDist = (sideDistX - deltaDistX);
|
||||
} else {
|
||||
perpWallDist = (sideDistY - deltaDistY);
|
||||
}
|
||||
}
|
||||
|
||||
// Log the distance so sprites know if they are occluded
|
||||
zBuffer[x] = perpWallDist;
|
||||
|
||||
double wallX = (side == 0)
|
||||
? player.y + perpWallDist * rayDir.y
|
||||
: player.x + perpWallDist * rayDir.x;
|
||||
wallX -= wallX.floor();
|
||||
|
||||
_drawTexturedColumn(
|
||||
x,
|
||||
perpWallDist,
|
||||
wallX,
|
||||
side,
|
||||
hitWallId,
|
||||
engine.data.walls, // Wall textures
|
||||
textureOffset,
|
||||
buffer,
|
||||
player.angle,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _drawTexturedColumn(
|
||||
int x,
|
||||
double distance,
|
||||
double wallX,
|
||||
int side,
|
||||
int hitWallId,
|
||||
List<Sprite> textures,
|
||||
double textureOffset,
|
||||
FrameBuffer buffer,
|
||||
double playerAngle,
|
||||
) {
|
||||
const int viewHeight = 160;
|
||||
if (distance <= 0.01) distance = 0.01;
|
||||
|
||||
int lineHeight = (viewHeight / distance).toInt();
|
||||
|
||||
int drawStart = -lineHeight ~/ 2 + viewHeight ~/ 2;
|
||||
if (drawStart < 0) drawStart = 0;
|
||||
|
||||
int drawEnd = lineHeight ~/ 2 + viewHeight ~/ 2;
|
||||
if (drawEnd >= viewHeight) drawEnd = viewHeight - 1;
|
||||
|
||||
int texNum;
|
||||
if (hitWallId >= 90) {
|
||||
texNum = 98.clamp(0, textures.length - 1);
|
||||
} else {
|
||||
texNum = ((hitWallId - 1) * 2).clamp(0, textures.length - 2);
|
||||
if (side == 1) texNum += 1;
|
||||
}
|
||||
|
||||
int texX = (((wallX - textureOffset) % 1.0) * 64).toInt().clamp(0, 63);
|
||||
if (side == 0 && math.cos(playerAngle) > 0) texX = 63 - texX;
|
||||
if (side == 1 && math.sin(playerAngle) < 0) texX = 63 - texX;
|
||||
|
||||
double step = 64.0 / lineHeight;
|
||||
double texPos = (drawStart - viewHeight / 2 + lineHeight / 2) * step;
|
||||
|
||||
Sprite texture = textures[texNum];
|
||||
|
||||
for (int y = drawStart; y < drawEnd; y++) {
|
||||
int texY = texPos.toInt() & 63;
|
||||
texPos += step;
|
||||
|
||||
int colorByte = texture.pixels[texX * 64 + texY];
|
||||
int color32 = ColorPalette.vga32Bit[colorByte];
|
||||
|
||||
buffer.pixels[y * buffer.width + x] = color32;
|
||||
}
|
||||
}
|
||||
|
||||
void _castSprites(
|
||||
WolfEngine engine,
|
||||
FrameBuffer buffer,
|
||||
List<double> zBuffer,
|
||||
) {
|
||||
const int viewHeight = 160;
|
||||
final Player player = engine.player;
|
||||
final List<Entity> activeSprites = List.from(engine.entities);
|
||||
|
||||
// Sort entities from furthest to closest (Painter's Algorithm)
|
||||
activeSprites.sort((a, b) {
|
||||
double distA = player.position.distanceTo(a.position);
|
||||
double distB = player.position.distanceTo(b.position);
|
||||
return distB.compareTo(distA);
|
||||
});
|
||||
|
||||
Coordinate2D dir = Coordinate2D(
|
||||
math.cos(player.angle),
|
||||
math.sin(player.angle),
|
||||
);
|
||||
Coordinate2D plane =
|
||||
Coordinate2D(-dir.y, dir.x) * math.tan((math.pi / 3) / 2);
|
||||
|
||||
for (Entity entity in activeSprites) {
|
||||
Coordinate2D spritePos = entity.position - player.position;
|
||||
|
||||
double invDet = 1.0 / (plane.x * dir.y - dir.x * plane.y);
|
||||
double transformX = invDet * (dir.y * spritePos.x - dir.x * spritePos.y);
|
||||
double transformY =
|
||||
invDet * (-plane.y * spritePos.x + plane.x * spritePos.y);
|
||||
|
||||
if (transformY > 0) {
|
||||
int spriteScreenX = ((buffer.width / 2) * (1 + transformX / transformY))
|
||||
.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 + viewHeight ~/ 2;
|
||||
if (drawStartY < 0) drawStartY = 0;
|
||||
|
||||
int drawEndY = spriteHeight ~/ 2 + viewHeight ~/ 2;
|
||||
if (drawEndY >= buffer.height) drawEndY = viewHeight - 1;
|
||||
|
||||
int drawStartX = -spriteWidth ~/ 2 + spriteScreenX;
|
||||
int drawEndX = spriteWidth ~/ 2 + spriteScreenX;
|
||||
|
||||
int clipStartX = math.max(0, drawStartX);
|
||||
int clipEndX = math.min(buffer.width - 1, drawEndX);
|
||||
|
||||
int safeIndex = entity.spriteIndex.clamp(
|
||||
0,
|
||||
engine.data.sprites.length - 1,
|
||||
);
|
||||
Sprite spritePixels = engine.data.sprites[safeIndex];
|
||||
|
||||
for (int stripe = clipStartX; stripe < clipEndX; stripe++) {
|
||||
// Z-Buffer Check! Only draw the vertical stripe if it's in front of the wall
|
||||
if (transformY < zBuffer[stripe]) {
|
||||
int texX = ((stripe - drawStartX) * 64 ~/ spriteWidth).clamp(0, 63);
|
||||
|
||||
double step = 64.0 / spriteHeight;
|
||||
double texPos =
|
||||
(drawStartY - viewHeight / 2 + spriteHeight / 2) * step;
|
||||
|
||||
for (int y = drawStartY; y < drawEndY; y++) {
|
||||
int texY = texPos.toInt() & 63;
|
||||
texPos += step;
|
||||
|
||||
int colorByte = spritePixels.pixels[texX * 64 + texY];
|
||||
|
||||
if (colorByte != 255) {
|
||||
// 255 is transparent
|
||||
buffer.pixels[y * buffer.width + stripe] =
|
||||
ColorPalette.vga32Bit[colorByte];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _drawWeapon(WolfEngine engine, FrameBuffer buffer) {
|
||||
const int viewHeight = 160;
|
||||
int spriteIndex = engine.player.currentWeapon.getCurrentSpriteIndex(
|
||||
engine.data.sprites.length,
|
||||
);
|
||||
Sprite weaponSprite = engine.data.sprites[spriteIndex];
|
||||
|
||||
// Dropped the scale from 4 to 2 (a 50% reduction in size)
|
||||
const int scale = 2;
|
||||
const int weaponWidth = 64 * scale;
|
||||
const int weaponHeight = 64 * scale;
|
||||
|
||||
int startX = (buffer.width ~/ 2) - (weaponWidth ~/ 2);
|
||||
|
||||
// Kept the grounding to the bottom of the screen
|
||||
int startY =
|
||||
viewHeight - weaponHeight + (engine.player.weaponAnimOffset ~/ 2);
|
||||
|
||||
for (int x = 0; x < 64; x++) {
|
||||
for (int y = 0; y < 64; y++) {
|
||||
int colorByte = weaponSprite.pixels[x * 64 + y];
|
||||
|
||||
if (colorByte != 255) {
|
||||
int color32 = ColorPalette.vga32Bit[colorByte];
|
||||
|
||||
for (int sx = 0; sx < scale; sx++) {
|
||||
for (int sy = 0; sy < scale; sy++) {
|
||||
int drawX = startX + (x * scale) + sx;
|
||||
int drawY = startY + (y * scale) + sy;
|
||||
|
||||
if (drawX >= 0 &&
|
||||
drawX < buffer.width &&
|
||||
drawY >= 0 &&
|
||||
drawY < buffer.height) {
|
||||
buffer.pixels[drawY * buffer.width + drawX] = color32;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _drawDamageFlash(WolfEngine engine, FrameBuffer buffer) {
|
||||
if (engine.damageFlashOpacity <= 0) return;
|
||||
|
||||
int alpha = (engine.damageFlashOpacity * 256).toInt().clamp(0, 256);
|
||||
int invAlpha = 256 - alpha;
|
||||
|
||||
for (int i = 0; i < buffer.pixels.length; i++) {
|
||||
int color = buffer.pixels[i];
|
||||
|
||||
int r = color & 0xFF;
|
||||
int g = (color >> 8) & 0xFF;
|
||||
int b = (color >> 16) & 0xFF;
|
||||
int a = (color >> 24) & 0xFF;
|
||||
|
||||
// Blend with Red
|
||||
r = ((r * invAlpha) + (255 * alpha)) >> 8;
|
||||
g = (g * invAlpha) >> 8;
|
||||
b = (b * invAlpha) >> 8;
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
abstract class Rasterizer {
|
||||
dynamic render(WolfEngine engine, FrameBuffer buffer);
|
||||
}
|
||||
|
||||
561
packages/wolf_3d_engine/lib/src/software_rasterizer.dart
Normal file
561
packages/wolf_3d_engine/lib/src/software_rasterizer.dart
Normal file
@@ -0,0 +1,561 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
|
||||
import 'package:wolf_3d_engine/wolf_3d_engine.dart';
|
||||
import 'package:wolf_3d_entities/wolf_3d_entities.dart';
|
||||
|
||||
class SoftwareRasterizer implements Rasterizer {
|
||||
// Pre-calculated VGA colors for ceiling and floor
|
||||
final int ceilingColor = ColorPalette.vga32Bit[25];
|
||||
final int floorColor = ColorPalette.vga32Bit[29];
|
||||
|
||||
@override
|
||||
void render(WolfEngine engine, FrameBuffer buffer) {
|
||||
_clearScreen(buffer);
|
||||
|
||||
List<double> zBuffer = List.filled(buffer.width, 0.0);
|
||||
|
||||
_castWalls(engine, buffer, zBuffer);
|
||||
_castSprites(engine, buffer, zBuffer);
|
||||
|
||||
// NEW: Draw the weapon on top of the 3D world
|
||||
_drawWeapon(engine, buffer);
|
||||
|
||||
// NEW: Apply the full-screen damage tint last
|
||||
_drawDamageFlash(engine, buffer);
|
||||
|
||||
_drawHud(engine, buffer);
|
||||
}
|
||||
|
||||
void _clearScreen(FrameBuffer buffer) {
|
||||
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.width * viewHeight, floorColor);
|
||||
}
|
||||
|
||||
void _castWalls(WolfEngine engine, FrameBuffer buffer, List<double> zBuffer) {
|
||||
final double fov = math.pi / 3;
|
||||
final Player player = engine.player;
|
||||
final SpriteMap map = engine.currentLevel;
|
||||
|
||||
// Fetch dynamic states from the managers
|
||||
final Map<String, double> doorOffsets = engine.doorManager
|
||||
.getOffsetsForRenderer();
|
||||
final Pushwall? activePushwall = engine.pushwallManager.activePushwall;
|
||||
|
||||
Coordinate2D dir = Coordinate2D(
|
||||
math.cos(player.angle),
|
||||
math.sin(player.angle),
|
||||
);
|
||||
Coordinate2D plane = Coordinate2D(-dir.y, dir.x) * math.tan(fov / 2);
|
||||
|
||||
for (int x = 0; x < buffer.width; x++) {
|
||||
double cameraX = 2 * x / buffer.width - 1.0;
|
||||
Coordinate2D rayDir = dir + (plane * cameraX);
|
||||
|
||||
int mapX = player.x.toInt();
|
||||
int mapY = player.y.toInt();
|
||||
|
||||
double sideDistX;
|
||||
double sideDistY;
|
||||
double deltaDistX = (rayDir.x == 0) ? 1e30 : (1.0 / rayDir.x).abs();
|
||||
double deltaDistY = (rayDir.y == 0) ? 1e30 : (1.0 / rayDir.y).abs();
|
||||
double perpWallDist = 0.0;
|
||||
|
||||
int stepX;
|
||||
int stepY;
|
||||
bool hit = false;
|
||||
bool hitOutOfBounds = false;
|
||||
int side = 0;
|
||||
int hitWallId = 0;
|
||||
|
||||
double textureOffset = 0.0;
|
||||
bool customDistCalculated = false;
|
||||
Set<String> ignoredDoors = {};
|
||||
|
||||
if (rayDir.x < 0) {
|
||||
stepX = -1;
|
||||
sideDistX = (player.x - mapX) * deltaDistX;
|
||||
} else {
|
||||
stepX = 1;
|
||||
sideDistX = (mapX + 1.0 - player.x) * deltaDistX;
|
||||
}
|
||||
if (rayDir.y < 0) {
|
||||
stepY = -1;
|
||||
sideDistY = (player.y - mapY) * deltaDistY;
|
||||
} else {
|
||||
stepY = 1;
|
||||
sideDistY = (mapY + 1.0 - player.y) * deltaDistY;
|
||||
}
|
||||
|
||||
// --- DDA LOOP ---
|
||||
while (!hit) {
|
||||
if (sideDistX < sideDistY) {
|
||||
sideDistX += deltaDistX;
|
||||
mapX += stepX;
|
||||
side = 0;
|
||||
} else {
|
||||
sideDistY += deltaDistY;
|
||||
mapY += stepY;
|
||||
side = 1;
|
||||
}
|
||||
|
||||
if (mapY < 0 ||
|
||||
mapY >= map.length ||
|
||||
mapX < 0 ||
|
||||
mapX >= map[0].length) {
|
||||
hit = true;
|
||||
hitOutOfBounds = true;
|
||||
} else if (map[mapY][mapX] > 0) {
|
||||
String mapKey = '$mapX,$mapY';
|
||||
|
||||
// DOOR LOGIC
|
||||
if (map[mapY][mapX] >= 90 && !ignoredDoors.contains(mapKey)) {
|
||||
double currentOffset = doorOffsets[mapKey] ?? 0.0;
|
||||
if (currentOffset > 0.0) {
|
||||
double perpWallDistTemp = (side == 0)
|
||||
? (sideDistX - deltaDistX)
|
||||
: (sideDistY - deltaDistY);
|
||||
double wallXTemp = (side == 0)
|
||||
? player.y + perpWallDistTemp * rayDir.y
|
||||
: player.x + perpWallDistTemp * rayDir.x;
|
||||
wallXTemp -= wallXTemp.floor();
|
||||
if (wallXTemp < currentOffset) {
|
||||
ignoredDoors.add(mapKey);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
hit = true;
|
||||
hitWallId = map[mapY][mapX];
|
||||
textureOffset = currentOffset;
|
||||
}
|
||||
// PUSHWALL LOGIC
|
||||
else if (activePushwall != null &&
|
||||
mapX == activePushwall.x &&
|
||||
mapY == activePushwall.y) {
|
||||
hit = true;
|
||||
hitWallId = map[mapY][mapX];
|
||||
|
||||
double pOffset = activePushwall.offset;
|
||||
int pDirX = activePushwall.dirX;
|
||||
int pDirY = activePushwall.dirY;
|
||||
|
||||
perpWallDist = (side == 0)
|
||||
? (sideDistX - deltaDistX)
|
||||
: (sideDistY - deltaDistY);
|
||||
|
||||
if (side == 0 && pDirX != 0) {
|
||||
if (pDirX == stepX) {
|
||||
double intersect = perpWallDist + pOffset * deltaDistX;
|
||||
if (intersect < sideDistY) {
|
||||
perpWallDist = intersect;
|
||||
} else {
|
||||
side = 1;
|
||||
perpWallDist = sideDistY - deltaDistY;
|
||||
}
|
||||
} else {
|
||||
perpWallDist -= (1.0 - pOffset) * deltaDistX;
|
||||
}
|
||||
} else if (side == 1 && pDirY != 0) {
|
||||
if (pDirY == stepY) {
|
||||
double intersect = perpWallDist + pOffset * deltaDistY;
|
||||
if (intersect < sideDistX) {
|
||||
perpWallDist = intersect;
|
||||
} else {
|
||||
side = 0;
|
||||
perpWallDist = sideDistX - deltaDistX;
|
||||
}
|
||||
} else {
|
||||
perpWallDist -= (1.0 - pOffset) * deltaDistY;
|
||||
}
|
||||
} else {
|
||||
double wallFraction = (side == 0)
|
||||
? player.y + perpWallDist * rayDir.y
|
||||
: player.x + perpWallDist * rayDir.x;
|
||||
wallFraction -= wallFraction.floor();
|
||||
|
||||
if (side == 0) {
|
||||
if (pDirY == 1 && wallFraction < pOffset) hit = false;
|
||||
if (pDirY == -1 && wallFraction > (1.0 - pOffset)) hit = false;
|
||||
if (hit) textureOffset = pOffset * pDirY;
|
||||
} else {
|
||||
if (pDirX == 1 && wallFraction < pOffset) hit = false;
|
||||
if (pDirX == -1 && wallFraction > (1.0 - pOffset)) hit = false;
|
||||
if (hit) textureOffset = pOffset * pDirX;
|
||||
}
|
||||
}
|
||||
if (!hit) continue;
|
||||
customDistCalculated = true;
|
||||
}
|
||||
// STANDARD WALL
|
||||
else {
|
||||
hit = true;
|
||||
hitWallId = map[mapY][mapX];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hitOutOfBounds) continue;
|
||||
|
||||
if (!customDistCalculated) {
|
||||
if (side == 0) {
|
||||
perpWallDist = (sideDistX - deltaDistX);
|
||||
} else {
|
||||
perpWallDist = (sideDistY - deltaDistY);
|
||||
}
|
||||
}
|
||||
|
||||
// Log the distance so sprites know if they are occluded
|
||||
zBuffer[x] = perpWallDist;
|
||||
|
||||
double wallX = (side == 0)
|
||||
? player.y + perpWallDist * rayDir.y
|
||||
: player.x + perpWallDist * rayDir.x;
|
||||
wallX -= wallX.floor();
|
||||
|
||||
_drawTexturedColumn(
|
||||
x,
|
||||
perpWallDist,
|
||||
wallX,
|
||||
side,
|
||||
hitWallId,
|
||||
engine.data.walls, // Wall textures
|
||||
textureOffset,
|
||||
buffer,
|
||||
player.angle,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _drawTexturedColumn(
|
||||
int x,
|
||||
double distance,
|
||||
double wallX,
|
||||
int side,
|
||||
int hitWallId,
|
||||
List<Sprite> textures,
|
||||
double textureOffset,
|
||||
FrameBuffer buffer,
|
||||
double playerAngle,
|
||||
) {
|
||||
const int viewHeight = 160;
|
||||
if (distance <= 0.01) distance = 0.01;
|
||||
|
||||
int lineHeight = (viewHeight / distance).toInt();
|
||||
|
||||
int drawStart = -lineHeight ~/ 2 + viewHeight ~/ 2;
|
||||
if (drawStart < 0) drawStart = 0;
|
||||
|
||||
int drawEnd = lineHeight ~/ 2 + viewHeight ~/ 2;
|
||||
if (drawEnd >= viewHeight) drawEnd = viewHeight - 1;
|
||||
|
||||
int texNum;
|
||||
if (hitWallId >= 90) {
|
||||
texNum = 98.clamp(0, textures.length - 1);
|
||||
} else {
|
||||
texNum = ((hitWallId - 1) * 2).clamp(0, textures.length - 2);
|
||||
if (side == 1) texNum += 1;
|
||||
}
|
||||
|
||||
int texX = (((wallX - textureOffset) % 1.0) * 64).toInt().clamp(0, 63);
|
||||
if (side == 0 && math.cos(playerAngle) > 0) texX = 63 - texX;
|
||||
if (side == 1 && math.sin(playerAngle) < 0) texX = 63 - texX;
|
||||
|
||||
double step = 64.0 / lineHeight;
|
||||
double texPos = (drawStart - viewHeight / 2 + lineHeight / 2) * step;
|
||||
|
||||
Sprite texture = textures[texNum];
|
||||
|
||||
for (int y = drawStart; y < drawEnd; y++) {
|
||||
int texY = texPos.toInt() & 63;
|
||||
texPos += step;
|
||||
|
||||
int colorByte = texture.pixels[texX * 64 + texY];
|
||||
int color32 = ColorPalette.vga32Bit[colorByte];
|
||||
|
||||
buffer.pixels[y * buffer.width + x] = color32;
|
||||
}
|
||||
}
|
||||
|
||||
void _castSprites(
|
||||
WolfEngine engine,
|
||||
FrameBuffer buffer,
|
||||
List<double> zBuffer,
|
||||
) {
|
||||
const int viewHeight = 160;
|
||||
final Player player = engine.player;
|
||||
final List<Entity> activeSprites = List.from(engine.entities);
|
||||
|
||||
// Sort entities from furthest to closest (Painter's Algorithm)
|
||||
activeSprites.sort((a, b) {
|
||||
double distA = player.position.distanceTo(a.position);
|
||||
double distB = player.position.distanceTo(b.position);
|
||||
return distB.compareTo(distA);
|
||||
});
|
||||
|
||||
Coordinate2D dir = Coordinate2D(
|
||||
math.cos(player.angle),
|
||||
math.sin(player.angle),
|
||||
);
|
||||
Coordinate2D plane =
|
||||
Coordinate2D(-dir.y, dir.x) * math.tan((math.pi / 3) / 2);
|
||||
|
||||
for (Entity entity in activeSprites) {
|
||||
Coordinate2D spritePos = entity.position - player.position;
|
||||
|
||||
double invDet = 1.0 / (plane.x * dir.y - dir.x * plane.y);
|
||||
double transformX = invDet * (dir.y * spritePos.x - dir.x * spritePos.y);
|
||||
double transformY =
|
||||
invDet * (-plane.y * spritePos.x + plane.x * spritePos.y);
|
||||
|
||||
if (transformY > 0) {
|
||||
int spriteScreenX = ((buffer.width / 2) * (1 + transformX / transformY))
|
||||
.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 + viewHeight ~/ 2;
|
||||
if (drawStartY < 0) drawStartY = 0;
|
||||
|
||||
int drawEndY = spriteHeight ~/ 2 + viewHeight ~/ 2;
|
||||
if (drawEndY >= buffer.height) drawEndY = viewHeight - 1;
|
||||
|
||||
int drawStartX = -spriteWidth ~/ 2 + spriteScreenX;
|
||||
int drawEndX = spriteWidth ~/ 2 + spriteScreenX;
|
||||
|
||||
int clipStartX = math.max(0, drawStartX);
|
||||
int clipEndX = math.min(buffer.width - 1, drawEndX);
|
||||
|
||||
int safeIndex = entity.spriteIndex.clamp(
|
||||
0,
|
||||
engine.data.sprites.length - 1,
|
||||
);
|
||||
Sprite spritePixels = engine.data.sprites[safeIndex];
|
||||
|
||||
for (int stripe = clipStartX; stripe < clipEndX; stripe++) {
|
||||
// Z-Buffer Check! Only draw the vertical stripe if it's in front of the wall
|
||||
if (transformY < zBuffer[stripe]) {
|
||||
int texX = ((stripe - drawStartX) * 64 ~/ spriteWidth).clamp(0, 63);
|
||||
|
||||
double step = 64.0 / spriteHeight;
|
||||
double texPos =
|
||||
(drawStartY - viewHeight / 2 + spriteHeight / 2) * step;
|
||||
|
||||
for (int y = drawStartY; y < drawEndY; y++) {
|
||||
int texY = texPos.toInt() & 63;
|
||||
texPos += step;
|
||||
|
||||
int colorByte = spritePixels.pixels[texX * 64 + texY];
|
||||
|
||||
if (colorByte != 255) {
|
||||
// 255 is transparent
|
||||
buffer.pixels[y * buffer.width + stripe] =
|
||||
ColorPalette.vga32Bit[colorByte];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _drawWeapon(WolfEngine engine, FrameBuffer buffer) {
|
||||
const int viewHeight = 160;
|
||||
int spriteIndex = engine.player.currentWeapon.getCurrentSpriteIndex(
|
||||
engine.data.sprites.length,
|
||||
);
|
||||
Sprite weaponSprite = engine.data.sprites[spriteIndex];
|
||||
|
||||
// Dropped the scale from 4 to 2 (a 50% reduction in size)
|
||||
const int scale = 2;
|
||||
const int weaponWidth = 64 * scale;
|
||||
const int weaponHeight = 64 * scale;
|
||||
|
||||
int startX = (buffer.width ~/ 2) - (weaponWidth ~/ 2);
|
||||
|
||||
// Kept the grounding to the bottom of the screen
|
||||
int startY =
|
||||
viewHeight - weaponHeight + (engine.player.weaponAnimOffset ~/ 2);
|
||||
|
||||
for (int x = 0; x < 64; x++) {
|
||||
for (int y = 0; y < 64; y++) {
|
||||
int colorByte = weaponSprite.pixels[x * 64 + y];
|
||||
|
||||
if (colorByte != 255) {
|
||||
int color32 = ColorPalette.vga32Bit[colorByte];
|
||||
|
||||
for (int sx = 0; sx < scale; sx++) {
|
||||
for (int sy = 0; sy < scale; sy++) {
|
||||
int drawX = startX + (x * scale) + sx;
|
||||
int drawY = startY + (y * scale) + sy;
|
||||
|
||||
if (drawX >= 0 &&
|
||||
drawX < buffer.width &&
|
||||
drawY >= 0 &&
|
||||
drawY < buffer.height) {
|
||||
buffer.pixels[drawY * buffer.width + drawX] = color32;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _drawDamageFlash(WolfEngine engine, FrameBuffer buffer) {
|
||||
if (engine.damageFlashOpacity <= 0) return;
|
||||
|
||||
int alpha = (engine.damageFlashOpacity * 256).toInt().clamp(0, 256);
|
||||
int invAlpha = 256 - alpha;
|
||||
|
||||
for (int i = 0; i < buffer.pixels.length; i++) {
|
||||
int color = buffer.pixels[i];
|
||||
|
||||
int r = color & 0xFF;
|
||||
int g = (color >> 8) & 0xFF;
|
||||
int b = (color >> 16) & 0xFF;
|
||||
int a = (color >> 24) & 0xFF;
|
||||
|
||||
// Blend with Red
|
||||
r = ((r * invAlpha) + (255 * alpha)) >> 8;
|
||||
g = (g * invAlpha) >> 8;
|
||||
b = (b * invAlpha) >> 8;
|
||||
|
||||
buffer.pixels[i] = (a << 24) | (b << 16) | (g << 8) | r;
|
||||
}
|
||||
}
|
||||
|
||||
void _drawNumber(
|
||||
int value,
|
||||
int rightAlignX,
|
||||
int startY,
|
||||
FrameBuffer buffer,
|
||||
List<VgaImage> vgaImages,
|
||||
) {
|
||||
// The yellow 8x16 HUD digits start exactly at 96
|
||||
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, buffer);
|
||||
}
|
||||
currentX += 8;
|
||||
}
|
||||
}
|
||||
|
||||
void _drawFace(WolfEngine engine, FrameBuffer buffer) {
|
||||
int health = engine.player.health;
|
||||
int faceIndex;
|
||||
|
||||
if (health <= 0) {
|
||||
faceIndex = 127; // Dead face
|
||||
} else {
|
||||
int healthTier = ((100 - health) ~/ 16).clamp(0, 6);
|
||||
|
||||
// Base face is 106 (100% health, looking forward).
|
||||
faceIndex = 106 + (healthTier * 3);
|
||||
}
|
||||
|
||||
if (faceIndex < engine.data.vgaImages.length) {
|
||||
_blitVgaImage(engine.data.vgaImages[faceIndex], 142, 164, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
void _drawWeaponIcon(WolfEngine engine, FrameBuffer buffer) {
|
||||
int weaponIndex = 89; // Default to Pistol (Index 89)
|
||||
|
||||
if (engine.player.hasChainGun) {
|
||||
weaponIndex = 91;
|
||||
} else if (engine.player.hasMachineGun) {
|
||||
weaponIndex = 90;
|
||||
}
|
||||
|
||||
if (weaponIndex < engine.data.vgaImages.length) {
|
||||
// The weapon box starts at X=272. Let's center it vertically at 164.
|
||||
_blitVgaImage(engine.data.vgaImages[weaponIndex], 272, 164, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
void _drawHud(WolfEngine engine, FrameBuffer buffer) {
|
||||
int statusBarIndex = engine.data.vgaImages.indexWhere(
|
||||
(img) => img.width == 320 && img.height == 40,
|
||||
);
|
||||
|
||||
if (statusBarIndex == -1) return;
|
||||
|
||||
// 1. Draw Background
|
||||
_blitVgaImage(engine.data.vgaImages[statusBarIndex], 0, 160, buffer);
|
||||
|
||||
// 2. Draw Stats (Right-aligned to fit inside the dark blue boxes)
|
||||
// (Using a hardcoded '1' for the floor since your engine hides currentLevelIndex)
|
||||
_drawNumber(1, 40, 176, buffer, engine.data.vgaImages); // Floor
|
||||
_drawNumber(
|
||||
engine.player.score,
|
||||
104,
|
||||
176,
|
||||
buffer,
|
||||
engine.data.vgaImages,
|
||||
); // Score
|
||||
_drawNumber(3, 136, 176, buffer, engine.data.vgaImages); // Lives
|
||||
_drawNumber(
|
||||
engine.player.health,
|
||||
216,
|
||||
176,
|
||||
buffer,
|
||||
engine.data.vgaImages,
|
||||
); // Health
|
||||
|
||||
// Pulled the Ammo X coordinate back to 264 so it doesn't bleed into the weapon!
|
||||
_drawNumber(engine.player.ammo, 264, 176, buffer, engine.data.vgaImages);
|
||||
|
||||
// 3. Draw BJ's Face & Current Weapon
|
||||
_drawFace(engine, buffer);
|
||||
_drawWeaponIcon(engine, buffer);
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,12 @@
|
||||
/// More dartdocs go here.
|
||||
library;
|
||||
|
||||
export 'src/ascii_rasterizer.dart';
|
||||
export 'src/engine_audio.dart';
|
||||
export 'src/engine_input.dart';
|
||||
export 'src/managers/door_manager.dart';
|
||||
export 'src/managers/pushwall_manager.dart';
|
||||
export 'src/player/player.dart';
|
||||
export 'src/rasterizer.dart';
|
||||
export 'src/software_rasterizer.dart';
|
||||
export 'src/wolf_3d_engine_base.dart';
|
||||
|
||||
Reference in New Issue
Block a user