Refactor menu rendering and improve projection sampling

- Updated AsciiRasterizer to support game and episode selection menus with improved layout and cursor handling.
- Enhanced SixelRasterizer and SoftwareRasterizer to modularize menu drawing logic for game and episode selection.
- Introduced new methods for drawing menus and applying fade effects across rasterizers.
- Adjusted wall texture sampling in Rasterizer to anchor to projection height center for consistent rendering.
- Added tests for wall texture sampling behavior to ensure legacy compatibility and new functionality.
- Modified Flutter audio adapter to use nullable access for active game and adjusted game selection logic in the main class.
- Cleaned up input handling in Wolf3dFlutterInput by removing unused menu tap variables.

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-18 20:06:18 +01:00
parent d93f467163
commit 0e143892f0
15 changed files with 1090 additions and 204 deletions

View File

@@ -6,7 +6,7 @@ library;
import 'package:flutter/material.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_screen.dart';
/// Creates the application shell after loading available Wolf3D data sets.
void main() async {
@@ -16,7 +16,65 @@ void main() async {
runApp(
MaterialApp(
home: GameSelectScreen(wolf3d: wolf3d),
home: wolf3d.availableGames.isEmpty
? const _NoGameDataScreen()
: GameScreen(wolf3d: wolf3d),
),
);
}
class _NoGameDataScreen extends StatelessWidget {
const _NoGameDataScreen();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF140000),
body: Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 640),
child: DecoratedBox(
decoration: BoxDecoration(
color: const Color(0xFF590002),
border: Border.all(color: const Color(0xFFB00000), width: 2),
),
child: const Padding(
padding: EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'WOLF3D DATA NOT FOUND',
style: TextStyle(
color: Color(0xFFFFF700),
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 16),
Text(
'No game files were discovered.\n\n'
'Add Wolfenstein 3D data files to one of these locations:\n'
'- packages/wolf_3d_assets/assets/retail\n'
'- packages/wolf_3d_assets/assets/shareware\n'
'- or a discoverable local game-data folder.\n\n'
'Restart the app after adding the files.',
style: TextStyle(
color: Colors.white,
fontSize: 15,
height: 1.4,
),
),
],
),
),
),
),
),
),
);
}
}

View File

@@ -51,28 +51,9 @@ class _GameScreenState extends State<GameScreen> {
child: Scaffold(
body: LayoutBuilder(
builder: (context, constraints) {
final viewportRect = _menuViewportRect(
Size(constraints.maxWidth, constraints.maxHeight),
);
return Listener(
onPointerDown: (event) {
widget.wolf3d.input.onPointerDown(event);
if (_engine.difficulty == null &&
viewportRect.width > 0 &&
viewportRect.height > 0 &&
viewportRect.contains(event.localPosition)) {
final normalizedX =
(event.localPosition.dx - viewportRect.left) /
viewportRect.width;
final normalizedY =
(event.localPosition.dy - viewportRect.top) /
viewportRect.height;
widget.wolf3d.input.queueMenuTap(
x: normalizedX,
y: normalizedY,
);
}
},
onPointerUp: widget.wolf3d.input.onPointerUp,
onPointerMove: widget.wolf3d.input.onPointerMove,
@@ -148,32 +129,4 @@ class _GameScreenState extends State<GameScreen> {
),
);
}
Rect _menuViewportRect(Size availableSize) {
if (availableSize.width <= 0 || availableSize.height <= 0) {
return Rect.zero;
}
const double aspect = 4 / 3;
final double outerPadding = _useAsciiMode ? 0.0 : 16.0;
final double maxWidth = (availableSize.width - (outerPadding * 2)).clamp(
1.0,
double.infinity,
);
final double maxHeight = (availableSize.height - (outerPadding * 2)).clamp(
1.0,
double.infinity,
);
double viewportWidth = maxWidth;
double viewportHeight = viewportWidth / aspect;
if (viewportHeight > maxHeight) {
viewportHeight = maxHeight;
viewportWidth = viewportHeight * aspect;
}
final double left = (availableSize.width - viewportWidth) / 2;
final double top = (availableSize.height - viewportHeight) / 2;
return Rect.fromLTWH(left, top, viewportWidth, viewportHeight);
}
}

View File

@@ -1,43 +0,0 @@
/// Game-selection screen shown after the GUI host discovers available assets.
library;
import 'package:flutter/material.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
import 'package:wolf_3d_gui/screens/episode_screen.dart';
/// Lists every discovered data set and lets the user choose the active one.
class GameSelectScreen extends StatelessWidget {
/// Shared application facade that owns discovered games, audio, and input.
final Wolf3d wolf3d;
/// Creates the game-selection screen for the supplied [wolf3d] session.
const GameSelectScreen({super.key, required this.wolf3d});
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemCount: wolf3d.availableGames.length,
itemBuilder: (context, i) {
final WolfensteinData data = wolf3d.availableGames[i];
final GameVersion version = data.version;
return Card(
child: ListTile(
title: Text(version.name),
onTap: () {
wolf3d.setActiveGame(data);
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => EpisodeScreen(wolf3d: wolf3d),
),
);
},
),
);
},
),
);
}
}