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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user