WIP moving difficulty selection to engine

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-18 12:25:59 +01:00
parent 0f7c77e85a
commit e39dfd5da0
29 changed files with 1337 additions and 3360 deletions

View File

@@ -1,104 +0,0 @@
/// Difficulty picker shown after the player chooses an episode.
library;
import 'package:flutter/material.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
import 'package:wolf_3d_gui/screens/game_screen.dart';
/// Starts a new game session using the active game and episode from [Wolf3d].
class DifficultyScreen extends StatefulWidget {
/// Shared application facade carrying the active game, input, and audio.
final Wolf3d wolf3d;
/// Creates the difficulty-selection screen for [wolf3d].
const DifficultyScreen({
super.key,
required this.wolf3d,
});
@override
State<DifficultyScreen> createState() => _DifficultyScreenState();
}
class _DifficultyScreenState extends State<DifficultyScreen> {
bool get isShareware =>
widget.wolf3d.activeGame.version == GameVersion.shareware;
@override
void dispose() {
widget.wolf3d.audio.stopMusic();
super.dispose();
}
/// Replaces the menu flow with an active [GameScreen] using [difficulty].
void _startGame(Difficulty difficulty, {bool showGallery = false}) {
widget.wolf3d.audio.stopMusic();
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => GameScreen(
data: widget.wolf3d.activeGame,
difficulty: difficulty,
startingEpisode: widget.wolf3d.activeEpisode,
audio: widget.wolf3d.audio,
input: widget.wolf3d.input,
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.red[900],
onPressed: () => _startGame(Difficulty.medium, showGallery: true),
child: const Icon(Icons.bug_report, color: Colors.white),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'HOW TOUGH ARE YOU?',
style: TextStyle(
color: Colors.red,
fontSize: 32,
fontWeight: FontWeight.bold,
fontFamily: 'Courier',
),
),
const SizedBox(height: 40),
ListView.builder(
shrinkWrap: true,
itemCount: Difficulty.values.length,
itemBuilder: (context, index) {
final Difficulty difficulty = Difficulty.values[index];
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueGrey[900],
foregroundColor: Colors.white,
minimumSize: const Size(300, 50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
),
onPressed: () => _startGame(difficulty),
child: Text(
difficulty.title,
style: const TextStyle(fontSize: 18),
),
),
);
},
),
],
),
),
);
}
}

View File

@@ -4,7 +4,7 @@ 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/difficulty_screen.dart';
import 'package:wolf_3d_gui/screens/game_screen.dart';
import 'package:wolf_3d_gui/screens/sprite_gallery.dart';
import 'package:wolf_3d_gui/screens/vga_gallery.dart';
@@ -27,12 +27,13 @@ class _EpisodeScreenState extends State<EpisodeScreen> {
widget.wolf3d.audio.playMenuMusic();
}
/// Persists the chosen episode in [Wolf3d] and advances to difficulty select.
/// Persists the chosen episode and lets the engine present difficulty select.
void _selectEpisode(int index) {
widget.wolf3d.setActiveEpisode(index);
widget.wolf3d.clearActiveDifficulty();
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DifficultyScreen(wolf3d: widget.wolf3d),
builder: (context) => GameScreen(wolf3d: widget.wolf3d),
),
);
}

View File

