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,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',
|
||||
);
|
||||
|
||||
@@ -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<void> 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),
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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<DifficultyScreen> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Replaces the menu flow with an active [GameScreen] using [difficulty].
|
||||
void _startGame(Difficulty difficulty, {bool showGallery = false}) {
|
||||
widget.wolf3d.audio.stopMusic();
|
||||
|
||||
|
||||
@@ -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<EpisodeScreen> {
|
||||
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(
|
||||
|
||||
@@ -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<GameScreen> {
|
||||
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<GameScreen> {
|
||||
),
|
||||
),
|
||||
|
||||
// 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<GameScreen> {
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<VgaImage> images;
|
||||
|
||||
/// Creates the gallery for [images].
|
||||
const VgaGallery({super.key, required this.images});
|
||||
|
||||
@override
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<WolfInputAction, Set<LogicalKeyboardKey>> 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<LogicalKeyboardKey> _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<LogicalKeyboardKey> pressedKeys) {
|
||||
return bindings[action]!.any((key) => pressedKeys.contains(key));
|
||||
}
|
||||
|
||||
/// Returns whether [action] was pressed during the current frame only.
|
||||
bool _isNewlyPressed(
|
||||
WolfInputAction action,
|
||||
Set<LogicalKeyboardKey> 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;
|
||||
}
|
||||
|
||||
@@ -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<T extends BaseWolfRenderer>
|
||||
extends State<T>
|
||||
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<T extends BaseWolfRenderer>
|
||||
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
|
||||
|
||||
@@ -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<WolfAsciiRenderer> {
|
||||
@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<WolfAsciiRenderer> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<List<ColoredChar>> 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<TextSpan> 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);
|
||||
|
||||
|
||||
@@ -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<WolfAssetPainter> {
|
||||
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<WolfAssetPainter> {
|
||||
}
|
||||
|
||||
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) {
|
||||
_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<WolfAssetPainter> {
|
||||
_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<ui.Image> _buildSpriteImage(Sprite sprite) {
|
||||
final completer = Completer<ui.Image>();
|
||||
final pixels = Uint8List(64 * 64 * 4); // 4 bytes per pixel (RGBA)
|
||||
@@ -124,7 +135,7 @@ class _WolfAssetPainterState extends State<WolfAssetPainter> {
|
||||
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) {
|
||||
final completer = Completer<ui.Image>();
|
||||
final pixels = Uint8List(image.width * image.height * 4);
|
||||
@@ -185,7 +196,9 @@ class _WolfAssetPainterState extends State<WolfAssetPainter> {
|
||||
}
|
||||
|
||||
class _ImagePainter extends CustomPainter {
|
||||
/// Image already decoded into Flutter's native image representation.
|
||||
final ui.Image image;
|
||||
|
||||
_ImagePainter(this.image);
|
||||
|
||||
@override
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user