@@ -60,6 +60,11 @@ class ColoredChar {
|
||||
}
|
||||
|
||||
class AsciiRasterizer extends Rasterizer {
|
||||
static const double _targetAspectRatio = 4 / 3;
|
||||
static const int _terminalBackdropArgb = 0xFF009688;
|
||||
static const int _simpleHudMinWidth = 84;
|
||||
static const int _simpleHudMinRows = 7;
|
||||
|
||||
AsciiRasterizer({
|
||||
this.activeTheme = AsciiThemes.blocks,
|
||||
this.isTerminal = false,
|
||||
@@ -79,11 +84,26 @@ class AsciiRasterizer extends Rasterizer {
|
||||
@override
|
||||
final double verticalStretch;
|
||||
|
||||
@override
|
||||
int get projectionWidth => isTerminal
|
||||
? math.max(
|
||||
1,
|
||||
math.min(width, (_terminalPixelHeight * _targetAspectRatio).floor()),
|
||||
)
|
||||
: width;
|
||||
|
||||
@override
|
||||
int get projectionOffsetX => isTerminal ? (width - projectionWidth) ~/ 2 : 0;
|
||||
|
||||
@override
|
||||
int get projectionViewHeight => isTerminal ? viewHeight * 2 : viewHeight;
|
||||
|
||||
int get _terminalPixelHeight => isTerminal ? height * 2 : height;
|
||||
|
||||
int get _viewportRightX => projectionOffsetX + projectionWidth;
|
||||
|
||||
int get _terminalBackdropColor => _argbToRawColor(_terminalBackdropArgb);
|
||||
|
||||
// Intercept the base render call to initialize our text grid
|
||||
@override
|
||||
dynamic render(WolfEngine engine, FrameBuffer buffer) {
|
||||
@@ -101,18 +121,20 @@ class AsciiRasterizer extends Rasterizer {
|
||||
// Just grab the raw ints!
|
||||
final int ceilingColor = ColorPalette.vga32Bit[25];
|
||||
final int floorColor = ColorPalette.vga32Bit[29];
|
||||
final int black = ColorPalette.vga32Bit[0];
|
||||
final int backdropColor = isTerminal
|
||||
? _terminalBackdropColor
|
||||
: ColorPalette.vga32Bit[0];
|
||||
|
||||
_scenePixels = List.generate(
|
||||
_terminalPixelHeight,
|
||||
(_) => List.filled(width, black),
|
||||
(_) => List.filled(width, backdropColor),
|
||||
);
|
||||
|
||||
for (int y = 0; y < projectionViewHeight; y++) {
|
||||
final int color = y < projectionViewHeight / 2
|
||||
? ceilingColor
|
||||
: floorColor;
|
||||
for (int x = 0; x < width; x++) {
|
||||
for (int x = projectionOffsetX; x < _viewportRightX; x++) {
|
||||
_scenePixels[y][x] = color;
|
||||
}
|
||||
}
|
||||
@@ -217,10 +239,11 @@ class AsciiRasterizer extends Rasterizer {
|
||||
);
|
||||
Sprite weaponSprite = engine.data.sprites[spriteIndex];
|
||||
|
||||
int weaponWidth = (width * 0.5).toInt();
|
||||
int weaponWidth = (projectionWidth * 0.5).toInt();
|
||||
int weaponHeight = ((projectionViewHeight * 0.8)).toInt();
|
||||
|
||||
int startX = (width ~/ 2) - (weaponWidth ~/ 2);
|
||||
int startX =
|
||||
projectionOffsetX + (projectionWidth ~/ 2) - (weaponWidth ~/ 2);
|
||||
int startY =
|
||||
projectionViewHeight -
|
||||
weaponHeight +
|
||||
@@ -235,7 +258,9 @@ class AsciiRasterizer extends Rasterizer {
|
||||
if (colorByte != 255) {
|
||||
int sceneX = startX + dx;
|
||||
int drawY = startY + dy;
|
||||
if (sceneX >= 0 && sceneX < width && drawY >= 0) {
|
||||
if (sceneX >= projectionOffsetX &&
|
||||
sceneX < _viewportRightX &&
|
||||
drawY >= 0) {
|
||||
if (isTerminal && drawY < projectionViewHeight) {
|
||||
_scenePixels[drawY][sceneX] = ColorPalette.vga32Bit[colorByte];
|
||||
} else if (!isTerminal && drawY < viewHeight) {
|
||||
@@ -272,7 +297,8 @@ class AsciiRasterizer extends Rasterizer {
|
||||
void drawHud(WolfEngine engine) {
|
||||
// If the terminal is at least 160 columns wide and 50 rows tall,
|
||||
// there are enough "pixels" to downscale the VGA image clearly.
|
||||
if (width >= 160 && height >= 50) {
|
||||
int hudWidth = isTerminal ? projectionWidth : width;
|
||||
if (hudWidth >= 160 && height >= 50) {
|
||||
_drawFullVgaHud(engine);
|
||||
} else {
|
||||
_drawSimpleHud(engine);
|
||||
@@ -280,6 +306,13 @@ class AsciiRasterizer extends Rasterizer {
|
||||
}
|
||||
|
||||
void _drawSimpleHud(WolfEngine engine) {
|
||||
final int hudWidth = isTerminal ? projectionWidth : width;
|
||||
final int hudRows = height - viewHeight;
|
||||
if (hudWidth < _simpleHudMinWidth || hudRows < _simpleHudMinRows) {
|
||||
_drawMinimalHud(engine);
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Pull Retro Colors
|
||||
final int vgaStatusBarBlue = ColorPalette.vga32Bit[153];
|
||||
final int vgaPanelDark = ColorPalette.vga32Bit[0];
|
||||
@@ -287,21 +320,46 @@ class AsciiRasterizer extends Rasterizer {
|
||||
final int yellow = ColorPalette.vga32Bit[11];
|
||||
final int red = ColorPalette.vga32Bit[4];
|
||||
|
||||
// 2. Setup Centered Layout
|
||||
// The total width of our standard HUD elements is roughly 120 chars
|
||||
const int hudContentWidth = 120;
|
||||
final int offsetX = ((width - hudContentWidth) ~/ 2).clamp(0, width);
|
||||
// Compact full simple HUD layout.
|
||||
const int floorW = 10;
|
||||
const int scoreW = 14;
|
||||
const int livesW = 9;
|
||||
const int faceW = 10;
|
||||
const int healthW = 12;
|
||||
const int ammoW = 10;
|
||||
const int weaponW = 13;
|
||||
const int gap = 1;
|
||||
const int hudContentWidth =
|
||||
floorW +
|
||||
scoreW +
|
||||
livesW +
|
||||
faceW +
|
||||
healthW +
|
||||
ammoW +
|
||||
weaponW +
|
||||
(gap * 6);
|
||||
|
||||
final int offsetX =
|
||||
projectionOffsetX +
|
||||
((projectionWidth - hudContentWidth) ~/ 2).clamp(0, projectionWidth);
|
||||
final int baseY = viewHeight + 1;
|
||||
|
||||
// 3. Clear HUD Base
|
||||
if (isTerminal) {
|
||||
_fillTerminalRect(
|
||||
0,
|
||||
projectionOffsetX,
|
||||
viewHeight * 2,
|
||||
width,
|
||||
(height - viewHeight) * 2,
|
||||
projectionWidth,
|
||||
hudRows * 2,
|
||||
vgaStatusBarBlue,
|
||||
);
|
||||
_fillTerminalRect(0, viewHeight * 2, width, 1, white);
|
||||
_fillTerminalRect(
|
||||
projectionOffsetX,
|
||||
viewHeight * 2,
|
||||
projectionWidth,
|
||||
1,
|
||||
white,
|
||||
);
|
||||
} else {
|
||||
_fillRect(
|
||||
0,
|
||||
@@ -335,36 +393,35 @@ class AsciiRasterizer extends Rasterizer {
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Draw the Panels
|
||||
// FLOOR
|
||||
drawBorderedPanel(offsetX + 4, viewHeight + 2, 12, 5);
|
||||
_writeString(offsetX + 7, viewHeight + 3, "FLOOR", white, vgaPanelDark);
|
||||
_writeString(
|
||||
offsetX + 9,
|
||||
viewHeight + 5,
|
||||
engine.activeLevel.name.split(' ').last,
|
||||
white,
|
||||
vgaPanelDark,
|
||||
);
|
||||
// 5. Draw compact panels.
|
||||
int cursorX = offsetX;
|
||||
|
||||
// SCORE
|
||||
drawBorderedPanel(offsetX + 18, viewHeight + 2, 24, 5);
|
||||
_writeString(offsetX + 27, viewHeight + 3, "SCORE", white, vgaPanelDark);
|
||||
drawBorderedPanel(cursorX, baseY + 1, floorW, 4);
|
||||
_writeString(cursorX + 2, baseY + 2, "FLR", white, vgaPanelDark);
|
||||
String floorLabel = engine.activeLevel.name.split(' ').last;
|
||||
if (floorLabel.length > 4) {
|
||||
floorLabel = floorLabel.substring(floorLabel.length - 4);
|
||||
}
|
||||
_writeString(cursorX + 2, baseY + 3, floorLabel, white, vgaPanelDark);
|
||||
cursorX += floorW + gap;
|
||||
|
||||
drawBorderedPanel(cursorX, baseY + 1, scoreW, 4);
|
||||
_writeString(cursorX + 4, baseY + 2, "SCORE", white, vgaPanelDark);
|
||||
_writeString(
|
||||
offsetX + 27,
|
||||
viewHeight + 5,
|
||||
cursorX + 4,
|
||||
baseY + 3,
|
||||
engine.player.score.toString().padLeft(6, '0'),
|
||||
white,
|
||||
vgaPanelDark,
|
||||
);
|
||||
cursorX += scoreW + gap;
|
||||
|
||||
// LIVES
|
||||
drawBorderedPanel(offsetX + 44, viewHeight + 2, 12, 5);
|
||||
_writeString(offsetX + 47, viewHeight + 3, "LIVES", white, vgaPanelDark);
|
||||
_writeString(offsetX + 49, viewHeight + 5, "3", white, vgaPanelDark);
|
||||
drawBorderedPanel(cursorX, baseY + 1, livesW, 4);
|
||||
_writeString(cursorX + 2, baseY + 2, "LIV", white, vgaPanelDark);
|
||||
_writeString(cursorX + 3, baseY + 3, "3", white, vgaPanelDark);
|
||||
cursorX += livesW + gap;
|
||||
|
||||
// FACE (With Reactive BJ Logic)
|
||||
drawBorderedPanel(offsetX + 58, viewHeight + 1, 14, 7);
|
||||
drawBorderedPanel(cursorX, baseY, faceW, 5);
|
||||
String face = "ಠ⌣ಠ";
|
||||
if (engine.player.health <= 0) {
|
||||
face = "x⸑x";
|
||||
@@ -375,37 +432,89 @@ class AsciiRasterizer extends Rasterizer {
|
||||
} else if (engine.player.health <= 60) {
|
||||
face = "ಠ~ಠ";
|
||||
}
|
||||
_writeString(offsetX + 63, viewHeight + 4, face, yellow, vgaPanelDark);
|
||||
_writeString(cursorX + 3, baseY + 2, face, yellow, vgaPanelDark);
|
||||
cursorX += faceW + gap;
|
||||
|
||||
// HEALTH
|
||||
int healthColor = engine.player.health > 25 ? white : red;
|
||||
drawBorderedPanel(offsetX + 74, viewHeight + 2, 16, 5);
|
||||
_writeString(offsetX + 78, viewHeight + 3, "HEALTH", white, vgaPanelDark);
|
||||
drawBorderedPanel(cursorX, baseY + 1, healthW, 4);
|
||||
_writeString(cursorX + 2, baseY + 2, "HEALTH", white, vgaPanelDark);
|
||||
_writeString(
|
||||
offsetX + 79,
|
||||
viewHeight + 5,
|
||||
cursorX + 3,
|
||||
baseY + 3,
|
||||
"${engine.player.health}%",
|
||||
healthColor,
|
||||
vgaPanelDark,
|
||||
);
|
||||
cursorX += healthW + gap;
|
||||
|
||||
// AMMO
|
||||
drawBorderedPanel(offsetX + 92, viewHeight + 2, 12, 5);
|
||||
_writeString(offsetX + 95, viewHeight + 3, "AMMO", white, vgaPanelDark);
|
||||
drawBorderedPanel(cursorX, baseY + 1, ammoW, 4);
|
||||
_writeString(cursorX + 2, baseY + 2, "AMMO", white, vgaPanelDark);
|
||||
_writeString(
|
||||
offsetX + 97,
|
||||
viewHeight + 5,
|
||||
cursorX + 2,
|
||||
baseY + 3,
|
||||
"${engine.player.ammo}",
|
||||
white,
|
||||
vgaPanelDark,
|
||||
);
|
||||
cursorX += ammoW + gap;
|
||||
|
||||
// WEAPON
|
||||
drawBorderedPanel(offsetX + 106, viewHeight + 2, 14, 5);
|
||||
drawBorderedPanel(cursorX, baseY + 1, weaponW, 4);
|
||||
String weapon = engine.player.currentWeapon.type.name.spacePascalCase!
|
||||
.toUpperCase();
|
||||
if (weapon.length > 12) weapon = weapon.substring(0, 12);
|
||||
_writeString(offsetX + 107, viewHeight + 4, weapon, white, vgaPanelDark);
|
||||
if (weapon.length > weaponW - 2) {
|
||||
weapon = weapon.substring(0, weaponW - 2);
|
||||
}
|
||||
_writeString(cursorX + 1, baseY + 3, weapon, white, vgaPanelDark);
|
||||
}
|
||||
|
||||
void _drawMinimalHud(WolfEngine engine) {
|
||||
final int vgaStatusBarBlue = ColorPalette.vga32Bit[153];
|
||||
final int white = ColorPalette.vga32Bit[15];
|
||||
final int red = ColorPalette.vga32Bit[4];
|
||||
|
||||
final int hudRows = height - viewHeight;
|
||||
if (isTerminal) {
|
||||
_fillTerminalRect(
|
||||
projectionOffsetX,
|
||||
viewHeight * 2,
|
||||
projectionWidth,
|
||||
hudRows * 2,
|
||||
vgaStatusBarBlue,
|
||||
);
|
||||
_fillTerminalRect(
|
||||
projectionOffsetX,
|
||||
viewHeight * 2,
|
||||
projectionWidth,
|
||||
1,
|
||||
white,
|
||||
);
|
||||
} else {
|
||||
_fillRect(0, viewHeight, width, hudRows, ' ', vgaStatusBarBlue);
|
||||
_writeString(0, viewHeight, "═" * width, white);
|
||||
}
|
||||
|
||||
final int healthColor = engine.player.health > 25 ? white : red;
|
||||
String weapon = engine.player.currentWeapon.type.name.spacePascalCase!
|
||||
.toUpperCase();
|
||||
if (weapon.length > 8) {
|
||||
weapon = weapon.substring(0, 8);
|
||||
}
|
||||
final String hudText =
|
||||
'H:${engine.player.health}% A:${engine.player.ammo} S:${engine.player.score} W:$weapon';
|
||||
|
||||
final int lineY = viewHeight + 1;
|
||||
if (lineY >= height) return;
|
||||
|
||||
final int drawStartX = isTerminal ? projectionOffsetX : 0;
|
||||
final int drawWidth = isTerminal ? projectionWidth : width;
|
||||
final int maxTextLen = math.max(0, drawWidth - 2);
|
||||
String clipped = hudText;
|
||||
if (clipped.length > maxTextLen) {
|
||||
clipped = clipped.substring(0, maxTextLen);
|
||||
}
|
||||
|
||||
final int startX = drawStartX + ((drawWidth - clipped.length) ~/ 2);
|
||||
_writeString(startX, lineY, clipped, healthColor, vgaStatusBarBlue);
|
||||
}
|
||||
|
||||
void _drawFullVgaHud(WolfEngine engine) {
|
||||
@@ -526,11 +635,13 @@ class AsciiRasterizer extends Rasterizer {
|
||||
int planeWidth = image.width ~/ 4;
|
||||
int planeSize = planeWidth * image.height;
|
||||
int maxDrawHeight = isTerminal ? _terminalPixelHeight : height;
|
||||
int maxDrawWidth = isTerminal ? _viewportRightX : width;
|
||||
|
||||
double scaleX = width / 320.0;
|
||||
double scaleX = (isTerminal ? projectionWidth : width) / 320.0;
|
||||
double scaleY = (isTerminal ? _terminalPixelHeight : height) / 200.0;
|
||||
|
||||
int destStartX = (startX_320 * scaleX).toInt();
|
||||
int destStartX =
|
||||
(isTerminal ? projectionOffsetX : 0) + (startX_320 * scaleX).toInt();
|
||||
int destStartY = (startY_200 * scaleY).toInt();
|
||||
int destWidth = (image.width * scaleX).toInt();
|
||||
int destHeight = (image.height * scaleY).toInt();
|
||||
@@ -541,7 +652,7 @@ class AsciiRasterizer extends Rasterizer {
|
||||
int drawY = destStartY + dy;
|
||||
|
||||
if (drawX >= 0 &&
|
||||
drawX < width &&
|
||||
drawX < maxDrawWidth &&
|
||||
drawY >= 0 &&
|
||||
drawY < maxDrawHeight) {
|
||||
int srcX = (dx / scaleX).toInt().clamp(0, image.width - 1);
|
||||
@@ -597,12 +708,19 @@ class AsciiRasterizer extends Rasterizer {
|
||||
|
||||
void _applyDamageFlashToScene() {
|
||||
for (int y = 0; y < _terminalPixelHeight; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
for (int x = projectionOffsetX; x < _viewportRightX; x++) {
|
||||
_scenePixels[y][x] = _applyDamageFlashToColor(_scenePixels[y][x]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int _argbToRawColor(int argb) {
|
||||
int r = (argb >> 16) & 0xFF;
|
||||
int g = (argb >> 8) & 0xFF;
|
||||
int b = argb & 0xFF;
|
||||
return (0xFF000000) | (b << 16) | (g << 8) | r;
|
||||
}
|
||||
|
||||
int _applyDamageFlashToColor(int color) {
|
||||
double intensity = _engine.player.damageFlash;
|
||||
int redBoost = (150 * intensity).toInt();
|
||||
|
||||
@@ -19,6 +19,13 @@ abstract class Rasterizer {
|
||||
/// Defaults to 1.0 (no squish) for standard pixel rendering.
|
||||
double get verticalStretch => 1.0;
|
||||
|
||||
/// The logical width of the projection area used for raycasting and sprites.
|
||||
/// Most renderers use the full buffer width.
|
||||
int get projectionWidth => width;
|
||||
|
||||
/// Horizontal offset of the projection area within the output buffer.
|
||||
int get projectionOffsetX => 0;
|
||||
|
||||
/// The logical height of the 3D projection before a renderer maps rows to output pixels.
|
||||
/// Most renderers use the visible view height. Terminal ASCII can override this to render
|
||||
/// more vertical detail and collapse it into half-block glyphs.
|
||||
@@ -31,7 +38,7 @@ abstract class Rasterizer {
|
||||
height = buffer.height;
|
||||
// The 3D view typically takes up the top 80% of the screen
|
||||
viewHeight = (height * 0.8).toInt();
|
||||
zBuffer = List.filled(width, 0.0);
|
||||
zBuffer = List.filled(projectionWidth, 0.0);
|
||||
|
||||
// 1. Setup the frame (clear screen, draw floor/ceiling)
|
||||
prepareFrame(engine);
|
||||
@@ -108,6 +115,7 @@ abstract class Rasterizer {
|
||||
final Player player = engine.player;
|
||||
final SpriteMap map = engine.currentLevel;
|
||||
final List<Sprite> wallTextures = engine.data.walls;
|
||||
final int sceneWidth = projectionWidth;
|
||||
final int sceneHeight = projectionViewHeight;
|
||||
|
||||
final Map<String, double> doorOffsets = engine.doorManager
|
||||
@@ -121,8 +129,8 @@ abstract class Rasterizer {
|
||||
);
|
||||
Coordinate2D plane = Coordinate2D(-dir.y, dir.x) * math.tan(fov / 2);
|
||||
|
||||
for (int x = 0; x < width; x++) {
|
||||
double cameraX = 2 * x / width - 1.0;
|
||||
for (int x = 0; x < sceneWidth; x++) {
|
||||
double cameraX = 2 * x / sceneWidth - 1.0;
|
||||
Coordinate2D rayDir = dir + (plane * cameraX);
|
||||
|
||||
int mapX = player.x.toInt();
|
||||
@@ -302,7 +310,7 @@ abstract class Rasterizer {
|
||||
|
||||
// Tell the implementation to draw this column
|
||||
drawWallColumn(
|
||||
x,
|
||||
projectionOffsetX + x,
|
||||
drawStart,
|
||||
drawEnd,
|
||||
columnHeight,
|
||||
@@ -317,6 +325,7 @@ abstract class Rasterizer {
|
||||
void _castSprites(WolfEngine engine) {
|
||||
final Player player = engine.player;
|
||||
final List<Entity> activeSprites = List.from(engine.entities);
|
||||
final int sceneWidth = projectionWidth;
|
||||
final int sceneHeight = projectionViewHeight;
|
||||
|
||||
// Sort from furthest to closest (Painter's Algorithm)
|
||||
@@ -343,7 +352,7 @@ abstract class Rasterizer {
|
||||
|
||||
// Only process if the sprite is in front of the camera
|
||||
if (transformY > 0) {
|
||||
int spriteScreenX = ((width / 2) * (1 + transformX / transformY))
|
||||
int spriteScreenX = ((sceneWidth / 2) * (1 + transformX / transformY))
|
||||
.toInt();
|
||||
int spriteHeight = ((sceneHeight / transformY).abs() * verticalStretch)
|
||||
.toInt();
|
||||
@@ -361,7 +370,7 @@ abstract class Rasterizer {
|
||||
int drawEndX = spriteWidth ~/ 2 + spriteScreenX;
|
||||
|
||||
int clipStartX = math.max(0, drawStartX);
|
||||
int clipEndX = math.min(width, drawEndX);
|
||||
int clipEndX = math.min(sceneWidth, drawEndX);
|
||||
|
||||
int safeIndex = entity.spriteIndex.clamp(
|
||||
0,
|
||||
@@ -377,7 +386,7 @@ abstract class Rasterizer {
|
||||
|
||||
// Tell the implementation to draw this stripe
|
||||
drawSpriteStripe(
|
||||
stripe,
|
||||
projectionOffsetX + stripe,
|
||||
drawStartY,
|
||||
drawEndY,
|
||||
spriteHeight,
|
||||
|
||||
Reference in New Issue
Block a user