@@ -3,36 +3,19 @@ library;
import 'package:flutter/material.dart';
import 'package:flutter/services.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_input_flutter.dart';
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
import 'package:wolf_3d_renderer/wolf_3d_ascii_renderer.dart';
import 'package:wolf_3d_renderer/wolf_3d_flutter_renderer.dart';
/// Owns a [WolfEngine] instance and exposes renderer/input integrations to Flutter.
/// Launches a [WolfEngine] via [Wolf3d] and exposes renderer/input integrations.
class GameScreen extends StatefulWidget {
/// Fully parsed game data for the selected version.
final WolfensteinData data;
/// Shared application facade owning the engine, audio, and input.
final Wolf3d wolf3d;
/// Difficulty applied when creating the engine session.
final Difficulty difficulty;
/// Episode index used as the starting world.
final int startingEpisode;
/// Shared audio backend reused across menu and gameplay screens.
final EngineAudio audio;
/// Flutter input adapter that translates widget events into engine input.
final Wolf3dFlutterInput input;
/// Creates a gameplay screen with the supplied game session configuration.
/// Creates a gameplay screen driven by [wolf3d].
const GameScreen({
required this.data,
required this.difficulty,
required this.startingEpisode,
required this.audio,
required this.input,
required this.wolf3d,
super.key,
});
@@ -47,90 +30,147 @@ class _GameScreenState extends State<GameScreen> {
@override
void initState() {
super.initState();
_engine = WolfEngine(
data: widget.data,
difficulty: widget.difficulty,
startingEpisode: widget.startingEpisode,
frameBuffer: FrameBuffer(320, 200),
audio: widget.audio,
input: widget.input,
_engine = widget.wolf3d.launchEngine(
onGameWon: () => Navigator.of(context).pop(),
);
_engine.init();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Listener(
onPointerDown: widget.input.onPointerDown,
onPointerUp: widget.input.onPointerUp,
onPointerMove: widget.input.onPointerMove,
onPointerHover: widget.input.onPointerMove,
child: Stack(
children: [
// Keep both renderers behind the same engine so mode switching does
// not reset level state or audio playback.
_useAsciiMode
? WolfAsciiRenderer(engine: _engine)
: WolfFlutterRenderer(engine: _engine),
return WillPopScope(
onWillPop: () async {
if (_engine.isDifficultySelectionPending) {
widget.wolf3d.input.queueBackAction();
return false;
}
return true;
},
child: Scaffold(
body: LayoutBuilder(
builder: (context, constraints) {
final viewportRect = _menuViewportRect(
Size(constraints.maxWidth, constraints.maxHeight),
);
if (!_engine.isInitialized)
Container(
color: Colors.black,
child: const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(color: Colors.teal),
SizedBox(height: 20),
Text(
"GET PSYCHED!",
style: TextStyle(
color: Colors.teal,
fontFamily: 'monospace',
return Listener(
onPointerDown: (event) {
widget.wolf3d.input.onPointerDown(event);
if (_engine.isDifficultySelectionPending &&
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,
onPointerHover: widget.wolf3d.input.onPointerMove,
child: Stack(
children: [
// Keep both renderers behind the same engine so mode switching does
// not reset level state or audio playback.
_useAsciiMode
? WolfAsciiRenderer(engine: _engine)
: WolfFlutterRenderer(engine: _engine),
if (!_engine.isInitialized)
Container(
color: Colors.black,
child: const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(color: Colors.teal),
SizedBox(height: 20),
Text(
"GET PSYCHED!",
style: TextStyle(
color: Colors.teal,
fontFamily: 'monospace',
),
),
],
),
),
],
),
// Tab toggles the renderer implementation for quick visual debugging.
Focus(
autofocus: true,
onKeyEvent: (node, event) {
if (event is KeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.tab) {
setState(() => _useAsciiMode = !_useAsciiMode);
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
child: const SizedBox.shrink(),
),
),
),
// Tab toggles the renderer implementation for quick visual debugging.
Focus(
autofocus: true,
onKeyEvent: (node, event) {
if (event is KeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.tab) {
setState(() => _useAsciiMode = !_useAsciiMode);
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
child: const SizedBox.shrink(),
),
// A second full-screen overlay keeps the presentation simple while
// the engine is still warming up or decoding the first frame.
if (!_engine.isInitialized)
Container(
color: Colors.black,
child: const Center(
child: CircularProgressIndicator(color: Colors.teal),
),
),
// A second full-screen overlay keeps the presentation simple while
// the engine is still warming up or decoding the first frame.
if (!_engine.isInitialized)
Container(
color: Colors.black,
child: const Center(
child: CircularProgressIndicator(color: Colors.teal),
),
Positioned(
top: 16,
right: 16,
child: Text(
'TAB: Swap Renderer',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.5),
),
),
),
],
),
Positioned(
top: 16,
right: 16,
child: Text(
'TAB: Swap Renderer',
style: TextStyle(color: Colors.white.withValues(alpha: 0.5)),
),
),
],
);
},
),
),
);
}
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

@@ -0,0 +1,95 @@
/// Shared shell for Wolf3D-style menu screens.
library;
import 'package:flutter/material.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_renderer/wolf_3d_asset_painter.dart';
/// Provides a common menu layout with panel framing and optional bottom art.
class WolfMenuShell extends StatelessWidget {
/// Full-screen background color behind the panel.
final Color backgroundColor;
/// Solid panel fill used for the menu content area.
final Color panelColor;
/// Optional heading shown above the panel (text or image).
final Widget? header;
/// Primary menu content rendered inside the panel.
final Widget panelChild;
/// Optional centered VGA image anchored near the bottom of the screen.
final VgaImage? bottomSprite;
/// Width of the menu panel.
final double panelWidth;
/// Padding applied around [panelChild] inside the panel.
final EdgeInsets panelPadding;
/// Scale factor for [bottomSprite].
final double bottomSpriteScale;
/// Distance from the bottom edge for [bottomSprite].
final double bottomOffset;
/// Vertical spacing between [header] and the panel.
final double headerSpacing;
const WolfMenuShell({
super.key,
required this.backgroundColor,
required this.panelColor,
required this.panelChild,
this.header,
this.bottomSprite,
this.panelWidth = 520,
this.panelPadding = const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
this.bottomSpriteScale = 3,
this.bottomOffset = 20,
this.headerSpacing = 14,
});
@override
Widget build(BuildContext context) {
return Stack(
children: [
Positioned.fill(
child: ColoredBox(color: backgroundColor),
),
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
?header,
if (header != null) SizedBox(height: headerSpacing),
Container(
width: panelWidth,
padding: panelPadding,
color: panelColor,
child: panelChild,
),
],
),
),
if (bottomSprite != null)
Positioned(
left: 0,
right: 0,
bottom: bottomOffset,
child: Center(
child: SizedBox(
width: bottomSprite!.width * bottomSpriteScale,
height: bottomSprite!.height * bottomSpriteScale,
child: WolfAssetPainter.vga(bottomSprite),
),
),
),
],
);
}
}