diff --git a/apps/wolf_3d_cli/bin/main.dart b/apps/wolf_3d_cli/bin/main.dart index 6bce90e..c502eb9 100644 --- a/apps/wolf_3d_cli/bin/main.dart +++ b/apps/wolf_3d_cli/bin/main.dart @@ -1,3 +1,9 @@ +/// CLI entry point for the terminal Wolf3D host. +/// +/// This executable locates bundled retail assets, constructs a [WolfEngine] +/// configured for terminal rendering, and then hands control to [CliGameLoop]. +library; + import 'dart:io'; import 'package:wolf_3d_cli/cli_game_loop.dart'; @@ -6,7 +12,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_input.dart'; -// Helper to gracefully exit and restore the terminal +/// Restores terminal state before exiting the process with [code]. void exitCleanly(int code) { stdout.write('\x1b[0m'); // Reset color stdout.write('\x1b[2J\x1b[H'); // Clear screen @@ -14,12 +20,13 @@ void exitCleanly(int code) { exit(code); } +/// Launches the CLI renderer against the bundled retail asset set. void main() async { stdout.write("Discovering game data..."); - // 1. Get the absolute URI of where this exact script lives + // Resolve the asset package relative to this executable so the CLI can run + // from the repo without additional configuration. final scriptUri = Platform.script; - // 2. Resolve the path mathematically. final targetUri = scriptUri.resolve( '../../../packages/wolf_3d_assets/assets/retail', ); diff --git a/apps/wolf_3d_cli/lib/cli_game_loop.dart b/apps/wolf_3d_cli/lib/cli_game_loop.dart index 70e112d..f50a7c8 100644 --- a/apps/wolf_3d_cli/lib/cli_game_loop.dart +++ b/apps/wolf_3d_cli/lib/cli_game_loop.dart @@ -1,3 +1,6 @@ +/// Terminal game loop that ties engine ticks, raw input, and CLI rendering together. +library; + import 'dart:async'; import 'dart:io'; @@ -5,6 +8,11 @@ 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'; +/// Runs the Wolf3D engine inside a terminal using CLI-specific rasterizers. +/// +/// The loop owns raw-stdin handling, renderer switching, terminal size checks, +/// and frame pacing. It expects [engine.input] to be a [CliInput] instance so +/// raw key bytes can be queued directly into the engine input adapter. class CliGameLoop { CliGameLoop({ required this.engine, @@ -35,6 +43,7 @@ class CliGameLoop { bool _isRunning = false; Duration _lastTick = Duration.zero; + /// Starts terminal probing, enters raw input mode, and begins the frame timer. Future start() async { if (_isRunning) { return; @@ -64,6 +73,7 @@ class CliGameLoop { _isRunning = true; } + /// Stops the timer, unsubscribes from stdin, and restores terminal settings. void stop() { if (!_isRunning) { return; @@ -102,6 +112,8 @@ class CliGameLoop { } if (bytes.contains(9)) { + // Tab swaps between rasterizers so renderer debugging stays available + // without restarting the process. _rasterizer = identical(_rasterizer, secondaryRasterizer) ? primaryRasterizer : secondaryRasterizer; @@ -125,6 +137,8 @@ class CliGameLoop { columns: cols, rows: rows, )) { + // Size warnings are rendered instead of running the simulation so the + // game does not keep advancing while the user resizes the terminal. stdout.write('\x1b[2J\x1b[H'); stdout.write( _rasterizer.buildTerminalSizeWarning(columns: cols, rows: rows), diff --git a/apps/wolf_3d_gui/lib/main.dart b/apps/wolf_3d_gui/lib/main.dart index bdd8fa5..8ebe414 100644 --- a/apps/wolf_3d_gui/lib/main.dart +++ b/apps/wolf_3d_gui/lib/main.dart @@ -1,7 +1,14 @@ +/// Flutter entry point for the GUI host application. +/// +/// The GUI bootstraps bundled and discoverable game data through [Wolf3d] +/// before presenting the game-selection flow. +library; + import 'package:flutter/material.dart'; -import 'package:wolf_3d_flutter/wolf_3d.dart'; +import 'package:wolf_3d_flutter/wolf_3d_flutter.dart'; import 'package:wolf_3d_gui/screens/game_select_screen.dart'; +/// Creates the application shell after loading available Wolf3D data sets. void main() async { WidgetsFlutterBinding.ensureInitialized(); diff --git a/apps/wolf_3d_gui/lib/screens/difficulty_screen.dart b/apps/wolf_3d_gui/lib/screens/difficulty_screen.dart index b6bc80d..c9dfe89 100644 --- a/apps/wolf_3d_gui/lib/screens/difficulty_screen.dart +++ b/apps/wolf_3d_gui/lib/screens/difficulty_screen.dart @@ -1,11 +1,17 @@ +/// Difficulty picker shown after the player chooses an episode. +library; + import 'package:flutter/material.dart'; import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; -import 'package:wolf_3d_flutter/wolf_3d.dart'; +import 'package:wolf_3d_flutter/wolf_3d_flutter.dart'; import 'package:wolf_3d_gui/screens/game_screen.dart'; +/// Starts a new game session using the active game and episode from [Wolf3d]. class DifficultyScreen extends StatefulWidget { + /// Shared application facade carrying the active game, input, and audio. final Wolf3d wolf3d; + /// Creates the difficulty-selection screen for [wolf3d]. const DifficultyScreen({ super.key, required this.wolf3d, @@ -25,6 +31,7 @@ class _DifficultyScreenState extends State { super.dispose(); } + /// Replaces the menu flow with an active [GameScreen] using [difficulty]. void _startGame(Difficulty difficulty, {bool showGallery = false}) { widget.wolf3d.audio.stopMusic(); diff --git a/apps/wolf_3d_gui/lib/screens/episode_screen.dart b/apps/wolf_3d_gui/lib/screens/episode_screen.dart index 810bbe6..1361652 100644 --- a/apps/wolf_3d_gui/lib/screens/episode_screen.dart +++ b/apps/wolf_3d_gui/lib/screens/episode_screen.dart @@ -1,13 +1,19 @@ +/// Episode picker and asset-browser entry point for the selected game version. +library; + import 'package:flutter/material.dart'; import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; -import 'package:wolf_3d_flutter/wolf_3d.dart'; +import 'package:wolf_3d_flutter/wolf_3d_flutter.dart'; import 'package:wolf_3d_gui/screens/difficulty_screen.dart'; import 'package:wolf_3d_gui/screens/sprite_gallery.dart'; import 'package:wolf_3d_gui/screens/vga_gallery.dart'; +/// Presents the episode list and shortcuts into the asset gallery screens. class EpisodeScreen extends StatefulWidget { + /// Shared application facade whose active game must already be set. final Wolf3d wolf3d; + /// Creates the episode-selection screen for [wolf3d]. const EpisodeScreen({super.key, required this.wolf3d}); @override @@ -21,6 +27,7 @@ class _EpisodeScreenState extends State { widget.wolf3d.audio.playMenuMusic(); } + /// Persists the chosen episode in [Wolf3d] and advances to difficulty select. void _selectEpisode(int index) { widget.wolf3d.setActiveEpisode(index); Navigator.of(context).push( diff --git a/apps/wolf_3d_gui/lib/screens/game_screen.dart b/apps/wolf_3d_gui/lib/screens/game_screen.dart index b73da3e..bd39271 100644 --- a/apps/wolf_3d_gui/lib/screens/game_screen.dart +++ b/apps/wolf_3d_gui/lib/screens/game_screen.dart @@ -1,3 +1,6 @@ +/// Active gameplay screen for the Flutter host. +library; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; @@ -6,13 +9,24 @@ import 'package:wolf_3d_flutter/wolf_3d_input_flutter.dart'; import 'package:wolf_3d_renderer/wolf_3d_ascii_renderer.dart'; import 'package:wolf_3d_renderer/wolf_3d_flutter_renderer.dart'; +/// Owns a [WolfEngine] instance and exposes renderer/input integrations to Flutter. class GameScreen extends StatefulWidget { + /// Fully parsed game data for the selected version. final WolfensteinData data; + + /// Difficulty applied when creating the engine session. final Difficulty difficulty; + + /// Episode index used as the starting world. final int startingEpisode; + + /// Shared audio backend reused across menu and gameplay screens. final EngineAudio audio; + + /// Flutter input adapter that translates widget events into engine input. final Wolf3dFlutterInput input; + /// Creates a gameplay screen with the supplied game session configuration. const GameScreen({ required this.data, required this.difficulty, @@ -55,6 +69,8 @@ class _GameScreenState extends State { onPointerHover: widget.input.onPointerMove, child: Stack( children: [ + // Keep both renderers behind the same engine so mode switching does + // not reset level state or audio playback. _useAsciiMode ? WolfAsciiRenderer(engine: _engine) : WolfFlutterRenderer(engine: _engine), @@ -80,7 +96,7 @@ class _GameScreenState extends State { ), ), - // TAB listener + // Tab toggles the renderer implementation for quick visual debugging. Focus( autofocus: true, onKeyEvent: (node, event) { @@ -94,7 +110,8 @@ class _GameScreenState extends State { child: const SizedBox.shrink(), ), - // Loading Overlay + // A second full-screen overlay keeps the presentation simple while + // the engine is still warming up or decoding the first frame. if (!_engine.isInitialized) Container( color: Colors.black, diff --git a/apps/wolf_3d_gui/lib/screens/game_select_screen.dart b/apps/wolf_3d_gui/lib/screens/game_select_screen.dart index b4cc9d1..8bfa0f9 100644 --- a/apps/wolf_3d_gui/lib/screens/game_select_screen.dart +++ b/apps/wolf_3d_gui/lib/screens/game_select_screen.dart @@ -1,11 +1,17 @@ +/// Game-selection screen shown after the GUI host discovers available assets. +library; + import 'package:flutter/material.dart'; import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; -import 'package:wolf_3d_flutter/wolf_3d.dart'; +import 'package:wolf_3d_flutter/wolf_3d_flutter.dart'; import 'package:wolf_3d_gui/screens/episode_screen.dart'; +/// Lists every discovered data set and lets the user choose the active one. class GameSelectScreen extends StatelessWidget { + /// Shared application facade that owns discovered games, audio, and input. final Wolf3d wolf3d; + /// Creates the game-selection screen for the supplied [wolf3d] session. const GameSelectScreen({super.key, required this.wolf3d}); @override diff --git a/apps/wolf_3d_gui/lib/screens/sprite_gallery.dart b/apps/wolf_3d_gui/lib/screens/sprite_gallery.dart index 777cb2e..92a4a59 100644 --- a/apps/wolf_3d_gui/lib/screens/sprite_gallery.dart +++ b/apps/wolf_3d_gui/lib/screens/sprite_gallery.dart @@ -1,12 +1,18 @@ +/// Visual browser for decoded sprite assets and their inferred gameplay roles. +library; + import 'package:flutter/material.dart'; import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; import 'package:wolf_3d_dart/wolf_3d_entities.dart'; -import 'package:wolf_3d_flutter/wolf_3d.dart'; +import 'package:wolf_3d_flutter/wolf_3d_flutter.dart'; import 'package:wolf_3d_renderer/wolf_3d_asset_painter.dart'; +/// Displays every sprite frame in the active game along with enemy metadata. class SpriteGallery extends StatelessWidget { + /// Shared application facade used to access the active game's sprite set. final Wolf3d wolf3d; + /// Creates the sprite gallery for [wolf3d]. const SpriteGallery({super.key, required this.wolf3d}); bool get isShareware => wolf3d.activeGame.version == GameVersion.shareware; @@ -29,6 +35,8 @@ class SpriteGallery extends StatelessWidget { itemBuilder: (context, index) { String label = "Sprite Index: $index"; for (final enemy in EnemyType.values) { + // The gallery infers likely ownership from sprite index ranges so + // debugging art packs does not require cross-referencing source. if (enemy.claimsSpriteIndex(index, isShareware: isShareware)) { final EnemyAnimation? animation = enemy.getAnimationFromSprite( index, diff --git a/apps/wolf_3d_gui/lib/screens/vga_gallery.dart b/apps/wolf_3d_gui/lib/screens/vga_gallery.dart index f4801b8..7066c62 100644 --- a/apps/wolf_3d_gui/lib/screens/vga_gallery.dart +++ b/apps/wolf_3d_gui/lib/screens/vga_gallery.dart @@ -1,10 +1,16 @@ +/// Visual browser for decoded VGA pictures and UI art. +library; + import 'package:flutter/material.dart'; import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; import 'package:wolf_3d_renderer/wolf_3d_asset_painter.dart'; +/// Shows each VGA image extracted from the currently selected game data set. class VgaGallery extends StatelessWidget { + /// Raw VGA images decoded from the active asset pack. final List images; + /// Creates the gallery for [images]. const VgaGallery({super.key, required this.images}); @override diff --git a/packages/wolf_3d_dart/lib/src/input/cli_input.dart b/packages/wolf_3d_dart/lib/src/input/cli_input.dart index 1d9f5cb..cfefebe 100644 --- a/packages/wolf_3d_dart/lib/src/input/cli_input.dart +++ b/packages/wolf_3d_dart/lib/src/input/cli_input.dart @@ -1,8 +1,13 @@ +/// CLI-specific input adapter that converts raw key bytes into engine actions. +library; + import 'package:wolf_3d_dart/src/input/wolf_3d_input.dart'; import 'package:wolf_3d_dart/wolf_3d_entities.dart'; +/// Buffers one-frame terminal key presses for consumption by the engine loop. class CliInput extends Wolf3dInput { - // Pending buffer for asynchronous stdin events + // Raw stdin arrives asynchronously, so presses are staged here until the + // next engine frame snapshots them into the active state. bool _pForward = false; bool _pBackward = false; bool _pLeft = false; @@ -11,7 +16,7 @@ class CliInput extends Wolf3dInput { bool _pInteract = false; WeaponType? _pWeapon; - /// Call this directly from the stdin listener to queue inputs for the next frame + /// Queues a raw terminal key sequence for the next engine frame. void handleKey(List bytes) { String char = String.fromCharCodes(bytes).toLowerCase(); @@ -20,7 +25,8 @@ class CliInput extends Wolf3dInput { if (char == 'a') _pLeft = true; if (char == 'd') _pRight = true; - // --- NEW MAPPINGS --- + // Fire and interact stay on separate keys so the terminal host can avoid + // ambiguous control sequences used by some shells and terminals. if (char == 'j') _pFire = true; if (char == ' ') _pInteract = true; @@ -32,7 +38,7 @@ class CliInput extends Wolf3dInput { @override void update() { - // 1. Move pending inputs to the active state + // Promote buffered presses into the engine-visible state for this frame. isMovingForward = _pForward; isMovingBackward = _pBackward; isTurningLeft = _pLeft; @@ -41,7 +47,7 @@ class CliInput extends Wolf3dInput { isInteracting = _pInteract; requestedWeapon = _pWeapon; - // 2. Wipe the pending slate clean for the next frame + // Reset the pending buffer so each keypress behaves like a frame impulse. _pForward = _pBackward = _pLeft = _pRight = _pFire = _pInteract = false; _pWeapon = null; } diff --git a/packages/wolf_3d_dart/lib/src/rasterizer/sixel_rasterizer.dart b/packages/wolf_3d_dart/lib/src/rasterizer/sixel_rasterizer.dart index d9b3ece..f1739e4 100644 --- a/packages/wolf_3d_dart/lib/src/rasterizer/sixel_rasterizer.dart +++ b/packages/wolf_3d_dart/lib/src/rasterizer/sixel_rasterizer.dart @@ -1,3 +1,6 @@ +/// Terminal rasterizer that encodes engine frames as Sixel graphics. +library; + import 'dart:async'; import 'dart:io'; import 'dart:math' as math; @@ -8,6 +11,11 @@ import 'package:wolf_3d_dart/wolf_3d_engine.dart'; import 'cli_rasterizer.dart'; +/// Renders the game into an indexed off-screen buffer and emits Sixel output. +/// +/// The rasterizer adapts the engine framebuffer to the current terminal size, +/// preserving a 4:3 presentation while falling back to size warnings when the +/// terminal is too small. class SixelRasterizer extends CliRasterizer { static const double _targetAspectRatio = 4 / 3; static const int _defaultLineHeightPx = 18; @@ -125,12 +133,15 @@ class SixelRasterizer extends CliRasterizer { // RENDERING ENGINE // =========================================================================== + /// Builds a temporary framebuffer sized to the drawable terminal region. FrameBuffer _createScaledBuffer(FrameBuffer terminalBuffer) { final int previousOffsetColumns = _offsetColumns; final int previousOffsetRows = _offsetRows; final int previousOutputWidth = _outputWidth; final int previousOutputHeight = _outputHeight; + // First fit a terminal cell rectangle that respects the minimum usable + // column/row envelope for status text and centered output. final double fitScale = math.min( terminalBuffer.width / _minimumTerminalColumns, terminalBuffer.height / _minimumTerminalRows, @@ -158,6 +169,8 @@ class SixelRasterizer extends CliRasterizer { targetRows * _defaultLineHeightPx, ); + // Then translate terminal cells into approximate pixels so the Sixel image + // lands on a 4:3 surface inside the available bounds. final double boundsAspect = boundsPixelWidth / boundsPixelHeight; if (boundsAspect > _targetAspectRatio) { _outputHeight = boundsPixelHeight; @@ -193,7 +206,8 @@ class SixelRasterizer extends CliRasterizer { final FrameBuffer originalBuffer = engine.frameBuffer; final FrameBuffer scaledBuffer = _createScaledBuffer(originalBuffer); - // We only need 8-bit indices for the 256 VGA colors + // Sixel output references palette indices directly, so there is no need to + // materialize a 32-bit RGBA buffer during the rasterization pass. _screen = Uint8List(scaledBuffer.width * scaledBuffer.height); engine.frameBuffer = scaledBuffer; try { diff --git a/packages/wolf_3d_dart/lib/src/synth/opl2_emulator.dart b/packages/wolf_3d_dart/lib/src/synth/opl2_emulator.dart index 63069bb..34445c2 100644 --- a/packages/wolf_3d_dart/lib/src/synth/opl2_emulator.dart +++ b/packages/wolf_3d_dart/lib/src/synth/opl2_emulator.dart @@ -1,7 +1,9 @@ import 'dart:math' as math; +/// States used by the simplified ADSR envelope inside the OPL2 emulator. enum EnvelopeState { off, attack, decay, sustain, release } +/// One OPL2 operator, combining waveform generation and envelope progression. class Opl2Operator { double phase = 0.0; double phaseIncrement = 0.0; @@ -16,24 +18,27 @@ class Opl2Operator { double multiplier = 1.0; double volume = 1.0; - // Waveform Selection (0-3) + /// Selected waveform index as exposed by the OPL2 register set. int waveform = 0; + /// Recomputes oscillator increment from the shared channel base frequency. void updateFrequency(double baseFreq) { phaseIncrement = (baseFreq * multiplier * 2 * math.pi) / Opl2Emulator.sampleRate; } + /// Starts a new note by resetting the phase and entering attack. void triggerOn() { phase = 0.0; envState = EnvelopeState.attack; } + /// Releases the note so the envelope decays back to silence. void triggerOff() { envState = EnvelopeState.release; } - // Applies the OPL2 hardware waveform math + // Waveform handling mirrors the small set of shapes exposed by the OPL2 chip. double _getOscillatorOutput(double currentPhase) { // Normalize phase between 0 and 2*pi double p = currentPhase % (2 * math.pi); @@ -52,6 +57,7 @@ class Opl2Operator { } } + /// Produces one sample for this operator using [phaseOffset] modulation. double getSample(double phaseOffset) { switch (envState) { case EnvelopeState.attack: @@ -83,7 +89,8 @@ class Opl2Operator { return 0.0; } - // Pass the phase + modulation offset into our waveform generator! + // Modulation is expressed as a phase offset, which is how the carrier is + // driven by the modulator in two-operator FM synthesis. double out = _getOscillatorOutput(phase + phaseOffset); phase += phaseIncrement; @@ -93,6 +100,7 @@ class Opl2Operator { } } +/// Two-operator OPL2 channel with optional additive mode and self-feedback. class Opl2Channel { Opl2Operator modulator = Opl2Operator(); Opl2Operator carrier = Opl2Operator(); @@ -107,12 +115,14 @@ class Opl2Channel { double _prevModOutput1 = 0.0; double _prevModOutput2 = 0.0; + /// Updates both operators after frequency register changes. void updateFrequency() { double baseFreq = (fNum * math.pow(2, block)) * (49716.0 / 1048576.0); modulator.updateFrequency(baseFreq); carrier.updateFrequency(baseFreq); } + /// Mixes one audio sample from the channel's current operator state. double getSample() { if (!keyOn && carrier.envState == EnvelopeState.off && @@ -122,6 +132,8 @@ class Opl2Channel { double feedbackPhase = 0.0; if (feedbackStrength > 0) { + // Feedback reuses the previous modulator outputs to create the harsher + // timbres that classic OPL instruments rely on. double averageMod = (_prevModOutput1 + _prevModOutput2) / 2.0; double feedbackFactor = math.pow(2, feedbackStrength - 1) * (math.pi / 16.0); @@ -136,9 +148,11 @@ class Opl2Channel { double channelOutput = 0.0; if (isAdditive) { + // Additive mode mixes both operators as audible oscillators. double carOutput = carrier.getSample(0.0); channelOutput = modOutput + carOutput; } else { + // Standard FM mode feeds the modulator into the carrier's phase. double carOutput = carrier.getSample(modOutput * 2.0); channelOutput = carOutput; } @@ -147,6 +161,7 @@ class Opl2Channel { } } +/// Lightweight pseudo-random noise source for percussion voices. class Opl2Noise { int _seed = 0xFFFF; @@ -158,12 +173,17 @@ class Opl2Noise { } } +/// Simplified register-driven OPL2 emulator used for IMF playback. +/// +/// The implementation focuses on the subset of FM behavior needed by the game +/// assets: melodic channels, rhythm mode, waveform selection, and a practical +/// ADSR envelope approximation. class Opl2Emulator { static const int sampleRate = 44100; bool rhythmMode = false; - // Key states for the 5 drums + // Rhythm mode steals the final three channels and exposes them as drum bits. bool bassDrumKey = false; bool snareDrumKey = false; bool tomTomKey = false; @@ -174,7 +194,7 @@ class Opl2Emulator { final List channels = List.generate(9, (_) => Opl2Channel()); - // The master lock for waveforms + // The chip only honors waveform writes after the global enable bit is set. bool _waveformSelectionEnabled = false; static const List _operatorMap = [ @@ -202,6 +222,7 @@ class Opl2Emulator { 8, ]; + /// Resolves a register offset to the affected operator, if any. Opl2Operator? _getOperator(int offset) { if (offset < 0 || offset >= _operatorMap.length || @@ -215,6 +236,7 @@ class Opl2Emulator { : channels[channelIdx].modulator; } + /// Applies a single OPL2 register write. void writeRegister(int reg, int data) { // --- 0x01: Test / Waveform Enable --- if (reg == 0x01) { @@ -310,6 +332,7 @@ class Opl2Emulator { } } + /// Generates one normalized mono sample from the current register state. double generateSample() { double mixedOutput = 0.0; @@ -319,12 +342,12 @@ class Opl2Emulator { } if (!rhythmMode) { - // Standard mode: play channels 6, 7, and 8 normally + // Standard mode keeps the final channels melodic. for (int i = 6; i < 9; i++) { mixedOutput += channels[i].getSample(); } } else { - // RHYTHM MODE: The last 3 channels are re-routed + // Rhythm mode repurposes the last three channels into drum voices. mixedOutput += _generateBassDrum(); mixedOutput += _generateSnareAndHiHat(); mixedOutput += _generateTomAndCymbal(); @@ -333,13 +356,14 @@ class Opl2Emulator { return mixedOutput.clamp(-1.0, 1.0); } - // Example of Bass Drum logic (Channel 6) + /// Generates the bass drum voice routed through channel 6. double _generateBassDrum() { if (!bassDrumKey) return 0.0; // Bass drum uses standard FM (Mod -> Car) but usually with very low frequency return channels[6].getSample(); } + /// Generates the combined snare and hi-hat voices from channel 7. double _generateSnareAndHiHat() { double snareOut = 0.0; double hiHatOut = 0.0; @@ -363,6 +387,7 @@ class Opl2Emulator { return (snareOut + hiHatOut) * 0.1; } + /// Generates the combined tom and cymbal voices from channel 8. double _generateTomAndCymbal() { double tomOut = 0.0; double cymbalOut = 0.0; diff --git a/packages/wolf_3d_dart/lib/wolf_3d_data.dart b/packages/wolf_3d_dart/lib/wolf_3d_data.dart index 50bf7de..6477e7c 100644 --- a/packages/wolf_3d_dart/lib/wolf_3d_data.dart +++ b/packages/wolf_3d_dart/lib/wolf_3d_data.dart @@ -1,6 +1,8 @@ -/// Support for doing something awesome. +/// Public data-loading exports for Wolfenstein 3D assets. /// -/// More dartdocs go here. +/// This library exposes the low-level parser and the higher-level loader used +/// to discover, validate, and decode original game data files into strongly +/// typed Dart models. library; export 'src/data/wl_parser.dart' show WLParser; diff --git a/packages/wolf_3d_dart/lib/wolf_3d_data_types.dart b/packages/wolf_3d_dart/lib/wolf_3d_data_types.dart index 3076a39..98fab69 100644 --- a/packages/wolf_3d_dart/lib/wolf_3d_data_types.dart +++ b/packages/wolf_3d_dart/lib/wolf_3d_data_types.dart @@ -1,6 +1,8 @@ -/// Support for doing something awesome. +/// Public asset and world model types used by the Wolf3D engine. /// -/// More dartdocs go here. +/// Import this library when you need access to parsed levels, sprites, music, +/// frame buffers, geometry helpers, and version metadata without bringing in +/// the full engine runtime. library; export 'src/data_types/cardinal_direction.dart' show CardinalDirection; diff --git a/packages/wolf_3d_dart/lib/wolf_3d_engine.dart b/packages/wolf_3d_dart/lib/wolf_3d_engine.dart index 396b499..d4da979 100644 --- a/packages/wolf_3d_dart/lib/wolf_3d_engine.dart +++ b/packages/wolf_3d_dart/lib/wolf_3d_engine.dart @@ -1,6 +1,8 @@ -/// Support for doing something awesome. +/// Public engine exports for the Wolfenstein 3D runtime. /// -/// More dartdocs go here. +/// Import this library when building a host around the core simulation. +/// It re-exports the frame-based engine, audio abstractions, input DTOs, +/// state managers, and player model needed by CLI, Flutter, and tests. library; export 'src/engine/audio/engine_audio.dart'; diff --git a/packages/wolf_3d_flutter/lib/audio/audio_adaptor.dart b/packages/wolf_3d_flutter/lib/audio/audio_adaptor.dart index 7d0ce34..24fd2a6 100644 --- a/packages/wolf_3d_flutter/lib/audio/audio_adaptor.dart +++ b/packages/wolf_3d_flutter/lib/audio/audio_adaptor.dart @@ -1,6 +1,6 @@ import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; import 'package:wolf_3d_dart/wolf_3d_engine.dart'; -import 'package:wolf_3d_flutter/wolf_3d.dart'; +import 'package:wolf_3d_flutter/wolf_3d_flutter.dart'; class FlutterAudioAdapter implements EngineAudio { final Wolf3d wolf3d; diff --git a/packages/wolf_3d_flutter/lib/wolf_3d.dart b/packages/wolf_3d_flutter/lib/wolf_3d.dart deleted file mode 100644 index b4ea9d5..0000000 --- a/packages/wolf_3d_flutter/lib/wolf_3d.dart +++ /dev/null @@ -1,131 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.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_engine.dart'; -import 'package:wolf_3d_dart/wolf_3d_synth.dart'; -import 'package:wolf_3d_flutter/wolf_3d_input_flutter.dart'; - -class Wolf3d { - Wolf3d(); - - // --- State --- - final List availableGames = []; - WolfensteinData? _activeGame; - - // --- Core Systems --- - final EngineAudio audio = WolfAudio(); - final Wolf3dFlutterInput input = Wolf3dFlutterInput(); - - // --- Getters --- - WolfensteinData get activeGame { - if (_activeGame == null) { - throw StateError("No active game selected. Call setActiveGame() first."); - } - return _activeGame!; - } - - // --- Episode --- - int _activeEpisode = 0; - - int get activeEpisode => _activeEpisode; - - void setActiveEpisode(int episodeIndex) { - if (_activeGame == null) { - throw StateError("No active game selected. Call setActiveGame() first."); - } - if (episodeIndex < 0 || episodeIndex >= _activeGame!.episodes.length) { - throw RangeError("Episode index out of range for the active game."); - } - - _activeEpisode = episodeIndex; - } - - // Convenience getters for the active game's assets - List get levels => activeGame.episodes[activeEpisode].levels; - List get walls => activeGame.walls; - List get sprites => activeGame.sprites; - List get sounds => activeGame.sounds; - List get adLibSounds => activeGame.adLibSounds; - List get music => activeGame.music; - List get vgaImages => activeGame.vgaImages; - - // --- Actions --- - void setActiveGame(WolfensteinData game) { - if (!availableGames.contains(game)) { - throw ArgumentError( - "The provided game data is not in the list of available games.", - ); - } - - if (_activeGame == game) { - return; // No change needed - } - - _activeGame = game; - audio.activeGame = game; - } - - /// Initializes the engine by loading available game data. - Future init({String? directory}) async { - await audio.init(); - availableGames.clear(); - - // 1. Bundle asset loading (migrated from GameSelectScreen) - final versionsToTry = [ - (version: GameVersion.retail, path: 'retail'), - (version: GameVersion.shareware, path: 'shareware'), - ]; - - for (final version in versionsToTry) { - try { - final ext = version.version.fileExtension; - final folder = 'packages/wolf_3d_assets/assets/${version.path}'; - - final data = WolfensteinLoader.loadFromBytes( - version: version.version, - vswap: await _tryLoad('$folder/VSWAP.$ext'), - mapHead: await _tryLoad('$folder/MAPHEAD.$ext'), - gameMaps: await _tryLoad('$folder/GAMEMAPS.$ext'), - vgaDict: await _tryLoad('$folder/VGADICT.$ext'), - vgaHead: await _tryLoad('$folder/VGAHEAD.$ext'), - vgaGraph: await _tryLoad('$folder/VGAGRAPH.$ext'), - audioHed: await _tryLoad('$folder/AUDIOHED.$ext'), - audioT: await _tryLoad('$folder/AUDIOT.$ext'), - ); - - availableGames.add(data); - } catch (e) { - debugPrint(e.toString()); - } - } - - // 2. External side-loading (non-web) - if (!kIsWeb) { - try { - final externalGames = await WolfensteinLoader.discover( - directoryPath: directory, - recursive: true, - ); - for (var entry in externalGames.entries) { - if (!availableGames.any((g) => g.version == entry.key)) { - availableGames.add(entry.value); - } - } - } catch (e) { - debugPrint("External discovery failed: $e"); - } - } - - return this; - } - - Future _tryLoad(String path) async { - try { - return await rootBundle.load(path); - } catch (e) { - debugPrint("Asset not found: $path"); - return null; - } - } -} diff --git a/packages/wolf_3d_flutter/lib/wolf_3d_flutter.dart b/packages/wolf_3d_flutter/lib/wolf_3d_flutter.dart index 298576d..f516bc1 100644 --- a/packages/wolf_3d_flutter/lib/wolf_3d_flutter.dart +++ b/packages/wolf_3d_flutter/lib/wolf_3d_flutter.dart @@ -1,5 +1,157 @@ -/// A Calculator. -class Calculator { - /// Returns [value] plus 1. - int addOne(int value) => value + 1; +/// High-level Flutter facade for discovering game data and sharing runtime services. +library; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.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_engine.dart'; +import 'package:wolf_3d_dart/wolf_3d_synth.dart'; +import 'package:wolf_3d_flutter/wolf_3d_input_flutter.dart'; + +/// Coordinates asset discovery, audio initialization, and input reuse for apps. +class Wolf3d { + /// Creates an empty facade that must be initialized with [init]. + Wolf3d(); + + /// All successfully discovered or bundled game data sets. + final List availableGames = []; + WolfensteinData? _activeGame; + + /// Shared engine audio backend used by menus and gameplay sessions. + final EngineAudio audio = WolfAudio(); + + /// Shared Flutter input adapter reused by gameplay screens. + final Wolf3dFlutterInput input = Wolf3dFlutterInput(); + + /// The currently selected game data set. + /// + /// Throws a [StateError] until [setActiveGame] has been called. + WolfensteinData get activeGame { + if (_activeGame == null) { + throw StateError("No active game selected. Call setActiveGame() first."); + } + return _activeGame!; + } + + // Episode selection lives on the facade so menus can configure gameplay + // before constructing a new engine instance. + int _activeEpisode = 0; + + /// Index of the episode currently selected in the UI flow. + int get activeEpisode => _activeEpisode; + + /// Sets the active episode for the current [activeGame]. + void setActiveEpisode(int episodeIndex) { + if (_activeGame == null) { + throw StateError("No active game selected. Call setActiveGame() first."); + } + if (episodeIndex < 0 || episodeIndex >= _activeGame!.episodes.length) { + throw RangeError("Episode index out of range for the active game."); + } + + _activeEpisode = episodeIndex; + } + + /// Convenience access to the active episode's level list. + List get levels => activeGame.episodes[activeEpisode].levels; + + /// Convenience access to the active game's wall textures. + List get walls => activeGame.walls; + + /// Convenience access to the active game's sprite set. + List get sprites => activeGame.sprites; + + /// Convenience access to digitized PCM effects. + List get sounds => activeGame.sounds; + + /// Convenience access to AdLib/OPL effect assets. + List get adLibSounds => activeGame.adLibSounds; + + /// Convenience access to level music tracks. + List get music => activeGame.music; + + /// Convenience access to VGA UI and splash images. + List get vgaImages => activeGame.vgaImages; + + /// Makes [game] the active data set and points shared services at it. + void setActiveGame(WolfensteinData game) { + if (!availableGames.contains(game)) { + throw ArgumentError( + "The provided game data is not in the list of available games.", + ); + } + + if (_activeGame == game) { + return; // No change needed + } + + _activeGame = game; + audio.activeGame = game; + } + + /// Initializes the engine by loading available game data. + Future init({String? directory}) async { + await audio.init(); + availableGames.clear(); + + // Bundled assets let the GUI work out of the box on supported platforms. + final versionsToTry = [ + (version: GameVersion.retail, path: 'retail'), + (version: GameVersion.shareware, path: 'shareware'), + ]; + + for (final version in versionsToTry) { + try { + final ext = version.version.fileExtension; + final folder = 'packages/wolf_3d_assets/assets/${version.path}'; + + final data = WolfensteinLoader.loadFromBytes( + version: version.version, + vswap: await _tryLoad('$folder/VSWAP.$ext'), + mapHead: await _tryLoad('$folder/MAPHEAD.$ext'), + gameMaps: await _tryLoad('$folder/GAMEMAPS.$ext'), + vgaDict: await _tryLoad('$folder/VGADICT.$ext'), + vgaHead: await _tryLoad('$folder/VGAHEAD.$ext'), + vgaGraph: await _tryLoad('$folder/VGAGRAPH.$ext'), + audioHed: await _tryLoad('$folder/AUDIOHED.$ext'), + audioT: await _tryLoad('$folder/AUDIOT.$ext'), + ); + + availableGames.add(data); + } catch (e) { + debugPrint(e.toString()); + } + } + + // On non-web platforms, also scan the local filesystem for user-supplied + // data folders so the host can pick up extra versions automatically. + if (!kIsWeb) { + try { + final externalGames = await WolfensteinLoader.discover( + directoryPath: directory, + recursive: true, + ); + for (var entry in externalGames.entries) { + if (!availableGames.any((g) => g.version == entry.key)) { + availableGames.add(entry.value); + } + } + } catch (e) { + debugPrint("External discovery failed: $e"); + } + } + + return this; + } + + /// Loads an asset from the Flutter bundle, returning `null` when absent. + Future _tryLoad(String path) async { + try { + return await rootBundle.load(path); + } catch (e) { + debugPrint("Asset not found: $path"); + return null; + } + } } diff --git a/packages/wolf_3d_flutter/lib/wolf_3d_input_flutter.dart b/packages/wolf_3d_flutter/lib/wolf_3d_input_flutter.dart index 88f2325..8a6b7f1 100644 --- a/packages/wolf_3d_flutter/lib/wolf_3d_input_flutter.dart +++ b/packages/wolf_3d_flutter/lib/wolf_3d_input_flutter.dart @@ -1,10 +1,21 @@ +/// Flutter-specific input adapter for the Wolf3D engine. +/// +/// This class merges keyboard and pointer events into the frame-based fields +/// exposed by [Wolf3dInput]. It is designed to be owned by higher-level app +/// code and reused across menu and gameplay screens. +library; + import 'package:flutter/gestures.dart'; import 'package:flutter/services.dart'; import 'package:wolf_3d_dart/wolf_3d_entities.dart'; import 'package:wolf_3d_dart/wolf_3d_input.dart'; +/// Translates Flutter keyboard and mouse state into engine-friendly actions. class Wolf3dFlutterInput extends Wolf3dInput { - // 1. Customizable Key Bindings (Multiple keys per action) + /// Mapping from logical game actions to one or more keyboard bindings. + /// + /// Each action can be rebound by replacing the matching set. The defaults + /// support both WASD and arrow-key movement for desktop hosts. Map> bindings = { WolfInputAction.forward: { LogicalKeyboardKey.keyW, @@ -33,18 +44,23 @@ class Wolf3dFlutterInput extends Wolf3dInput { WolfInputAction.weapon4: {LogicalKeyboardKey.digit4}, }; - // 2. Mouse State Variables + /// Whether the primary mouse button is currently held. bool isMouseLeftDown = false; + + /// Whether the secondary mouse button is currently held. bool isMouseRightDown = false; double _mouseDeltaX = 0.0; double _mouseDeltaY = 0.0; bool _previousMouseRightDown = false; - // 3. Mouselook Toggle + // Mouse-look is optional so touch or keyboard-only hosts can keep the same + // adapter without incurring accidental pointer-driven movement. bool _isMouseLookEnabled = false; + /// Whether pointer deltas should be interpreted as movement/turn input. bool get mouseLookEnabled => _isMouseLookEnabled; + /// Enables or disables mouse-look style control. set mouseLookEnabled(bool value) { _isMouseLookEnabled = value; // Clear any built-up delta when turning it off so it doesn't @@ -57,27 +73,30 @@ class Wolf3dFlutterInput extends Wolf3dInput { Set _previousKeys = {}; - // --- Customization Helpers --- + /// Rebinds [action] to a single [key], replacing any previous bindings. void bindKey(WolfInputAction action, LogicalKeyboardKey key) { bindings[action] = {}; bindings[action]?.add(key); } + /// Removes [key] from the current binding set for [action]. void unbindKey(WolfInputAction action, LogicalKeyboardKey key) { bindings[action]?.remove(key); } - // --- Mouse Event Handlers --- + /// Updates button state for a newly pressed pointer. void onPointerDown(PointerDownEvent event) { if (event.buttons & kPrimaryMouseButton != 0) isMouseLeftDown = true; if (event.buttons & kSecondaryMouseButton != 0) isMouseRightDown = true; } + /// Updates button state when a pointer is released. void onPointerUp(PointerUpEvent event) { if (event.buttons & kPrimaryMouseButton == 0) isMouseLeftDown = false; if (event.buttons & kSecondaryMouseButton == 0) isMouseRightDown = false; } + /// Accumulates pointer delta so it can be consumed on the next engine frame. void onPointerMove(PointerEvent event) { // Only capture movement if mouselook is actually enabled if (_isMouseLookEnabled) { @@ -86,11 +105,12 @@ class Wolf3dFlutterInput extends Wolf3dInput { } } - // --- Input Helpers --- + /// Returns whether any bound key for [action] is currently pressed. bool _isActive(WolfInputAction action, Set pressedKeys) { return bindings[action]!.any((key) => pressedKeys.contains(key)); } + /// Returns whether [action] was pressed during the current frame only. bool _isNewlyPressed( WolfInputAction action, Set newlyPressed, @@ -103,20 +123,21 @@ class Wolf3dFlutterInput extends Wolf3dInput { final pressedKeys = HardwareKeyboard.instance.logicalKeysPressed; final newlyPressedKeys = pressedKeys.difference(_previousKeys); - // Evaluate keyboard first + // Resolve digital keyboard state first so mouse-look only augments input. bool kbForward = _isActive(WolfInputAction.forward, pressedKeys); bool kbBackward = _isActive(WolfInputAction.backward, pressedKeys); bool kbLeft = _isActive(WolfInputAction.turnLeft, pressedKeys); bool kbRight = _isActive(WolfInputAction.turnRight, pressedKeys); - // Add mouse delta if mouselook is enabled + // Mouse-look intentionally maps pointer deltas back onto the engine's + // simple boolean input contract instead of introducing analog turns. isMovingForward = kbForward || (_isMouseLookEnabled && _mouseDeltaY < -1.5); isMovingBackward = kbBackward || (_isMouseLookEnabled && _mouseDeltaY > 1.5); isTurningLeft = kbLeft || (_isMouseLookEnabled && _mouseDeltaX < -1.5); isTurningRight = kbRight || (_isMouseLookEnabled && _mouseDeltaX > 1.5); - // Reset mouse deltas after consumption for digital engine movement + // Deltas are one-frame impulses, so consume them immediately after use. _mouseDeltaX = 0.0; _mouseDeltaY = 0.0; @@ -144,6 +165,8 @@ class Wolf3dFlutterInput extends Wolf3dInput { requestedWeapon = WeaponType.chainGun; } + // Preserve prior frame state so edge-triggered actions like interact and + // weapon switching only fire once per physical key press. _previousKeys = Set.from(pressedKeys); _previousMouseRightDown = isMouseRightDown; } diff --git a/packages/wolf_3d_renderer/lib/base_renderer.dart b/packages/wolf_3d_renderer/lib/base_renderer.dart index 43d7ed9..0a86aa5 100644 --- a/packages/wolf_3d_renderer/lib/base_renderer.dart +++ b/packages/wolf_3d_renderer/lib/base_renderer.dart @@ -1,21 +1,30 @@ +/// Shared Flutter renderer shell for driving the Wolf3D engine from a widget tree. +library; + import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:wolf_3d_dart/wolf_3d_engine.dart'; -// 1. The widget now only requires the engine! +/// Base widget for renderers that present frames from a [WolfEngine]. abstract class BaseWolfRenderer extends StatefulWidget { + /// Engine instance that owns world state and the shared framebuffer. final WolfEngine engine; + /// Creates a renderer bound to [engine]. const BaseWolfRenderer({ required this.engine, super.key, }); } +/// Base [State] implementation that provides a ticker-driven render loop. abstract class BaseWolfRendererState extends State with SingleTickerProviderStateMixin { + /// Per-frame ticker used to advance the engine and request renders. late final Ticker gameLoop; + + /// Focus node used by the enclosing [KeyboardListener]. final FocusNode focusNode = FocusNode(); Duration _lastTick = Duration.zero; @@ -50,8 +59,13 @@ abstract class BaseWolfRendererState super.dispose(); } + /// Renders the latest engine state into the concrete renderer's output type. void performRender(); + + /// Builds the visible viewport widget for the latest rendered frame. Widget buildViewport(BuildContext context); + + /// Background color used by the surrounding scaffold. Color get scaffoldColor; @override 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 0adbc66..cf63cc0 100644 --- a/packages/wolf_3d_renderer/lib/wolf_3d_ascii_renderer.dart +++ b/packages/wolf_3d_renderer/lib/wolf_3d_ascii_renderer.dart @@ -1,8 +1,13 @@ +/// Flutter widget that renders Wolf3D frames using the ASCII rasterizer. +library; + import 'package:flutter/material.dart'; import 'package:wolf_3d_dart/wolf_3d_rasterizer.dart'; import 'package:wolf_3d_renderer/base_renderer.dart'; +/// Displays the game using a text-mode approximation of the original renderer. class WolfAsciiRenderer extends BaseWolfRenderer { + /// Creates an ASCII renderer bound to [engine]. const WolfAsciiRenderer({ required super.engine, super.key, @@ -22,6 +27,8 @@ class _WolfAsciiRendererState extends BaseWolfRendererState { @override void initState() { super.initState(); + // ASCII output uses a reduced logical framebuffer because glyph rendering + // expands the final view significantly once laid out in Flutter text. if (widget.engine.frameBuffer.width != _renderWidth || widget.engine.frameBuffer.height != _renderHeight) { widget.engine.setFrameBuffer(_renderWidth, _renderHeight); @@ -46,9 +53,12 @@ class _WolfAsciiRendererState extends BaseWolfRendererState { } } +/// Paints a pre-rasterized ASCII frame using grouped text spans per color run. class AsciiFrameWidget extends StatelessWidget { + /// Two-dimensional text grid generated by [AsciiRasterizer.render]. final List> frameData; + /// Creates a widget that displays [frameData]. const AsciiFrameWidget({super.key, required this.frameData}); @override @@ -65,6 +75,8 @@ class AsciiFrameWidget extends StatelessWidget { children: frameData.map((row) { List optimizedSpans = []; if (row.isNotEmpty) { + // Merge adjacent cells with the same color to keep the rich + // text tree smaller and reduce per-frame layout overhead. Color currentColor = Color(row[0].argb); StringBuffer currentSegment = StringBuffer(row[0].char); diff --git a/packages/wolf_3d_renderer/lib/wolf_3d_asset_painter.dart b/packages/wolf_3d_renderer/lib/wolf_3d_asset_painter.dart index 29d763c..0f04e40 100644 --- a/packages/wolf_3d_renderer/lib/wolf_3d_asset_painter.dart +++ b/packages/wolf_3d_renderer/lib/wolf_3d_asset_painter.dart @@ -7,18 +7,26 @@ import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; /// A unified widget to display and cache Wolf3D assets. class WolfAssetPainter extends StatefulWidget { + /// Decoded sprite source, when painting a sprite asset. final Sprite? sprite; + + /// Decoded VGA image source, when painting a VGA asset. final VgaImage? vgaImage; + + /// Pre-rendered game frame, when painting live gameplay output. final ui.Image? frame; + /// Creates a painter for a palette-indexed [Sprite]. const WolfAssetPainter.sprite(this.sprite, {super.key}) : vgaImage = null, frame = null; + /// Creates a painter for a planar VGA image. const WolfAssetPainter.vga(this.vgaImage, {super.key}) : sprite = null, frame = null; + /// Creates a painter for an already decoded [ui.Image] frame. const WolfAssetPainter.frame(this.frame, {super.key}) : sprite = null, vgaImage = null; @@ -30,7 +38,8 @@ class WolfAssetPainter extends StatefulWidget { class _WolfAssetPainterState extends State { ui.Image? _cachedImage; - // Tracks if we should dispose the image to free native memory + // Only images created inside this widget should be disposed here. Frames + // handed in from elsewhere remain owned by their producer. bool _ownsImage = false; @override @@ -58,13 +67,14 @@ class _WolfAssetPainterState extends State { } Future _prepareImage() async { - // Clean up previous internally generated image + // Dispose previously generated images before creating a replacement so the + // widget does not retain stale native image allocations. if (_ownsImage && _cachedImage != null) { _cachedImage!.dispose(); _cachedImage = null; } - // If a pre-rendered frame is passed in, just use it directly + // Pre-decoded frames can be used as-is and stay owned by the caller. if (widget.frame != null) { _ownsImage = false; if (mounted) { @@ -86,12 +96,13 @@ class _WolfAssetPainterState extends State { _cachedImage = newImage; }); } else { - // If the widget was unmounted while building, dispose the unused image + // If the widget was unmounted while work completed, dispose the image + // immediately to avoid leaking native resources. newImage?.dispose(); } } - /// Converts a Sprite's 8-bit palette data to a 32-bit RGBA ui.Image + /// Converts a sprite's indexed palette data into a Flutter [ui.Image]. Future _buildSpriteImage(Sprite sprite) { final completer = Completer(); final pixels = Uint8List(64 * 64 * 4); // 4 bytes per pixel (RGBA) @@ -124,7 +135,7 @@ class _WolfAssetPainterState extends State { return completer.future; } - /// Converts a VgaImage's planar 8-bit palette data to a 32-bit RGBA ui.Image + /// Converts a planar VGA image into a row-major Flutter [ui.Image]. Future _buildVgaImage(VgaImage image) { final completer = Completer(); final pixels = Uint8List(image.width * image.height * 4); @@ -185,7 +196,9 @@ class _WolfAssetPainterState extends State { } class _ImagePainter extends CustomPainter { + /// Image already decoded into Flutter's native image representation. final ui.Image image; + _ImagePainter(this.image); @override 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 9eb5990..d1905cc 100644 --- a/packages/wolf_3d_renderer/lib/wolf_3d_flutter_renderer.dart +++ b/packages/wolf_3d_renderer/lib/wolf_3d_flutter_renderer.dart @@ -1,3 +1,6 @@ +/// Flutter widget that renders Wolf3D frames as native pixel images. +library; + import 'dart:ui' as ui; import 'package:flutter/material.dart'; @@ -6,7 +9,9 @@ import 'package:wolf_3d_dart/wolf_3d_rasterizer.dart'; import 'package:wolf_3d_renderer/base_renderer.dart'; import 'package:wolf_3d_renderer/wolf_3d_asset_painter.dart'; +/// Presents the software rasterizer output by decoding the shared framebuffer. class WolfFlutterRenderer extends BaseWolfRenderer { + /// Creates a pixel renderer bound to [engine]. const WolfFlutterRenderer({ required super.engine, super.key, @@ -28,6 +33,7 @@ class _WolfFlutterRendererState @override void initState() { super.initState(); + // Match the original Wolf3D software resolution for the pixel renderer. if (widget.engine.frameBuffer.width != _renderWidth || widget.engine.frameBuffer.height != _renderHeight) { widget.engine.setFrameBuffer(_renderWidth, _renderHeight); @@ -45,6 +51,8 @@ class _WolfFlutterRendererState final FrameBuffer frameBuffer = widget.engine.frameBuffer; _rasterizer.render(widget.engine); + // Convert the engine-owned framebuffer into a GPU-friendly ui.Image on + // the Flutter side while preserving nearest-neighbor pixel fidelity. ui.decodeImageFromPixels( frameBuffer.pixels.buffer.asUint8List(), frameBuffer.width, @@ -64,7 +72,7 @@ class _WolfFlutterRendererState @override Widget buildViewport(BuildContext context) { - // If we don't have a frame yet, show the loading state + // Delay painting until at least one decoded frame is available. if (_renderedFrame == null) { return const CircularProgressIndicator(color: Colors.white24); }