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 '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',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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.
|
/// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user