diff --git a/apps/wolf_3d_cli/bin/main.dart b/apps/wolf_3d_cli/bin/main.dart index 33eab19..0642d75 100644 --- a/apps/wolf_3d_cli/bin/main.dart +++ b/apps/wolf_3d_cli/bin/main.dart @@ -38,9 +38,9 @@ void main() async { final AsciiRasterizer asciiRasterizer = AsciiRasterizer(isTerminal: true); final SixelRasterizer sixelRasterizer = SixelRasterizer(); - Rasterizer rasterizer = sixelRasterizer; + CliRasterizer rasterizer = sixelRasterizer; - FrameBuffer buffer = FrameBuffer( + final FrameBuffer initialFrameBuffer = FrameBuffer( stdout.terminalColumns, stdout.terminalLines, ); @@ -49,6 +49,7 @@ void main() async { data: availableGames.values.first, difficulty: Difficulty.medium, startingEpisode: 0, + frameBuffer: initialFrameBuffer, audio: CliSilentAudio(), input: CliInput(), onGameWon: () { @@ -83,24 +84,17 @@ void main() async { if (stdout.hasTerminal) { int cols = stdout.terminalColumns; int rows = stdout.terminalLines; - if (!rasterizer.isTerminalSizeSupported(cols, rows)) { + 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('\x1b[31m[ ERROR ] TERMINAL TOO SMALL\x1b[0m\n\n'); - stdout.write('${rasterizer.terminalSizeRequirement}\n'); stdout.write( - 'Current size: \x1b[33m${stdout.terminalColumns}x${stdout.terminalLines}\x1b[0m\n\n', + rasterizer.buildTerminalSizeWarning(columns: cols, rows: rows), ); - stdout.write('Please resize your window to resume the game...'); // Prevent the engine from simulating a massive time jump when resumed lastTick = stopwatch.elapsed; return; } - - if (buffer.width != cols || buffer.height != rows) { - buffer = FrameBuffer(cols, rows); - } } // 2. Normal Game Loop @@ -112,8 +106,6 @@ void main() async { stdout.write('\x1b[H'); engine.tick(elapsed); - rasterizer.render(engine, buffer); - - stdout.write(rasterizer.finalizeFrame()); + stdout.write(rasterizer.render(engine)); }); } diff --git a/apps/wolf_3d_gui/lib/screens/game_screen.dart b/apps/wolf_3d_gui/lib/screens/game_screen.dart index cf4ad63..b73da3e 100644 --- a/apps/wolf_3d_gui/lib/screens/game_screen.dart +++ b/apps/wolf_3d_gui/lib/screens/game_screen.dart @@ -37,6 +37,7 @@ class _GameScreenState extends State { data: widget.data, difficulty: widget.difficulty, startingEpisode: widget.startingEpisode, + frameBuffer: FrameBuffer(320, 200), audio: widget.audio, input: widget.input, onGameWon: () => Navigator.of(context).pop(), diff --git a/packages/wolf_3d_dart/lib/src/engine/rasterizer/ascii_rasterizer.dart b/packages/wolf_3d_dart/lib/src/engine/rasterizer/ascii_rasterizer.dart index 2942f5d..b65c94e 100644 --- a/packages/wolf_3d_dart/lib/src/engine/rasterizer/ascii_rasterizer.dart +++ b/packages/wolf_3d_dart/lib/src/engine/rasterizer/ascii_rasterizer.dart @@ -59,7 +59,7 @@ class ColoredChar { int get argb => (0xFF000000) | (r << 16) | (g << 8) | b; } -class AsciiRasterizer extends Rasterizer { +class AsciiRasterizer extends CliRasterizer { static const double _targetAspectRatio = 4 / 3; static const int _terminalBackdropArgb = 0xFF009688; static const int _minimumTerminalColumns = 80; @@ -121,14 +121,16 @@ class AsciiRasterizer extends Rasterizer { // Intercept the base render call to initialize our text grid @override - dynamic render(WolfEngine engine, FrameBuffer buffer) { + dynamic render(WolfEngine engine) { _engine = engine; _screen = List.generate( - buffer.height, - (_) => - List.filled(buffer.width, ColoredChar(' ', ColorPalette.vga32Bit[0])), + engine.frameBuffer.height, + (_) => List.filled( + engine.frameBuffer.width, + ColoredChar(' ', ColorPalette.vga32Bit[0]), + ), ); - return super.render(engine, buffer); + return super.render(engine); } @override @@ -630,7 +632,7 @@ class AsciiRasterizer extends Rasterizer { } @override - String finalizeFrame() { + dynamic finalizeFrame() { if (_engine.player.damageFlash > 0.0) { if (isTerminal) { _applyDamageFlashToScene(); @@ -640,8 +642,9 @@ class AsciiRasterizer extends Rasterizer { } if (isTerminal) { _composeTerminalScene(); + return toAnsiString(); } - return toAnsiString(); + return _screen; } // --- PRIVATE HUD DRAWING HELPERS --- @@ -787,7 +790,7 @@ class AsciiRasterizer extends Rasterizer { } /// Converts the current frame to a single printable ANSI string - String toAnsiString() { + StringBuffer toAnsiString() { StringBuffer buffer = StringBuffer(); int? lastForeground; @@ -828,6 +831,6 @@ class AsciiRasterizer extends Rasterizer { // Reset the terminal color at the very end buffer.write('\x1b[0m'); - return buffer.toString(); + return buffer; } } diff --git a/packages/wolf_3d_dart/lib/src/engine/rasterizer/cli_rasterizer.dart b/packages/wolf_3d_dart/lib/src/engine/rasterizer/cli_rasterizer.dart new file mode 100644 index 0000000..ce43030 --- /dev/null +++ b/packages/wolf_3d_dart/lib/src/engine/rasterizer/cli_rasterizer.dart @@ -0,0 +1,36 @@ +import 'package:wolf_3d_dart/wolf_3d_engine.dart'; + +/// Shared terminal orchestration for CLI rasterizers. +abstract class CliRasterizer extends Rasterizer { + /// 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...'; + } +} diff --git a/packages/wolf_3d_dart/lib/src/engine/rasterizer/rasterizer.dart b/packages/wolf_3d_dart/lib/src/engine/rasterizer/rasterizer.dart index 250a87c..de63473 100644 --- a/packages/wolf_3d_dart/lib/src/engine/rasterizer/rasterizer.dart +++ b/packages/wolf_3d_dart/lib/src/engine/rasterizer/rasterizer.dart @@ -4,7 +4,7 @@ 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 { +abstract class Rasterizer { late List zBuffer; late int width; late int height; @@ -40,9 +40,9 @@ abstract class Rasterizer { /// The main entry point called by the game loop. /// Orchestrates the mathematical rendering pipeline. - dynamic render(WolfEngine engine, FrameBuffer buffer) { - width = buffer.width; - height = buffer.height; + 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); @@ -101,7 +101,7 @@ abstract class Rasterizer { void drawHud(WolfEngine engine); /// Return the finished frame (e.g., the FrameBuffer itself, or an ASCII list). - dynamic finalizeFrame(); + T finalizeFrame(); // =========================================================================== // SHARED LIGHTING MATH diff --git a/packages/wolf_3d_dart/lib/src/engine/rasterizer/sixel_rasterizer.dart b/packages/wolf_3d_dart/lib/src/engine/rasterizer/sixel_rasterizer.dart index 92a4236..c33be5f 100644 --- a/packages/wolf_3d_dart/lib/src/engine/rasterizer/sixel_rasterizer.dart +++ b/packages/wolf_3d_dart/lib/src/engine/rasterizer/sixel_rasterizer.dart @@ -4,7 +4,7 @@ import 'dart:typed_data'; import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; import 'package:wolf_3d_dart/wolf_3d_engine.dart'; -class SixelRasterizer extends Rasterizer { +class SixelRasterizer extends CliRasterizer { static const double _targetAspectRatio = 4 / 3; static const int _defaultLineHeightPx = 18; static const double _defaultCellWidthToHeight = 0.55; @@ -85,12 +85,18 @@ class SixelRasterizer extends Rasterizer { } @override - dynamic render(WolfEngine engine, FrameBuffer buffer) { + String render(WolfEngine engine) { _engine = engine; - final FrameBuffer scaledBuffer = _createScaledBuffer(buffer); + 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); - return super.render(engine, scaledBuffer); + engine.frameBuffer = scaledBuffer; + try { + return super.render(engine); + } finally { + engine.frameBuffer = originalBuffer; + } } @override diff --git a/packages/wolf_3d_dart/lib/src/engine/rasterizer/software_rasterizer.dart b/packages/wolf_3d_dart/lib/src/engine/rasterizer/software_rasterizer.dart index 539ed01..bccf246 100644 --- a/packages/wolf_3d_dart/lib/src/engine/rasterizer/software_rasterizer.dart +++ b/packages/wolf_3d_dart/lib/src/engine/rasterizer/software_rasterizer.dart @@ -3,16 +3,16 @@ import 'dart:math' as math; import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; import 'package:wolf_3d_dart/wolf_3d_engine.dart'; -class SoftwareRasterizer extends Rasterizer { +class SoftwareRasterizer extends Rasterizer { late FrameBuffer _buffer; late WolfEngine _engine; // Intercept the base render call to store our references @override - dynamic render(WolfEngine engine, FrameBuffer buffer) { + FrameBuffer render(WolfEngine engine) { _engine = engine; - _buffer = buffer; - return super.render(engine, buffer); + _buffer = engine.frameBuffer; + return super.render(engine); } @override diff --git a/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart b/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart index 17ad316..1503998 100644 --- a/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart +++ b/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart @@ -18,6 +18,7 @@ class WolfEngine { required this.onGameWon, required this.audio, required this.input, + required this.frameBuffer, }) : doorManager = DoorManager( onPlaySound: (sfxId) => audio.playSoundEffect(sfxId), ); @@ -48,6 +49,9 @@ class WolfEngine { /// Polls and processes raw user input into actionable engine commands. final Wolf3dInput input; + /// The shared render target used by all renderer frontends. + FrameBuffer frameBuffer; + /// Handles the detection and movement of secret "Pushwalls". final PushwallManager pushwallManager = PushwallManager(); @@ -86,6 +90,17 @@ class WolfEngine { isInitialized = true; } + /// Replaces the shared framebuffer when dimensions change. + void setFrameBuffer(int width, int height) { + if (width <= 0 || height <= 0) { + throw ArgumentError('FrameBuffer dimensions must be greater than zero.'); + } + if (frameBuffer.width == width && frameBuffer.height == height) { + return; + } + frameBuffer = FrameBuffer(width, height); + } + /// The primary heartbeat of the engine. /// /// Updates all world subsystems based on the [elapsed] time. diff --git a/packages/wolf_3d_dart/lib/wolf_3d_engine.dart b/packages/wolf_3d_dart/lib/wolf_3d_engine.dart index 4952bcf..47aadd2 100644 --- a/packages/wolf_3d_dart/lib/wolf_3d_engine.dart +++ b/packages/wolf_3d_dart/lib/wolf_3d_engine.dart @@ -11,6 +11,7 @@ export 'src/engine/managers/pushwall_manager.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'; diff --git a/packages/wolf_3d_renderer/lib/wolf_3d_ascii_renderer.dart b/packages/wolf_3d_renderer/lib/wolf_3d_ascii_renderer.dart index b9ef415..2a90eb8 100644 --- a/packages/wolf_3d_renderer/lib/wolf_3d_ascii_renderer.dart +++ b/packages/wolf_3d_renderer/lib/wolf_3d_ascii_renderer.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; import 'package:wolf_3d_dart/wolf_3d_engine.dart'; import 'package:wolf_3d_renderer/base_renderer.dart'; @@ -14,19 +13,28 @@ class WolfAsciiRenderer extends BaseWolfRenderer { } class _WolfAsciiRendererState extends BaseWolfRendererState { + static const int _renderWidth = 160; + static const int _renderHeight = 100; + List> _asciiFrame = []; final AsciiRasterizer _asciiRasterizer = AsciiRasterizer(); + @override + void initState() { + super.initState(); + if (widget.engine.frameBuffer.width != _renderWidth || + widget.engine.frameBuffer.height != _renderHeight) { + widget.engine.setFrameBuffer(_renderWidth, _renderHeight); + } + } + @override Color get scaffoldColor => const Color.fromARGB(255, 4, 64, 64); @override void performRender() { setState(() { - _asciiFrame = _asciiRasterizer.render( - widget.engine, - FrameBuffer(160, 100), - ); + _asciiFrame = _asciiRasterizer.render(widget.engine); }); } diff --git a/packages/wolf_3d_renderer/lib/wolf_3d_flutter_renderer.dart b/packages/wolf_3d_renderer/lib/wolf_3d_flutter_renderer.dart index b5764fd..b90a76e 100644 --- a/packages/wolf_3d_renderer/lib/wolf_3d_flutter_renderer.dart +++ b/packages/wolf_3d_renderer/lib/wolf_3d_flutter_renderer.dart @@ -18,12 +18,22 @@ class WolfFlutterRenderer extends BaseWolfRenderer { class _WolfFlutterRendererState extends BaseWolfRendererState { - final FrameBuffer _frameBuffer = FrameBuffer(320, 200); + static const int _renderWidth = 320; + static const int _renderHeight = 200; final SoftwareRasterizer _rasterizer = SoftwareRasterizer(); ui.Image? _renderedFrame; bool _isRendering = false; + @override + void initState() { + super.initState(); + if (widget.engine.frameBuffer.width != _renderWidth || + widget.engine.frameBuffer.height != _renderHeight) { + widget.engine.setFrameBuffer(_renderWidth, _renderHeight); + } + } + @override Color get scaffoldColor => const Color.fromARGB(255, 4, 64, 64); @@ -32,12 +42,13 @@ class _WolfFlutterRendererState if (_isRendering) return; _isRendering = true; - _rasterizer.render(widget.engine, _frameBuffer); + final FrameBuffer frameBuffer = widget.engine.frameBuffer; + _rasterizer.render(widget.engine); ui.decodeImageFromPixels( - _frameBuffer.pixels.buffer.asUint8List(), - _frameBuffer.width, - _frameBuffer.height, + frameBuffer.pixels.buffer.asUint8List(), + frameBuffer.width, + frameBuffer.height, ui.PixelFormat.rgba8888, (ui.Image image) { if (mounted) {