265 lines
7.6 KiB
Dart
265 lines
7.6 KiB
Dart
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';
|
|
|
|
class SoftwareRasterizer extends Rasterizer {
|
|
late FrameBuffer _buffer;
|
|
late WolfEngine _engine;
|
|
|
|
// Intercept the base render call to store our references
|
|
@override
|
|
dynamic render(WolfEngine engine, FrameBuffer buffer) {
|
|
_engine = engine;
|
|
_buffer = buffer;
|
|
return super.render(engine, buffer);
|
|
}
|
|
|
|
@override
|
|
void prepareFrame(WolfEngine engine) {
|
|
// Top half is ceiling color (25), bottom half is floor color (29)
|
|
int ceilingColor = ColorPalette.vga32Bit[25];
|
|
int floorColor = ColorPalette.vga32Bit[29];
|
|
|
|
for (int y = 0; y < viewHeight; y++) {
|
|
int color = (y < viewHeight / 2) ? ceilingColor : floorColor;
|
|
for (int x = 0; x < width; x++) {
|
|
_buffer.pixels[y * width + x] = color;
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void drawWallColumn(
|
|
int x,
|
|
int drawStart,
|
|
int drawEnd,
|
|
int columnHeight,
|
|
Sprite texture,
|
|
int texX,
|
|
double perpWallDist,
|
|
int side,
|
|
) {
|
|
for (int y = drawStart; y < drawEnd; y++) {
|
|
// Calculate which Y pixel of the texture to sample
|
|
double relativeY =
|
|
(y - (-columnHeight ~/ 2 + viewHeight ~/ 2)) / columnHeight;
|
|
int texY = (relativeY * 64).toInt().clamp(0, 63);
|
|
|
|
int colorByte = texture.pixels[texX * 64 + texY];
|
|
int pixelColor = ColorPalette.vga32Bit[colorByte];
|
|
|
|
// Darken Y-side walls for faux directional lighting
|
|
if (side == 1) {
|
|
pixelColor = shadeColor(pixelColor);
|
|
}
|
|
|
|
_buffer.pixels[y * width + x] = pixelColor;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void drawSpriteStripe(
|
|
int stripeX,
|
|
int drawStartY,
|
|
int drawEndY,
|
|
int spriteHeight,
|
|
Sprite texture,
|
|
int texX,
|
|
double transformY,
|
|
) {
|
|
for (
|
|
int y = math.max(0, drawStartY);
|
|
y < math.min(viewHeight, drawEndY);
|
|
y++
|
|
) {
|
|
double relativeY = (y - drawStartY) / spriteHeight;
|
|
int texY = (relativeY * 64).toInt().clamp(0, 63);
|
|
|
|
int colorByte = texture.pixels[texX * 64 + texY];
|
|
|
|
// 255 is the "transparent" color index in VGA Wolfenstein
|
|
if (colorByte != 255) {
|
|
_buffer.pixels[y * width + stripeX] = ColorPalette.vga32Bit[colorByte];
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void drawWeapon(WolfEngine engine) {
|
|
int spriteIndex = engine.player.currentWeapon.getCurrentSpriteIndex(
|
|
engine.data.sprites.length,
|
|
);
|
|
Sprite weaponSprite = engine.data.sprites[spriteIndex];
|
|
|
|
int weaponWidth = (width * 0.5).toInt();
|
|
int weaponHeight = (viewHeight * 0.8).toInt();
|
|
|
|
int startX = (width ~/ 2) - (weaponWidth ~/ 2);
|
|
int startY =
|
|
viewHeight - weaponHeight + (engine.player.weaponAnimOffset ~/ 4);
|
|
|
|
for (int dy = 0; dy < weaponHeight; dy++) {
|
|
for (int dx = 0; dx < weaponWidth; dx++) {
|
|
int texX = (dx * 64 ~/ weaponWidth).clamp(0, 63);
|
|
int texY = (dy * 64 ~/ weaponHeight).clamp(0, 63);
|
|
|
|
int colorByte = weaponSprite.pixels[texX * 64 + texY];
|
|
if (colorByte != 255) {
|
|
int drawX = startX + dx;
|
|
int drawY = startY + dy;
|
|
if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < viewHeight) {
|
|
_buffer.pixels[drawY * width + drawX] =
|
|
ColorPalette.vga32Bit[colorByte];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void drawHud(WolfEngine engine) {
|
|
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);
|
|
|
|
// 2. Draw Stats (100% mathematically accurate right-aligned coordinates)
|
|
_drawNumber(1, 32, 176, engine.data.vgaImages); // Floor
|
|
_drawNumber(engine.player.score, 96, 176, engine.data.vgaImages); // Score
|
|
_drawNumber(3, 120, 176, engine.data.vgaImages); // Lives
|
|
_drawNumber(
|
|
engine.player.health,
|
|
192,
|
|
176,
|
|
engine.data.vgaImages,
|
|
); // Health
|
|
_drawNumber(engine.player.ammo, 232, 176, engine.data.vgaImages); // Ammo
|
|
|
|
// 3. Draw BJ's Face & Current Weapon
|
|
_drawFace(engine);
|
|
_drawWeaponIcon(engine);
|
|
}
|
|
|
|
@override
|
|
FrameBuffer finalizeFrame() {
|
|
// If the player took damage, overlay a red tint across the 3D view
|
|
if (_engine.player.damageFlash > 0) {
|
|
_applyDamageFlash();
|
|
}
|
|
return _buffer; // Return the fully painted pixel array
|
|
}
|
|
|
|
// ===========================================================================
|
|
// PRIVATE HELPER METHODS
|
|
// ===========================================================================
|
|
|
|
/// Maps the planar VGA image data directly to 32-bit pixels.
|
|
/// (Assuming a 1:1 scale, which is standard for the 320x200 software renderer).
|
|
void _blitVgaImage(VgaImage image, int startX, int startY) {
|
|
int planeWidth = image.width ~/ 4;
|
|
int planeSize = planeWidth * image.height;
|
|
|
|
for (int dy = 0; dy < image.height; dy++) {
|
|
for (int dx = 0; dx < image.width; dx++) {
|
|
int drawX = startX + dx;
|
|
int drawY = startY + dy;
|
|
|
|
if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < height) {
|
|
int srcX = dx.clamp(0, image.width - 1);
|
|
int srcY = dy.clamp(0, image.height - 1);
|
|
|
|
int plane = srcX % 4;
|
|
int sx = srcX ~/ 4;
|
|
int index = (plane * planeSize) + (srcY * planeWidth) + sx;
|
|
|
|
int colorByte = image.pixels[index];
|
|
if (colorByte != 255) {
|
|
_buffer.pixels[drawY * width + drawX] =
|
|
ColorPalette.vga32Bit[colorByte];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void _drawNumber(
|
|
int value,
|
|
int rightAlignX,
|
|
int startY,
|
|
List<VgaImage> vgaImages,
|
|
) {
|
|
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);
|
|
}
|
|
currentX += 8;
|
|
}
|
|
}
|
|
|
|
void _drawFace(WolfEngine engine) {
|
|
int health = engine.player.health;
|
|
int faceIndex;
|
|
|
|
if (health <= 0) {
|
|
faceIndex = 127; // Dead face
|
|
} else {
|
|
int healthTier = ((100 - health) ~/ 16).clamp(0, 6);
|
|
faceIndex = 106 + (healthTier * 3);
|
|
}
|
|
|
|
if (faceIndex < engine.data.vgaImages.length) {
|
|
_blitVgaImage(engine.data.vgaImages[faceIndex], 136, 164);
|
|
}
|
|
}
|
|
|
|
void _drawWeaponIcon(WolfEngine engine) {
|
|
int weaponIndex = 89; // Default to Pistol
|
|
|
|
if (engine.player.hasChainGun) {
|
|
weaponIndex = 91;
|
|
} else if (engine.player.hasMachineGun) {
|
|
weaponIndex = 90;
|
|
}
|
|
|
|
if (weaponIndex < engine.data.vgaImages.length) {
|
|
_blitVgaImage(engine.data.vgaImages[weaponIndex], 256, 164);
|
|
}
|
|
}
|
|
|
|
/// Tints the top 80% of the screen red based on player.damageFlash intensity
|
|
void _applyDamageFlash() {
|
|
// Grab the intensity (0.0 to 1.0)
|
|
double intensity = _engine.player.damageFlash;
|
|
|
|
// Calculate how much to boost red and drop green/blue
|
|
int redBoost = (150 * intensity).toInt();
|
|
double colorDrop = 1.0 - (0.5 * intensity);
|
|
|
|
for (int y = 0; y < viewHeight; y++) {
|
|
for (int x = 0; x < width; x++) {
|
|
int index = y * width + x;
|
|
int color = _buffer.pixels[index];
|
|
|
|
int r = color & 0xFF;
|
|
int g = (color >> 8) & 0xFF;
|
|
int b = (color >> 16) & 0xFF;
|
|
|
|
r = (r + redBoost).clamp(0, 255);
|
|
g = (g * colorDrop).toInt();
|
|
b = (b * colorDrop).toInt();
|
|
|
|
_buffer.pixels[index] = (0xFF000000) | (b << 16) | (g << 8) | r;
|
|
}
|
|
}
|
|
}
|
|
}
|