Abstracted more functionality into the base rasterizer

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-16 10:42:09 +01:00
parent 4f790d8fb7
commit f7ca65ab6e
5 changed files with 836 additions and 667 deletions

View File

@@ -3,7 +3,6 @@ import 'dart:math' as math;
import 'package:flutter/material.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_entities/wolf_3d_entities.dart';
class ColoredChar {
final String char;
@@ -11,231 +10,329 @@ class ColoredChar {
ColoredChar(this.char, this.color);
}
class AsciiRasterizer {
class AsciiRasterizer extends Rasterizer {
static const String _charset = "@%#*+=-:. ";
// NEW: Helper to safely convert and artificially boost your raw memory colors
late List<List<ColoredChar>> _screen;
late WolfEngine _engine;
// Terminal characters are usually twice as tall as they are wide.
// We override the base multiplier to squish sprites horizontally.
@override
double get aspectMultiplier => 0.6;
// --- HELPER: Color Conversion ---
Color _vgaToColor(int vgaColor, {double brightnessBoost = 2.0}) {
int r = vgaColor & 0xFF;
int g = (vgaColor >> 8) & 0xFF;
int b = (vgaColor >> 16) & 0xFF;
// Apply the boost and clamp to 255 to prevent color overflow
r = (r * brightnessBoost).toInt().clamp(0, 255);
g = (g * brightnessBoost).toInt().clamp(0, 255);
b = (b * brightnessBoost).toInt().clamp(0, 255);
// Force Alpha to 255 (fully opaque)
return Color.fromARGB(255, r, g, b);
}
List<List<ColoredChar>> render(WolfEngine engine, FrameBuffer framebuffer) {
final int width = framebuffer.width;
final int height = framebuffer.height;
// Intercept the base render call to initialize our text grid
@override
dynamic render(WolfEngine engine, FrameBuffer buffer) {
_engine = engine;
_screen = List.generate(
buffer.height,
(_) => List.filled(buffer.width, ColoredChar(' ', Colors.black)),
);
return super.render(engine, buffer);
}
// Grab ceiling and floor colors from the original palette
@override
void prepareFrame(WolfEngine engine) {
final Color ceilingColor = _vgaToColor(ColorPalette.vga32Bit[25]);
final Color floorColor = _vgaToColor(ColorPalette.vga32Bit[29]);
final List<List<ColoredChar>> screen = List.generate(
height,
(_) => List.filled(width, ColoredChar(' ', ceilingColor)),
);
final List<double> zBuffer = List.filled(width, 0.0);
final Player player = engine.player;
final SpriteMap map = engine.currentLevel;
final List<Sprite> wallTextures = engine.data.walls;
final double fov = math.pi / 3;
Coordinate2D dir = Coordinate2D(
math.cos(player.angle),
math.sin(player.angle),
);
Coordinate2D plane = Coordinate2D(-dir.y, dir.x) * math.tan(fov / 2);
// 1. CAST WALLS
for (int x = 0; x < width; x++) {
double cameraX = 2 * x / width - 1.0;
Coordinate2D rayDir = dir + (plane * cameraX);
int mapX = player.x.toInt();
int mapY = player.y.toInt();
double deltaDistX = (rayDir.x == 0) ? 1e30 : (1.0 / rayDir.x).abs();
double deltaDistY = (rayDir.y == 0) ? 1e30 : (1.0 / rayDir.y).abs();
double sideDistX, sideDistY;
int stepX, stepY, side = 0, hitWallId = 0;
bool hit = false;
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;
}
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) {
break;
}
if (map[mapY][mapX] > 0) {
hit = true;
hitWallId = map[mapY][mapX];
}
}
double perpWallDist = (side == 0)
? (sideDistX - deltaDistX)
: (sideDistY - deltaDistY);
if (perpWallDist < 0.1) perpWallDist = 0.1;
zBuffer[x] = perpWallDist;
double wallX = (side == 0)
? player.y + perpWallDist * rayDir.y
: player.x + perpWallDist * rayDir.x;
wallX -= wallX.floor();
int texX = (wallX * 64).toInt().clamp(0, 63);
int texNum = ((hitWallId - 1) * 2).clamp(0, wallTextures.length - 2);
if (side == 1) texNum += 1;
Sprite texture = wallTextures[texNum];
int columnHeight = (height / perpWallDist).toInt();
int drawStart = (-columnHeight ~/ 2 + height ~/ 2).clamp(0, height);
int drawEnd = (columnHeight ~/ 2 + height ~/ 2).clamp(0, height);
double brightness = (1.5 / (perpWallDist + 1.0)).clamp(0.0, 1.0);
String wallChar =
_charset[((1.0 - brightness) * (_charset.length - 1)).toInt().clamp(
0,
_charset.length - 1,
)];
for (int y = 0; y < height; y++) {
if (y >= drawStart && y < drawEnd) {
double relativeY = (y - drawStart) / (drawEnd - drawStart);
int texY = (relativeY * 64).toInt().clamp(0, 63);
int colorByte = texture.pixels[texX * 64 + texY];
// Use our new color conversion!
Color pixelColor = _vgaToColor(ColorPalette.vga32Bit[colorByte]);
// Optional: slightly darken the Y-side walls for a faux-lighting effect
// if (side == 1) {
// pixelColor = Color.fromARGB(
// 255,
// (pixelColor.r * 0.7).toInt(),
// (pixelColor.g * 0.7).toInt(),
// (pixelColor.b * 0.7).toInt(),
// );
// }
screen[y][x] = ColoredChar(wallChar, pixelColor);
} else if (y >= drawEnd) {
// Floor
screen[y][x] = ColoredChar('.', floorColor);
} else {
// Ceiling
screen[y][x] = ColoredChar(' ', ceilingColor);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
if (y < viewHeight / 2) {
_screen[y][x] = ColoredChar(' ', ceilingColor);
} else if (y < viewHeight) {
_screen[y][x] = ColoredChar('.', floorColor);
}
}
}
}
// 2. CAST SPRITES (Enemies/Items)
final List<Entity> activeSprites = List.from(engine.entities);
activeSprites.sort((a, b) {
double distA = player.position.distanceTo(a.position);
double distB = player.position.distanceTo(b.position);
return distB.compareTo(distA);
});
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 = ((width / 2) * (1 + transformX / transformY))
.toInt();
int spriteHeight = (height / transformY).abs().toInt();
int spriteWidth = (spriteHeight * (width / height) * 0.6).toInt();
int drawStartY = -spriteHeight ~/ 2 + height ~/ 2;
int drawEndY = spriteHeight ~/ 2 + height ~/ 2;
int drawStartX = -spriteWidth ~/ 2 + spriteScreenX;
int drawEndX = spriteWidth ~/ 2 + spriteScreenX;
int clipStartX = math.max(0, drawStartX);
int clipEndX = math.min(width - 1, drawEndX);
int safeIndex = entity.spriteIndex.clamp(
@override
void drawWallColumn(
int x,
int drawStart,
int drawEnd,
int columnHeight,
Sprite texture,
int texX,
double perpWallDist,
int side,
) {
double brightness = (1.5 / (perpWallDist + 1.0)).clamp(0.0, 1.0);
String wallChar =
_charset[((1.0 - brightness) * (_charset.length - 1)).toInt().clamp(
0,
engine.data.sprites.length - 1,
_charset.length - 1,
)];
for (int y = drawStart; y < drawEnd; y++) {
double relativeY =
(y - (-columnHeight ~/ 2 + viewHeight ~/ 2)) / columnHeight;
int texY = (relativeY * 64).toInt().clamp(0, 63);
int colorByte = texture.pixels[texX * 64 + texY];
Color pixelColor = _vgaToColor(ColorPalette.vga32Bit[colorByte]);
// Faux directional lighting
if (side == 1) {
pixelColor = Color.fromARGB(
255,
(pixelColor.r * 0.9).toInt().clamp(0, 255),
(pixelColor.g * 0.9).toInt().clamp(0, 255),
(pixelColor.b * 0.9).toInt().clamp(0, 255),
);
Sprite spritePixels = engine.data.sprites[safeIndex];
}
double brightness = (1.5 / (transformY + 1.0)).clamp(0.0, 1.0);
String spriteChar =
_charset[((1.0 - brightness) * (_charset.length - 1)).toInt().clamp(
0,
_charset.length - 1,
)];
_screen[y][x] = ColoredChar(wallChar, pixelColor);
}
}
for (int stripe = clipStartX; stripe < clipEndX; stripe++) {
if (transformY < zBuffer[stripe]) {
int texX = ((stripe - drawStartX) * 64 ~/ spriteWidth).clamp(0, 63);
@override
void drawSpriteStripe(
int stripeX,
int drawStartY,
int drawEndY,
int spriteHeight,
Sprite texture,
int texX,
double transformY,
) {
double brightness = (1.5 / (transformY + 1.0)).clamp(0.0, 1.0);
String spriteChar =
_charset[((1.0 - brightness) * (_charset.length - 1)).toInt().clamp(
0,
_charset.length - 1,
)];
for (
int y = math.max(0, drawStartY);
y < math.min(height, drawEndY);
y++
) {
double relativeY = (y - drawStartY) / (drawEndY - drawStartY);
int texY = (relativeY * 64).toInt().clamp(0, 63);
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 = spritePixels.pixels[texX * 64 + texY];
int colorByte = texture.pixels[texX * 64 + texY];
if (colorByte != 255) {
_screen[y][stripeX] = ColoredChar(
spriteChar,
_vgaToColor(ColorPalette.vga32Bit[colorByte]),
);
}
}
}
if (colorByte != 255) {
// Apply the safe color conversion here as well
Color pixelColor = _vgaToColor(
ColorPalette.vga32Bit[colorByte],
);
screen[y][stripe] = ColoredChar(spriteChar, pixelColor);
}
}
@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) {
_screen[drawY][drawX] = ColoredChar(
'@',
_vgaToColor(ColorPalette.vga32Bit[colorByte]),
);
}
}
}
}
}
return screen;
@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
_blitVgaImageAscii(engine.data.vgaImages[statusBarIndex], 0, 160);
// 2. Draw Stats
_drawNumberAscii(1, 32, 176, engine.data.vgaImages); // Floor
_drawNumberAscii(
engine.player.score,
96,
176,
engine.data.vgaImages,
); // Score
_drawNumberAscii(3, 120, 176, engine.data.vgaImages); // Lives
_drawNumberAscii(
engine.player.health,
192,
176,
engine.data.vgaImages,
); // Health
_drawNumberAscii(
engine.player.ammo,
232,
176,
engine.data.vgaImages,
); // Ammo
// 3. Draw BJ's Face & Current Weapon
_drawFaceAscii(engine);
_drawWeaponIconAscii(engine);
}
void _drawNumberAscii(
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) {
_blitVgaImageAscii(vgaImages[zeroIndex + digit], currentX, startY);
}
currentX += 8;
}
}
void _drawFaceAscii(WolfEngine engine) {
int health = engine.player.health;
int faceIndex;
if (health <= 0) {
faceIndex = 127;
} else {
int healthTier = ((100 - health) ~/ 16).clamp(0, 6);
faceIndex = 106 + (healthTier * 3);
}
if (faceIndex < engine.data.vgaImages.length) {
_blitVgaImageAscii(engine.data.vgaImages[faceIndex], 136, 164);
}
}
void _drawWeaponIconAscii(WolfEngine engine) {
int weaponIndex = 89;
if (engine.player.hasChainGun) {
weaponIndex = 91;
} else if (engine.player.hasMachineGun) {
weaponIndex = 90;
}
if (weaponIndex < engine.data.vgaImages.length) {
_blitVgaImageAscii(engine.data.vgaImages[weaponIndex], 256, 164);
}
}
@override
dynamic finalizeFrame() {
if (_engine.player.damageFlash > 0.0) {
_applyDamageFlash();
}
return _screen;
}
// --- PRIVATE HUD DRAWING HELPERS ---
void _blitVgaImageAscii(VgaImage image, int startX_320, int startY_200) {
int planeWidth = image.width ~/ 4;
int planeSize = planeWidth * image.height;
double scaleX = width / 320.0;
double scaleY = height / 200.0;
int destStartX = (startX_320 * scaleX).toInt();
int destStartY = (startY_200 * scaleY).toInt();
int destWidth = (image.width * scaleX).toInt();
int destHeight = (image.height * scaleY).toInt();
for (int dy = 0; dy < destHeight; dy++) {
for (int dx = 0; dx < destWidth; dx++) {
int drawX = destStartX + dx;
int drawY = destStartY + dy;
if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < height) {
int srcX = (dx / scaleX).toInt().clamp(0, image.width - 1);
int srcY = (dy / scaleY).toInt().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) {
// Using '█' for UI to make it look solid
_screen[drawY][drawX] = ColoredChar(
'',
_vgaToColor(
ColorPalette.vga32Bit[colorByte],
brightnessBoost: 1.5,
),
);
}
}
}
}
}
// --- DAMAGE FLASH ---
void _applyDamageFlash() {
double intensity = _engine.player.damageFlash;
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++) {
Color c = _screen[y][x].color;
int r = ((c.r * 255).round().clamp(0, 255) + redBoost).clamp(0, 255);
int g = ((c.g * 255).round().clamp(0, 255) * colorDrop).toInt().clamp(
0,
255,
);
int b = ((c.b * 255).round().clamp(0, 255) * colorDrop).toInt().clamp(
0,
255,
);
// Replace the existing character with a red-tinted version
_screen[y][x] = ColoredChar(
_screen[y][x].char,
Color.fromARGB(255, r, g, b),
);
}
}
}
}