Moved all widgets and logic from gui app to Flutter package

- Implemented DebugToolsScreen for navigation to asset galleries.
- Created GameScreen to manage gameplay and renderer integrations.
- Added NoGameDataScreen to handle scenarios with missing game data.
- Developed SpriteGallery for visual browsing of sprite assets.
- Introduced VgaGallery for displaying VGA images from game data.
- Added GalleryGameSelector widget for selecting game variants in galleries.
- Created Wolf3dApp as the main application shell for managing game states.
- Implemented WolfMenuShell for consistent menu layouts across screens.
- Enhanced Wolf3d class to support debug mode and related functionalities.
- Updated pubspec.yaml to include window_manager dependency.
- Added tests for game screen lifecycle and debug mode functionalities.

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-23 18:44:32 +01:00
parent cbe2633ceb
commit 5a2681e89b
21 changed files with 963 additions and 590 deletions
@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
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_flutter.dart';
class _CountingAudio implements EngineAudio {
@override
WolfensteinData? activeGame;
int stopAllAudioCallCount = 0;
int disposeCallCount = 0;
@override
Future<void> debugSoundTest() async {}
@override
Future<void> init() async {}
@override
void playLevelMusic(Music music) {}
@override
void playMenuMusic() {}
@override
void playSoundEffect(SoundEffect effect) {}
@override
void playSoundEffectId(int sfxId) {}
@override
Future<void> stopAllAudio() async {
stopAllAudioCallCount++;
await Future<void>.delayed(const Duration(milliseconds: 1));
}
@override
void stopMusic() {}
@override
void dispose() {
disposeCallCount++;
}
}
void main() {
testWidgets('dispose path shuts down audio', (tester) async {
final audio = _CountingAudio();
final wolf3d = Wolf3d(audioBackend: audio);
await tester.pumpWidget(
MaterialApp(
home: GameScreen(
wolf3d: wolf3d,
skipEngineBootstrapForTest: true,
appExitHandler: () async {},
),
),
);
await tester.pumpWidget(const MaterialApp(home: SizedBox.shrink()));
await tester.pump(const Duration(milliseconds: 10));
expect(audio.stopAllAudioCallCount, 1);
expect(audio.disposeCallCount, 1);
});
}
@@ -0,0 +1,180 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
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_flutter.dart';
class _NoopAudio implements EngineAudio {
@override
WolfensteinData? activeGame;
@override
Future<void> debugSoundTest() async {}
@override
Future<void> init() async {}
@override
void playLevelMusic(Music music) {}
@override
void playMenuMusic() {}
@override
void playSoundEffect(SoundEffect effect) {}
@override
void playSoundEffectId(int sfxId) {}
@override
Future<void> stopAllAudio() async {}
@override
void stopMusic() {}
@override
void dispose() {}
}
void main() {
group('Wolf3d debug mode', () {
test('is disabled by default', () {
final wolf3d = Wolf3d(audioBackend: _NoopAudio());
expect(wolf3d.isDebugEnabled, isFalse);
});
test('enableDebug toggles debug mode', () {
final wolf3d = Wolf3d(audioBackend: _NoopAudio());
final returned = wolf3d.enableDebug();
expect(returned, same(wolf3d));
expect(wolf3d.isDebugEnabled, isTrue);
});
test('init(debug: true) enables debug mode', () async {
final wolf3d = Wolf3d(audioBackend: _NoopAudio());
await wolf3d.init(debug: true);
expect(wolf3d.isDebugEnabled, isTrue);
});
testWidgets('GameScreen hides debug FAB when debug mode is disabled', (
tester,
) async {
final wolf3d = _TestWolf3d(audioBackend: _NoopAudio());
await tester.pumpWidget(
MaterialApp(
home: GameScreen(
wolf3d: wolf3d,
appExitHandler: () async {},
),
),
);
await tester.pump();
expect(find.byType(FloatingActionButton), findsNothing);
expect(find.byIcon(Icons.bug_report), findsNothing);
});
testWidgets('GameScreen shows debug FAB when debug mode is enabled', (
tester,
) async {
final wolf3d = _TestWolf3d(audioBackend: _NoopAudio())..enableDebug();
await tester.pumpWidget(
MaterialApp(
home: GameScreen(
wolf3d: wolf3d,
appExitHandler: () async {},
),
),
);
await tester.pump();
expect(find.byType(FloatingActionButton), findsOneWidget);
expect(find.byIcon(Icons.bug_report), findsOneWidget);
});
});
}
class _TestWolf3d extends Wolf3d {
_TestWolf3d({required super.audioBackend});
@override
WolfEngine launchEngine({
required void Function() onGameWon,
void Function()? onQuit,
SaveGamePersistence? saveGamePersistence,
WolfRendererCapabilities? rendererCapabilities,
WolfRendererSettings? rendererSettings,
void Function(WolfRendererSettings settings)? onRendererSettingsChanged,
}) {
final engine = WolfEngine(
data: _buildTestData(),
difficulty: Difficulty.easy,
startingEpisode: 0,
frameBuffer: FrameBuffer(64, 64),
engineAudio: audio,
input: input,
onGameWon: onGameWon,
onMenuExit: () {},
onQuit: onQuit,
saveGamePersistence: saveGamePersistence,
rendererCapabilities: rendererCapabilities,
rendererSettings: const WolfRendererSettings(
mode: WolfRendererMode.software,
),
onRendererSettingsChanged: onRendererSettingsChanged,
);
engine.init();
return engine;
}
}
WolfensteinData _buildTestData() {
final wallGrid = List.generate(64, (_) => List.filled(64, 0));
final objectGrid = List.generate(64, (_) => List.filled(64, 0));
for (int i = 0; i < 64; i++) {
wallGrid[0][i] = 2;
wallGrid[63][i] = 2;
wallGrid[i][0] = 2;
wallGrid[i][63] = 2;
}
objectGrid[2][2] = MapObject.playerEast;
return WolfensteinData(
version: GameVersion.retail,
dataVersion: DataVersion.unknown,
registry: RetailAssetRegistry(),
walls: [_sprite(1), _sprite(1), _sprite(2), _sprite(2)],
sprites: List.generate(436, (_) => _sprite(255)),
sounds: List.generate(200, (_) => PcmSound(Uint8List(1))),
adLibSounds: const [],
music: const [],
vgaImages: const [],
episodes: [
Episode(
name: 'Test Episode',
levels: [
WolfLevel(
name: 'Test Level',
wallGrid: wallGrid,
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
objectGrid: objectGrid,
music: Music.level01,
),
],
),
],
);
}
Sprite _sprite(int color) =>
Sprite(Uint8List.fromList(List.filled(64 * 64, color)));