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,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<int> 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;
}

View File

@@ -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<String> {
static const double _targetAspectRatio = 4 / 3;
static const int _defaultLineHeightPx = 18;
@@ -125,12 +133,15 @@ class SixelRasterizer extends CliRasterizer<String> {
// 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<String> {
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<String> {
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 {

View File

@@ -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<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;
static const List<int> _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;

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