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'; class ColoredChar { final String char; final Color color; ColoredChar(this.char, this.color); } class AsciiRasterizer extends Rasterizer { static const String _charset = "@%#*+=-:. "; late List> _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; r = (r * brightnessBoost).toInt().clamp(0, 255); g = (g * brightnessBoost).toInt().clamp(0, 255); b = (b * brightnessBoost).toInt().clamp(0, 255); return Color.fromARGB(255, r, g, b); } // 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); } @override void prepareFrame(WolfEngine engine) { final Color ceilingColor = _vgaToColor(ColorPalette.vga32Bit[25]); final Color floorColor = _vgaToColor(ColorPalette.vga32Bit[29]); 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); } } } } @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, _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), ); } _screen[y][x] = ColoredChar(wallChar, pixelColor); } } @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(viewHeight, drawEndY); y++ ) { double relativeY = (y - drawStartY) / spriteHeight; int texY = (relativeY * 64).toInt().clamp(0, 63); int colorByte = texture.pixels[texX * 64 + texY]; if (colorByte != 255) { _screen[y][stripeX] = ColoredChar( spriteChar, _vgaToColor(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) { _screen[drawY][drawX] = ColoredChar( '@', _vgaToColor(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 _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 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), ); } } } }