Fixed ASCII rasterizer, abstracted out input and audio, and created CLI client (untested)
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -1,353 +0,0 @@
|
||||
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 AsciiTheme {
|
||||
/// The character ramp, ordered from most dense (index 0) to least dense (last index).
|
||||
final String ramp;
|
||||
|
||||
const AsciiTheme(this.ramp);
|
||||
|
||||
/// Always returns the densest character (e.g., for walls, UI, floors)
|
||||
String get solid => ramp[0];
|
||||
|
||||
/// Always returns the completely empty character (e.g., for pitch black darkness)
|
||||
String get empty => ramp[ramp.length - 1];
|
||||
|
||||
/// Returns a character based on a 0.0 to 1.0 brightness scale.
|
||||
/// 1.0 returns the [solid] character, 0.0 returns the [empty] character.
|
||||
String getByBrightness(double brightness) {
|
||||
double b = brightness.clamp(0.0, 1.0);
|
||||
int index = ((1.0 - b) * (ramp.length - 1)).round();
|
||||
return ramp[index];
|
||||
}
|
||||
}
|
||||
|
||||
/// A collection of pre-defined character sets
|
||||
abstract class AsciiThemes {
|
||||
static const AsciiTheme blocks = AsciiTheme("█▓▒░ ");
|
||||
static const AsciiTheme classic = AsciiTheme("@%#*+=-:. ");
|
||||
static const AsciiTheme dense = AsciiTheme("█▇▆▅▄▃▂ ");
|
||||
}
|
||||
|
||||
class ColoredChar {
|
||||
final String char;
|
||||
final Color color;
|
||||
ColoredChar(this.char, this.color);
|
||||
}
|
||||
|
||||
class AsciiRasterizer extends Rasterizer {
|
||||
final AsciiTheme activeTheme = AsciiThemes.blocks;
|
||||
|
||||
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) {
|
||||
int r = vgaColor & 0xFF;
|
||||
int g = (vgaColor >> 8) & 0xFF;
|
||||
int b = (vgaColor >> 16) & 0xFF;
|
||||
|
||||
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) {
|
||||
// Fetch the solid character from the theme
|
||||
_screen[y][x] = ColoredChar(activeTheme.solid, ceilingColor);
|
||||
} else if (y < viewHeight) {
|
||||
// Fetch the solid character from the theme
|
||||
_screen[y][x] = ColoredChar(activeTheme.solid, floorColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void drawWallColumn(
|
||||
int x,
|
||||
int drawStart,
|
||||
int drawEnd,
|
||||
int columnHeight,
|
||||
Sprite texture,
|
||||
int texX,
|
||||
double perpWallDist,
|
||||
int side,
|
||||
) {
|
||||
double brightness = (4.0 / (perpWallDist + 1.0));
|
||||
String wallChar = activeTheme.getByBrightness(brightness);
|
||||
|
||||
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 = (4.0 / (transformY + 1.0)).clamp(0.0, 1.0);
|
||||
String spriteChar = activeTheme.getByBrightness(brightness);
|
||||
|
||||
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(
|
||||
activeTheme.solid,
|
||||
_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<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(
|
||||
activeTheme.solid,
|
||||
_vgaToColor(ColorPalette.vga32Bit[colorByte]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.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_flutter/wolf_3d_input_flutter.dart';
|
||||
import 'package:wolf_3d_input/wolf_3d_input.dart';
|
||||
import 'package:wolf_3d_renderer/ascii_rasterizer.dart';
|
||||
|
||||
class WolfAsciiRenderer extends StatefulWidget {
|
||||
const WolfAsciiRenderer(
|
||||
@@ -25,7 +25,7 @@ class WolfAsciiRenderer extends StatefulWidget {
|
||||
|
||||
class _WolfAsciiRendererState extends State<WolfAsciiRenderer>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final WolfInput inputManager = WolfInput();
|
||||
final Wolf3dInput inputManager = Wolf3dFlutterInput();
|
||||
late final WolfEngine engine;
|
||||
late Ticker _gameLoop;
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
@@ -116,11 +116,11 @@ class AsciiFrameWidget extends StatelessWidget {
|
||||
children: frameData.map((row) {
|
||||
List<TextSpan> optimizedSpans = [];
|
||||
if (row.isNotEmpty) {
|
||||
Color currentColor = row[0].color;
|
||||
Color currentColor = Color(row[0].argb);
|
||||
StringBuffer currentSegment = StringBuffer(row[0].char);
|
||||
|
||||
for (int i = 1; i < row.length; i++) {
|
||||
if (row[i].color == currentColor) {
|
||||
if (Color(row[i].argb) == currentColor) {
|
||||
currentSegment.write(row[i].char);
|
||||
} else {
|
||||
optimizedSpans.add(
|
||||
@@ -129,7 +129,7 @@ class AsciiFrameWidget extends StatelessWidget {
|
||||
style: TextStyle(color: currentColor),
|
||||
),
|
||||
);
|
||||
currentColor = row[i].color;
|
||||
currentColor = Color(row[i].argb);
|
||||
currentSegment = StringBuffer(row[i].char);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.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_flutter/wolf_3d_input_flutter.dart';
|
||||
import 'package:wolf_3d_input/wolf_3d_input.dart';
|
||||
|
||||
class WolfRenderer extends StatefulWidget {
|
||||
@@ -26,7 +27,7 @@ class WolfRenderer extends StatefulWidget {
|
||||
|
||||
class _WolfRendererState extends State<WolfRenderer>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final WolfInput inputManager = WolfInput();
|
||||
final Wolf3dInput inputManager = Wolf3dFlutterInput();
|
||||
late final WolfEngine engine;
|
||||
late Ticker _gameLoop;
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
Reference in New Issue
Block a user