Refactor and enhance documentation across the Wolf3D project

- Updated library imports to use the correct package paths for consistency.
- Added detailed documentation comments to various classes and methods, improving code readability and maintainability.
- Refined the GameSelectScreen, SpriteGallery, and VgaGallery classes with clearer descriptions of their functionality.
- Enhanced the CliInput class to better explain the input handling process and its interaction with the engine.
- Improved the SixelRasterizer and Opl2Emulator classes with comprehensive comments on their operations and state management.
- Removed the deprecated wolf_3d.dart file and consolidated its functionality into wolf_3d_flutter.dart for a cleaner architecture.
- Updated the Wolf3dFlutterInput class to clarify its role in merging keyboard and pointer events.
- Enhanced the rendering classes to provide better context on their purpose and usage within the Flutter framework.

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-18 10:01:12 +01:00
parent 28938f7301
commit 3c6a4672f7
23 changed files with 404 additions and 183 deletions

View File

@@ -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 'dart:io';
import 'package:wolf_3d_cli/cli_game_loop.dart'; 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_engine.dart';
import 'package:wolf_3d_dart/wolf_3d_input.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) { void exitCleanly(int code) {
stdout.write('\x1b[0m'); // Reset color stdout.write('\x1b[0m'); // Reset color
stdout.write('\x1b[2J\x1b[H'); // Clear screen stdout.write('\x1b[2J\x1b[H'); // Clear screen
@@ -14,12 +20,13 @@ void exitCleanly(int code) {
exit(code); exit(code);
} }
/// Launches the CLI renderer against the bundled retail asset set.
void main() async { void main() async {
stdout.write("Discovering game data..."); 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; final scriptUri = Platform.script;
// 2. Resolve the path mathematically.
final targetUri = scriptUri.resolve( final targetUri = scriptUri.resolve(
'../../../packages/wolf_3d_assets/assets/retail', '../../../packages/wolf_3d_assets/assets/retail',
); );

View File

@@ -1,3 +1,6 @@
/// Terminal game loop that ties engine ticks, raw input, and CLI rendering together.
library;
import 'dart:async'; import 'dart:async';
import 'dart:io'; 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_input.dart';
import 'package:wolf_3d_dart/wolf_3d_rasterizer.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 { class CliGameLoop {
CliGameLoop({ CliGameLoop({
required this.engine, required this.engine,
@@ -35,6 +43,7 @@ class CliGameLoop {
bool _isRunning = false; bool _isRunning = false;
Duration _lastTick = Duration.zero; Duration _lastTick = Duration.zero;
/// Starts terminal probing, enters raw input mode, and begins the frame timer.
Future<void> start() async { Future<void> start() async {
if (_isRunning) { if (_isRunning) {
return; return;
@@ -64,6 +73,7 @@ class CliGameLoop {
_isRunning = true; _isRunning = true;
} }
/// Stops the timer, unsubscribes from stdin, and restores terminal settings.
void stop() { void stop() {
if (!_isRunning) { if (!_isRunning) {
return; return;
@@ -102,6 +112,8 @@ class CliGameLoop {
} }
if (bytes.contains(9)) { if (bytes.contains(9)) {
// Tab swaps between rasterizers so renderer debugging stays available
// without restarting the process.
_rasterizer = identical(_rasterizer, secondaryRasterizer) _rasterizer = identical(_rasterizer, secondaryRasterizer)
? primaryRasterizer ? primaryRasterizer
: secondaryRasterizer; : secondaryRasterizer;
@@ -125,6 +137,8 @@ class CliGameLoop {
columns: cols, columns: cols,
rows: rows, 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('\x1b[2J\x1b[H');
stdout.write( stdout.write(
_rasterizer.buildTerminalSizeWarning(columns: cols, rows: rows), _rasterizer.buildTerminalSizeWarning(columns: cols, rows: rows),

View File

@@ -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: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'; import 'package:wolf_3d_gui/screens/game_select_screen.dart';
/// Creates the application shell after loading available Wolf3D data sets.
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();

View File

@@ -1,11 +1,17 @@
/// Difficulty picker shown after the player chooses an episode.
library;
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_flutter/wolf_3d.dart'; import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
import 'package:wolf_3d_gui/screens/game_screen.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 { class DifficultyScreen extends StatefulWidget {
/// Shared application facade carrying the active game, input, and audio.
final Wolf3d wolf3d; final Wolf3d wolf3d;
/// Creates the difficulty-selection screen for [wolf3d].
const DifficultyScreen({ const DifficultyScreen({
super.key, super.key,
required this.wolf3d, required this.wolf3d,
@@ -25,6 +31,7 @@ class _DifficultyScreenState extends State<DifficultyScreen> {
super.dispose(); super.dispose();
} }
/// Replaces the menu flow with an active [GameScreen] using [difficulty].
void _startGame(Difficulty difficulty, {bool showGallery = false}) { void _startGame(Difficulty difficulty, {bool showGallery = false}) {
widget.wolf3d.audio.stopMusic(); widget.wolf3d.audio.stopMusic();

View File

@@ -1,13 +1,19 @@
/// Episode picker and asset-browser entry point for the selected game version.
library;
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_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/difficulty_screen.dart';
import 'package:wolf_3d_gui/screens/sprite_gallery.dart'; import 'package:wolf_3d_gui/screens/sprite_gallery.dart';
import 'package:wolf_3d_gui/screens/vga_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 { class EpisodeScreen extends StatefulWidget {
/// Shared application facade whose active game must already be set.
final Wolf3d wolf3d; final Wolf3d wolf3d;
/// Creates the episode-selection screen for [wolf3d].
const EpisodeScreen({super.key, required this.wolf3d}); const EpisodeScreen({super.key, required this.wolf3d});
@override @override
@@ -21,6 +27,7 @@ class _EpisodeScreenState extends State<EpisodeScreen> {
widget.wolf3d.audio.playMenuMusic(); widget.wolf3d.audio.playMenuMusic();
} }
/// Persists the chosen episode in [Wolf3d] and advances to difficulty select.
void _selectEpisode(int index) { void _selectEpisode(int index) {
widget.wolf3d.setActiveEpisode(index); widget.wolf3d.setActiveEpisode(index);
Navigator.of(context).push( Navigator.of(context).push(

View File

@@ -1,3 +1,6 @@
/// Active gameplay screen for the Flutter host.
library;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.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_ascii_renderer.dart';
import 'package:wolf_3d_renderer/wolf_3d_flutter_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 { class GameScreen extends StatefulWidget {
/// Fully parsed game data for the selected version.
final WolfensteinData data; final WolfensteinData data;
/// Difficulty applied when creating the engine session.
final Difficulty difficulty; final Difficulty difficulty;
/// Episode index used as the starting world.
final int startingEpisode; final int startingEpisode;
/// Shared audio backend reused across menu and gameplay screens.
final EngineAudio audio; final EngineAudio audio;
/// Flutter input adapter that translates widget events into engine input.
final Wolf3dFlutterInput input; final Wolf3dFlutterInput input;
/// Creates a gameplay screen with the supplied game session configuration.
const GameScreen({ const GameScreen({
required this.data, required this.data,
required this.difficulty, required this.difficulty,
@@ -55,6 +69,8 @@ class _GameScreenState extends State<GameScreen> {
onPointerHover: widget.input.onPointerMove, onPointerHover: widget.input.onPointerMove,
child: Stack( child: Stack(
children: [ children: [
// Keep both renderers behind the same engine so mode switching does
// not reset level state or audio playback.
_useAsciiMode _useAsciiMode
? WolfAsciiRenderer(engine: _engine) ? WolfAsciiRenderer(engine: _engine)
: WolfFlutterRenderer(engine: _engine), : WolfFlutterRenderer(engine: _engine),
@@ -80,7 +96,7 @@ class _GameScreenState extends State<GameScreen> {
), ),
), ),
// TAB listener // Tab toggles the renderer implementation for quick visual debugging.
Focus( Focus(
autofocus: true, autofocus: true,
onKeyEvent: (node, event) { onKeyEvent: (node, event) {
@@ -94,7 +110,8 @@ class _GameScreenState extends State<GameScreen> {
child: const SizedBox.shrink(), 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) if (!_engine.isInitialized)
Container( Container(
color: Colors.black, color: Colors.black,

View File

@@ -1,11 +1,17 @@
/// Game-selection screen shown after the GUI host discovers available assets.
library;
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_flutter/wolf_3d.dart'; import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
import 'package:wolf_3d_gui/screens/episode_screen.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 { class GameSelectScreen extends StatelessWidget {
/// Shared application facade that owns discovered games, audio, and input.
final Wolf3d wolf3d; final Wolf3d wolf3d;
/// Creates the game-selection screen for the supplied [wolf3d] session.
const GameSelectScreen({super.key, required this.wolf3d}); const GameSelectScreen({super.key, required this.wolf3d});
@override @override

View File

@@ -1,12 +1,18 @@
/// Visual browser for decoded sprite assets and their inferred gameplay roles.
library;
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_entities.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'; 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 { class SpriteGallery extends StatelessWidget {
/// Shared application facade used to access the active game's sprite set.
final Wolf3d wolf3d; final Wolf3d wolf3d;
/// Creates the sprite gallery for [wolf3d].
const SpriteGallery({super.key, required this.wolf3d}); const SpriteGallery({super.key, required this.wolf3d});
bool get isShareware => wolf3d.activeGame.version == GameVersion.shareware; bool get isShareware => wolf3d.activeGame.version == GameVersion.shareware;
@@ -29,6 +35,8 @@ class SpriteGallery extends StatelessWidget {
itemBuilder: (context, index) { itemBuilder: (context, index) {
String label = "Sprite Index: $index"; String label = "Sprite Index: $index";
for (final enemy in EnemyType.values) { 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)) { if (enemy.claimsSpriteIndex(index, isShareware: isShareware)) {
final EnemyAnimation? animation = enemy.getAnimationFromSprite( final EnemyAnimation? animation = enemy.getAnimationFromSprite(
index, index,

View File

@@ -1,10 +1,16 @@
/// Visual browser for decoded VGA pictures and UI art.
library;
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_renderer/wolf_3d_asset_painter.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 { class VgaGallery extends StatelessWidget {
/// Raw VGA images decoded from the active asset pack.
final List<VgaImage> images; final List<VgaImage> images;
/// Creates the gallery for [images].
const VgaGallery({super.key, required this.images}); const VgaGallery({super.key, required this.images});
@override @override

View File

@@ -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/src/input/wolf_3d_input.dart';
import 'package:wolf_3d_dart/wolf_3d_entities.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 { 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 _pForward = false;
bool _pBackward = false; bool _pBackward = false;
bool _pLeft = false; bool _pLeft = false;
@@ -11,7 +16,7 @@ class CliInput extends Wolf3dInput {
bool _pInteract = false; bool _pInteract = false;
WeaponType? _pWeapon; 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<int> bytes) { void handleKey(List<int> bytes) {
String char = String.fromCharCodes(bytes).toLowerCase(); String char = String.fromCharCodes(bytes).toLowerCase();
@@ -20,7 +25,8 @@ class CliInput extends Wolf3dInput {
if (char == 'a') _pLeft = true; if (char == 'a') _pLeft = true;
if (char == 'd') _pRight = 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 == 'j') _pFire = true;
if (char == ' ') _pInteract = true; if (char == ' ') _pInteract = true;
@@ -32,7 +38,7 @@ class CliInput extends Wolf3dInput {
@override @override
void update() { void update() {
// 1. Move pending inputs to the active state // Promote buffered presses into the engine-visible state for this frame.
isMovingForward = _pForward; isMovingForward = _pForward;
isMovingBackward = _pBackward; isMovingBackward = _pBackward;
isTurningLeft = _pLeft; isTurningLeft = _pLeft;
@@ -41,7 +47,7 @@ class CliInput extends Wolf3dInput {
isInteracting = _pInteract; isInteracting = _pInteract;
requestedWeapon = _pWeapon; 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; _pForward = _pBackward = _pLeft = _pRight = _pFire = _pInteract = false;
_pWeapon = null; _pWeapon = null;
} }

View File

@@ -1,3 +1,6 @@
/// Terminal rasterizer that encodes engine frames as Sixel graphics.
library;
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
@@ -8,6 +11,11 @@ import 'package:wolf_3d_dart/wolf_3d_engine.dart';
import 'cli_rasterizer.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<String> { class SixelRasterizer extends CliRasterizer<String> {
static const double _targetAspectRatio = 4 / 3; static const double _targetAspectRatio = 4 / 3;
static const int _defaultLineHeightPx = 18; static const int _defaultLineHeightPx = 18;
@@ -125,12 +133,15 @@ class SixelRasterizer extends CliRasterizer<String> {
// RENDERING ENGINE // RENDERING ENGINE
// =========================================================================== // ===========================================================================
/// Builds a temporary framebuffer sized to the drawable terminal region.
FrameBuffer _createScaledBuffer(FrameBuffer terminalBuffer) { FrameBuffer _createScaledBuffer(FrameBuffer terminalBuffer) {
final int previousOffsetColumns = _offsetColumns; final int previousOffsetColumns = _offsetColumns;
final int previousOffsetRows = _offsetRows; final int previousOffsetRows = _offsetRows;
final int previousOutputWidth = _outputWidth; final int previousOutputWidth = _outputWidth;
final int previousOutputHeight = _outputHeight; 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( final double fitScale = math.min(
terminalBuffer.width / _minimumTerminalColumns, terminalBuffer.width / _minimumTerminalColumns,
terminalBuffer.height / _minimumTerminalRows, terminalBuffer.height / _minimumTerminalRows,
@@ -158,6 +169,8 @@ class SixelRasterizer extends CliRasterizer<String> {
targetRows * _defaultLineHeightPx, 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; final double boundsAspect = boundsPixelWidth / boundsPixelHeight;
if (boundsAspect > _targetAspectRatio) { if (boundsAspect > _targetAspectRatio) {
_outputHeight = boundsPixelHeight; _outputHeight = boundsPixelHeight;
@@ -193,7 +206,8 @@ class SixelRasterizer extends CliRasterizer<String> {
final FrameBuffer originalBuffer = engine.frameBuffer; final FrameBuffer originalBuffer = engine.frameBuffer;
final FrameBuffer scaledBuffer = _createScaledBuffer(originalBuffer); 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); _screen = Uint8List(scaledBuffer.width * scaledBuffer.height);
engine.frameBuffer = scaledBuffer; engine.frameBuffer = scaledBuffer;
try { try {

View File

@@ -1,7 +1,9 @@
import 'dart:math' as math; import 'dart:math' as math;
/// States used by the simplified ADSR envelope inside the OPL2 emulator.
enum EnvelopeState { off, attack, decay, sustain, release } enum EnvelopeState { off, attack, decay, sustain, release }
/// One OPL2 operator, combining waveform generation and envelope progression.
class Opl2Operator { class Opl2Operator {
double phase = 0.0; double phase = 0.0;
double phaseIncrement = 0.0; double phaseIncrement = 0.0;
@@ -16,24 +18,27 @@ class Opl2Operator {
double multiplier = 1.0; double multiplier = 1.0;
double volume = 1.0; double volume = 1.0;
// Waveform Selection (0-3) /// Selected waveform index as exposed by the OPL2 register set.
int waveform = 0; int waveform = 0;
/// Recomputes oscillator increment from the shared channel base frequency.
void updateFrequency(double baseFreq) { void updateFrequency(double baseFreq) {
phaseIncrement = phaseIncrement =
(baseFreq * multiplier * 2 * math.pi) / Opl2Emulator.sampleRate; (baseFreq * multiplier * 2 * math.pi) / Opl2Emulator.sampleRate;
} }
/// Starts a new note by resetting the phase and entering attack.
void triggerOn() { void triggerOn() {
phase = 0.0; phase = 0.0;
envState = EnvelopeState.attack; envState = EnvelopeState.attack;
} }
/// Releases the note so the envelope decays back to silence.
void triggerOff() { void triggerOff() {
envState = EnvelopeState.release; 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) { double _getOscillatorOutput(double currentPhase) {
// Normalize phase between 0 and 2*pi // Normalize phase between 0 and 2*pi
double p = currentPhase % (2 * math.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) { double getSample(double phaseOffset) {
switch (envState) { switch (envState) {
case EnvelopeState.attack: case EnvelopeState.attack:
@@ -83,7 +89,8 @@ class Opl2Operator {
return 0.0; 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); double out = _getOscillatorOutput(phase + phaseOffset);
phase += phaseIncrement; phase += phaseIncrement;
@@ -93,6 +100,7 @@ class Opl2Operator {
} }
} }
/// Two-operator OPL2 channel with optional additive mode and self-feedback.
class Opl2Channel { class Opl2Channel {
Opl2Operator modulator = Opl2Operator(); Opl2Operator modulator = Opl2Operator();
Opl2Operator carrier = Opl2Operator(); Opl2Operator carrier = Opl2Operator();
@@ -107,12 +115,14 @@ class Opl2Channel {
double _prevModOutput1 = 0.0; double _prevModOutput1 = 0.0;
double _prevModOutput2 = 0.0; double _prevModOutput2 = 0.0;
/// Updates both operators after frequency register changes.
void updateFrequency() { void updateFrequency() {
double baseFreq = (fNum * math.pow(2, block)) * (49716.0 / 1048576.0); double baseFreq = (fNum * math.pow(2, block)) * (49716.0 / 1048576.0);
modulator.updateFrequency(baseFreq); modulator.updateFrequency(baseFreq);
carrier.updateFrequency(baseFreq); carrier.updateFrequency(baseFreq);
} }
/// Mixes one audio sample from the channel's current operator state.
double getSample() { double getSample() {
if (!keyOn && if (!keyOn &&
carrier.envState == EnvelopeState.off && carrier.envState == EnvelopeState.off &&
@@ -122,6 +132,8 @@ class Opl2Channel {
double feedbackPhase = 0.0; double feedbackPhase = 0.0;
if (feedbackStrength > 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 averageMod = (_prevModOutput1 + _prevModOutput2) / 2.0;
double feedbackFactor = double feedbackFactor =
math.pow(2, feedbackStrength - 1) * (math.pi / 16.0); math.pow(2, feedbackStrength - 1) * (math.pi / 16.0);
@@ -136,9 +148,11 @@ class Opl2Channel {
double channelOutput = 0.0; double channelOutput = 0.0;
if (isAdditive) { if (isAdditive) {
// Additive mode mixes both operators as audible oscillators.
double carOutput = carrier.getSample(0.0); double carOutput = carrier.getSample(0.0);
channelOutput = modOutput + carOutput; channelOutput = modOutput + carOutput;
} else { } else {
// Standard FM mode feeds the modulator into the carrier's phase.
double carOutput = carrier.getSample(modOutput * 2.0); double carOutput = carrier.getSample(modOutput * 2.0);
channelOutput = carOutput; channelOutput = carOutput;
} }
@@ -147,6 +161,7 @@ class Opl2Channel {
} }
} }
/// Lightweight pseudo-random noise source for percussion voices.
class Opl2Noise { class Opl2Noise {
int _seed = 0xFFFF; 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 { class Opl2Emulator {
static const int sampleRate = 44100; static const int sampleRate = 44100;
bool rhythmMode = false; 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 bassDrumKey = false;
bool snareDrumKey = false; bool snareDrumKey = false;
bool tomTomKey = false; bool tomTomKey = false;
@@ -174,7 +194,7 @@ class Opl2Emulator {
final List<Opl2Channel> channels = List.generate(9, (_) => Opl2Channel()); final List<Opl2Channel> 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; bool _waveformSelectionEnabled = false;
static const List<int> _operatorMap = [ static const List<int> _operatorMap = [
@@ -202,6 +222,7 @@ class Opl2Emulator {
8, 8,
]; ];
/// Resolves a register offset to the affected operator, if any.
Opl2Operator? _getOperator(int offset) { Opl2Operator? _getOperator(int offset) {
if (offset < 0 || if (offset < 0 ||
offset >= _operatorMap.length || offset >= _operatorMap.length ||
@@ -215,6 +236,7 @@ class Opl2Emulator {
: channels[channelIdx].modulator; : channels[channelIdx].modulator;
} }
/// Applies a single OPL2 register write.
void writeRegister(int reg, int data) { void writeRegister(int reg, int data) {
// --- 0x01: Test / Waveform Enable --- // --- 0x01: Test / Waveform Enable ---
if (reg == 0x01) { if (reg == 0x01) {
@@ -310,6 +332,7 @@ class Opl2Emulator {
} }
} }
/// Generates one normalized mono sample from the current register state.
double generateSample() { double generateSample() {
double mixedOutput = 0.0; double mixedOutput = 0.0;
@@ -319,12 +342,12 @@ class Opl2Emulator {
} }
if (!rhythmMode) { 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++) { for (int i = 6; i < 9; i++) {
mixedOutput += channels[i].getSample(); mixedOutput += channels[i].getSample();
} }
} else { } else {
// RHYTHM MODE: The last 3 channels are re-routed // Rhythm mode repurposes the last three channels into drum voices.
mixedOutput += _generateBassDrum(); mixedOutput += _generateBassDrum();
mixedOutput += _generateSnareAndHiHat(); mixedOutput += _generateSnareAndHiHat();
mixedOutput += _generateTomAndCymbal(); mixedOutput += _generateTomAndCymbal();
@@ -333,13 +356,14 @@ class Opl2Emulator {
return mixedOutput.clamp(-1.0, 1.0); 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() { double _generateBassDrum() {
if (!bassDrumKey) return 0.0; if (!bassDrumKey) return 0.0;
// Bass drum uses standard FM (Mod -> Car) but usually with very low frequency // Bass drum uses standard FM (Mod -> Car) but usually with very low frequency
return channels[6].getSample(); return channels[6].getSample();
} }
/// Generates the combined snare and hi-hat voices from channel 7.
double _generateSnareAndHiHat() { double _generateSnareAndHiHat() {
double snareOut = 0.0; double snareOut = 0.0;
double hiHatOut = 0.0; double hiHatOut = 0.0;
@@ -363,6 +387,7 @@ class Opl2Emulator {
return (snareOut + hiHatOut) * 0.1; return (snareOut + hiHatOut) * 0.1;
} }
/// Generates the combined tom and cymbal voices from channel 8.
double _generateTomAndCymbal() { double _generateTomAndCymbal() {
double tomOut = 0.0; double tomOut = 0.0;
double cymbalOut = 0.0; double cymbalOut = 0.0;

View File

@@ -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; library;
export 'src/data/wl_parser.dart' show WLParser; export 'src/data/wl_parser.dart' show WLParser;

View File

@@ -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; library;
export 'src/data_types/cardinal_direction.dart' show CardinalDirection; export 'src/data_types/cardinal_direction.dart' show CardinalDirection;

View File

@@ -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; library;
export 'src/engine/audio/engine_audio.dart'; export 'src/engine/audio/engine_audio.dart';

View File

@@ -1,6 +1,6 @@
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';
import 'package:wolf_3d_flutter/wolf_3d.dart'; import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
class FlutterAudioAdapter implements EngineAudio { class FlutterAudioAdapter implements EngineAudio {
final Wolf3d wolf3d; final Wolf3d wolf3d;

View File

@@ -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<WolfensteinData> 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<WolfLevel> get levels => activeGame.episodes[activeEpisode].levels;
List<Sprite> get walls => activeGame.walls;
List<Sprite> get sprites => activeGame.sprites;
List<PcmSound> get sounds => activeGame.sounds;
List<PcmSound> get adLibSounds => activeGame.adLibSounds;
List<ImfMusic> get music => activeGame.music;
List<VgaImage> 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<Wolf3d> 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<ByteData?> _tryLoad(String path) async {
try {
return await rootBundle.load(path);
} catch (e) {
debugPrint("Asset not found: $path");
return null;
}
}
}

View File

@@ -1,5 +1,157 @@
/// A Calculator. /// High-level Flutter facade for discovering game data and sharing runtime services.
class Calculator { library;
/// Returns [value] plus 1.
int addOne(int value) => value + 1; 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<WolfensteinData> 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<WolfLevel> get levels => activeGame.episodes[activeEpisode].levels;
/// Convenience access to the active game's wall textures.
List<Sprite> get walls => activeGame.walls;
/// Convenience access to the active game's sprite set.
List<Sprite> get sprites => activeGame.sprites;
/// Convenience access to digitized PCM effects.
List<PcmSound> get sounds => activeGame.sounds;
/// Convenience access to AdLib/OPL effect assets.
List<PcmSound> get adLibSounds => activeGame.adLibSounds;
/// Convenience access to level music tracks.
List<ImfMusic> get music => activeGame.music;
/// Convenience access to VGA UI and splash images.
List<VgaImage> 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<Wolf3d> 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<ByteData?> _tryLoad(String path) async {
try {
return await rootBundle.load(path);
} catch (e) {
debugPrint("Asset not found: $path");
return null;
}
}
} }

View File

@@ -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/gestures.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:wolf_3d_dart/wolf_3d_entities.dart'; import 'package:wolf_3d_dart/wolf_3d_entities.dart';
import 'package:wolf_3d_dart/wolf_3d_input.dart'; import 'package:wolf_3d_dart/wolf_3d_input.dart';
/// Translates Flutter keyboard and mouse state into engine-friendly actions.
class Wolf3dFlutterInput extends Wolf3dInput { 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<WolfInputAction, Set<LogicalKeyboardKey>> bindings = { Map<WolfInputAction, Set<LogicalKeyboardKey>> bindings = {
WolfInputAction.forward: { WolfInputAction.forward: {
LogicalKeyboardKey.keyW, LogicalKeyboardKey.keyW,
@@ -33,18 +44,23 @@ class Wolf3dFlutterInput extends Wolf3dInput {
WolfInputAction.weapon4: {LogicalKeyboardKey.digit4}, WolfInputAction.weapon4: {LogicalKeyboardKey.digit4},
}; };
// 2. Mouse State Variables /// Whether the primary mouse button is currently held.
bool isMouseLeftDown = false; bool isMouseLeftDown = false;
/// Whether the secondary mouse button is currently held.
bool isMouseRightDown = false; bool isMouseRightDown = false;
double _mouseDeltaX = 0.0; double _mouseDeltaX = 0.0;
double _mouseDeltaY = 0.0; double _mouseDeltaY = 0.0;
bool _previousMouseRightDown = false; 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; bool _isMouseLookEnabled = false;
/// Whether pointer deltas should be interpreted as movement/turn input.
bool get mouseLookEnabled => _isMouseLookEnabled; bool get mouseLookEnabled => _isMouseLookEnabled;
/// Enables or disables mouse-look style control.
set mouseLookEnabled(bool value) { set mouseLookEnabled(bool value) {
_isMouseLookEnabled = value; _isMouseLookEnabled = value;
// Clear any built-up delta when turning it off so it doesn't // Clear any built-up delta when turning it off so it doesn't
@@ -57,27 +73,30 @@ class Wolf3dFlutterInput extends Wolf3dInput {
Set<LogicalKeyboardKey> _previousKeys = {}; Set<LogicalKeyboardKey> _previousKeys = {};
// --- Customization Helpers --- /// Rebinds [action] to a single [key], replacing any previous bindings.
void bindKey(WolfInputAction action, LogicalKeyboardKey key) { void bindKey(WolfInputAction action, LogicalKeyboardKey key) {
bindings[action] = {}; bindings[action] = {};
bindings[action]?.add(key); bindings[action]?.add(key);
} }
/// Removes [key] from the current binding set for [action].
void unbindKey(WolfInputAction action, LogicalKeyboardKey key) { void unbindKey(WolfInputAction action, LogicalKeyboardKey key) {
bindings[action]?.remove(key); bindings[action]?.remove(key);
} }
// --- Mouse Event Handlers --- /// Updates button state for a newly pressed pointer.
void onPointerDown(PointerDownEvent event) { void onPointerDown(PointerDownEvent event) {
if (event.buttons & kPrimaryMouseButton != 0) isMouseLeftDown = true; if (event.buttons & kPrimaryMouseButton != 0) isMouseLeftDown = true;
if (event.buttons & kSecondaryMouseButton != 0) isMouseRightDown = true; if (event.buttons & kSecondaryMouseButton != 0) isMouseRightDown = true;
} }
/// Updates button state when a pointer is released.
void onPointerUp(PointerUpEvent event) { void onPointerUp(PointerUpEvent event) {
if (event.buttons & kPrimaryMouseButton == 0) isMouseLeftDown = false; if (event.buttons & kPrimaryMouseButton == 0) isMouseLeftDown = false;
if (event.buttons & kSecondaryMouseButton == 0) isMouseRightDown = 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) { void onPointerMove(PointerEvent event) {
// Only capture movement if mouselook is actually enabled // Only capture movement if mouselook is actually enabled
if (_isMouseLookEnabled) { 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<LogicalKeyboardKey> pressedKeys) { bool _isActive(WolfInputAction action, Set<LogicalKeyboardKey> pressedKeys) {
return bindings[action]!.any((key) => pressedKeys.contains(key)); return bindings[action]!.any((key) => pressedKeys.contains(key));
} }
/// Returns whether [action] was pressed during the current frame only.
bool _isNewlyPressed( bool _isNewlyPressed(
WolfInputAction action, WolfInputAction action,
Set<LogicalKeyboardKey> newlyPressed, Set<LogicalKeyboardKey> newlyPressed,
@@ -103,20 +123,21 @@ class Wolf3dFlutterInput extends Wolf3dInput {
final pressedKeys = HardwareKeyboard.instance.logicalKeysPressed; final pressedKeys = HardwareKeyboard.instance.logicalKeysPressed;
final newlyPressedKeys = pressedKeys.difference(_previousKeys); 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 kbForward = _isActive(WolfInputAction.forward, pressedKeys);
bool kbBackward = _isActive(WolfInputAction.backward, pressedKeys); bool kbBackward = _isActive(WolfInputAction.backward, pressedKeys);
bool kbLeft = _isActive(WolfInputAction.turnLeft, pressedKeys); bool kbLeft = _isActive(WolfInputAction.turnLeft, pressedKeys);
bool kbRight = _isActive(WolfInputAction.turnRight, 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); isMovingForward = kbForward || (_isMouseLookEnabled && _mouseDeltaY < -1.5);
isMovingBackward = isMovingBackward =
kbBackward || (_isMouseLookEnabled && _mouseDeltaY > 1.5); kbBackward || (_isMouseLookEnabled && _mouseDeltaY > 1.5);
isTurningLeft = kbLeft || (_isMouseLookEnabled && _mouseDeltaX < -1.5); isTurningLeft = kbLeft || (_isMouseLookEnabled && _mouseDeltaX < -1.5);
isTurningRight = kbRight || (_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; _mouseDeltaX = 0.0;
_mouseDeltaY = 0.0; _mouseDeltaY = 0.0;
@@ -144,6 +165,8 @@ class Wolf3dFlutterInput extends Wolf3dInput {
requestedWeapon = WeaponType.chainGun; 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); _previousKeys = Set.from(pressedKeys);
_previousMouseRightDown = isMouseRightDown; _previousMouseRightDown = isMouseRightDown;
} }

View File

@@ -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/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.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 { abstract class BaseWolfRenderer extends StatefulWidget {
/// Engine instance that owns world state and the shared framebuffer.
final WolfEngine engine; final WolfEngine engine;
/// Creates a renderer bound to [engine].
const BaseWolfRenderer({ const BaseWolfRenderer({
required this.engine, required this.engine,
super.key, super.key,
}); });
} }
/// Base [State] implementation that provides a ticker-driven render loop.
abstract class BaseWolfRendererState<T extends BaseWolfRenderer> abstract class BaseWolfRendererState<T extends BaseWolfRenderer>
extends State<T> extends State<T>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
/// Per-frame ticker used to advance the engine and request renders.
late final Ticker gameLoop; late final Ticker gameLoop;
/// Focus node used by the enclosing [KeyboardListener].
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
Duration _lastTick = Duration.zero; Duration _lastTick = Duration.zero;
@@ -50,8 +59,13 @@ abstract class BaseWolfRendererState<T extends BaseWolfRenderer>
super.dispose(); super.dispose();
} }
/// Renders the latest engine state into the concrete renderer's output type.
void performRender(); void performRender();
/// Builds the visible viewport widget for the latest rendered frame.
Widget buildViewport(BuildContext context); Widget buildViewport(BuildContext context);
/// Background color used by the surrounding scaffold.
Color get scaffoldColor; Color get scaffoldColor;
@override @override

View File

@@ -1,8 +1,13 @@
/// Flutter widget that renders Wolf3D frames using the ASCII rasterizer.
library;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:wolf_3d_dart/wolf_3d_rasterizer.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';
/// Displays the game using a text-mode approximation of the original renderer.
class WolfAsciiRenderer extends BaseWolfRenderer { class WolfAsciiRenderer extends BaseWolfRenderer {
/// Creates an ASCII renderer bound to [engine].
const WolfAsciiRenderer({ const WolfAsciiRenderer({
required super.engine, required super.engine,
super.key, super.key,
@@ -22,6 +27,8 @@ class _WolfAsciiRendererState extends BaseWolfRendererState<WolfAsciiRenderer> {
@override @override
void initState() { void initState() {
super.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 || if (widget.engine.frameBuffer.width != _renderWidth ||
widget.engine.frameBuffer.height != _renderHeight) { widget.engine.frameBuffer.height != _renderHeight) {
widget.engine.setFrameBuffer(_renderWidth, _renderHeight); widget.engine.setFrameBuffer(_renderWidth, _renderHeight);
@@ -46,9 +53,12 @@ class _WolfAsciiRendererState extends BaseWolfRendererState<WolfAsciiRenderer> {
} }
} }
/// Paints a pre-rasterized ASCII frame using grouped text spans per color run.
class AsciiFrameWidget extends StatelessWidget { class AsciiFrameWidget extends StatelessWidget {
/// Two-dimensional text grid generated by [AsciiRasterizer.render].
final List<List<ColoredChar>> frameData; final List<List<ColoredChar>> frameData;
/// Creates a widget that displays [frameData].
const AsciiFrameWidget({super.key, required this.frameData}); const AsciiFrameWidget({super.key, required this.frameData});
@override @override
@@ -65,6 +75,8 @@ class AsciiFrameWidget extends StatelessWidget {
children: frameData.map((row) { children: frameData.map((row) {
List<TextSpan> optimizedSpans = []; List<TextSpan> optimizedSpans = [];
if (row.isNotEmpty) { 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); Color currentColor = Color(row[0].argb);
StringBuffer currentSegment = StringBuffer(row[0].char); StringBuffer currentSegment = StringBuffer(row[0].char);

View File

@@ -7,18 +7,26 @@ import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
/// A unified widget to display and cache Wolf3D assets. /// A unified widget to display and cache Wolf3D assets.
class WolfAssetPainter extends StatefulWidget { class WolfAssetPainter extends StatefulWidget {
/// Decoded sprite source, when painting a sprite asset.
final Sprite? sprite; final Sprite? sprite;
/// Decoded VGA image source, when painting a VGA asset.
final VgaImage? vgaImage; final VgaImage? vgaImage;
/// Pre-rendered game frame, when painting live gameplay output.
final ui.Image? frame; final ui.Image? frame;
/// Creates a painter for a palette-indexed [Sprite].
const WolfAssetPainter.sprite(this.sprite, {super.key}) const WolfAssetPainter.sprite(this.sprite, {super.key})
: vgaImage = null, : vgaImage = null,
frame = null; frame = null;
/// Creates a painter for a planar VGA image.
const WolfAssetPainter.vga(this.vgaImage, {super.key}) const WolfAssetPainter.vga(this.vgaImage, {super.key})
: sprite = null, : sprite = null,
frame = null; frame = null;
/// Creates a painter for an already decoded [ui.Image] frame.
const WolfAssetPainter.frame(this.frame, {super.key}) const WolfAssetPainter.frame(this.frame, {super.key})
: sprite = null, : sprite = null,
vgaImage = null; vgaImage = null;
@@ -30,7 +38,8 @@ class WolfAssetPainter extends StatefulWidget {
class _WolfAssetPainterState extends State<WolfAssetPainter> { class _WolfAssetPainterState extends State<WolfAssetPainter> {
ui.Image? _cachedImage; 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; bool _ownsImage = false;
@override @override
@@ -58,13 +67,14 @@ class _WolfAssetPainterState extends State<WolfAssetPainter> {
} }
Future<void> _prepareImage() async { Future<void> _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) { if (_ownsImage && _cachedImage != null) {
_cachedImage!.dispose(); _cachedImage!.dispose();
_cachedImage = null; _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) { if (widget.frame != null) {
_ownsImage = false; _ownsImage = false;
if (mounted) { if (mounted) {
@@ -86,12 +96,13 @@ class _WolfAssetPainterState extends State<WolfAssetPainter> {
_cachedImage = newImage; _cachedImage = newImage;
}); });
} else { } 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(); 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<ui.Image> _buildSpriteImage(Sprite sprite) { Future<ui.Image> _buildSpriteImage(Sprite sprite) {
final completer = Completer<ui.Image>(); final completer = Completer<ui.Image>();
final pixels = Uint8List(64 * 64 * 4); // 4 bytes per pixel (RGBA) final pixels = Uint8List(64 * 64 * 4); // 4 bytes per pixel (RGBA)
@@ -124,7 +135,7 @@ class _WolfAssetPainterState extends State<WolfAssetPainter> {
return completer.future; 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<ui.Image> _buildVgaImage(VgaImage image) { Future<ui.Image> _buildVgaImage(VgaImage image) {
final completer = Completer<ui.Image>(); final completer = Completer<ui.Image>();
final pixels = Uint8List(image.width * image.height * 4); final pixels = Uint8List(image.width * image.height * 4);
@@ -185,7 +196,9 @@ class _WolfAssetPainterState extends State<WolfAssetPainter> {
} }
class _ImagePainter extends CustomPainter { class _ImagePainter extends CustomPainter {
/// Image already decoded into Flutter's native image representation.
final ui.Image image; final ui.Image image;
_ImagePainter(this.image); _ImagePainter(this.image);
@override @override

View File

@@ -1,3 +1,6 @@
/// Flutter widget that renders Wolf3D frames as native pixel images.
library;
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:flutter/material.dart'; 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/base_renderer.dart';
import 'package:wolf_3d_renderer/wolf_3d_asset_painter.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 { class WolfFlutterRenderer extends BaseWolfRenderer {
/// Creates a pixel renderer bound to [engine].
const WolfFlutterRenderer({ const WolfFlutterRenderer({
required super.engine, required super.engine,
super.key, super.key,
@@ -28,6 +33,7 @@ class _WolfFlutterRendererState
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Match the original Wolf3D software resolution for the pixel renderer.
if (widget.engine.frameBuffer.width != _renderWidth || if (widget.engine.frameBuffer.width != _renderWidth ||
widget.engine.frameBuffer.height != _renderHeight) { widget.engine.frameBuffer.height != _renderHeight) {
widget.engine.setFrameBuffer(_renderWidth, _renderHeight); widget.engine.setFrameBuffer(_renderWidth, _renderHeight);
@@ -45,6 +51,8 @@ class _WolfFlutterRendererState
final FrameBuffer frameBuffer = widget.engine.frameBuffer; final FrameBuffer frameBuffer = widget.engine.frameBuffer;
_rasterizer.render(widget.engine); _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( ui.decodeImageFromPixels(
frameBuffer.pixels.buffer.asUint8List(), frameBuffer.pixels.buffer.asUint8List(),
frameBuffer.width, frameBuffer.width,
@@ -64,7 +72,7 @@ class _WolfFlutterRendererState
@override @override
Widget buildViewport(BuildContext context) { 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) { if (_renderedFrame == null) {
return const CircularProgressIndicator(color: Colors.white24); return const CircularProgressIndicator(color: Colors.white24);
} }