diff --git a/lib/features/difficulty/difficulty_screen.dart b/lib/features/difficulty/difficulty_screen.dart index 4321462..af6f74f 100644 --- a/lib/features/difficulty/difficulty_screen.dart +++ b/lib/features/difficulty/difficulty_screen.dart @@ -1,16 +1,17 @@ import 'package:flutter/material.dart'; +import 'package:wolf_3d_data/wolf_3d_data.dart'; import 'package:wolf_dart/features/difficulty/difficulty.dart'; import 'package:wolf_dart/features/renderer/renderer.dart'; -class DifficultyScreen extends StatefulWidget { - const DifficultyScreen({super.key}); +class DifficultyScreen extends StatelessWidget { + const DifficultyScreen( + this.data, { + super.key, + }); - @override - State createState() => _DifficultyScreenState(); -} + final WolfensteinData data; -class _DifficultyScreenState extends State { - bool isShareware = true; // Default to Shareware (WL1) + bool get isShareware => data.version == GameVersion.shareware; @override Widget build(BuildContext context) { @@ -22,6 +23,7 @@ class _DifficultyScreenState extends State { Navigator.of(context).push( MaterialPageRoute( builder: (_) => WolfRenderer( + data, difficulty: Difficulty.bringEmOn, isShareware: isShareware, showSpriteGallery: true, @@ -44,27 +46,6 @@ class _DifficultyScreenState extends State { fontFamily: 'Courier', ), ), - const SizedBox(height: 20), - - // --- Version Toggle --- - Theme( - data: ThemeData(unselectedWidgetColor: Colors.grey), - child: CheckboxListTile( - title: const Text( - "Play Shareware Version (WL1)", - style: TextStyle(color: Colors.white), - ), - value: isShareware, - onChanged: (bool? value) { - setState(() { - isShareware = value ?? true; - }); - }, - controlAffinity: ListTileControlAffinity.leading, - contentPadding: const EdgeInsets.symmetric(horizontal: 100), - ), - ), - const SizedBox(height: 20), // --- Difficulty Buttons --- ListView.builder( @@ -87,6 +68,7 @@ class _DifficultyScreenState extends State { Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (_) => WolfRenderer( + data, difficulty: difficulty, isShareware: isShareware, ), diff --git a/lib/features/renderer/renderer.dart b/lib/features/renderer/renderer.dart index 0ff8d47..b63c67a 100644 --- a/lib/features/renderer/renderer.dart +++ b/lib/features/renderer/renderer.dart @@ -2,7 +2,6 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'package:flutter/services.dart'; import 'package:wolf_3d_data/wolf_3d_data.dart'; import 'package:wolf_dart/classes/coordinate_2d.dart'; import 'package:wolf_dart/features/difficulty/difficulty.dart'; @@ -21,13 +20,15 @@ import 'package:wolf_dart/features/ui/hud.dart'; import 'package:wolf_dart/sprite_gallery.dart'; class WolfRenderer extends StatefulWidget { - const WolfRenderer({ + const WolfRenderer( + this.data, { super.key, this.difficulty = Difficulty.bringEmOn, this.showSpriteGallery = false, this.isShareware = true, }); + final WolfensteinData data; final Difficulty difficulty; final bool showSpriteGallery; final bool isShareware; @@ -44,7 +45,6 @@ class _WolfRendererState extends State late Ticker _gameLoop; final FocusNode _focusNode = FocusNode(); - late WolfensteinData gameData; late Level currentLevel; late WolfLevel activeLevel; @@ -61,22 +61,12 @@ class _WolfRendererState extends State @override void initState() { super.initState(); - _initGame(widget.isShareware); + _initGame(); } - Future _initGame(bool isShareware) async { - gameData = await WLParser.loadAsync( - (filename) => rootBundle.load( - 'assets/${isShareware ? "shareware" : "retail"}/$filename', - ), - ); - - print('Detected Game Version: ${gameData.version.name}'); - print('Loaded ${gameData.levels.length} levels!'); - print('Loaded ${gameData.vgaImages.length} images!'); - + Future _initGame() async { // Get the first level out of the data class - activeLevel = gameData.levels.first; + activeLevel = widget.data.levels.first; // Set up your grids directly from the active level currentLevel = activeLevel.wallGrid; @@ -118,8 +108,8 @@ class _WolfRendererState extends State x + 0.5, y + 0.5, widget.difficulty, - gameData.sprites.length, - isSharewareMode: isShareware, + widget.data.sprites.length, + isSharewareMode: widget.isShareware, ); if (newEntity != null) { @@ -364,7 +354,7 @@ class _WolfRendererState extends State entity.x, entity.y, widget.difficulty, - gameData.sprites.length, + widget.data.sprites.length, ); if (droppedAmmo != null) { @@ -413,7 +403,7 @@ class _WolfRendererState extends State } if (widget.showSpriteGallery) { - return SpriteGallery(sprites: gameData.sprites); + return SpriteGallery(sprites: widget.data.sprites); } return Scaffold( @@ -439,12 +429,12 @@ class _WolfRendererState extends State ), painter: RaycasterPainter( map: currentLevel, - textures: gameData.walls, + textures: widget.data.walls, player: player, fov: fov, doorOffsets: doorManager.getOffsetsForRenderer(), entities: entities, - sprites: gameData.sprites, + sprites: widget.data.sprites, activePushwall: pushwallManager.activePushwall, ), ), @@ -461,9 +451,10 @@ class _WolfRendererState extends State child: CustomPaint( painter: WeaponPainter( sprite: - gameData.sprites[player.currentWeapon + widget.data.sprites[player + .currentWeapon .getCurrentSpriteIndex( - gameData.sprites.length, + widget.data.sprites.length, )], ), ), diff --git a/lib/game_select_screen.dart b/lib/game_select_screen.dart new file mode 100644 index 0000000..8e92c82 --- /dev/null +++ b/lib/game_select_screen.dart @@ -0,0 +1,106 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:wolf_3d_data/wolf_3d_data.dart'; +import 'package:wolf_dart/features/difficulty/difficulty_screen.dart'; + +class GameSelectScreen extends StatelessWidget { + const GameSelectScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: FutureBuilder( + future: loadData(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return CircularProgressIndicator(); + } + + if (!snapshot.hasData) { + return Text("Unable to load data"); + } + + final List loadedGames = snapshot.data!; + + if (loadedGames.length == 1) { + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => DifficultyScreen(loadedGames.first), + ), + ); + }); + } + + return ListView.builder( + itemCount: loadedGames.length, + itemBuilder: (context, i) { + final WolfensteinData data = loadedGames[i]; + final GameVersion version = data.version; + + return Card( + child: ListTile( + title: Text(version.name), + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => DifficultyScreen(data), + ), + ); + }, + ), + ); + }, + ); + }, + ), + ); + } + + Future> loadData([ + bool? isShareware, + ]) async { + final List loadedGames = []; + if (kIsWeb) { + switch (isShareware) { + case false: + loadedGames.add( + WolfensteinLoader.loadFromBytes( + version: GameVersion.retail, + vswap: await rootBundle.load('assets/retail/VSWAP.WL6'), + mapHead: await rootBundle.load('assets/retail/MAPHEAD.WL6'), + gameMaps: await rootBundle.load('assets/retail/GAMEMAPS.WL6'), + vgaDict: await rootBundle.load('assets/retail/VGADICT.WL6'), + vgaHead: await rootBundle.load('assets/retail/VGAHEAD.WL6'), + vgaGraph: await rootBundle.load('assets/retail/VGAGRAPH.WL6'), + ), + ); + break; + default: + loadedGames.add( + WolfensteinLoader.loadFromBytes( + version: GameVersion.shareware, + vswap: await rootBundle.load('assets/shareware/VSWAP.WL1'), + mapHead: await rootBundle.load('assets/shareware/MAPHEAD.WL1'), + gameMaps: await rootBundle.load('assets/shareware/GAMEMAPS.WL1'), + vgaDict: await rootBundle.load('assets/shareware/VGADICT.WL1'), + vgaHead: await rootBundle.load('assets/shareware/VGAHEAD.WL1'), + vgaGraph: await rootBundle.load('assets/shareware/VGAGRAPH.WL1'), + ), + ); + break; + } + } else { + final Map discoveredVersions = + await WolfensteinLoader.discover( + directoryPath: 'assets', + recursive: true, + ); + + loadedGames.addAll(discoveredVersions.values); + } + + return loadedGames; + } +} diff --git a/lib/main.dart b/lib/main.dart index 64b7216..53d3dd7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:wolf_dart/features/difficulty/difficulty_screen.dart'; +import 'package:wolf_dart/game_select_screen.dart'; void main() { runApp( const MaterialApp( - home: DifficultyScreen(), + home: GameSelectScreen(), ), ); } diff --git a/packages/wolf_3d_data/lib/src/classes/game_file.dart b/packages/wolf_3d_data/lib/src/classes/game_file.dart new file mode 100644 index 0000000..87c5d35 --- /dev/null +++ b/packages/wolf_3d_data/lib/src/classes/game_file.dart @@ -0,0 +1,13 @@ +enum GameFile { + vswap('VSWAP'), + mapHead('MAPHEAD'), + gameMaps('GAMEMAPS'), + vgaDict('VGADICT'), + vgaHead('VGAHEAD'), + vgaGraph('VGAGRAPH') + ; + + final String baseName; + + const GameFile(this.baseName); +} diff --git a/packages/wolf_3d_data/lib/src/io/discovery_io.dart b/packages/wolf_3d_data/lib/src/io/discovery_io.dart new file mode 100644 index 0000000..c56a7e2 --- /dev/null +++ b/packages/wolf_3d_data/lib/src/io/discovery_io.dart @@ -0,0 +1,79 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import '../classes/game_file.dart'; +import '../classes/game_version.dart'; +import '../classes/wolfenstein_data.dart'; +import '../wl_parser.dart'; + +/// dart:io implementation for directory discovery. +Future> discoverInDirectory({ + String? directoryPath, + bool recursive = false, +}) async { + final dir = Directory(directoryPath ?? Directory.current.path); + if (!await dir.exists()) { + print('Warning: Directory does not exist -> ${dir.path}'); + return {}; + } + + final allFiles = await dir + .list(recursive: recursive) + .where((entity) => entity is File) + .cast() + .toList(); + + final Map loadedVersions = {}; + + for (final version in GameVersion.values) { + final ext = version.fileExtension.toUpperCase(); + final Map foundFiles = {}; + + for (final requiredFile in GameFile.values) { + final expectedName = '${requiredFile.baseName}.$ext'; + + final match = allFiles.where((file) { + final fileName = file.uri.pathSegments.last.toUpperCase(); + return fileName == expectedName; + }).firstOrNull; + + if (match != null) { + foundFiles[requiredFile] = match; + } + } + + if (foundFiles.isEmpty) continue; + + if (foundFiles.length < GameFile.values.length) { + final missingFiles = GameFile.values + .where((f) => !foundFiles.containsKey(f)) + .map((f) => '${f.baseName}.$ext') + .join(', '); + print('Found partial data for ${version.name}. Missing: $missingFiles'); + continue; + } + + try { + final data = WLParser.load( + version: version, + vswap: await _readFile(foundFiles[GameFile.vswap]!), + mapHead: await _readFile(foundFiles[GameFile.mapHead]!), + gameMaps: await _readFile(foundFiles[GameFile.gameMaps]!), + vgaDict: await _readFile(foundFiles[GameFile.vgaDict]!), + vgaHead: await _readFile(foundFiles[GameFile.vgaHead]!), + vgaGraph: await _readFile(foundFiles[GameFile.vgaGraph]!), + ); + + loadedVersions[version] = data; + } catch (e) { + print('Error parsing data for ${version.name}: $e'); + } + } + + return loadedVersions; +} + +Future _readFile(File file) async { + final bytes = await file.readAsBytes(); + return bytes.buffer.asByteData(); +} diff --git a/packages/wolf_3d_data/lib/src/io/discovery_stub.dart b/packages/wolf_3d_data/lib/src/io/discovery_stub.dart new file mode 100644 index 0000000..f420637 --- /dev/null +++ b/packages/wolf_3d_data/lib/src/io/discovery_stub.dart @@ -0,0 +1,13 @@ +import '../classes/game_version.dart'; +import '../classes/wolfenstein_data.dart'; + +/// Web-safe stub for directory discovery. +Future> discoverInDirectory({ + String? directoryPath, + bool recursive = false, +}) async { + throw UnsupportedError( + 'Directory scanning is not supported on Web. ' + 'Please load the files manually using WolfensteinLoader.loadFromBytes().', + ); +} diff --git a/packages/wolf_3d_data/lib/src/wolfenstein_loader.dart b/packages/wolf_3d_data/lib/src/wolfenstein_loader.dart new file mode 100644 index 0000000..96ac9f3 --- /dev/null +++ b/packages/wolf_3d_data/lib/src/wolfenstein_loader.dart @@ -0,0 +1,48 @@ +import 'dart:typed_data'; + +import 'classes/game_version.dart'; +import 'classes/wolfenstein_data.dart'; +// --- The Magic Conditional Import --- +// If dart:io is available, use the real scanner. Otherwise, use the stub. +import 'io/discovery_stub.dart' + if (dart.library.io) 'io/discovery_io.dart' + as platform; +import 'wl_parser.dart'; + +class WolfensteinLoader { + /// Scans a directory for Wolfenstein 3D data files and loads all available versions. + /// + /// NOTE: This will throw an [UnsupportedError] on Web platforms. + static Future> discover({ + String? directoryPath, + bool recursive = false, + }) { + return platform.discoverInDirectory( + directoryPath: directoryPath, + recursive: recursive, + ); + } + + /// Parses WolfensteinData directly from raw ByteData. + /// This is 100% pure Dart and is safe to use on all platforms, including Web. + static WolfensteinData loadFromBytes({ + required GameVersion version, + required ByteData vswap, + required ByteData mapHead, + required ByteData gameMaps, + required ByteData vgaDict, + required ByteData vgaHead, + required ByteData vgaGraph, + }) { + // We just act as a clean pass-through to the core parser + return WLParser.load( + version: version, + vswap: vswap, + mapHead: mapHead, + gameMaps: gameMaps, + vgaDict: vgaDict, + vgaHead: vgaHead, + vgaGraph: vgaGraph, + ); + } +} diff --git a/packages/wolf_3d_data/lib/wolf_3d_data.dart b/packages/wolf_3d_data/lib/wolf_3d_data.dart index ec85972..666bd4b 100644 --- a/packages/wolf_3d_data/lib/wolf_3d_data.dart +++ b/packages/wolf_3d_data/lib/wolf_3d_data.dart @@ -3,6 +3,7 @@ /// More dartdocs go here. library; +export 'src/classes/game_file.dart' show GameFile; export 'src/classes/game_version.dart' show GameVersion; export 'src/classes/image.dart' show VgaImage; export 'src/classes/sound.dart' show PcmSound; @@ -10,3 +11,4 @@ export 'src/classes/sprite.dart' hide Matrix; export 'src/classes/wolf_level.dart' show WolfLevel; export 'src/classes/wolfenstein_data.dart' show WolfensteinData; export 'src/wl_parser.dart' show WLParser; +export 'src/wolfenstein_loader.dart' show WolfensteinLoader;