Fixes pushwalls and a bunch of ASCII/sixel rasterizer issues
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:wolf_3d_cli/cli_game_loop.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
@@ -15,13 +15,7 @@ void exitCleanly(int code) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
stdin.echoMode = false;
|
stdout.write("Discovering game data...");
|
||||||
stdin.lineMode = false;
|
|
||||||
|
|
||||||
// HIDE the blinking cursor and clear the screen to prep the canvas
|
|
||||||
stdout.write('\x1b[?25l\x1b[2J');
|
|
||||||
|
|
||||||
print("Discovering game data...");
|
|
||||||
// 1. Get the absolute URI of where this exact script lives
|
// 1. Get the absolute URI of where this exact script lives
|
||||||
final scriptUri = Platform.script;
|
final scriptUri = Platform.script;
|
||||||
|
|
||||||
@@ -36,76 +30,31 @@ void main() async {
|
|||||||
recursive: true,
|
recursive: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
final AsciiRasterizer asciiRasterizer = AsciiRasterizer(isTerminal: true);
|
CliGameLoop? gameLoop;
|
||||||
final SixelRasterizer sixelRasterizer = SixelRasterizer();
|
|
||||||
CliRasterizer rasterizer = sixelRasterizer;
|
|
||||||
|
|
||||||
final FrameBuffer initialFrameBuffer = FrameBuffer(
|
void stopAndExit(int code) {
|
||||||
stdout.terminalColumns,
|
gameLoop?.stop();
|
||||||
stdout.terminalLines,
|
exitCleanly(code);
|
||||||
);
|
}
|
||||||
|
|
||||||
final engine = WolfEngine(
|
final engine = WolfEngine(
|
||||||
data: availableGames.values.first,
|
data: availableGames.values.first,
|
||||||
difficulty: Difficulty.medium,
|
difficulty: Difficulty.medium,
|
||||||
startingEpisode: 0,
|
startingEpisode: 0,
|
||||||
frameBuffer: initialFrameBuffer,
|
frameBuffer: FrameBuffer(
|
||||||
audio: CliSilentAudio(),
|
stdout.terminalColumns,
|
||||||
|
stdout.terminalLines,
|
||||||
|
),
|
||||||
input: CliInput(),
|
input: CliInput(),
|
||||||
onGameWon: () {
|
onGameWon: () => stopAndExit(0),
|
||||||
exitCleanly(0);
|
|
||||||
print("YOU WON!");
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
engine.init();
|
engine.init();
|
||||||
|
|
||||||
stdin.listen((List<int> bytes) {
|
gameLoop = CliGameLoop(
|
||||||
if (bytes.contains(113) || bytes.contains(27)) {
|
engine: engine,
|
||||||
exitCleanly(0);
|
input: engine.input as CliInput,
|
||||||
}
|
onExit: stopAndExit,
|
||||||
|
|
||||||
if (bytes.contains(9)) {
|
|
||||||
rasterizer = identical(rasterizer, sixelRasterizer)
|
|
||||||
? asciiRasterizer
|
|
||||||
: sixelRasterizer;
|
|
||||||
stdout.write('\x1b[2J\x1b[H');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
(engine.input as CliInput).handleKey(bytes);
|
|
||||||
});
|
|
||||||
|
|
||||||
Stopwatch stopwatch = Stopwatch()..start();
|
|
||||||
Duration lastTick = Duration.zero;
|
|
||||||
|
|
||||||
Timer.periodic(const Duration(milliseconds: 33), (timer) {
|
|
||||||
// 1. Terminal Size Safety Check
|
|
||||||
if (stdout.hasTerminal) {
|
|
||||||
int cols = stdout.terminalColumns;
|
|
||||||
int rows = stdout.terminalLines;
|
|
||||||
if (!rasterizer.prepareTerminalFrame(engine, columns: cols, rows: rows)) {
|
|
||||||
// Clear the screen and print the warning at the top left
|
|
||||||
stdout.write('\x1b[2J\x1b[H');
|
|
||||||
stdout.write(
|
|
||||||
rasterizer.buildTerminalSizeWarning(columns: cols, rows: rows),
|
|
||||||
);
|
);
|
||||||
|
gameLoop.start();
|
||||||
// Prevent the engine from simulating a massive time jump when resumed
|
|
||||||
lastTick = stopwatch.elapsed;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Normal Game Loop
|
|
||||||
Duration currentTick = stopwatch.elapsed;
|
|
||||||
Duration elapsed = currentTick - lastTick;
|
|
||||||
lastTick = currentTick;
|
|
||||||
|
|
||||||
// Move cursor to top-left (0,0) before drawing the frame
|
|
||||||
stdout.write('\x1b[H');
|
|
||||||
|
|
||||||
engine.tick(elapsed);
|
|
||||||
stdout.write(rasterizer.render(engine));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
123
apps/wolf_3d_cli/lib/cli_game_loop.dart
Normal file
123
apps/wolf_3d_cli/lib/cli_game_loop.dart
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_input.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_rasterizer.dart';
|
||||||
|
|
||||||
|
class CliGameLoop {
|
||||||
|
CliGameLoop({
|
||||||
|
required this.engine,
|
||||||
|
required this.input,
|
||||||
|
required this.onExit,
|
||||||
|
}) : primaryRasterizer = SixelRasterizer(),
|
||||||
|
secondaryRasterizer = AsciiRasterizer(isTerminal: true) {
|
||||||
|
_rasterizer = primaryRasterizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
final WolfEngine engine;
|
||||||
|
final CliRasterizer primaryRasterizer;
|
||||||
|
final CliRasterizer secondaryRasterizer;
|
||||||
|
final CliInput input;
|
||||||
|
final void Function(int code) onExit;
|
||||||
|
|
||||||
|
final Stopwatch _stopwatch = Stopwatch();
|
||||||
|
late CliRasterizer _rasterizer;
|
||||||
|
StreamSubscription<List<int>>? _stdinSubscription;
|
||||||
|
Timer? _timer;
|
||||||
|
bool _isRunning = false;
|
||||||
|
Duration _lastTick = Duration.zero;
|
||||||
|
|
||||||
|
void start() {
|
||||||
|
if (_isRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stdin.echoMode = false;
|
||||||
|
stdin.lineMode = false;
|
||||||
|
|
||||||
|
stdout.write('\x1b[?25l\x1b[2J');
|
||||||
|
|
||||||
|
_stdinSubscription = stdin.listen(_handleInput);
|
||||||
|
_stopwatch.start();
|
||||||
|
_timer = Timer.periodic(const Duration(milliseconds: 33), _tick);
|
||||||
|
_isRunning = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop() {
|
||||||
|
if (!_isRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = null;
|
||||||
|
_stdinSubscription?.cancel();
|
||||||
|
_stdinSubscription = null;
|
||||||
|
|
||||||
|
if (_stopwatch.isRunning) {
|
||||||
|
_stopwatch.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stdin.hasTerminal) {
|
||||||
|
stdin.echoMode = true;
|
||||||
|
stdin.lineMode = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stdout.hasTerminal) {
|
||||||
|
stdout.write('\x1b[0m\x1b[?25h');
|
||||||
|
}
|
||||||
|
|
||||||
|
_isRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleInput(List<int> bytes) {
|
||||||
|
if (bytes.contains(113) || bytes.contains(27)) {
|
||||||
|
stop();
|
||||||
|
onExit(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes.contains(9)) {
|
||||||
|
_rasterizer = identical(_rasterizer, secondaryRasterizer)
|
||||||
|
? primaryRasterizer
|
||||||
|
: secondaryRasterizer;
|
||||||
|
stdout.write('\x1b[2J\x1b[H');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.handleKey(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _tick(Timer timer) {
|
||||||
|
if (!_isRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stdout.hasTerminal) {
|
||||||
|
final int cols = stdout.terminalColumns;
|
||||||
|
final int rows = stdout.terminalLines;
|
||||||
|
if (!_rasterizer.prepareTerminalFrame(
|
||||||
|
engine,
|
||||||
|
columns: cols,
|
||||||
|
rows: rows,
|
||||||
|
)) {
|
||||||
|
stdout.write('\x1b[2J\x1b[H');
|
||||||
|
stdout.write(
|
||||||
|
_rasterizer.buildTerminalSizeWarning(columns: cols, rows: rows),
|
||||||
|
);
|
||||||
|
|
||||||
|
_lastTick = _stopwatch.elapsed;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final Duration currentTick = _stopwatch.elapsed;
|
||||||
|
final Duration elapsed = currentTick - _lastTick;
|
||||||
|
_lastTick = currentTick;
|
||||||
|
|
||||||
|
stdout.write('\x1b[H');
|
||||||
|
|
||||||
|
engine.tick(elapsed);
|
||||||
|
stdout.write(_rasterizer.render(engine));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:arcane_helper_utils/arcane_helper_utils.dart';
|
import 'package:arcane_helper_utils/arcane_helper_utils.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/rasterizer/cli_rasterizer.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:wolf_3d_dart/src/rasterizer/cli_rasterizer.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:wolf_3d_dart/src/rasterizer/rasterizer.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -16,11 +16,12 @@ class WolfEngine {
|
|||||||
required this.difficulty,
|
required this.difficulty,
|
||||||
required this.startingEpisode,
|
required this.startingEpisode,
|
||||||
required this.onGameWon,
|
required this.onGameWon,
|
||||||
required this.audio,
|
|
||||||
required this.input,
|
required this.input,
|
||||||
required this.frameBuffer,
|
required this.frameBuffer,
|
||||||
}) : doorManager = DoorManager(
|
EngineAudio? audio,
|
||||||
onPlaySound: (sfxId) => audio.playSoundEffect(sfxId),
|
}) : audio = audio ?? CliSilentAudio(),
|
||||||
|
doorManager = DoorManager(
|
||||||
|
onPlaySound: (sfxId) => audio?.playSoundEffect(sfxId),
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Total milliseconds elapsed since the engine was initialized.
|
/// Total milliseconds elapsed since the engine was initialized.
|
||||||
@@ -36,7 +37,7 @@ class WolfEngine {
|
|||||||
final int startingEpisode;
|
final int startingEpisode;
|
||||||
|
|
||||||
/// Handles music and sound effect playback.
|
/// Handles music and sound effect playback.
|
||||||
final EngineAudio audio;
|
late final EngineAudio audio;
|
||||||
|
|
||||||
/// Callback triggered when the final level of an episode is completed.
|
/// Callback triggered when the final level of an episode is completed.
|
||||||
final void Function() onGameWon;
|
final void Function() onGameWon;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
|
||||||
import 'package:wolf_3d_dart/src/input/wolf_3d_input.dart';
|
import 'package:wolf_3d_dart/src/input/wolf_3d_input.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
||||||
|
|
||||||
class CliInput extends Wolf3dInput {
|
class CliInput extends Wolf3dInput {
|
||||||
// Pending buffer for asynchronous stdin events
|
// Pending buffer for asynchronous stdin events
|
||||||
|
|||||||
838
packages/wolf_3d_dart/lib/src/rasterizer/ascii_rasterizer.dart
Normal file
838
packages/wolf_3d_dart/lib/src/rasterizer/ascii_rasterizer.dart
Normal file
@@ -0,0 +1,838 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:arcane_helper_utils/arcane_helper_utils.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
|
|
||||||
|
import 'cli_rasterizer.dart';
|
||||||
|
|
||||||
|
class AsciiTheme {
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
/// The character ramp, ordered from most dense (index 0) to least dense (last index).
|
||||||
|
final String ramp;
|
||||||
|
|
||||||
|
const AsciiTheme(this.name, 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('Blocks', "█▓▒░ ");
|
||||||
|
static const AsciiTheme classic = AsciiTheme('Classic', "@%#*+=-:. ");
|
||||||
|
|
||||||
|
static const List<AsciiTheme> values = [blocks, classic];
|
||||||
|
|
||||||
|
static AsciiTheme nextOf(AsciiTheme current) {
|
||||||
|
final int currentIndex = values.indexOf(current);
|
||||||
|
final int nextIndex = currentIndex == -1
|
||||||
|
? 0
|
||||||
|
: (currentIndex + 1) % values.length;
|
||||||
|
return values[nextIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ColoredChar {
|
||||||
|
final String char;
|
||||||
|
final int rawColor; // Stores the AABBGGRR integer from the palette
|
||||||
|
final int? rawBackgroundColor;
|
||||||
|
|
||||||
|
ColoredChar(this.char, this.rawColor, [this.rawBackgroundColor]);
|
||||||
|
|
||||||
|
// Safely extract the exact RGB channels regardless of framework
|
||||||
|
int get r => rawColor & 0xFF;
|
||||||
|
int get g => (rawColor >> 8) & 0xFF;
|
||||||
|
int get b => (rawColor >> 16) & 0xFF;
|
||||||
|
|
||||||
|
// Outputs standard AARRGGBB for Flutter's Color(int) constructor
|
||||||
|
int get argb => (0xFF000000) | (r << 16) | (g << 8) | b;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AsciiRasterizer extends CliRasterizer<dynamic> {
|
||||||
|
static const double _targetAspectRatio = 4 / 3;
|
||||||
|
static const int _terminalBackdropArgb = 0xFF009688;
|
||||||
|
static const int _minimumTerminalColumns = 80;
|
||||||
|
static const int _minimumTerminalRows = 24;
|
||||||
|
static const int _simpleHudMinWidth = 84;
|
||||||
|
static const int _simpleHudMinRows = 7;
|
||||||
|
|
||||||
|
AsciiRasterizer({
|
||||||
|
this.activeTheme = AsciiThemes.blocks,
|
||||||
|
this.isTerminal = false,
|
||||||
|
this.aspectMultiplier = 1.0,
|
||||||
|
this.verticalStretch = 1.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
AsciiTheme activeTheme = AsciiThemes.blocks;
|
||||||
|
final bool isTerminal;
|
||||||
|
|
||||||
|
late List<List<ColoredChar>> _screen;
|
||||||
|
late List<List<int>> _scenePixels;
|
||||||
|
late WolfEngine _engine;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final double aspectMultiplier;
|
||||||
|
@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;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool isTerminalSizeSupported(int columns, int rows) {
|
||||||
|
if (!isTerminal) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return columns >= _minimumTerminalColumns && rows >= _minimumTerminalRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get terminalSizeRequirement =>
|
||||||
|
'ASCII renderer requires a minimum resolution of '
|
||||||
|
'${_minimumTerminalColumns}x$_minimumTerminalRows.';
|
||||||
|
|
||||||
|
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) {
|
||||||
|
_engine = engine;
|
||||||
|
_screen = List.generate(
|
||||||
|
engine.frameBuffer.height,
|
||||||
|
(_) => List.filled(
|
||||||
|
engine.frameBuffer.width,
|
||||||
|
ColoredChar(' ', ColorPalette.vga32Bit[0]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return super.render(engine);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void prepareFrame(WolfEngine engine) {
|
||||||
|
// Just grab the raw ints!
|
||||||
|
final int ceilingColor = ColorPalette.vga32Bit[25];
|
||||||
|
final int floorColor = ColorPalette.vga32Bit[29];
|
||||||
|
final int backdropColor = isTerminal
|
||||||
|
? _terminalBackdropColor
|
||||||
|
: ColorPalette.vga32Bit[0];
|
||||||
|
|
||||||
|
_scenePixels = List.generate(
|
||||||
|
_terminalPixelHeight,
|
||||||
|
(_) => List.filled(width, backdropColor),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (int y = 0; y < projectionViewHeight; y++) {
|
||||||
|
final int color = y < projectionViewHeight / 2
|
||||||
|
? ceilingColor
|
||||||
|
: floorColor;
|
||||||
|
for (int x = projectionOffsetX; x < _viewportRightX; x++) {
|
||||||
|
_scenePixels[y][x] = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isTerminal) {
|
||||||
|
for (int y = 0; y < height; y++) {
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
if (y < viewHeight / 2) {
|
||||||
|
_screen[y][x] = ColoredChar(activeTheme.solid, ceilingColor);
|
||||||
|
} else if (y < viewHeight) {
|
||||||
|
_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 = calculateDepthBrightness(perpWallDist);
|
||||||
|
|
||||||
|
for (int y = drawStart; y < drawEnd; y++) {
|
||||||
|
double relativeY =
|
||||||
|
(y - (-columnHeight ~/ 2 + projectionViewHeight ~/ 2)) / columnHeight;
|
||||||
|
int texY = (relativeY * 64).toInt().clamp(0, 63);
|
||||||
|
|
||||||
|
int colorByte = texture.pixels[texX * 64 + texY];
|
||||||
|
int pixelColor = ColorPalette.vga32Bit[colorByte]; // Raw int
|
||||||
|
|
||||||
|
// Faux directional lighting using your new base class method
|
||||||
|
if (side == 1) {
|
||||||
|
pixelColor = shadeColor(pixelColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTerminal) {
|
||||||
|
_scenePixels[y][x] = _scaleColor(pixelColor, brightness);
|
||||||
|
} else {
|
||||||
|
String wallChar = activeTheme.getByBrightness(brightness);
|
||||||
|
_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 = calculateDepthBrightness(transformY);
|
||||||
|
|
||||||
|
for (
|
||||||
|
int y = math.max(0, drawStartY);
|
||||||
|
y < math.min(projectionViewHeight, 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) {
|
||||||
|
int rawColor = ColorPalette.vga32Bit[colorByte];
|
||||||
|
|
||||||
|
// Shade the sprite's actual RGB color based on distance
|
||||||
|
int r = (rawColor & 0xFF);
|
||||||
|
int g = ((rawColor >> 8) & 0xFF);
|
||||||
|
int b = ((rawColor >> 16) & 0xFF);
|
||||||
|
|
||||||
|
r = (r * brightness).toInt();
|
||||||
|
g = (g * brightness).toInt();
|
||||||
|
b = (b * brightness).toInt();
|
||||||
|
|
||||||
|
int shadedColor = (0xFF000000) | (b << 16) | (g << 8) | r;
|
||||||
|
|
||||||
|
if (isTerminal) {
|
||||||
|
_scenePixels[y][stripeX] = shadedColor;
|
||||||
|
} else {
|
||||||
|
// Force sprites to be SOLID so they don't vanish into the terminal background
|
||||||
|
_screen[y][stripeX] = ColoredChar(activeTheme.solid, shadedColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void drawWeapon(WolfEngine engine) {
|
||||||
|
int spriteIndex = engine.player.currentWeapon.getCurrentSpriteIndex(
|
||||||
|
engine.data.sprites.length,
|
||||||
|
);
|
||||||
|
Sprite weaponSprite = engine.data.sprites[spriteIndex];
|
||||||
|
|
||||||
|
int weaponWidth = (projectionWidth * 0.5).toInt();
|
||||||
|
int weaponHeight = ((projectionViewHeight * 0.8)).toInt();
|
||||||
|
|
||||||
|
int startX =
|
||||||
|
projectionOffsetX + (projectionWidth ~/ 2) - (weaponWidth ~/ 2);
|
||||||
|
int startY =
|
||||||
|
projectionViewHeight -
|
||||||
|
weaponHeight +
|
||||||
|
(engine.player.weaponAnimOffset * (isTerminal ? 2 : 1) ~/ 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 sceneX = startX + dx;
|
||||||
|
int drawY = startY + dy;
|
||||||
|
if (sceneX >= projectionOffsetX &&
|
||||||
|
sceneX < _viewportRightX &&
|
||||||
|
drawY >= 0) {
|
||||||
|
if (isTerminal && drawY < projectionViewHeight) {
|
||||||
|
_scenePixels[drawY][sceneX] = ColorPalette.vga32Bit[colorByte];
|
||||||
|
} else if (!isTerminal && drawY < viewHeight) {
|
||||||
|
_screen[drawY][sceneX] = ColoredChar(
|
||||||
|
activeTheme.solid,
|
||||||
|
ColorPalette.vga32Bit[colorByte],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- PRIVATE HUD DRAWING HELPER ---
|
||||||
|
|
||||||
|
/// Injects a pure text string directly into the rasterizer grid
|
||||||
|
void _writeString(
|
||||||
|
int startX,
|
||||||
|
int y,
|
||||||
|
String text,
|
||||||
|
int color, [
|
||||||
|
int? backgroundColor,
|
||||||
|
]) {
|
||||||
|
for (int i = 0; i < text.length; i++) {
|
||||||
|
int x = startX + i;
|
||||||
|
if (x >= 0 && x < width && y >= 0 && y < height) {
|
||||||
|
_screen[y][x] = ColoredChar(text[i], color, backgroundColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
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.
|
||||||
|
int hudWidth = isTerminal ? projectionWidth : width;
|
||||||
|
if (hudWidth >= 160 && height >= 50) {
|
||||||
|
_drawFullVgaHud(engine);
|
||||||
|
} else {
|
||||||
|
_drawSimpleHud(engine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
|
final int white = ColorPalette.vga32Bit[15];
|
||||||
|
final int yellow = ColorPalette.vga32Bit[11];
|
||||||
|
final int red = ColorPalette.vga32Bit[4];
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
projectionOffsetX,
|
||||||
|
viewHeight * 2,
|
||||||
|
projectionWidth,
|
||||||
|
hudRows * 2,
|
||||||
|
vgaStatusBarBlue,
|
||||||
|
);
|
||||||
|
_fillTerminalRect(
|
||||||
|
projectionOffsetX,
|
||||||
|
viewHeight * 2,
|
||||||
|
projectionWidth,
|
||||||
|
1,
|
||||||
|
white,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_fillRect(
|
||||||
|
0,
|
||||||
|
viewHeight,
|
||||||
|
width,
|
||||||
|
height - viewHeight,
|
||||||
|
' ',
|
||||||
|
vgaStatusBarBlue,
|
||||||
|
);
|
||||||
|
_writeString(0, viewHeight, "═" * width, white);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Panel Drawing Helper
|
||||||
|
void drawBorderedPanel(int startX, int startY, int w, int h) {
|
||||||
|
if (isTerminal) {
|
||||||
|
_fillTerminalRect(startX, startY * 2, w, h * 2, vgaPanelDark);
|
||||||
|
_fillTerminalRect(startX, startY * 2, w, 1, white);
|
||||||
|
_fillTerminalRect(startX, (startY + h) * 2 - 1, w, 1, white);
|
||||||
|
_fillTerminalRect(startX, startY * 2, 1, h * 2, white);
|
||||||
|
_fillTerminalRect(startX + w - 1, startY * 2, 1, h * 2, white);
|
||||||
|
} else {
|
||||||
|
_fillRect(startX, startY, w, h, ' ', vgaPanelDark);
|
||||||
|
// Horizontal lines
|
||||||
|
_writeString(startX, startY, "┌${"─" * (w - 2)}┐", white);
|
||||||
|
_writeString(startX, startY + h - 1, "└${"─" * (w - 2)}┘", white);
|
||||||
|
// Vertical sides
|
||||||
|
for (int i = 1; i < h - 1; i++) {
|
||||||
|
_writeString(startX, startY + i, "│", white);
|
||||||
|
_writeString(startX + w - 1, startY + i, "│", white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Draw compact panels.
|
||||||
|
int cursorX = offsetX;
|
||||||
|
|
||||||
|
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(
|
||||||
|
cursorX + 4,
|
||||||
|
baseY + 3,
|
||||||
|
engine.player.score.toString().padLeft(6, '0'),
|
||||||
|
white,
|
||||||
|
vgaPanelDark,
|
||||||
|
);
|
||||||
|
cursorX += scoreW + gap;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
drawBorderedPanel(cursorX, baseY, faceW, 5);
|
||||||
|
String face = "ಠ⌣ಠ";
|
||||||
|
if (engine.player.health <= 0) {
|
||||||
|
face = "x⸑x";
|
||||||
|
} else if (engine.player.damageFlash > 0.1) {
|
||||||
|
face = "ಠoಠ";
|
||||||
|
} else if (engine.player.health <= 25) {
|
||||||
|
face = "ಥ_ಥ";
|
||||||
|
} else if (engine.player.health <= 60) {
|
||||||
|
face = "ಠ~ಠ";
|
||||||
|
}
|
||||||
|
_writeString(cursorX + 3, baseY + 2, face, yellow, vgaPanelDark);
|
||||||
|
cursorX += faceW + gap;
|
||||||
|
|
||||||
|
int healthColor = engine.player.health > 25 ? white : red;
|
||||||
|
drawBorderedPanel(cursorX, baseY + 1, healthW, 4);
|
||||||
|
_writeString(cursorX + 2, baseY + 2, "HEALTH", white, vgaPanelDark);
|
||||||
|
_writeString(
|
||||||
|
cursorX + 3,
|
||||||
|
baseY + 3,
|
||||||
|
"${engine.player.health}%",
|
||||||
|
healthColor,
|
||||||
|
vgaPanelDark,
|
||||||
|
);
|
||||||
|
cursorX += healthW + gap;
|
||||||
|
|
||||||
|
drawBorderedPanel(cursorX, baseY + 1, ammoW, 4);
|
||||||
|
_writeString(cursorX + 2, baseY + 2, "AMMO", white, vgaPanelDark);
|
||||||
|
_writeString(
|
||||||
|
cursorX + 2,
|
||||||
|
baseY + 3,
|
||||||
|
"${engine.player.ammo}",
|
||||||
|
white,
|
||||||
|
vgaPanelDark,
|
||||||
|
);
|
||||||
|
cursorX += ammoW + gap;
|
||||||
|
|
||||||
|
drawBorderedPanel(cursorX, baseY + 1, weaponW, 4);
|
||||||
|
String weapon = engine.player.currentWeapon.type.name.spacePascalCase!
|
||||||
|
.toUpperCase();
|
||||||
|
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) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to fill a rectangular area with a specific char and background color
|
||||||
|
void _fillRect(int startX, int startY, int w, int h, String char, int color) {
|
||||||
|
for (int dy = 0; dy < h; dy++) {
|
||||||
|
for (int dx = 0; dx < w; dx++) {
|
||||||
|
int x = startX + dx;
|
||||||
|
int y = startY + dy;
|
||||||
|
if (x >= 0 && x < width && y >= 0 && y < height) {
|
||||||
|
_screen[y][x] = ColoredChar(char, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic finalizeFrame() {
|
||||||
|
if (_engine.player.damageFlash > 0.0) {
|
||||||
|
if (isTerminal) {
|
||||||
|
_applyDamageFlashToScene();
|
||||||
|
} else {
|
||||||
|
_applyDamageFlash();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isTerminal) {
|
||||||
|
_composeTerminalScene();
|
||||||
|
return toAnsiString();
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
int maxDrawHeight = isTerminal ? _terminalPixelHeight : height;
|
||||||
|
int maxDrawWidth = isTerminal ? _viewportRightX : width;
|
||||||
|
|
||||||
|
double scaleX = (isTerminal ? projectionWidth : width) / 320.0;
|
||||||
|
double scaleY = (isTerminal ? _terminalPixelHeight : height) / 200.0;
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
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 < maxDrawWidth &&
|
||||||
|
drawY >= 0 &&
|
||||||
|
drawY < maxDrawHeight) {
|
||||||
|
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) {
|
||||||
|
if (isTerminal) {
|
||||||
|
_scenePixels[drawY][drawX] = ColorPalette.vga32Bit[colorByte];
|
||||||
|
} else {
|
||||||
|
_screen[drawY][drawX] = ColoredChar(
|
||||||
|
activeTheme.solid,
|
||||||
|
ColorPalette.vga32Bit[colorByte],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _fillTerminalRect(int startX, int startY, int w, int h, int color) {
|
||||||
|
for (int dy = 0; dy < h; dy++) {
|
||||||
|
for (int dx = 0; dx < w; dx++) {
|
||||||
|
int x = startX + dx;
|
||||||
|
int y = startY + dy;
|
||||||
|
if (x >= 0 && x < width && y >= 0 && y < _terminalPixelHeight) {
|
||||||
|
_scenePixels[y][x] = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- DAMAGE FLASH ---
|
||||||
|
void _applyDamageFlash() {
|
||||||
|
for (int y = 0; y < viewHeight; y++) {
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
ColoredChar cell = _screen[y][x];
|
||||||
|
_screen[y][x] = ColoredChar(
|
||||||
|
cell.char,
|
||||||
|
_applyDamageFlashToColor(cell.rawColor),
|
||||||
|
cell.rawBackgroundColor == null
|
||||||
|
? null
|
||||||
|
: _applyDamageFlashToColor(cell.rawBackgroundColor!),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyDamageFlashToScene() {
|
||||||
|
for (int y = 0; y < _terminalPixelHeight; y++) {
|
||||||
|
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();
|
||||||
|
double colorDrop = 1.0 - (0.5 * intensity);
|
||||||
|
|
||||||
|
int r = color & 0xFF;
|
||||||
|
int g = (color >> 8) & 0xFF;
|
||||||
|
int b = (color >> 16) & 0xFF;
|
||||||
|
|
||||||
|
r = (r + redBoost).clamp(0, 255);
|
||||||
|
g = (g * colorDrop).toInt().clamp(0, 255);
|
||||||
|
b = (b * colorDrop).toInt().clamp(0, 255);
|
||||||
|
|
||||||
|
return (0xFF000000) | (b << 16) | (g << 8) | r;
|
||||||
|
}
|
||||||
|
|
||||||
|
int _scaleColor(int color, double brightness) {
|
||||||
|
int r = ((color & 0xFF) * brightness).toInt().clamp(0, 255);
|
||||||
|
int g = (((color >> 8) & 0xFF) * brightness).toInt().clamp(0, 255);
|
||||||
|
int b = (((color >> 16) & 0xFF) * brightness).toInt().clamp(0, 255);
|
||||||
|
return (0xFF000000) | (b << 16) | (g << 8) | r;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _composeTerminalScene() {
|
||||||
|
for (int y = 0; y < height; y++) {
|
||||||
|
int topY = y * 2;
|
||||||
|
int bottomY = math.min(topY + 1, _terminalPixelHeight - 1);
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
int topColor = _scenePixels[topY][x];
|
||||||
|
int bottomColor = _scenePixels[bottomY][x];
|
||||||
|
|
||||||
|
ColoredChar overlay = _screen[y][x];
|
||||||
|
if (overlay.char != ' ') {
|
||||||
|
if (overlay.rawBackgroundColor == null) {
|
||||||
|
_screen[y][x] = ColoredChar(
|
||||||
|
overlay.char,
|
||||||
|
overlay.rawColor,
|
||||||
|
bottomColor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_screen[y][x] = topColor == bottomColor
|
||||||
|
? ColoredChar('█', topColor)
|
||||||
|
: ColoredChar('▀', topColor, bottomColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts the current frame to a single printable ANSI string
|
||||||
|
StringBuffer toAnsiString() {
|
||||||
|
StringBuffer buffer = StringBuffer();
|
||||||
|
|
||||||
|
int? lastForeground;
|
||||||
|
int? lastBackground;
|
||||||
|
|
||||||
|
for (int y = 0; y < _screen.length; y++) {
|
||||||
|
List<ColoredChar> row = _screen[y];
|
||||||
|
for (ColoredChar cell in row) {
|
||||||
|
if (cell.rawColor != lastForeground) {
|
||||||
|
buffer.write('\x1b[38;2;${cell.r};${cell.g};${cell.b}m');
|
||||||
|
lastForeground = cell.rawColor;
|
||||||
|
}
|
||||||
|
if (cell.rawBackgroundColor != lastBackground) {
|
||||||
|
if (cell.rawBackgroundColor == null) {
|
||||||
|
buffer.write('\x1b[49m');
|
||||||
|
} else {
|
||||||
|
int background = cell.rawBackgroundColor!;
|
||||||
|
int bgR = background & 0xFF;
|
||||||
|
int bgG = (background >> 8) & 0xFF;
|
||||||
|
int bgB = (background >> 16) & 0xFF;
|
||||||
|
buffer.write(
|
||||||
|
'\x1b[48;2;$bgR;$bgG;$bgB'
|
||||||
|
'm',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
lastBackground = cell.rawBackgroundColor;
|
||||||
|
}
|
||||||
|
buffer.write(cell.char);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only print a newline if we are NOT on the very last row.
|
||||||
|
// This stops the terminal from scrolling down!
|
||||||
|
if (y < _screen.length - 1) {
|
||||||
|
buffer.write('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the terminal color at the very end
|
||||||
|
buffer.write('\x1b[0m');
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
38
packages/wolf_3d_dart/lib/src/rasterizer/cli_rasterizer.dart
Normal file
38
packages/wolf_3d_dart/lib/src/rasterizer/cli_rasterizer.dart
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import 'package:wolf_3d_dart/src/engine/wolf_3d_engine_base.dart';
|
||||||
|
|
||||||
|
import 'rasterizer.dart';
|
||||||
|
|
||||||
|
/// Shared terminal orchestration for CLI rasterizers.
|
||||||
|
abstract class CliRasterizer<T> extends Rasterizer<T> {
|
||||||
|
/// Resolves the framebuffer dimensions required by this renderer.
|
||||||
|
///
|
||||||
|
/// The default uses the full terminal size.
|
||||||
|
({int width, int height}) terminalFrameBufferSize(int columns, int rows) {
|
||||||
|
return (width: columns, height: rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies terminal-size policy and updates the engine framebuffer.
|
||||||
|
///
|
||||||
|
/// Returns `false` when the terminal is too small for this renderer.
|
||||||
|
bool prepareTerminalFrame(
|
||||||
|
WolfEngine engine, {
|
||||||
|
required int columns,
|
||||||
|
required int rows,
|
||||||
|
}) {
|
||||||
|
if (!isTerminalSizeSupported(columns, rows)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final size = terminalFrameBufferSize(columns, rows);
|
||||||
|
engine.setFrameBuffer(size.width, size.height);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the standard terminal size warning shown by the CLI host.
|
||||||
|
String buildTerminalSizeWarning({required int columns, required int rows}) {
|
||||||
|
return '\x1b[31m[ ERROR ] TERMINAL TOO SMALL\x1b[0m\n\n'
|
||||||
|
'$terminalSizeRequirement\n'
|
||||||
|
'Current size: \x1b[33m${columns}x$rows\x1b[0m\n\n'
|
||||||
|
'Please resize your window to resume the game...';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -114,6 +114,84 @@ abstract class Rasterizer<T> {
|
|||||||
return (10.0 / (distance + 2.0)).clamp(0.0, 1.0);
|
return (10.0 / (distance + 2.0)).clamp(0.0, 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
({double distance, int side, int hitWallId, double wallX})?
|
||||||
|
_intersectActivePushwall(
|
||||||
|
Player player,
|
||||||
|
Coordinate2D rayDir,
|
||||||
|
Pushwall activePushwall,
|
||||||
|
) {
|
||||||
|
double minX = activePushwall.x.toDouble();
|
||||||
|
double maxX = activePushwall.x + 1.0;
|
||||||
|
double minY = activePushwall.y.toDouble();
|
||||||
|
double maxY = activePushwall.y + 1.0;
|
||||||
|
|
||||||
|
if (activePushwall.dirX != 0) {
|
||||||
|
final double delta = activePushwall.dirX * activePushwall.offset;
|
||||||
|
minX += delta;
|
||||||
|
maxX += delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activePushwall.dirY != 0) {
|
||||||
|
final double delta = activePushwall.dirY * activePushwall.offset;
|
||||||
|
minY += delta;
|
||||||
|
maxY += delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double epsilon = 1e-9;
|
||||||
|
|
||||||
|
double tMinX = double.negativeInfinity;
|
||||||
|
double tMaxX = double.infinity;
|
||||||
|
if (rayDir.x.abs() < epsilon) {
|
||||||
|
if (player.x < minX || player.x > maxX) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final double tx1 = (minX - player.x) / rayDir.x;
|
||||||
|
final double tx2 = (maxX - player.x) / rayDir.x;
|
||||||
|
tMinX = math.min(tx1, tx2);
|
||||||
|
tMaxX = math.max(tx1, tx2);
|
||||||
|
}
|
||||||
|
|
||||||
|
double tMinY = double.negativeInfinity;
|
||||||
|
double tMaxY = double.infinity;
|
||||||
|
if (rayDir.y.abs() < epsilon) {
|
||||||
|
if (player.y < minY || player.y > maxY) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final double ty1 = (minY - player.y) / rayDir.y;
|
||||||
|
final double ty2 = (maxY - player.y) / rayDir.y;
|
||||||
|
tMinY = math.min(ty1, ty2);
|
||||||
|
tMaxY = math.max(ty1, ty2);
|
||||||
|
}
|
||||||
|
|
||||||
|
final double entryDistance = math.max(tMinX, tMinY);
|
||||||
|
final double exitDistance = math.min(tMaxX, tMaxY);
|
||||||
|
|
||||||
|
if (exitDistance < 0 || entryDistance > exitDistance) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final double hitDistance = entryDistance >= 0
|
||||||
|
? entryDistance
|
||||||
|
: exitDistance;
|
||||||
|
if (hitDistance < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int side = tMinX > tMinY ? 0 : 1;
|
||||||
|
final double wallCoord = side == 0
|
||||||
|
? player.y + hitDistance * rayDir.y
|
||||||
|
: player.x + hitDistance * rayDir.x;
|
||||||
|
|
||||||
|
return (
|
||||||
|
distance: hitDistance,
|
||||||
|
side: side,
|
||||||
|
hitWallId: activePushwall.mapId,
|
||||||
|
wallX: wallCoord - wallCoord.floor(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// CORE ENGINE MATH (Shared across all renderers)
|
// CORE ENGINE MATH (Shared across all renderers)
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
@@ -139,6 +217,9 @@ abstract class Rasterizer<T> {
|
|||||||
for (int x = 0; x < sceneWidth; x++) {
|
for (int x = 0; x < sceneWidth; x++) {
|
||||||
double cameraX = 2 * x / sceneWidth - 1.0;
|
double cameraX = 2 * x / sceneWidth - 1.0;
|
||||||
Coordinate2D rayDir = dir + (plane * cameraX);
|
Coordinate2D rayDir = dir + (plane * cameraX);
|
||||||
|
final pushwallHit = activePushwall == null
|
||||||
|
? null
|
||||||
|
: _intersectActivePushwall(player, rayDir, activePushwall);
|
||||||
|
|
||||||
int mapX = player.x.toInt();
|
int mapX = player.x.toInt();
|
||||||
int mapY = player.y.toInt();
|
int mapY = player.y.toInt();
|
||||||
@@ -150,6 +231,7 @@ abstract class Rasterizer<T> {
|
|||||||
int stepX, stepY, side = 0, hitWallId = 0;
|
int stepX, stepY, side = 0, hitWallId = 0;
|
||||||
bool hit = false, hitOutOfBounds = false, customDistCalculated = false;
|
bool hit = false, hitOutOfBounds = false, customDistCalculated = false;
|
||||||
double textureOffset = 0.0;
|
double textureOffset = 0.0;
|
||||||
|
double? wallXOverride;
|
||||||
Set<String> ignoredDoors = {};
|
Set<String> ignoredDoors = {};
|
||||||
|
|
||||||
if (rayDir.x < 0) {
|
if (rayDir.x < 0) {
|
||||||
@@ -186,6 +268,12 @@ abstract class Rasterizer<T> {
|
|||||||
hit = true;
|
hit = true;
|
||||||
hitOutOfBounds = true;
|
hitOutOfBounds = true;
|
||||||
} else if (map[mapY][mapX] > 0) {
|
} else if (map[mapY][mapX] > 0) {
|
||||||
|
if (activePushwall != null &&
|
||||||
|
mapX == activePushwall.x &&
|
||||||
|
mapY == activePushwall.y) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
String mapKey = '$mapX,$mapY';
|
String mapKey = '$mapX,$mapY';
|
||||||
|
|
||||||
// DOOR LOGIC
|
// DOOR LOGIC
|
||||||
@@ -207,86 +295,54 @@ abstract class Rasterizer<T> {
|
|||||||
hit = true;
|
hit = true;
|
||||||
hitWallId = map[mapY][mapX];
|
hitWallId = map[mapY][mapX];
|
||||||
textureOffset = currentOffset;
|
textureOffset = currentOffset;
|
||||||
}
|
} else {
|
||||||
// PUSHWALL LOGIC
|
|
||||||
else if (activePushwall != null &&
|
|
||||||
mapX == activePushwall.x &&
|
|
||||||
mapY == activePushwall.y) {
|
|
||||||
hit = true;
|
hit = true;
|
||||||
hitWallId = map[mapY][mapX];
|
hitWallId = map[mapY][mapX];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
double pOffset = activePushwall.offset;
|
if (hitOutOfBounds || !hit) {
|
||||||
int pDirX = activePushwall.dirX;
|
if (pushwallHit == null) {
|
||||||
int pDirY = activePushwall.dirY;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
perpWallDist = (side == 0)
|
|
||||||
? (sideDistX - deltaDistX)
|
|
||||||
: (sideDistY - deltaDistY);
|
|
||||||
|
|
||||||
if (side == 0 && pDirX != 0) {
|
|
||||||
if (pDirX == stepX) {
|
|
||||||
double intersect = perpWallDist + pOffset * deltaDistX;
|
|
||||||
if (intersect < sideDistY) {
|
|
||||||
perpWallDist = intersect;
|
|
||||||
} else {
|
|
||||||
side = 1;
|
|
||||||
perpWallDist = sideDistY - deltaDistY;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
perpWallDist -= (1.0 - pOffset) * deltaDistX;
|
|
||||||
}
|
|
||||||
} else if (side == 1 && pDirY != 0) {
|
|
||||||
if (pDirY == stepY) {
|
|
||||||
double intersect = perpWallDist + pOffset * deltaDistY;
|
|
||||||
if (intersect < sideDistX) {
|
|
||||||
perpWallDist = intersect;
|
|
||||||
} else {
|
|
||||||
side = 0;
|
|
||||||
perpWallDist = sideDistX - deltaDistX;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
perpWallDist -= (1.0 - pOffset) * deltaDistY;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
double wallFraction = (side == 0)
|
|
||||||
? player.y + perpWallDist * rayDir.y
|
|
||||||
: player.x + perpWallDist * rayDir.x;
|
|
||||||
wallFraction -= wallFraction.floor();
|
|
||||||
if (side == 0) {
|
|
||||||
if (pDirY == 1 && wallFraction < pOffset) hit = false;
|
|
||||||
if (pDirY == -1 && wallFraction > (1.0 - pOffset)) hit = false;
|
|
||||||
if (hit) textureOffset = pOffset * pDirY;
|
|
||||||
} else {
|
|
||||||
if (pDirX == 1 && wallFraction < pOffset) hit = false;
|
|
||||||
if (pDirX == -1 && wallFraction > (1.0 - pOffset)) hit = false;
|
|
||||||
if (hit) textureOffset = pOffset * pDirX;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!hit) continue;
|
|
||||||
customDistCalculated = true;
|
customDistCalculated = true;
|
||||||
} else {
|
perpWallDist = pushwallHit.distance;
|
||||||
|
side = pushwallHit.side;
|
||||||
|
hitWallId = pushwallHit.hitWallId;
|
||||||
|
wallXOverride = pushwallHit.wallX;
|
||||||
|
textureOffset = 0.0;
|
||||||
hit = true;
|
hit = true;
|
||||||
hitWallId = map[mapY][mapX];
|
hitOutOfBounds = false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hitOutOfBounds) continue;
|
|
||||||
|
|
||||||
if (!customDistCalculated) {
|
if (!customDistCalculated) {
|
||||||
perpWallDist = (side == 0)
|
perpWallDist = (side == 0)
|
||||||
? (sideDistX - deltaDistX)
|
? (sideDistX - deltaDistX)
|
||||||
: (sideDistY - deltaDistY);
|
: (sideDistY - deltaDistY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pushwallHit != null && pushwallHit.distance < perpWallDist) {
|
||||||
|
customDistCalculated = true;
|
||||||
|
perpWallDist = pushwallHit.distance;
|
||||||
|
side = pushwallHit.side;
|
||||||
|
hitWallId = pushwallHit.hitWallId;
|
||||||
|
wallXOverride = pushwallHit.wallX;
|
||||||
|
textureOffset = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
if (perpWallDist < 0.1) perpWallDist = 0.1;
|
if (perpWallDist < 0.1) perpWallDist = 0.1;
|
||||||
|
|
||||||
// Save for sprite depth checks
|
// Save for sprite depth checks
|
||||||
zBuffer[x] = perpWallDist;
|
zBuffer[x] = perpWallDist;
|
||||||
|
|
||||||
// Calculate Texture X Coordinate
|
// Calculate Texture X Coordinate
|
||||||
double wallX = (side == 0)
|
double wallX =
|
||||||
|
wallXOverride ??
|
||||||
|
((side == 0)
|
||||||
? player.y + perpWallDist * rayDir.y
|
? player.y + perpWallDist * rayDir.y
|
||||||
: player.x + perpWallDist * rayDir.x;
|
: player.x + perpWallDist * rayDir.x);
|
||||||
wallX -= wallX.floor();
|
wallX -= wallX.floor();
|
||||||
|
|
||||||
int texNum;
|
int texNum;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:wolf_3d_dart/src/rasterizer/rasterizer.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
|
|
||||||
/// Shared terminal orchestration for CLI rasterizers.
|
/// Shared terminal orchestration for CLI rasterizers.
|
||||||
@@ -0,0 +1,423 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:wolf_3d_dart/src/rasterizer/cli_rasterizer.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
|
|
||||||
|
class SixelRasterizer extends CliRasterizer<String> {
|
||||||
|
static const double _targetAspectRatio = 4 / 3;
|
||||||
|
static const int _defaultLineHeightPx = 18;
|
||||||
|
static const double _defaultCellWidthToHeight = 0.55;
|
||||||
|
static const int _minimumTerminalColumns = 117;
|
||||||
|
static const int _minimumTerminalRows = 34;
|
||||||
|
static const int _maxRenderWidth = 320;
|
||||||
|
static const int _maxRenderHeight = 240;
|
||||||
|
static const String _terminalTealBackground = '\x1b[48;2;0;150;136m';
|
||||||
|
|
||||||
|
late Uint8List _screen;
|
||||||
|
late WolfEngine _engine;
|
||||||
|
int _offsetColumns = 0;
|
||||||
|
int _offsetRows = 0;
|
||||||
|
int _outputWidth = 1;
|
||||||
|
int _outputHeight = 1;
|
||||||
|
bool _needsBackgroundClear = true;
|
||||||
|
|
||||||
|
FrameBuffer _createScaledBuffer(FrameBuffer terminalBuffer) {
|
||||||
|
final int previousOffsetColumns = _offsetColumns;
|
||||||
|
final int previousOffsetRows = _offsetRows;
|
||||||
|
final int previousOutputWidth = _outputWidth;
|
||||||
|
final int previousOutputHeight = _outputHeight;
|
||||||
|
|
||||||
|
final double fitScale = math.min(
|
||||||
|
terminalBuffer.width / _minimumTerminalColumns,
|
||||||
|
terminalBuffer.height / _minimumTerminalRows,
|
||||||
|
);
|
||||||
|
|
||||||
|
final int targetColumns = math.max(
|
||||||
|
1,
|
||||||
|
(_minimumTerminalColumns * fitScale).floor(),
|
||||||
|
);
|
||||||
|
final int targetRows = math.max(
|
||||||
|
1,
|
||||||
|
(_minimumTerminalRows * fitScale).floor(),
|
||||||
|
);
|
||||||
|
|
||||||
|
_offsetColumns = math.max(0, (terminalBuffer.width - targetColumns) ~/ 2);
|
||||||
|
_offsetRows = math.max(0, (terminalBuffer.height - targetRows) ~/ 2);
|
||||||
|
|
||||||
|
final int boundsPixelWidth = math.max(
|
||||||
|
1,
|
||||||
|
(targetColumns * _defaultLineHeightPx * _defaultCellWidthToHeight)
|
||||||
|
.floor(),
|
||||||
|
);
|
||||||
|
final int boundsPixelHeight = math.max(
|
||||||
|
1,
|
||||||
|
targetRows * _defaultLineHeightPx,
|
||||||
|
);
|
||||||
|
|
||||||
|
final double boundsAspect = boundsPixelWidth / boundsPixelHeight;
|
||||||
|
if (boundsAspect > _targetAspectRatio) {
|
||||||
|
_outputHeight = boundsPixelHeight;
|
||||||
|
_outputWidth = math.max(1, (_outputHeight * _targetAspectRatio).floor());
|
||||||
|
} else {
|
||||||
|
_outputWidth = boundsPixelWidth;
|
||||||
|
_outputHeight = math.max(1, (_outputWidth / _targetAspectRatio).floor());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_offsetColumns != previousOffsetColumns ||
|
||||||
|
_offsetRows != previousOffsetRows ||
|
||||||
|
_outputWidth != previousOutputWidth ||
|
||||||
|
_outputHeight != previousOutputHeight) {
|
||||||
|
_needsBackgroundClear = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final double renderScale = math.min(
|
||||||
|
1.0,
|
||||||
|
math.min(
|
||||||
|
_maxRenderWidth / _outputWidth,
|
||||||
|
_maxRenderHeight / _outputHeight,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final int renderWidth = math.max(1, (_outputWidth * renderScale).floor());
|
||||||
|
final int renderHeight = math.max(1, (_outputHeight * renderScale).floor());
|
||||||
|
|
||||||
|
return FrameBuffer(renderWidth, renderHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String render(WolfEngine engine) {
|
||||||
|
_engine = engine;
|
||||||
|
final FrameBuffer originalBuffer = engine.frameBuffer;
|
||||||
|
final FrameBuffer scaledBuffer = _createScaledBuffer(originalBuffer);
|
||||||
|
// We only need 8-bit indices for the 256 VGA colors
|
||||||
|
_screen = Uint8List(scaledBuffer.width * scaledBuffer.height);
|
||||||
|
engine.frameBuffer = scaledBuffer;
|
||||||
|
try {
|
||||||
|
return super.render(engine);
|
||||||
|
} finally {
|
||||||
|
engine.frameBuffer = originalBuffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void prepareFrame(WolfEngine engine) {
|
||||||
|
// Top half is ceiling color index (25), bottom half is floor color index (29)
|
||||||
|
for (int y = 0; y < viewHeight; y++) {
|
||||||
|
int colorIndex = (y < viewHeight / 2) ? 25 : 29;
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
_screen[y * width + x] = colorIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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++) {
|
||||||
|
double relativeY =
|
||||||
|
(y - (-columnHeight ~/ 2 + viewHeight ~/ 2)) / columnHeight;
|
||||||
|
int texY = (relativeY * 64).toInt().clamp(0, 63);
|
||||||
|
|
||||||
|
int colorByte = texture.pixels[texX * 64 + texY];
|
||||||
|
|
||||||
|
// Note: Directional shading is omitted here to preserve strict VGA palette indices.
|
||||||
|
// Sixel uses a fixed 256-color palette, so real-time shading requires a lookup table.
|
||||||
|
_screen[y * width + x] = colorByte;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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
|
||||||
|
if (colorByte != 255) {
|
||||||
|
_screen[y * width + stripeX] = 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 * width + drawX] = colorByte;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void drawHud(WolfEngine engine) {
|
||||||
|
int statusBarIndex = engine.data.vgaImages.indexWhere(
|
||||||
|
(img) => img.width == 320 && img.height == 40,
|
||||||
|
);
|
||||||
|
if (statusBarIndex == -1) return;
|
||||||
|
|
||||||
|
_blitVgaImage(engine.data.vgaImages[statusBarIndex], 0, 160);
|
||||||
|
|
||||||
|
_drawNumber(1, 32, 176, engine.data.vgaImages);
|
||||||
|
_drawNumber(engine.player.score, 96, 176, engine.data.vgaImages);
|
||||||
|
_drawNumber(3, 120, 176, engine.data.vgaImages);
|
||||||
|
_drawNumber(engine.player.health, 192, 176, engine.data.vgaImages);
|
||||||
|
_drawNumber(engine.player.ammo, 232, 176, engine.data.vgaImages);
|
||||||
|
|
||||||
|
_drawFace(engine);
|
||||||
|
_drawWeaponIcon(engine);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String finalizeFrame() {
|
||||||
|
final String clearPrefix = _needsBackgroundClear
|
||||||
|
? '$_terminalTealBackground\x1b[2J\x1b[0m'
|
||||||
|
: '';
|
||||||
|
_needsBackgroundClear = false;
|
||||||
|
return '$clearPrefix\x1b[${_offsetRows + 1};${_offsetColumns + 1}H${toSixelString()}';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// SIXEL ENCODER
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/// Converts the 8-bit index buffer into a standard Sixel sequence
|
||||||
|
String toSixelString() {
|
||||||
|
StringBuffer sb = StringBuffer();
|
||||||
|
|
||||||
|
// Start Sixel sequence (q = Sixel format)
|
||||||
|
sb.write('\x1bPq');
|
||||||
|
|
||||||
|
// 1. Define the Palette (and apply damage flash directly to the palette!)
|
||||||
|
double damageIntensity = _engine.player.damageFlash;
|
||||||
|
int redBoost = (150 * damageIntensity).toInt();
|
||||||
|
double colorDrop = 1.0 - (0.5 * damageIntensity);
|
||||||
|
|
||||||
|
for (int i = 0; i < 256; i++) {
|
||||||
|
int color = ColorPalette.vga32Bit[i];
|
||||||
|
int r = color & 0xFF;
|
||||||
|
int g = (color >> 8) & 0xFF;
|
||||||
|
int b = (color >> 16) & 0xFF;
|
||||||
|
|
||||||
|
if (damageIntensity > 0) {
|
||||||
|
r = (r + redBoost).clamp(0, 255);
|
||||||
|
g = (g * colorDrop).toInt().clamp(0, 255);
|
||||||
|
b = (b * colorDrop).toInt().clamp(0, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sixel RGB ranges from 0 to 100
|
||||||
|
int sixelR = (r * 100) ~/ 255;
|
||||||
|
int sixelG = (g * 100) ~/ 255;
|
||||||
|
int sixelB = (b * 100) ~/ 255;
|
||||||
|
|
||||||
|
sb.write('#$i;2;$sixelR;$sixelG;$sixelB');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Encode scaled image in 6-pixel vertical bands.
|
||||||
|
for (int band = 0; band < _outputHeight; band += 6) {
|
||||||
|
Map<int, Uint8List> colorMap = {};
|
||||||
|
|
||||||
|
// Map out which pixels use which color in this 6px high band
|
||||||
|
for (int x = 0; x < _outputWidth; x++) {
|
||||||
|
for (int yOffset = 0; yOffset < 6; yOffset++) {
|
||||||
|
int y = band + yOffset;
|
||||||
|
if (y >= _outputHeight) break;
|
||||||
|
|
||||||
|
int colorIdx = _sampleScaledPixel(x, y);
|
||||||
|
if (!colorMap.containsKey(colorIdx)) {
|
||||||
|
colorMap[colorIdx] = Uint8List(_outputWidth);
|
||||||
|
}
|
||||||
|
// Set the bit corresponding to the vertical position (0-5)
|
||||||
|
colorMap[colorIdx]![x] |= (1 << yOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the encoded Sixel characters for each color present in the band
|
||||||
|
bool firstColor = true;
|
||||||
|
for (var entry in colorMap.entries) {
|
||||||
|
if (!firstColor) {
|
||||||
|
// Carriage return to overlay colors on the same band
|
||||||
|
sb.write('\$');
|
||||||
|
}
|
||||||
|
firstColor = false;
|
||||||
|
|
||||||
|
// Select color index
|
||||||
|
sb.write('#${entry.key}');
|
||||||
|
|
||||||
|
Uint8List cols = entry.value;
|
||||||
|
int currentVal = -1;
|
||||||
|
int runLength = 0;
|
||||||
|
|
||||||
|
// Run-Length Encoding (RLE) loop
|
||||||
|
for (int x = 0; x < _outputWidth; x++) {
|
||||||
|
int val = cols[x];
|
||||||
|
if (val == currentVal) {
|
||||||
|
runLength++;
|
||||||
|
} else {
|
||||||
|
if (runLength > 0) _writeSixelRle(sb, currentVal, runLength);
|
||||||
|
currentVal = val;
|
||||||
|
runLength = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (runLength > 0) _writeSixelRle(sb, currentVal, runLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (band + 6 < _outputHeight) {
|
||||||
|
sb.write('-');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// End Sixel sequence
|
||||||
|
sb.write('\x1b\\');
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
int _sampleScaledPixel(int outX, int outY) {
|
||||||
|
final int srcX = ((((outX + 0.5) * width) / _outputWidth) - 0.5)
|
||||||
|
.round()
|
||||||
|
.clamp(
|
||||||
|
0,
|
||||||
|
width - 1,
|
||||||
|
);
|
||||||
|
final int srcY = ((((outY + 0.5) * height) / _outputHeight) - 0.5)
|
||||||
|
.round()
|
||||||
|
.clamp(
|
||||||
|
0,
|
||||||
|
height - 1,
|
||||||
|
);
|
||||||
|
return _screen[srcY * width + srcX];
|
||||||
|
}
|
||||||
|
|
||||||
|
void _writeSixelRle(StringBuffer sb, int value, int runLength) {
|
||||||
|
String char = String.fromCharCode(value + 63);
|
||||||
|
// Sixel RLE format: !<count><char> (only worth it if count > 3)
|
||||||
|
if (runLength > 3) {
|
||||||
|
sb.write('!$runLength$char');
|
||||||
|
} else {
|
||||||
|
sb.write(char * runLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// PRIVATE HUD HELPERS (Adapted for 8-bit index buffer)
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
void _blitVgaImage(VgaImage image, int startX, int startY) {
|
||||||
|
int planeWidth = image.width ~/ 4;
|
||||||
|
int planeSize = planeWidth * image.height;
|
||||||
|
final double scaleX = width / 320.0;
|
||||||
|
final double scaleY = height / 200.0;
|
||||||
|
|
||||||
|
final int destStartX = (startX * scaleX).toInt();
|
||||||
|
final int destStartY = (startY * scaleY).toInt();
|
||||||
|
final int destWidth = math.max(1, (image.width * scaleX).toInt());
|
||||||
|
final int destHeight = math.max(1, (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) {
|
||||||
|
_screen[drawY * width + drawX] = 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 = (health <= 0)
|
||||||
|
? 127
|
||||||
|
: 106 + (((100 - health) ~/ 16).clamp(0, 6) * 3);
|
||||||
|
if (faceIndex < engine.data.vgaImages.length) {
|
||||||
|
_blitVgaImage(engine.data.vgaImages[faceIndex], 136, 164);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _drawWeaponIcon(WolfEngine engine) {
|
||||||
|
int weaponIndex = 89;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:wolf_3d_dart/src/rasterizer/rasterizer.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
|
|
||||||
|
class SoftwareRasterizer extends Rasterizer<FrameBuffer> {
|
||||||
|
late FrameBuffer _buffer;
|
||||||
|
late WolfEngine _engine;
|
||||||
|
|
||||||
|
// Intercept the base render call to store our references
|
||||||
|
@override
|
||||||
|
FrameBuffer render(WolfEngine engine) {
|
||||||
|
_engine = engine;
|
||||||
|
_buffer = engine.frameBuffer;
|
||||||
|
return super.render(engine);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
424
packages/wolf_3d_dart/lib/src/rasterizer/sixel_rasterizer.dart
Normal file
424
packages/wolf_3d_dart/lib/src/rasterizer/sixel_rasterizer.dart
Normal file
@@ -0,0 +1,424 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
|
|
||||||
|
import 'cli_rasterizer.dart';
|
||||||
|
|
||||||
|
class SixelRasterizer extends CliRasterizer<String> {
|
||||||
|
static const double _targetAspectRatio = 4 / 3;
|
||||||
|
static const int _defaultLineHeightPx = 18;
|
||||||
|
static const double _defaultCellWidthToHeight = 0.55;
|
||||||
|
static const int _minimumTerminalColumns = 117;
|
||||||
|
static const int _minimumTerminalRows = 34;
|
||||||
|
static const int _maxRenderWidth = 320;
|
||||||
|
static const int _maxRenderHeight = 240;
|
||||||
|
static const String _terminalTealBackground = '\x1b[48;2;0;150;136m';
|
||||||
|
|
||||||
|
late Uint8List _screen;
|
||||||
|
late WolfEngine _engine;
|
||||||
|
int _offsetColumns = 0;
|
||||||
|
int _offsetRows = 0;
|
||||||
|
int _outputWidth = 1;
|
||||||
|
int _outputHeight = 1;
|
||||||
|
bool _needsBackgroundClear = true;
|
||||||
|
|
||||||
|
FrameBuffer _createScaledBuffer(FrameBuffer terminalBuffer) {
|
||||||
|
final int previousOffsetColumns = _offsetColumns;
|
||||||
|
final int previousOffsetRows = _offsetRows;
|
||||||
|
final int previousOutputWidth = _outputWidth;
|
||||||
|
final int previousOutputHeight = _outputHeight;
|
||||||
|
|
||||||
|
final double fitScale = math.min(
|
||||||
|
terminalBuffer.width / _minimumTerminalColumns,
|
||||||
|
terminalBuffer.height / _minimumTerminalRows,
|
||||||
|
);
|
||||||
|
|
||||||
|
final int targetColumns = math.max(
|
||||||
|
1,
|
||||||
|
(_minimumTerminalColumns * fitScale).floor(),
|
||||||
|
);
|
||||||
|
final int targetRows = math.max(
|
||||||
|
1,
|
||||||
|
(_minimumTerminalRows * fitScale).floor(),
|
||||||
|
);
|
||||||
|
|
||||||
|
_offsetColumns = math.max(0, (terminalBuffer.width - targetColumns) ~/ 2);
|
||||||
|
_offsetRows = math.max(0, (terminalBuffer.height - targetRows) ~/ 2);
|
||||||
|
|
||||||
|
final int boundsPixelWidth = math.max(
|
||||||
|
1,
|
||||||
|
(targetColumns * _defaultLineHeightPx * _defaultCellWidthToHeight)
|
||||||
|
.floor(),
|
||||||
|
);
|
||||||
|
final int boundsPixelHeight = math.max(
|
||||||
|
1,
|
||||||
|
targetRows * _defaultLineHeightPx,
|
||||||
|
);
|
||||||
|
|
||||||
|
final double boundsAspect = boundsPixelWidth / boundsPixelHeight;
|
||||||
|
if (boundsAspect > _targetAspectRatio) {
|
||||||
|
_outputHeight = boundsPixelHeight;
|
||||||
|
_outputWidth = math.max(1, (_outputHeight * _targetAspectRatio).floor());
|
||||||
|
} else {
|
||||||
|
_outputWidth = boundsPixelWidth;
|
||||||
|
_outputHeight = math.max(1, (_outputWidth / _targetAspectRatio).floor());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_offsetColumns != previousOffsetColumns ||
|
||||||
|
_offsetRows != previousOffsetRows ||
|
||||||
|
_outputWidth != previousOutputWidth ||
|
||||||
|
_outputHeight != previousOutputHeight) {
|
||||||
|
_needsBackgroundClear = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final double renderScale = math.min(
|
||||||
|
1.0,
|
||||||
|
math.min(
|
||||||
|
_maxRenderWidth / _outputWidth,
|
||||||
|
_maxRenderHeight / _outputHeight,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final int renderWidth = math.max(1, (_outputWidth * renderScale).floor());
|
||||||
|
final int renderHeight = math.max(1, (_outputHeight * renderScale).floor());
|
||||||
|
|
||||||
|
return FrameBuffer(renderWidth, renderHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String render(WolfEngine engine) {
|
||||||
|
_engine = engine;
|
||||||
|
final FrameBuffer originalBuffer = engine.frameBuffer;
|
||||||
|
final FrameBuffer scaledBuffer = _createScaledBuffer(originalBuffer);
|
||||||
|
// We only need 8-bit indices for the 256 VGA colors
|
||||||
|
_screen = Uint8List(scaledBuffer.width * scaledBuffer.height);
|
||||||
|
engine.frameBuffer = scaledBuffer;
|
||||||
|
try {
|
||||||
|
return super.render(engine);
|
||||||
|
} finally {
|
||||||
|
engine.frameBuffer = originalBuffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void prepareFrame(WolfEngine engine) {
|
||||||
|
// Top half is ceiling color index (25), bottom half is floor color index (29)
|
||||||
|
for (int y = 0; y < viewHeight; y++) {
|
||||||
|
int colorIndex = (y < viewHeight / 2) ? 25 : 29;
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
_screen[y * width + x] = colorIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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++) {
|
||||||
|
double relativeY =
|
||||||
|
(y - (-columnHeight ~/ 2 + viewHeight ~/ 2)) / columnHeight;
|
||||||
|
int texY = (relativeY * 64).toInt().clamp(0, 63);
|
||||||
|
|
||||||
|
int colorByte = texture.pixels[texX * 64 + texY];
|
||||||
|
|
||||||
|
// Note: Directional shading is omitted here to preserve strict VGA palette indices.
|
||||||
|
// Sixel uses a fixed 256-color palette, so real-time shading requires a lookup table.
|
||||||
|
_screen[y * width + x] = colorByte;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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
|
||||||
|
if (colorByte != 255) {
|
||||||
|
_screen[y * width + stripeX] = 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 * width + drawX] = colorByte;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void drawHud(WolfEngine engine) {
|
||||||
|
int statusBarIndex = engine.data.vgaImages.indexWhere(
|
||||||
|
(img) => img.width == 320 && img.height == 40,
|
||||||
|
);
|
||||||
|
if (statusBarIndex == -1) return;
|
||||||
|
|
||||||
|
_blitVgaImage(engine.data.vgaImages[statusBarIndex], 0, 160);
|
||||||
|
|
||||||
|
_drawNumber(1, 32, 176, engine.data.vgaImages);
|
||||||
|
_drawNumber(engine.player.score, 96, 176, engine.data.vgaImages);
|
||||||
|
_drawNumber(3, 120, 176, engine.data.vgaImages);
|
||||||
|
_drawNumber(engine.player.health, 192, 176, engine.data.vgaImages);
|
||||||
|
_drawNumber(engine.player.ammo, 232, 176, engine.data.vgaImages);
|
||||||
|
|
||||||
|
_drawFace(engine);
|
||||||
|
_drawWeaponIcon(engine);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String finalizeFrame() {
|
||||||
|
final String clearPrefix = _needsBackgroundClear
|
||||||
|
? '$_terminalTealBackground\x1b[2J\x1b[0m'
|
||||||
|
: '';
|
||||||
|
_needsBackgroundClear = false;
|
||||||
|
return '$clearPrefix\x1b[${_offsetRows + 1};${_offsetColumns + 1}H${toSixelString()}';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// SIXEL ENCODER
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/// Converts the 8-bit index buffer into a standard Sixel sequence
|
||||||
|
String toSixelString() {
|
||||||
|
StringBuffer sb = StringBuffer();
|
||||||
|
|
||||||
|
// Start Sixel sequence (q = Sixel format)
|
||||||
|
sb.write('\x1bPq');
|
||||||
|
|
||||||
|
// 1. Define the Palette (and apply damage flash directly to the palette!)
|
||||||
|
double damageIntensity = _engine.player.damageFlash;
|
||||||
|
int redBoost = (150 * damageIntensity).toInt();
|
||||||
|
double colorDrop = 1.0 - (0.5 * damageIntensity);
|
||||||
|
|
||||||
|
for (int i = 0; i < 256; i++) {
|
||||||
|
int color = ColorPalette.vga32Bit[i];
|
||||||
|
int r = color & 0xFF;
|
||||||
|
int g = (color >> 8) & 0xFF;
|
||||||
|
int b = (color >> 16) & 0xFF;
|
||||||
|
|
||||||
|
if (damageIntensity > 0) {
|
||||||
|
r = (r + redBoost).clamp(0, 255);
|
||||||
|
g = (g * colorDrop).toInt().clamp(0, 255);
|
||||||
|
b = (b * colorDrop).toInt().clamp(0, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sixel RGB ranges from 0 to 100
|
||||||
|
int sixelR = (r * 100) ~/ 255;
|
||||||
|
int sixelG = (g * 100) ~/ 255;
|
||||||
|
int sixelB = (b * 100) ~/ 255;
|
||||||
|
|
||||||
|
sb.write('#$i;2;$sixelR;$sixelG;$sixelB');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Encode scaled image in 6-pixel vertical bands.
|
||||||
|
for (int band = 0; band < _outputHeight; band += 6) {
|
||||||
|
Map<int, Uint8List> colorMap = {};
|
||||||
|
|
||||||
|
// Map out which pixels use which color in this 6px high band
|
||||||
|
for (int x = 0; x < _outputWidth; x++) {
|
||||||
|
for (int yOffset = 0; yOffset < 6; yOffset++) {
|
||||||
|
int y = band + yOffset;
|
||||||
|
if (y >= _outputHeight) break;
|
||||||
|
|
||||||
|
int colorIdx = _sampleScaledPixel(x, y);
|
||||||
|
if (!colorMap.containsKey(colorIdx)) {
|
||||||
|
colorMap[colorIdx] = Uint8List(_outputWidth);
|
||||||
|
}
|
||||||
|
// Set the bit corresponding to the vertical position (0-5)
|
||||||
|
colorMap[colorIdx]![x] |= (1 << yOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the encoded Sixel characters for each color present in the band
|
||||||
|
bool firstColor = true;
|
||||||
|
for (var entry in colorMap.entries) {
|
||||||
|
if (!firstColor) {
|
||||||
|
// Carriage return to overlay colors on the same band
|
||||||
|
sb.write('\$');
|
||||||
|
}
|
||||||
|
firstColor = false;
|
||||||
|
|
||||||
|
// Select color index
|
||||||
|
sb.write('#${entry.key}');
|
||||||
|
|
||||||
|
Uint8List cols = entry.value;
|
||||||
|
int currentVal = -1;
|
||||||
|
int runLength = 0;
|
||||||
|
|
||||||
|
// Run-Length Encoding (RLE) loop
|
||||||
|
for (int x = 0; x < _outputWidth; x++) {
|
||||||
|
int val = cols[x];
|
||||||
|
if (val == currentVal) {
|
||||||
|
runLength++;
|
||||||
|
} else {
|
||||||
|
if (runLength > 0) _writeSixelRle(sb, currentVal, runLength);
|
||||||
|
currentVal = val;
|
||||||
|
runLength = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (runLength > 0) _writeSixelRle(sb, currentVal, runLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (band + 6 < _outputHeight) {
|
||||||
|
sb.write('-');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// End Sixel sequence
|
||||||
|
sb.write('\x1b\\');
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
int _sampleScaledPixel(int outX, int outY) {
|
||||||
|
final int srcX = ((((outX + 0.5) * width) / _outputWidth) - 0.5)
|
||||||
|
.round()
|
||||||
|
.clamp(
|
||||||
|
0,
|
||||||
|
width - 1,
|
||||||
|
);
|
||||||
|
final int srcY = ((((outY + 0.5) * height) / _outputHeight) - 0.5)
|
||||||
|
.round()
|
||||||
|
.clamp(
|
||||||
|
0,
|
||||||
|
height - 1,
|
||||||
|
);
|
||||||
|
return _screen[srcY * width + srcX];
|
||||||
|
}
|
||||||
|
|
||||||
|
void _writeSixelRle(StringBuffer sb, int value, int runLength) {
|
||||||
|
String char = String.fromCharCode(value + 63);
|
||||||
|
// Sixel RLE format: !<count><char> (only worth it if count > 3)
|
||||||
|
if (runLength > 3) {
|
||||||
|
sb.write('!$runLength$char');
|
||||||
|
} else {
|
||||||
|
sb.write(char * runLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// PRIVATE HUD HELPERS (Adapted for 8-bit index buffer)
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
void _blitVgaImage(VgaImage image, int startX, int startY) {
|
||||||
|
int planeWidth = image.width ~/ 4;
|
||||||
|
int planeSize = planeWidth * image.height;
|
||||||
|
final double scaleX = width / 320.0;
|
||||||
|
final double scaleY = height / 200.0;
|
||||||
|
|
||||||
|
final int destStartX = (startX * scaleX).toInt();
|
||||||
|
final int destStartY = (startY * scaleY).toInt();
|
||||||
|
final int destWidth = math.max(1, (image.width * scaleX).toInt());
|
||||||
|
final int destHeight = math.max(1, (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) {
|
||||||
|
_screen[drawY * width + drawX] = 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 = (health <= 0)
|
||||||
|
? 127
|
||||||
|
: 106 + (((100 - health) ~/ 16).clamp(0, 6) * 3);
|
||||||
|
if (faceIndex < engine.data.vgaImages.length) {
|
||||||
|
_blitVgaImage(engine.data.vgaImages[faceIndex], 136, 164);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _drawWeaponIcon(WolfEngine engine) {
|
||||||
|
int weaponIndex = 89;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:wolf_3d_dart/src/rasterizer/rasterizer.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
|
|
||||||
|
class SoftwareRasterizer extends Rasterizer<FrameBuffer> {
|
||||||
|
late FrameBuffer _buffer;
|
||||||
|
late WolfEngine _engine;
|
||||||
|
|
||||||
|
// Intercept the base render call to store our references
|
||||||
|
@override
|
||||||
|
FrameBuffer render(WolfEngine engine) {
|
||||||
|
_engine = engine;
|
||||||
|
_buffer = engine.frameBuffer;
|
||||||
|
return super.render(engine);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
473
packages/wolf_3d_dart/lib/src/rasterizer/src/rasterizer.dart
Normal file
473
packages/wolf_3d_dart/lib/src/rasterizer/src/rasterizer.dart
Normal file
@@ -0,0 +1,473 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
||||||
|
|
||||||
|
abstract class Rasterizer<T> {
|
||||||
|
late List<double> zBuffer;
|
||||||
|
late int width;
|
||||||
|
late int height;
|
||||||
|
late int viewHeight;
|
||||||
|
|
||||||
|
/// A multiplier to adjust the width of sprites.
|
||||||
|
/// Pixel renderers usually keep this at 1.0.
|
||||||
|
/// ASCII renderers can override this (e.g., 0.6) to account for tall characters.
|
||||||
|
double get aspectMultiplier => 1.0;
|
||||||
|
|
||||||
|
/// A multiplier to counteract tall pixel formats (like 1:2 terminal fonts).
|
||||||
|
/// 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.
|
||||||
|
int get projectionViewHeight => viewHeight;
|
||||||
|
|
||||||
|
/// Whether the current terminal dimensions are supported by this renderer.
|
||||||
|
/// Default renderers accept all sizes.
|
||||||
|
bool isTerminalSizeSupported(int columns, int rows) => true;
|
||||||
|
|
||||||
|
/// Human-readable requirement text used by the host app when size checks fail.
|
||||||
|
String get terminalSizeRequirement => 'Please resize your terminal window.';
|
||||||
|
|
||||||
|
/// The main entry point called by the game loop.
|
||||||
|
/// Orchestrates the mathematical rendering pipeline.
|
||||||
|
T render(WolfEngine engine) {
|
||||||
|
width = engine.frameBuffer.width;
|
||||||
|
height = engine.frameBuffer.height;
|
||||||
|
// The 3D view typically takes up the top 80% of the screen
|
||||||
|
viewHeight = (height * 0.8).toInt();
|
||||||
|
zBuffer = List.filled(projectionWidth, 0.0);
|
||||||
|
|
||||||
|
// 1. Setup the frame (clear screen, draw floor/ceiling)
|
||||||
|
prepareFrame(engine);
|
||||||
|
|
||||||
|
// 2. Do the heavy math for Raycasting Walls
|
||||||
|
_castWalls(engine);
|
||||||
|
|
||||||
|
// 3. Do the heavy math for Projecting Sprites
|
||||||
|
_castSprites(engine);
|
||||||
|
|
||||||
|
// 4. Draw 2D Overlays
|
||||||
|
drawWeapon(engine);
|
||||||
|
drawHud(engine);
|
||||||
|
|
||||||
|
// 5. Finalize and return the frame data (Buffer or String/List)
|
||||||
|
return finalizeFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// ABSTRACT METHODS (Implemented by the child renderers)
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/// Initialize buffers, clear the screen, and draw the floor/ceiling.
|
||||||
|
void prepareFrame(WolfEngine engine);
|
||||||
|
|
||||||
|
/// Draw a single vertical column of a wall.
|
||||||
|
void drawWallColumn(
|
||||||
|
int x,
|
||||||
|
int drawStart,
|
||||||
|
int drawEnd,
|
||||||
|
int columnHeight,
|
||||||
|
Sprite texture,
|
||||||
|
int texX,
|
||||||
|
double perpWallDist,
|
||||||
|
int side,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Draw a single vertical stripe of a sprite (enemy/item).
|
||||||
|
void drawSpriteStripe(
|
||||||
|
int stripeX,
|
||||||
|
int drawStartY,
|
||||||
|
int drawEndY,
|
||||||
|
int spriteHeight,
|
||||||
|
Sprite texture,
|
||||||
|
int texX,
|
||||||
|
double transformY,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Draw the player's weapon overlay at the bottom of the 3D view.
|
||||||
|
void drawWeapon(WolfEngine engine);
|
||||||
|
|
||||||
|
/// Draw the 2D status bar at the bottom 20% of the screen.
|
||||||
|
void drawHud(WolfEngine engine);
|
||||||
|
|
||||||
|
/// Return the finished frame (e.g., the FrameBuffer itself, or an ASCII list).
|
||||||
|
T finalizeFrame();
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// SHARED LIGHTING MATH
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/// Calculates depth-based lighting falloff (0.0 to 1.0).
|
||||||
|
/// While the original Wolf3D didn't use depth fog, this provides a great
|
||||||
|
/// atmospheric effect for custom renderers (like ASCII dithering).
|
||||||
|
double calculateDepthBrightness(double distance) {
|
||||||
|
return (10.0 / (distance + 2.0)).clamp(0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
({double distance, int side, int hitWallId, double wallX})?
|
||||||
|
_intersectActivePushwall(
|
||||||
|
Player player,
|
||||||
|
Coordinate2D rayDir,
|
||||||
|
Pushwall activePushwall,
|
||||||
|
) {
|
||||||
|
double minX = activePushwall.x.toDouble();
|
||||||
|
double maxX = activePushwall.x + 1.0;
|
||||||
|
double minY = activePushwall.y.toDouble();
|
||||||
|
double maxY = activePushwall.y + 1.0;
|
||||||
|
|
||||||
|
if (activePushwall.dirX != 0) {
|
||||||
|
final double delta = activePushwall.dirX * activePushwall.offset;
|
||||||
|
minX += delta;
|
||||||
|
maxX += delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activePushwall.dirY != 0) {
|
||||||
|
final double delta = activePushwall.dirY * activePushwall.offset;
|
||||||
|
minY += delta;
|
||||||
|
maxY += delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double epsilon = 1e-9;
|
||||||
|
|
||||||
|
double tMinX = double.negativeInfinity;
|
||||||
|
double tMaxX = double.infinity;
|
||||||
|
if (rayDir.x.abs() < epsilon) {
|
||||||
|
if (player.x < minX || player.x > maxX) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final double tx1 = (minX - player.x) / rayDir.x;
|
||||||
|
final double tx2 = (maxX - player.x) / rayDir.x;
|
||||||
|
tMinX = math.min(tx1, tx2);
|
||||||
|
tMaxX = math.max(tx1, tx2);
|
||||||
|
}
|
||||||
|
|
||||||
|
double tMinY = double.negativeInfinity;
|
||||||
|
double tMaxY = double.infinity;
|
||||||
|
if (rayDir.y.abs() < epsilon) {
|
||||||
|
if (player.y < minY || player.y > maxY) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final double ty1 = (minY - player.y) / rayDir.y;
|
||||||
|
final double ty2 = (maxY - player.y) / rayDir.y;
|
||||||
|
tMinY = math.min(ty1, ty2);
|
||||||
|
tMaxY = math.max(ty1, ty2);
|
||||||
|
}
|
||||||
|
|
||||||
|
final double entryDistance = math.max(tMinX, tMinY);
|
||||||
|
final double exitDistance = math.min(tMaxX, tMaxY);
|
||||||
|
|
||||||
|
if (exitDistance < 0 || entryDistance > exitDistance) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final double hitDistance = entryDistance >= 0
|
||||||
|
? entryDistance
|
||||||
|
: exitDistance;
|
||||||
|
if (hitDistance < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int side = tMinX > tMinY ? 0 : 1;
|
||||||
|
final double wallCoord = side == 0
|
||||||
|
? player.y + hitDistance * rayDir.y
|
||||||
|
: player.x + hitDistance * rayDir.x;
|
||||||
|
|
||||||
|
return (
|
||||||
|
distance: hitDistance,
|
||||||
|
side: side,
|
||||||
|
hitWallId: activePushwall.mapId,
|
||||||
|
wallX: wallCoord - wallCoord.floor(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// CORE ENGINE MATH (Shared across all renderers)
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
void _castWalls(WolfEngine engine) {
|
||||||
|
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
|
||||||
|
.getOffsetsForRenderer();
|
||||||
|
final Pushwall? activePushwall = engine.pushwallManager.activePushwall;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
for (int x = 0; x < sceneWidth; x++) {
|
||||||
|
double cameraX = 2 * x / sceneWidth - 1.0;
|
||||||
|
Coordinate2D rayDir = dir + (plane * cameraX);
|
||||||
|
final pushwallHit = activePushwall == null
|
||||||
|
? null
|
||||||
|
: _intersectActivePushwall(player, rayDir, activePushwall);
|
||||||
|
|
||||||
|
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, perpWallDist = 0.0;
|
||||||
|
int stepX, stepY, side = 0, hitWallId = 0;
|
||||||
|
bool hit = false, hitOutOfBounds = false, customDistCalculated = false;
|
||||||
|
double textureOffset = 0.0;
|
||||||
|
double? wallXOverride;
|
||||||
|
Set<String> ignoredDoors = {};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DDA Loop
|
||||||
|
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) {
|
||||||
|
hit = true;
|
||||||
|
hitOutOfBounds = true;
|
||||||
|
} else if (map[mapY][mapX] > 0) {
|
||||||
|
if (activePushwall != null &&
|
||||||
|
mapX == activePushwall.x &&
|
||||||
|
mapY == activePushwall.y) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String mapKey = '$mapX,$mapY';
|
||||||
|
|
||||||
|
// DOOR LOGIC
|
||||||
|
if (map[mapY][mapX] >= 90 && !ignoredDoors.contains(mapKey)) {
|
||||||
|
double currentOffset = doorOffsets[mapKey] ?? 0.0;
|
||||||
|
if (currentOffset > 0.0) {
|
||||||
|
double perpWallDistTemp = (side == 0)
|
||||||
|
? (sideDistX - deltaDistX)
|
||||||
|
: (sideDistY - deltaDistY);
|
||||||
|
double wallXTemp = (side == 0)
|
||||||
|
? player.y + perpWallDistTemp * rayDir.y
|
||||||
|
: player.x + perpWallDistTemp * rayDir.x;
|
||||||
|
wallXTemp -= wallXTemp.floor();
|
||||||
|
if (wallXTemp < currentOffset) {
|
||||||
|
ignoredDoors.add(mapKey);
|
||||||
|
continue; // Ray passes through the open part of the door
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hit = true;
|
||||||
|
hitWallId = map[mapY][mapX];
|
||||||
|
textureOffset = currentOffset;
|
||||||
|
} else {
|
||||||
|
hit = true;
|
||||||
|
hitWallId = map[mapY][mapX];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hitOutOfBounds || !hit) {
|
||||||
|
if (pushwallHit == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
customDistCalculated = true;
|
||||||
|
perpWallDist = pushwallHit.distance;
|
||||||
|
side = pushwallHit.side;
|
||||||
|
hitWallId = pushwallHit.hitWallId;
|
||||||
|
wallXOverride = pushwallHit.wallX;
|
||||||
|
textureOffset = 0.0;
|
||||||
|
hit = true;
|
||||||
|
hitOutOfBounds = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!customDistCalculated) {
|
||||||
|
perpWallDist = (side == 0)
|
||||||
|
? (sideDistX - deltaDistX)
|
||||||
|
: (sideDistY - deltaDistY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pushwallHit != null && pushwallHit.distance < perpWallDist) {
|
||||||
|
customDistCalculated = true;
|
||||||
|
perpWallDist = pushwallHit.distance;
|
||||||
|
side = pushwallHit.side;
|
||||||
|
hitWallId = pushwallHit.hitWallId;
|
||||||
|
wallXOverride = pushwallHit.wallX;
|
||||||
|
textureOffset = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (perpWallDist < 0.1) perpWallDist = 0.1;
|
||||||
|
|
||||||
|
// Save for sprite depth checks
|
||||||
|
zBuffer[x] = perpWallDist;
|
||||||
|
|
||||||
|
// Calculate Texture X Coordinate
|
||||||
|
double wallX =
|
||||||
|
wallXOverride ??
|
||||||
|
((side == 0)
|
||||||
|
? player.y + perpWallDist * rayDir.y
|
||||||
|
: player.x + perpWallDist * rayDir.x);
|
||||||
|
wallX -= wallX.floor();
|
||||||
|
|
||||||
|
int texNum;
|
||||||
|
if (hitWallId >= 90) {
|
||||||
|
texNum = 98.clamp(0, wallTextures.length - 1);
|
||||||
|
} else {
|
||||||
|
texNum = ((hitWallId - 1) * 2).clamp(0, wallTextures.length - 2);
|
||||||
|
if (side == 1) texNum += 1;
|
||||||
|
}
|
||||||
|
Sprite texture = wallTextures[texNum];
|
||||||
|
|
||||||
|
// Texture flipping for specific orientations
|
||||||
|
int texX = (((wallX - textureOffset) % 1.0) * 64).toInt().clamp(0, 63);
|
||||||
|
if (side == 0 && math.cos(player.angle) > 0) texX = 63 - texX;
|
||||||
|
if (side == 1 && math.sin(player.angle) < 0) texX = 63 - texX;
|
||||||
|
|
||||||
|
// Calculate drawing dimensions
|
||||||
|
int columnHeight = ((sceneHeight / perpWallDist) * verticalStretch)
|
||||||
|
.toInt();
|
||||||
|
int drawStart = (-columnHeight ~/ 2 + sceneHeight ~/ 2).clamp(
|
||||||
|
0,
|
||||||
|
sceneHeight,
|
||||||
|
);
|
||||||
|
int drawEnd = (columnHeight ~/ 2 + sceneHeight ~/ 2).clamp(
|
||||||
|
0,
|
||||||
|
sceneHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Tell the implementation to draw this column
|
||||||
|
drawWallColumn(
|
||||||
|
projectionOffsetX + x,
|
||||||
|
drawStart,
|
||||||
|
drawEnd,
|
||||||
|
columnHeight,
|
||||||
|
texture,
|
||||||
|
texX,
|
||||||
|
perpWallDist,
|
||||||
|
side,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
activeSprites.sort((a, b) {
|
||||||
|
double distA = player.position.distanceTo(a.position);
|
||||||
|
double distB = player.position.distanceTo(b.position);
|
||||||
|
return distB.compareTo(distA);
|
||||||
|
});
|
||||||
|
|
||||||
|
Coordinate2D dir = Coordinate2D(
|
||||||
|
math.cos(player.angle),
|
||||||
|
math.sin(player.angle),
|
||||||
|
);
|
||||||
|
Coordinate2D plane =
|
||||||
|
Coordinate2D(-dir.y, dir.x) * math.tan((math.pi / 3) / 2);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Only process if the sprite is in front of the camera
|
||||||
|
if (transformY > 0) {
|
||||||
|
int spriteScreenX = ((sceneWidth / 2) * (1 + transformX / transformY))
|
||||||
|
.toInt();
|
||||||
|
int spriteHeight = ((sceneHeight / transformY).abs() * verticalStretch)
|
||||||
|
.toInt();
|
||||||
|
int displayedSpriteHeight =
|
||||||
|
((viewHeight / transformY).abs() * verticalStretch).toInt();
|
||||||
|
|
||||||
|
// Scale width based on the aspectMultiplier (useful for ASCII)
|
||||||
|
int spriteWidth =
|
||||||
|
(displayedSpriteHeight * aspectMultiplier / verticalStretch)
|
||||||
|
.toInt();
|
||||||
|
|
||||||
|
int drawStartY = -spriteHeight ~/ 2 + sceneHeight ~/ 2;
|
||||||
|
int drawEndY = spriteHeight ~/ 2 + sceneHeight ~/ 2;
|
||||||
|
int drawStartX = -spriteWidth ~/ 2 + spriteScreenX;
|
||||||
|
int drawEndX = spriteWidth ~/ 2 + spriteScreenX;
|
||||||
|
|
||||||
|
int clipStartX = math.max(0, drawStartX);
|
||||||
|
int clipEndX = math.min(sceneWidth, drawEndX);
|
||||||
|
|
||||||
|
int safeIndex = entity.spriteIndex.clamp(
|
||||||
|
0,
|
||||||
|
engine.data.sprites.length - 1,
|
||||||
|
);
|
||||||
|
Sprite texture = engine.data.sprites[safeIndex];
|
||||||
|
|
||||||
|
// Loop through the visible vertical stripes
|
||||||
|
for (int stripe = clipStartX; stripe < clipEndX; stripe++) {
|
||||||
|
// Check the Z-Buffer to see if a wall is in front of this stripe
|
||||||
|
if (transformY < zBuffer[stripe]) {
|
||||||
|
int texX = ((stripe - drawStartX) * 64 ~/ spriteWidth).clamp(0, 63);
|
||||||
|
|
||||||
|
// Tell the implementation to draw this stripe
|
||||||
|
drawSpriteStripe(
|
||||||
|
projectionOffsetX + stripe,
|
||||||
|
drawStartY,
|
||||||
|
drawEndY,
|
||||||
|
spriteHeight,
|
||||||
|
texture,
|
||||||
|
texX,
|
||||||
|
transformY,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Darkens a 32-bit 0xAABBGGRR color by roughly 30% without touching Alpha
|
||||||
|
int shadeColor(int color) {
|
||||||
|
int r = (color & 0xFF) * 7 ~/ 10;
|
||||||
|
int g = ((color >> 8) & 0xFF) * 7 ~/ 10;
|
||||||
|
int b = ((color >> 16) & 0xFF) * 7 ~/ 10;
|
||||||
|
return (0xFF000000) | (b << 16) | (g << 8) | r;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,423 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:wolf_3d_dart/src/rasterizer/cli_rasterizer.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
|
|
||||||
|
class SixelRasterizer extends CliRasterizer<String> {
|
||||||
|
static const double _targetAspectRatio = 4 / 3;
|
||||||
|
static const int _defaultLineHeightPx = 18;
|
||||||
|
static const double _defaultCellWidthToHeight = 0.55;
|
||||||
|
static const int _minimumTerminalColumns = 117;
|
||||||
|
static const int _minimumTerminalRows = 34;
|
||||||
|
static const int _maxRenderWidth = 320;
|
||||||
|
static const int _maxRenderHeight = 240;
|
||||||
|
static const String _terminalTealBackground = '\x1b[48;2;0;150;136m';
|
||||||
|
|
||||||
|
late Uint8List _screen;
|
||||||
|
late WolfEngine _engine;
|
||||||
|
int _offsetColumns = 0;
|
||||||
|
int _offsetRows = 0;
|
||||||
|
int _outputWidth = 1;
|
||||||
|
int _outputHeight = 1;
|
||||||
|
bool _needsBackgroundClear = true;
|
||||||
|
|
||||||
|
FrameBuffer _createScaledBuffer(FrameBuffer terminalBuffer) {
|
||||||
|
final int previousOffsetColumns = _offsetColumns;
|
||||||
|
final int previousOffsetRows = _offsetRows;
|
||||||
|
final int previousOutputWidth = _outputWidth;
|
||||||
|
final int previousOutputHeight = _outputHeight;
|
||||||
|
|
||||||
|
final double fitScale = math.min(
|
||||||
|
terminalBuffer.width / _minimumTerminalColumns,
|
||||||
|
terminalBuffer.height / _minimumTerminalRows,
|
||||||
|
);
|
||||||
|
|
||||||
|
final int targetColumns = math.max(
|
||||||
|
1,
|
||||||
|
(_minimumTerminalColumns * fitScale).floor(),
|
||||||
|
);
|
||||||
|
final int targetRows = math.max(
|
||||||
|
1,
|
||||||
|
(_minimumTerminalRows * fitScale).floor(),
|
||||||
|
);
|
||||||
|
|
||||||
|
_offsetColumns = math.max(0, (terminalBuffer.width - targetColumns) ~/ 2);
|
||||||
|
_offsetRows = math.max(0, (terminalBuffer.height - targetRows) ~/ 2);
|
||||||
|
|
||||||
|
final int boundsPixelWidth = math.max(
|
||||||
|
1,
|
||||||
|
(targetColumns * _defaultLineHeightPx * _defaultCellWidthToHeight)
|
||||||
|
.floor(),
|
||||||
|
);
|
||||||
|
final int boundsPixelHeight = math.max(
|
||||||
|
1,
|
||||||
|
targetRows * _defaultLineHeightPx,
|
||||||
|
);
|
||||||
|
|
||||||
|
final double boundsAspect = boundsPixelWidth / boundsPixelHeight;
|
||||||
|
if (boundsAspect > _targetAspectRatio) {
|
||||||
|
_outputHeight = boundsPixelHeight;
|
||||||
|
_outputWidth = math.max(1, (_outputHeight * _targetAspectRatio).floor());
|
||||||
|
} else {
|
||||||
|
_outputWidth = boundsPixelWidth;
|
||||||
|
_outputHeight = math.max(1, (_outputWidth / _targetAspectRatio).floor());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_offsetColumns != previousOffsetColumns ||
|
||||||
|
_offsetRows != previousOffsetRows ||
|
||||||
|
_outputWidth != previousOutputWidth ||
|
||||||
|
_outputHeight != previousOutputHeight) {
|
||||||
|
_needsBackgroundClear = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final double renderScale = math.min(
|
||||||
|
1.0,
|
||||||
|
math.min(
|
||||||
|
_maxRenderWidth / _outputWidth,
|
||||||
|
_maxRenderHeight / _outputHeight,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final int renderWidth = math.max(1, (_outputWidth * renderScale).floor());
|
||||||
|
final int renderHeight = math.max(1, (_outputHeight * renderScale).floor());
|
||||||
|
|
||||||
|
return FrameBuffer(renderWidth, renderHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String render(WolfEngine engine) {
|
||||||
|
_engine = engine;
|
||||||
|
final FrameBuffer originalBuffer = engine.frameBuffer;
|
||||||
|
final FrameBuffer scaledBuffer = _createScaledBuffer(originalBuffer);
|
||||||
|
// We only need 8-bit indices for the 256 VGA colors
|
||||||
|
_screen = Uint8List(scaledBuffer.width * scaledBuffer.height);
|
||||||
|
engine.frameBuffer = scaledBuffer;
|
||||||
|
try {
|
||||||
|
return super.render(engine);
|
||||||
|
} finally {
|
||||||
|
engine.frameBuffer = originalBuffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void prepareFrame(WolfEngine engine) {
|
||||||
|
// Top half is ceiling color index (25), bottom half is floor color index (29)
|
||||||
|
for (int y = 0; y < viewHeight; y++) {
|
||||||
|
int colorIndex = (y < viewHeight / 2) ? 25 : 29;
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
_screen[y * width + x] = colorIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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++) {
|
||||||
|
double relativeY =
|
||||||
|
(y - (-columnHeight ~/ 2 + viewHeight ~/ 2)) / columnHeight;
|
||||||
|
int texY = (relativeY * 64).toInt().clamp(0, 63);
|
||||||
|
|
||||||
|
int colorByte = texture.pixels[texX * 64 + texY];
|
||||||
|
|
||||||
|
// Note: Directional shading is omitted here to preserve strict VGA palette indices.
|
||||||
|
// Sixel uses a fixed 256-color palette, so real-time shading requires a lookup table.
|
||||||
|
_screen[y * width + x] = colorByte;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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
|
||||||
|
if (colorByte != 255) {
|
||||||
|
_screen[y * width + stripeX] = 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 * width + drawX] = colorByte;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void drawHud(WolfEngine engine) {
|
||||||
|
int statusBarIndex = engine.data.vgaImages.indexWhere(
|
||||||
|
(img) => img.width == 320 && img.height == 40,
|
||||||
|
);
|
||||||
|
if (statusBarIndex == -1) return;
|
||||||
|
|
||||||
|
_blitVgaImage(engine.data.vgaImages[statusBarIndex], 0, 160);
|
||||||
|
|
||||||
|
_drawNumber(1, 32, 176, engine.data.vgaImages);
|
||||||
|
_drawNumber(engine.player.score, 96, 176, engine.data.vgaImages);
|
||||||
|
_drawNumber(3, 120, 176, engine.data.vgaImages);
|
||||||
|
_drawNumber(engine.player.health, 192, 176, engine.data.vgaImages);
|
||||||
|
_drawNumber(engine.player.ammo, 232, 176, engine.data.vgaImages);
|
||||||
|
|
||||||
|
_drawFace(engine);
|
||||||
|
_drawWeaponIcon(engine);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String finalizeFrame() {
|
||||||
|
final String clearPrefix = _needsBackgroundClear
|
||||||
|
? '$_terminalTealBackground\x1b[2J\x1b[0m'
|
||||||
|
: '';
|
||||||
|
_needsBackgroundClear = false;
|
||||||
|
return '$clearPrefix\x1b[${_offsetRows + 1};${_offsetColumns + 1}H${toSixelString()}';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// SIXEL ENCODER
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/// Converts the 8-bit index buffer into a standard Sixel sequence
|
||||||
|
String toSixelString() {
|
||||||
|
StringBuffer sb = StringBuffer();
|
||||||
|
|
||||||
|
// Start Sixel sequence (q = Sixel format)
|
||||||
|
sb.write('\x1bPq');
|
||||||
|
|
||||||
|
// 1. Define the Palette (and apply damage flash directly to the palette!)
|
||||||
|
double damageIntensity = _engine.player.damageFlash;
|
||||||
|
int redBoost = (150 * damageIntensity).toInt();
|
||||||
|
double colorDrop = 1.0 - (0.5 * damageIntensity);
|
||||||
|
|
||||||
|
for (int i = 0; i < 256; i++) {
|
||||||
|
int color = ColorPalette.vga32Bit[i];
|
||||||
|
int r = color & 0xFF;
|
||||||
|
int g = (color >> 8) & 0xFF;
|
||||||
|
int b = (color >> 16) & 0xFF;
|
||||||
|
|
||||||
|
if (damageIntensity > 0) {
|
||||||
|
r = (r + redBoost).clamp(0, 255);
|
||||||
|
g = (g * colorDrop).toInt().clamp(0, 255);
|
||||||
|
b = (b * colorDrop).toInt().clamp(0, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sixel RGB ranges from 0 to 100
|
||||||
|
int sixelR = (r * 100) ~/ 255;
|
||||||
|
int sixelG = (g * 100) ~/ 255;
|
||||||
|
int sixelB = (b * 100) ~/ 255;
|
||||||
|
|
||||||
|
sb.write('#$i;2;$sixelR;$sixelG;$sixelB');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Encode scaled image in 6-pixel vertical bands.
|
||||||
|
for (int band = 0; band < _outputHeight; band += 6) {
|
||||||
|
Map<int, Uint8List> colorMap = {};
|
||||||
|
|
||||||
|
// Map out which pixels use which color in this 6px high band
|
||||||
|
for (int x = 0; x < _outputWidth; x++) {
|
||||||
|
for (int yOffset = 0; yOffset < 6; yOffset++) {
|
||||||
|
int y = band + yOffset;
|
||||||
|
if (y >= _outputHeight) break;
|
||||||
|
|
||||||
|
int colorIdx = _sampleScaledPixel(x, y);
|
||||||
|
if (!colorMap.containsKey(colorIdx)) {
|
||||||
|
colorMap[colorIdx] = Uint8List(_outputWidth);
|
||||||
|
}
|
||||||
|
// Set the bit corresponding to the vertical position (0-5)
|
||||||
|
colorMap[colorIdx]![x] |= (1 << yOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the encoded Sixel characters for each color present in the band
|
||||||
|
bool firstColor = true;
|
||||||
|
for (var entry in colorMap.entries) {
|
||||||
|
if (!firstColor) {
|
||||||
|
// Carriage return to overlay colors on the same band
|
||||||
|
sb.write('\$');
|
||||||
|
}
|
||||||
|
firstColor = false;
|
||||||
|
|
||||||
|
// Select color index
|
||||||
|
sb.write('#${entry.key}');
|
||||||
|
|
||||||
|
Uint8List cols = entry.value;
|
||||||
|
int currentVal = -1;
|
||||||
|
int runLength = 0;
|
||||||
|
|
||||||
|
// Run-Length Encoding (RLE) loop
|
||||||
|
for (int x = 0; x < _outputWidth; x++) {
|
||||||
|
int val = cols[x];
|
||||||
|
if (val == currentVal) {
|
||||||
|
runLength++;
|
||||||
|
} else {
|
||||||
|
if (runLength > 0) _writeSixelRle(sb, currentVal, runLength);
|
||||||
|
currentVal = val;
|
||||||
|
runLength = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (runLength > 0) _writeSixelRle(sb, currentVal, runLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (band + 6 < _outputHeight) {
|
||||||
|
sb.write('-');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// End Sixel sequence
|
||||||
|
sb.write('\x1b\\');
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
int _sampleScaledPixel(int outX, int outY) {
|
||||||
|
final int srcX = ((((outX + 0.5) * width) / _outputWidth) - 0.5)
|
||||||
|
.round()
|
||||||
|
.clamp(
|
||||||
|
0,
|
||||||
|
width - 1,
|
||||||
|
);
|
||||||
|
final int srcY = ((((outY + 0.5) * height) / _outputHeight) - 0.5)
|
||||||
|
.round()
|
||||||
|
.clamp(
|
||||||
|
0,
|
||||||
|
height - 1,
|
||||||
|
);
|
||||||
|
return _screen[srcY * width + srcX];
|
||||||
|
}
|
||||||
|
|
||||||
|
void _writeSixelRle(StringBuffer sb, int value, int runLength) {
|
||||||
|
String char = String.fromCharCode(value + 63);
|
||||||
|
// Sixel RLE format: !<count><char> (only worth it if count > 3)
|
||||||
|
if (runLength > 3) {
|
||||||
|
sb.write('!$runLength$char');
|
||||||
|
} else {
|
||||||
|
sb.write(char * runLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// PRIVATE HUD HELPERS (Adapted for 8-bit index buffer)
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
void _blitVgaImage(VgaImage image, int startX, int startY) {
|
||||||
|
int planeWidth = image.width ~/ 4;
|
||||||
|
int planeSize = planeWidth * image.height;
|
||||||
|
final double scaleX = width / 320.0;
|
||||||
|
final double scaleY = height / 200.0;
|
||||||
|
|
||||||
|
final int destStartX = (startX * scaleX).toInt();
|
||||||
|
final int destStartY = (startY * scaleY).toInt();
|
||||||
|
final int destWidth = math.max(1, (image.width * scaleX).toInt());
|
||||||
|
final int destHeight = math.max(1, (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) {
|
||||||
|
_screen[drawY * width + drawX] = 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 = (health <= 0)
|
||||||
|
? 127
|
||||||
|
: 106 + (((100 - health) ~/ 16).clamp(0, 6) * 3);
|
||||||
|
if (faceIndex < engine.data.vgaImages.length) {
|
||||||
|
_blitVgaImage(engine.data.vgaImages[faceIndex], 136, 164);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _drawWeaponIcon(WolfEngine engine) {
|
||||||
|
int weaponIndex = 89;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:wolf_3d_dart/src/rasterizer/rasterizer.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
|
|
||||||
|
class SoftwareRasterizer extends Rasterizer<FrameBuffer> {
|
||||||
|
late FrameBuffer _buffer;
|
||||||
|
late WolfEngine _engine;
|
||||||
|
|
||||||
|
// Intercept the base render call to store our references
|
||||||
|
@override
|
||||||
|
FrameBuffer render(WolfEngine engine) {
|
||||||
|
_engine = engine;
|
||||||
|
_buffer = engine.frameBuffer;
|
||||||
|
return super.render(engine);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,10 +9,4 @@ export 'src/engine/input/engine_input.dart';
|
|||||||
export 'src/engine/managers/door_manager.dart';
|
export 'src/engine/managers/door_manager.dart';
|
||||||
export 'src/engine/managers/pushwall_manager.dart';
|
export 'src/engine/managers/pushwall_manager.dart';
|
||||||
export 'src/engine/player/player.dart';
|
export 'src/engine/player/player.dart';
|
||||||
export 'src/engine/rasterizer/ascii_rasterizer.dart'
|
|
||||||
show AsciiRasterizer, ColoredChar;
|
|
||||||
export 'src/engine/rasterizer/cli_rasterizer.dart';
|
|
||||||
export 'src/engine/rasterizer/rasterizer.dart';
|
|
||||||
export 'src/engine/rasterizer/sixel_rasterizer.dart';
|
|
||||||
export 'src/engine/rasterizer/software_rasterizer.dart';
|
|
||||||
export 'src/engine/wolf_3d_engine_base.dart';
|
export 'src/engine/wolf_3d_engine_base.dart';
|
||||||
|
|||||||
7
packages/wolf_3d_dart/lib/wolf_3d_rasterizer.dart
Normal file
7
packages/wolf_3d_dart/lib/wolf_3d_rasterizer.dart
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
library;
|
||||||
|
|
||||||
|
export 'src/rasterizer/ascii_rasterizer.dart' show AsciiRasterizer, ColoredChar;
|
||||||
|
export 'src/rasterizer/cli_rasterizer.dart';
|
||||||
|
export 'src/rasterizer/rasterizer.dart';
|
||||||
|
export 'src/rasterizer/sixel_rasterizer.dart';
|
||||||
|
export 'src/rasterizer/software_rasterizer.dart';
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_input.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_rasterizer.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Pushwall rasterization', () {
|
||||||
|
test('active pushwall occludes the wall behind it while sliding', () {
|
||||||
|
final wallGrid = _buildGrid();
|
||||||
|
final objectGrid = _buildGrid();
|
||||||
|
|
||||||
|
_fillBoundaries(wallGrid, 2);
|
||||||
|
objectGrid[2][2] = MapObject.playerEast;
|
||||||
|
|
||||||
|
wallGrid[2][4] = 1;
|
||||||
|
objectGrid[2][4] = MapObject.pushwallTrigger;
|
||||||
|
|
||||||
|
wallGrid[2][6] = 2;
|
||||||
|
|
||||||
|
final engine = WolfEngine(
|
||||||
|
data: WolfensteinData(
|
||||||
|
version: GameVersion.shareware,
|
||||||
|
walls: [
|
||||||
|
_solidSprite(1),
|
||||||
|
_solidSprite(1),
|
||||||
|
_solidSprite(2),
|
||||||
|
_solidSprite(2),
|
||||||
|
],
|
||||||
|
sprites: List.generate(436, (_) => _solidSprite(255)),
|
||||||
|
sounds: [],
|
||||||
|
adLibSounds: [],
|
||||||
|
music: [],
|
||||||
|
vgaImages: [],
|
||||||
|
episodes: [
|
||||||
|
Episode(
|
||||||
|
name: 'Episode 1',
|
||||||
|
levels: [
|
||||||
|
WolfLevel(
|
||||||
|
name: 'Test Level',
|
||||||
|
wallGrid: wallGrid,
|
||||||
|
objectGrid: objectGrid,
|
||||||
|
musicIndex: 0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
difficulty: Difficulty.medium,
|
||||||
|
startingEpisode: 0,
|
||||||
|
frameBuffer: FrameBuffer(64, 64),
|
||||||
|
input: CliInput(),
|
||||||
|
onGameWon: () {},
|
||||||
|
);
|
||||||
|
|
||||||
|
engine.init();
|
||||||
|
|
||||||
|
final pushwall = engine.pushwallManager.pushwalls['4,2']!;
|
||||||
|
pushwall
|
||||||
|
..dirX = 1
|
||||||
|
..dirY = 0
|
||||||
|
..offset = 0.5;
|
||||||
|
engine.pushwallManager.activePushwall = pushwall;
|
||||||
|
|
||||||
|
final frame = SoftwareRasterizer().render(engine);
|
||||||
|
final centerIndex =
|
||||||
|
(frame.height ~/ 2) * frame.width + (frame.width ~/ 2);
|
||||||
|
|
||||||
|
expect(frame.pixels[centerIndex], ColorPalette.vga32Bit[1]);
|
||||||
|
expect(frame.pixels[centerIndex], isNot(ColorPalette.vga32Bit[2]));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
SpriteMap _buildGrid() => List.generate(64, (_) => List.filled(64, 0));
|
||||||
|
|
||||||
|
void _fillBoundaries(SpriteMap grid, int wallId) {
|
||||||
|
for (int i = 0; i < 64; i++) {
|
||||||
|
grid[0][i] = wallId;
|
||||||
|
grid[63][i] = wallId;
|
||||||
|
grid[i][0] = wallId;
|
||||||
|
grid[i][63] = wallId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Sprite _solidSprite(int colorIndex) {
|
||||||
|
return Sprite(Uint8List.fromList(List.filled(64 * 64, colorIndex)));
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
import 'package:wolf_3d_dart/wolf_3d_rasterizer.dart';
|
||||||
import 'package:wolf_3d_renderer/base_renderer.dart';
|
import 'package:wolf_3d_renderer/base_renderer.dart';
|
||||||
|
|
||||||
class WolfAsciiRenderer extends BaseWolfRenderer {
|
class WolfAsciiRenderer extends BaseWolfRenderer {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'dart:ui' as ui;
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
import 'package:wolf_3d_dart/wolf_3d_rasterizer.dart';
|
||||||
import 'package:wolf_3d_renderer/base_renderer.dart';
|
import 'package:wolf_3d_renderer/base_renderer.dart';
|
||||||
import 'package:wolf_3d_renderer/wolf_3d_asset_painter.dart';
|
import 'package:wolf_3d_renderer/wolf_3d_asset_painter.dart';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user