feat: Enhance menu rendering and input handling
- Added support for new plugins: IrondashEngineContext and SuperNativeExtensions in the Flutter plugin registrant. - Updated CMake configuration to include new plugins. - Introduced a new dependency, super_clipboard, in pubspec.yaml. - Enhanced the WolfEngine to set the menu background color. - Implemented keyboard shortcuts for renderer mode toggling and ASCII theme cycling in CLI input handling. - Updated menu manager to include a universal menu background color. - Refactored ASCII and Sixel renderers to utilize the new menu background color and improved header drawing logic. - Simplified the drawing of menu options sidebars and header bars across different renderers. - Improved the layout and centering of menu titles in the header bar. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -115,9 +115,8 @@ class CliGameLoop {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bytes.contains(9)) {
|
if (input.matchesRendererToggleShortcut(bytes)) {
|
||||||
// Tab swaps between renderers so renderer debugging stays available
|
// Allow dynamic renderer-switch bindings configured on the CLI input.
|
||||||
// without restarting the process.
|
|
||||||
_renderer = identical(_renderer, secondaryRenderer)
|
_renderer = identical(_renderer, secondaryRenderer)
|
||||||
? primaryRenderer
|
? primaryRenderer
|
||||||
: secondaryRenderer;
|
: secondaryRenderer;
|
||||||
@@ -125,7 +124,7 @@ class CliGameLoop {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bytes.contains(116) || bytes.contains(84)) {
|
if (input.matchesAsciiThemeCycleShortcut(bytes)) {
|
||||||
_cycleAsciiTheme();
|
_cycleAsciiTheme();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -188,5 +187,37 @@ class CliGameLoop {
|
|||||||
|
|
||||||
engine.tick(elapsed);
|
engine.tick(elapsed);
|
||||||
stdout.write(_renderer.render(engine));
|
stdout.write(_renderer.render(engine));
|
||||||
|
_writeShortcutHintLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _writeShortcutHintLine() {
|
||||||
|
if (!stdout.hasTerminal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int cols = stdout.terminalColumns;
|
||||||
|
if (cols <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String hint = _buildShortcutHintText();
|
||||||
|
final String visible = hint.length > cols ? hint.substring(0, cols) : hint;
|
||||||
|
final String padded = visible.padRight(cols);
|
||||||
|
|
||||||
|
// Draw an overlay line without disturbing the renderer's cursor position.
|
||||||
|
stdout.write('\x1b[s\x1b[1;1H\x1b[0m\x1b[2m$padded\x1b[0m\x1b[u');
|
||||||
|
}
|
||||||
|
|
||||||
|
String _buildShortcutHintText() {
|
||||||
|
final String rendererMode = _renderer is SixelRenderer ? 'sixel' : 'ascii';
|
||||||
|
final String rendererHint =
|
||||||
|
'<${input.rendererToggleKeyLabel}> $rendererMode';
|
||||||
|
|
||||||
|
if (_renderer is AsciiRenderer) {
|
||||||
|
final AsciiRenderer ascii = _renderer as AsciiRenderer;
|
||||||
|
return '$rendererHint <${input.asciiThemeCycleKeyLabel}> ${ascii.activeTheme.name}';
|
||||||
|
}
|
||||||
|
|
||||||
|
return rendererHint;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
66
apps/wolf_3d_gui/lib/screens/debug_tools_screen.dart
Normal file
66
apps/wolf_3d_gui/lib/screens/debug_tools_screen.dart
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
/// Debug tools launcher for art and asset inspection screens.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
|
||||||
|
import 'package:wolf_3d_gui/screens/sprite_gallery.dart';
|
||||||
|
import 'package:wolf_3d_gui/screens/vga_gallery.dart';
|
||||||
|
|
||||||
|
/// Presents debug-only navigation shortcuts for asset galleries.
|
||||||
|
class DebugToolsScreen extends StatelessWidget {
|
||||||
|
/// Shared app facade used to access active game assets.
|
||||||
|
final Wolf3d wolf3d;
|
||||||
|
|
||||||
|
/// Creates the debug tools screen for [wolf3d].
|
||||||
|
const DebugToolsScreen({super.key, required this.wolf3d});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Debug Tools'),
|
||||||
|
automaticallyImplyLeading: true,
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Asset Galleries',
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Card(
|
||||||
|
child: ListTile(
|
||||||
|
leading: const Icon(Icons.image_search),
|
||||||
|
title: const Text('Sprite Gallery'),
|
||||||
|
subtitle: const Text('Browse decoded sprite frames.'),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => SpriteGallery(wolf3d: wolf3d),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Card(
|
||||||
|
child: ListTile(
|
||||||
|
leading: const Icon(Icons.photo_library),
|
||||||
|
title: const Text('VGA Gallery'),
|
||||||
|
subtitle: const Text('Browse decoded VGA images.'),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => VgaGallery(images: wolf3d.vgaImages),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
/// Episode picker and asset-browser entry point for the selected game version.
|
|
||||||
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';
|
|
||||||
import 'package:wolf_3d_gui/screens/sprite_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 {
|
|
||||||
/// Shared application facade whose active game must already be set.
|
|
||||||
final Wolf3d wolf3d;
|
|
||||||
|
|
||||||
/// Creates the episode-selection screen for [wolf3d].
|
|
||||||
const EpisodeScreen({super.key, required this.wolf3d});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<EpisodeScreen> createState() => _EpisodeScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _EpisodeScreenState extends State<EpisodeScreen> {
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
widget.wolf3d.audio.playMenuMusic();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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) => GameScreen(wolf3d: widget.wolf3d),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final List<Episode> episodes = widget.wolf3d.activeGame.episodes;
|
|
||||||
|
|
||||||
return Scaffold(
|
|
||||||
backgroundColor: Colors.teal,
|
|
||||||
floatingActionButtonLocation:
|
|
||||||
FloatingActionButtonLocation.miniCenterFloat,
|
|
||||||
floatingActionButton: Row(
|
|
||||||
children: [
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) {
|
|
||||||
return VgaGallery(images: widget.wolf3d.vgaImages);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Text('VGA Gallery'),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) {
|
|
||||||
return SpriteGallery(
|
|
||||||
wolf3d: widget.wolf3d,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Text('Sprites'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
'WHICH EPISODE TO PLAY?',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.red,
|
|
||||||
fontSize: 32,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontFamily: 'Courier',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 40),
|
|
||||||
ListView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
itemCount: episodes.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final Episode episode = episodes[index];
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
vertical: 8.0,
|
|
||||||
horizontal: 32.0,
|
|
||||||
),
|
|
||||||
child: ElevatedButton(
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Colors.blueGrey[900],
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
minimumSize: const Size(300, 60),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onPressed: () => _selectEpisode(index),
|
|
||||||
child: Text(
|
|
||||||
episode.name,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: const TextStyle(fontSize: 18),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,6 +12,7 @@ import 'package:wolf_3d_dart/wolf_3d_input.dart';
|
|||||||
import 'package:wolf_3d_dart/wolf_3d_renderer.dart';
|
import 'package:wolf_3d_dart/wolf_3d_renderer.dart';
|
||||||
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
|
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
|
||||||
import 'package:wolf_3d_flutter/wolf_3d_input_flutter.dart';
|
import 'package:wolf_3d_flutter/wolf_3d_input_flutter.dart';
|
||||||
|
import 'package:wolf_3d_gui/screens/debug_tools_screen.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';
|
||||||
import 'package:wolf_3d_renderer/wolf_3d_glsl_renderer.dart';
|
import 'package:wolf_3d_renderer/wolf_3d_glsl_renderer.dart';
|
||||||
@@ -173,6 +174,11 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
onPressed: _openDebugTools,
|
||||||
|
tooltip: 'Open Debug Tools',
|
||||||
|
child: const Icon(Icons.bug_report),
|
||||||
|
),
|
||||||
body: LayoutBuilder(
|
body: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
return Listener(
|
return Listener(
|
||||||
@@ -336,6 +342,14 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
_glslEffectsEnabled = !_glslEffectsEnabled;
|
_glslEffectsEnabled = !_glslEffectsEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _openDebugTools() {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => DebugToolsScreen(wolf3d: widget.wolf3d),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
bool _handleHostShortcut(KeyEvent event) {
|
bool _handleHostShortcut(KeyEvent event) {
|
||||||
final HostShortcutHandler? customHandler = widget.hostShortcutHandler;
|
final HostShortcutHandler? customHandler = widget.hostShortcutHandler;
|
||||||
if (customHandler != null) {
|
if (customHandler != null) {
|
||||||
|
|||||||
@@ -62,9 +62,11 @@ class SpriteGallery extends StatelessWidget {
|
|||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: AspectRatio(
|
child: Center(
|
||||||
aspectRatio: 4 / 3,
|
child: AspectRatio(
|
||||||
child: WolfAssetPainter.sprite(wolf3d.sprites[index]),
|
aspectRatio: 1,
|
||||||
|
child: WolfAssetPainter.sprite(wolf3d.sprites[index]),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
/// Visual browser for decoded VGA pictures and UI art.
|
/// Visual browser for decoded VGA pictures and UI art.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:super_clipboard/super_clipboard.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';
|
||||||
|
|
||||||
@@ -13,6 +18,110 @@ class VgaGallery extends StatelessWidget {
|
|||||||
/// Creates the gallery for [images].
|
/// Creates the gallery for [images].
|
||||||
const VgaGallery({super.key, required this.images});
|
const VgaGallery({super.key, required this.images});
|
||||||
|
|
||||||
|
Future<ui.Image> _buildVgaUiImage(VgaImage image) {
|
||||||
|
final completer = Completer<ui.Image>();
|
||||||
|
final Uint8List pixels = Uint8List(image.width * image.height * 4);
|
||||||
|
|
||||||
|
for (int y = 0; y < image.height; y++) {
|
||||||
|
for (int x = 0; x < image.width; x++) {
|
||||||
|
final int colorByte = image.decodePixel(x, y);
|
||||||
|
final int abgr = ColorPalette.vga32Bit[colorByte];
|
||||||
|
final int offset = (y * image.width + x) * 4;
|
||||||
|
pixels[offset] = abgr & 0xFF; // R
|
||||||
|
pixels[offset + 1] = (abgr >> 8) & 0xFF; // G
|
||||||
|
pixels[offset + 2] = (abgr >> 16) & 0xFF; // B
|
||||||
|
pixels[offset + 3] = (abgr >> 24) & 0xFF; // A
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.decodeImageFromPixels(
|
||||||
|
pixels,
|
||||||
|
image.width,
|
||||||
|
image.height,
|
||||||
|
ui.PixelFormat.rgba8888,
|
||||||
|
(ui.Image decoded) => completer.complete(decoded),
|
||||||
|
);
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _copyImageToClipboard(BuildContext context, int index) async {
|
||||||
|
final clipboard = SystemClipboard.instance;
|
||||||
|
if (clipboard == null) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Clipboard API is unavailable here.')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final VgaImage image = images[index];
|
||||||
|
ui.Image? uiImage;
|
||||||
|
try {
|
||||||
|
uiImage = await _buildVgaUiImage(image);
|
||||||
|
final ByteData? pngData = await uiImage.toByteData(
|
||||||
|
format: ui.ImageByteFormat.png,
|
||||||
|
);
|
||||||
|
if (pngData == null) {
|
||||||
|
throw StateError('Failed to encode image as PNG.');
|
||||||
|
}
|
||||||
|
|
||||||
|
final item = DataWriterItem();
|
||||||
|
item.add(Formats.png(pngData.buffer.asUint8List()));
|
||||||
|
await clipboard.write([item]);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Copied VGA image #$index to clipboard.')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Copy failed: $error')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
uiImage?.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showImagePreviewDialog(BuildContext context, int index) {
|
||||||
|
final VgaImage image = images[index];
|
||||||
|
|
||||||
|
showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(
|
||||||
|
'Index: $index ${image.width} x ${image.height}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||||
|
child: const Text('Close'),
|
||||||
|
),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () => _copyImageToClipboard(dialogContext, index),
|
||||||
|
icon: const Icon(Icons.copy),
|
||||||
|
label: const Text('Copy'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
content: Center(
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: image.width / image.height,
|
||||||
|
child: WolfAssetPainter.vga(image),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -28,21 +137,27 @@ class VgaGallery extends StatelessWidget {
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return Card(
|
return Card(
|
||||||
color: Colors.blueGrey,
|
color: Colors.blueGrey,
|
||||||
child: Column(
|
child: InkWell(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
onTap: () => _showImagePreviewDialog(context, index),
|
||||||
spacing: 8,
|
child: Column(
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
Text(
|
spacing: 8,
|
||||||
"Index: $index\n${images[index].width} x ${images[index].height}",
|
children: [
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 12),
|
Text(
|
||||||
textAlign: TextAlign.center,
|
"Index: $index\n${images[index].width} x ${images[index].height}",
|
||||||
),
|
style: const TextStyle(color: Colors.white, fontSize: 12),
|
||||||
Expanded(
|
textAlign: TextAlign.center,
|
||||||
child: Center(
|
|
||||||
child: WolfAssetPainter.vga(images[index]),
|
|
||||||
),
|
),
|
||||||
),
|
Expanded(
|
||||||
],
|
child: Center(
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: images[index].width / images[index].height,
|
||||||
|
child: WolfAssetPainter.vga(images[index]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,16 +7,24 @@
|
|||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <audioplayers_linux/audioplayers_linux_plugin.h>
|
#include <audioplayers_linux/audioplayers_linux_plugin.h>
|
||||||
|
#include <irondash_engine_context/irondash_engine_context_plugin.h>
|
||||||
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
|
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
|
||||||
|
#include <super_native_extensions/super_native_extensions_plugin.h>
|
||||||
#include <window_manager/window_manager_plugin.h>
|
#include <window_manager/window_manager_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar =
|
g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin");
|
||||||
audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar);
|
audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) irondash_engine_context_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "IrondashEngineContextPlugin");
|
||||||
|
irondash_engine_context_plugin_register_with_registrar(irondash_engine_context_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar =
|
g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin");
|
||||||
screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);
|
screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) super_native_extensions_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "SuperNativeExtensionsPlugin");
|
||||||
|
super_native_extensions_plugin_register_with_registrar(super_native_extensions_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) window_manager_registrar =
|
g_autoptr(FlPluginRegistrar) window_manager_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
|
||||||
window_manager_plugin_register_with_registrar(window_manager_registrar);
|
window_manager_plugin_register_with_registrar(window_manager_registrar);
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
audioplayers_linux
|
audioplayers_linux
|
||||||
|
irondash_engine_context
|
||||||
screen_retriever_linux
|
screen_retriever_linux
|
||||||
|
super_native_extensions
|
||||||
window_manager
|
window_manager
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ dependencies:
|
|||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
super_clipboard: ^0.9.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class WolfEngine {
|
|||||||
if (_availableGames.isEmpty) {
|
if (_availableGames.isEmpty) {
|
||||||
throw StateError('WolfEngine requires at least one game data set.');
|
throw StateError('WolfEngine requires at least one game data set.');
|
||||||
}
|
}
|
||||||
|
menuManager.menuBackgroundRgb = menuBackgroundRgb;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Total milliseconds elapsed since the engine was initialized.
|
/// Total milliseconds elapsed since the engine was initialized.
|
||||||
|
|||||||
@@ -6,6 +6,60 @@ import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
|||||||
|
|
||||||
/// Buffers one-frame terminal key presses for consumption by the engine loop.
|
/// Buffers one-frame terminal key presses for consumption by the engine loop.
|
||||||
class CliInput extends Wolf3dInput {
|
class CliInput extends Wolf3dInput {
|
||||||
|
/// Keyboard shortcut used by the CLI host to cycle renderer modes.
|
||||||
|
String rendererToggleKey = 'r';
|
||||||
|
|
||||||
|
/// Keyboard shortcut used by the CLI host to cycle ASCII themes.
|
||||||
|
String asciiThemeCycleKey = 't';
|
||||||
|
|
||||||
|
/// Human-friendly label for [rendererToggleKey] shown in CLI hints.
|
||||||
|
String get rendererToggleKeyLabel => _formatShortcutLabel(rendererToggleKey);
|
||||||
|
|
||||||
|
/// Human-friendly label for [asciiThemeCycleKey] shown in CLI hints.
|
||||||
|
String get asciiThemeCycleKeyLabel =>
|
||||||
|
_formatShortcutLabel(asciiThemeCycleKey);
|
||||||
|
|
||||||
|
/// Returns true when [bytes] triggers the renderer-toggle shortcut.
|
||||||
|
bool matchesRendererToggleShortcut(List<int> bytes) =>
|
||||||
|
_matchesShortcut(bytes, rendererToggleKey);
|
||||||
|
|
||||||
|
/// Returns true when [bytes] triggers the ASCII-theme shortcut.
|
||||||
|
bool matchesAsciiThemeCycleShortcut(List<int> bytes) =>
|
||||||
|
_matchesShortcut(bytes, asciiThemeCycleKey);
|
||||||
|
|
||||||
|
String _formatShortcutLabel(String key) {
|
||||||
|
final String trimmed = key.trim();
|
||||||
|
if (trimmed.isEmpty) {
|
||||||
|
return 'KEY';
|
||||||
|
}
|
||||||
|
if (trimmed == ' ') {
|
||||||
|
return 'SPACE';
|
||||||
|
}
|
||||||
|
return trimmed.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _matchesShortcut(List<int> bytes, String key) {
|
||||||
|
final String trimmed = key.trim();
|
||||||
|
if (trimmed.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed == ' ') {
|
||||||
|
return bytes.length == 1 && bytes[0] == 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes.length != 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int expected = trimmed.codeUnitAt(0);
|
||||||
|
final int actual = bytes[0];
|
||||||
|
return actual == expected ||
|
||||||
|
actual ==
|
||||||
|
(expected >= 97 && expected <= 122 ? expected - 32 : expected) ||
|
||||||
|
actual == (expected >= 65 && expected <= 90 ? expected + 32 : expected);
|
||||||
|
}
|
||||||
|
|
||||||
// Raw stdin arrives asynchronously, so presses are staged here until the
|
// Raw stdin arrives asynchronously, so presses are staged here until the
|
||||||
// next engine frame snapshots them into the active state.
|
// next engine frame snapshots them into the active state.
|
||||||
bool _pForward = false;
|
bool _pForward = false;
|
||||||
|
|||||||
@@ -93,6 +93,9 @@ class MenuManager {
|
|||||||
bool _prevConfirm = false;
|
bool _prevConfirm = false;
|
||||||
bool _prevBack = false;
|
bool _prevBack = false;
|
||||||
|
|
||||||
|
/// Universal menu background color in 24-bit RGB used by menu screens.
|
||||||
|
int menuBackgroundRgb = 0x890000;
|
||||||
|
|
||||||
WolfMenuScreen get activeMenu => _activeMenu;
|
WolfMenuScreen get activeMenu => _activeMenu;
|
||||||
|
|
||||||
bool get isTransitioning => _transitionTarget != null;
|
bool get isTransitioning => _transitionTarget != null;
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
static const int _menuHintKeyPaletteIndex = 12;
|
static const int _menuHintKeyPaletteIndex = 12;
|
||||||
static const int _menuHintLabelPaletteIndex = 4;
|
static const int _menuHintLabelPaletteIndex = 4;
|
||||||
static const int _menuHintBackgroundPaletteIndex = 0;
|
static const int _menuHintBackgroundPaletteIndex = 0;
|
||||||
|
static const int _headerHeadingY = 24;
|
||||||
|
|
||||||
AsciiRenderer({
|
AsciiRenderer({
|
||||||
this.activeTheme = AsciiThemes.blocks,
|
this.activeTheme = AsciiThemes.blocks,
|
||||||
@@ -129,6 +130,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
|
|
||||||
late List<List<ColoredChar>> _screen;
|
late List<List<ColoredChar>> _screen;
|
||||||
late List<List<int>> _scenePixels;
|
late List<List<int>> _scenePixels;
|
||||||
|
List<int>? _mainMenuBandFirstColumn;
|
||||||
String? _lastLoggedThemeName;
|
String? _lastLoggedThemeName;
|
||||||
|
|
||||||
static const List<String> _quadrantByMask = <String>[
|
static const List<String> _quadrantByMask = <String>[
|
||||||
@@ -403,7 +405,9 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void drawMenu(WolfEngine engine) {
|
void drawMenu(WolfEngine engine) {
|
||||||
final int bgColor = _rgbToPaletteColor(engine.menuBackgroundRgb);
|
final int bgColor = _rgbToPaletteColor(
|
||||||
|
engine.menuManager.menuBackgroundRgb,
|
||||||
|
);
|
||||||
final int panelColor = _rgbToPaletteColor(engine.menuPanelRgb);
|
final int panelColor = _rgbToPaletteColor(engine.menuPanelRgb);
|
||||||
final int headingColor = WolfMenuPalette.headerTextColor;
|
final int headingColor = WolfMenuPalette.headerTextColor;
|
||||||
final int selectedTextColor = WolfMenuPalette.selectedTextColor;
|
final int selectedTextColor = WolfMenuPalette.selectedTextColor;
|
||||||
@@ -418,6 +422,10 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final art = WolfClassicMenuArt(engine.data);
|
final art = WolfClassicMenuArt(engine.data);
|
||||||
|
final optionsLabel = art.optionsLabel;
|
||||||
|
if (optionsLabel != null) {
|
||||||
|
_mainMenuBandFirstColumn = _cacheFirstColumn(optionsLabel);
|
||||||
|
}
|
||||||
|
|
||||||
if (engine.menuManager.activeMenu == WolfMenuScreen.introSplash) {
|
if (engine.menuManager.activeMenu == WolfMenuScreen.introSplash) {
|
||||||
_drawIntroSplash(engine, art, menuTypography);
|
_drawIntroSplash(engine, art, menuTypography);
|
||||||
@@ -433,9 +441,14 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
_drawMainMenuOptionsSideBars(optionsLabel, optionsX);
|
_drawMainMenuOptionsSideBars(optionsLabel, optionsX);
|
||||||
_blitVgaImageAscii(optionsLabel, optionsX, 0);
|
_blitVgaImageAscii(optionsLabel, optionsX, 0);
|
||||||
} else {
|
} else {
|
||||||
|
_drawHeaderBarStack(
|
||||||
|
headingY200: _headerHeadingY,
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
barColor: ColorPalette.vga32Bit[0],
|
||||||
|
);
|
||||||
_drawMenuTextCentered(
|
_drawMenuTextCentered(
|
||||||
'OPTIONS',
|
'OPTIONS',
|
||||||
24,
|
_headerHeadingY,
|
||||||
headingColor,
|
headingColor,
|
||||||
scale: menuTypography.headingScale,
|
scale: menuTypography.headingScale,
|
||||||
);
|
);
|
||||||
@@ -480,6 +493,11 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (engine.menuManager.activeMenu == WolfMenuScreen.gameSelect) {
|
if (engine.menuManager.activeMenu == WolfMenuScreen.gameSelect) {
|
||||||
|
_drawHeaderBarStack(
|
||||||
|
headingY200: _headerHeadingY,
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
barColor: ColorPalette.vga32Bit[0],
|
||||||
|
);
|
||||||
_fillRect320(28, 58, 264, 104, panelColor);
|
_fillRect320(28, 58, 264, 104, panelColor);
|
||||||
|
|
||||||
final cursor = art.mappedPic(
|
final cursor = art.mappedPic(
|
||||||
@@ -492,7 +510,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
.toList(growable: false);
|
.toList(growable: false);
|
||||||
_drawMenuTextCentered(
|
_drawMenuTextCentered(
|
||||||
'SELECT GAME',
|
'SELECT GAME',
|
||||||
48,
|
_headerHeadingY,
|
||||||
headingColor,
|
headingColor,
|
||||||
scale: menuTypography.headingScale,
|
scale: menuTypography.headingScale,
|
||||||
);
|
);
|
||||||
@@ -525,6 +543,11 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (engine.menuManager.activeMenu == WolfMenuScreen.episodeSelect) {
|
if (engine.menuManager.activeMenu == WolfMenuScreen.episodeSelect) {
|
||||||
|
_drawHeaderBarStack(
|
||||||
|
headingY200: _headerHeadingY,
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
barColor: ColorPalette.vga32Bit[0],
|
||||||
|
);
|
||||||
_fillRect320(12, 18, 296, 168, panelColor);
|
_fillRect320(12, 18, 296, 168, panelColor);
|
||||||
|
|
||||||
final cursor = art.mappedPic(
|
final cursor = art.mappedPic(
|
||||||
@@ -539,7 +562,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
if (menuTypography.usesCompactRows) {
|
if (menuTypography.usesCompactRows) {
|
||||||
_drawMenuTextCentered(
|
_drawMenuTextCentered(
|
||||||
'WHICH EPISODE TO PLAY?',
|
'WHICH EPISODE TO PLAY?',
|
||||||
8,
|
_headerHeadingY,
|
||||||
headingColor,
|
headingColor,
|
||||||
scale: menuTypography.headingScale,
|
scale: menuTypography.headingScale,
|
||||||
);
|
);
|
||||||
@@ -580,7 +603,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
|
|
||||||
_drawMenuTextCentered(
|
_drawMenuTextCentered(
|
||||||
'WHICH EPISODE TO PLAY?',
|
'WHICH EPISODE TO PLAY?',
|
||||||
8,
|
_headerHeadingY,
|
||||||
headingColor,
|
headingColor,
|
||||||
scale: menuTypography.headingScale,
|
scale: menuTypography.headingScale,
|
||||||
);
|
);
|
||||||
@@ -622,6 +645,12 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
final int selectedDifficultyIndex =
|
final int selectedDifficultyIndex =
|
||||||
engine.menuManager.selectedDifficultyIndex;
|
engine.menuManager.selectedDifficultyIndex;
|
||||||
|
|
||||||
|
_drawHeaderBarStack(
|
||||||
|
headingY200: _headerHeadingY,
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
barColor: ColorPalette.vga32Bit[0],
|
||||||
|
);
|
||||||
|
|
||||||
_fillRect320(28, 70, 264, 82, panelColor);
|
_fillRect320(28, 70, 264, 82, panelColor);
|
||||||
|
|
||||||
final face = art.difficultyOption(
|
final face = art.difficultyOption(
|
||||||
@@ -640,7 +669,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
if (menuTypography.usesCompactRows) {
|
if (menuTypography.usesCompactRows) {
|
||||||
_drawMenuTextCentered(
|
_drawMenuTextCentered(
|
||||||
Difficulty.menuText,
|
Difficulty.menuText,
|
||||||
48,
|
_headerHeadingY,
|
||||||
headingColor,
|
headingColor,
|
||||||
scale: menuTypography.headingScale,
|
scale: menuTypography.headingScale,
|
||||||
);
|
);
|
||||||
@@ -671,7 +700,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
|
|
||||||
_drawMenuTextCentered(
|
_drawMenuTextCentered(
|
||||||
Difficulty.menuText,
|
Difficulty.menuText,
|
||||||
48,
|
_headerHeadingY,
|
||||||
headingColor,
|
headingColor,
|
||||||
scale: menuTypography.headingScale,
|
scale: menuTypography.headingScale,
|
||||||
);
|
);
|
||||||
@@ -719,7 +748,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
WolfIntroSlide.title => art.mappedPic(WolfMenuPic.title),
|
WolfIntroSlide.title => art.mappedPic(WolfMenuPic.title),
|
||||||
};
|
};
|
||||||
|
|
||||||
int splashBg = _rgbToPaletteColor(engine.menuManager.introBackgroundRgb);
|
int splashBg = _rgbToPaletteColor(engine.menuManager.menuBackgroundRgb);
|
||||||
if (engine.menuManager.isIntroPg13Slide &&
|
if (engine.menuManager.isIntroPg13Slide &&
|
||||||
image != null &&
|
image != null &&
|
||||||
image.pixels.isNotEmpty) {
|
image.pixels.isNotEmpty) {
|
||||||
@@ -759,16 +788,18 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
final int black = ColorPalette.vga32Bit[0];
|
final int black = ColorPalette.vga32Bit[0];
|
||||||
final int yellow = ColorPalette.vga32Bit[14];
|
final int yellow = ColorPalette.vga32Bit[14];
|
||||||
final int white = ColorPalette.vga32Bit[15];
|
final int white = ColorPalette.vga32Bit[15];
|
||||||
final int lineColor = ColorPalette.vga32Bit[4];
|
|
||||||
|
|
||||||
_fillRect320(0, 0, 320, 22, black);
|
_drawHeaderBarStack(
|
||||||
|
headingY200: _headerHeadingY,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
barColor: black,
|
||||||
|
);
|
||||||
_drawMenuTextCentered(
|
_drawMenuTextCentered(
|
||||||
'Attention',
|
'Attention',
|
||||||
6,
|
_headerHeadingY,
|
||||||
yellow,
|
yellow,
|
||||||
scale: menuTypography.headingScale,
|
scale: menuTypography.headingScale,
|
||||||
);
|
);
|
||||||
_fillRect320(0, 23, 320, 1, lineColor);
|
|
||||||
|
|
||||||
if (menuTypography.usesCompactRows) {
|
if (menuTypography.usesCompactRows) {
|
||||||
final int textLeft = _menuX320ToColumn(40);
|
final int textLeft = _menuX320ToColumn(40);
|
||||||
@@ -823,6 +854,56 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
return bestIndex;
|
return bestIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _drawHeaderBarStack({
|
||||||
|
required int headingY200,
|
||||||
|
required int backgroundColor,
|
||||||
|
required int barColor,
|
||||||
|
}) {
|
||||||
|
final List<int>? cachedColumn = _mainMenuBandFirstColumn;
|
||||||
|
if (cachedColumn != null && cachedColumn.isNotEmpty) {
|
||||||
|
final int bandHeight = cachedColumn.length.clamp(0, 200);
|
||||||
|
for (int y = 0; y < bandHeight; y++) {
|
||||||
|
final int paletteIndex = cachedColumn[y];
|
||||||
|
final int fillIndex = paletteIndex == 0 ? 0 : paletteIndex;
|
||||||
|
_fillRect320(0, y, 320, 1, ColorPalette.vga32Bit[fillIndex]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int mainBarTop = (headingY200 - 4).clamp(0, 199);
|
||||||
|
|
||||||
|
_fillRect320(0, mainBarTop, 320, 18, barColor);
|
||||||
|
|
||||||
|
int stripeY = mainBarTop + 18;
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
_fillRect320(
|
||||||
|
0,
|
||||||
|
(stripeY + i).clamp(0, 199),
|
||||||
|
320,
|
||||||
|
1,
|
||||||
|
i.isEven ? barColor : backgroundColor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _drawMainMenuOptionsSideBars(VgaImage optionsLabel, int optionsX320) {
|
||||||
|
_mainMenuBandFirstColumn = _cacheFirstColumn(optionsLabel);
|
||||||
|
final List<int> firstColumn = _mainMenuBandFirstColumn!;
|
||||||
|
for (int y = 0; y < optionsLabel.height; y++) {
|
||||||
|
final int paletteIndex = firstColumn[y];
|
||||||
|
final int fillIndex = paletteIndex == 0 ? 0 : paletteIndex;
|
||||||
|
_fillRect320(0, y, 320, 1, ColorPalette.vga32Bit[fillIndex]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> _cacheFirstColumn(VgaImage image) {
|
||||||
|
final List<int> column = List<int>.filled(image.height, 0);
|
||||||
|
for (int y = 0; y < image.height; y++) {
|
||||||
|
column[y] = image.decodePixel(0, y);
|
||||||
|
}
|
||||||
|
return column;
|
||||||
|
}
|
||||||
|
|
||||||
void _applyMenuFade(double alpha, int fadeColor) {
|
void _applyMenuFade(double alpha, int fadeColor) {
|
||||||
if (alpha <= 0.0) {
|
if (alpha <= 0.0) {
|
||||||
return;
|
return;
|
||||||
@@ -891,11 +972,52 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
int color, {
|
int color, {
|
||||||
int scale = 1,
|
int scale = 1,
|
||||||
}) {
|
}) {
|
||||||
|
if (y200 == _headerHeadingY) {
|
||||||
|
y200 = _centerHeaderTitleInBlackBand(defaultY: y200, scale: scale);
|
||||||
|
}
|
||||||
final int textWidth = WolfMenuFont.measureTextWidth(text, scale);
|
final int textWidth = WolfMenuFont.measureTextWidth(text, scale);
|
||||||
final int x320 = ((320 - textWidth) ~/ 2).clamp(0, 319);
|
final int x320 = ((320 - textWidth) ~/ 2).clamp(0, 319);
|
||||||
_drawMenuText(text, x320, y200, color, scale: scale);
|
_drawMenuText(text, x320, y200, color, scale: scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int _centerHeaderTitleInBlackBand({
|
||||||
|
required int defaultY,
|
||||||
|
required int scale,
|
||||||
|
}) {
|
||||||
|
final List<int>? column = _mainMenuBandFirstColumn;
|
||||||
|
if (column == null || column.isEmpty) {
|
||||||
|
return defaultY;
|
||||||
|
}
|
||||||
|
|
||||||
|
int bestStart = -1;
|
||||||
|
int bestLength = 0;
|
||||||
|
int runStart = -1;
|
||||||
|
|
||||||
|
for (int i = 0; i <= column.length; i++) {
|
||||||
|
final bool isBlack = i < column.length && column[i] == 0;
|
||||||
|
if (isBlack) {
|
||||||
|
runStart = runStart == -1 ? i : runStart;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (runStart != -1) {
|
||||||
|
final int runLength = i - runStart;
|
||||||
|
if (runLength > bestLength) {
|
||||||
|
bestLength = runLength;
|
||||||
|
bestStart = runStart;
|
||||||
|
}
|
||||||
|
runStart = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestStart == -1 || bestLength == 0) {
|
||||||
|
return defaultY;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int textHeight = 7 * scale;
|
||||||
|
final int centered = bestStart + ((bestLength - textHeight) ~/ 2);
|
||||||
|
return centered.clamp(0, 199 - textHeight);
|
||||||
|
}
|
||||||
|
|
||||||
_AsciiMenuTypography _resolveMenuTypography() {
|
_AsciiMenuTypography _resolveMenuTypography() {
|
||||||
final bool usesCompactRows = _menuGlyphHeightInRows(scale: 1) <= 4;
|
final bool usesCompactRows = _menuGlyphHeightInRows(scale: 1) <= 4;
|
||||||
return _AsciiMenuTypography(
|
return _AsciiMenuTypography(
|
||||||
@@ -1508,78 +1630,6 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _drawMainMenuOptionsSideBars(VgaImage optionsLabel, int optionsX320) {
|
|
||||||
final int barColor = ColorPalette.vga32Bit[0];
|
|
||||||
final int leftWidth = optionsX320.clamp(0, 320);
|
|
||||||
final int rightStart = (optionsX320 + optionsLabel.width).clamp(0, 320);
|
|
||||||
final int rightWidth = (320 - rightStart).clamp(0, 320);
|
|
||||||
|
|
||||||
for (int y = 0; y < optionsLabel.height; y++) {
|
|
||||||
final int leftEdge = optionsLabel.decodePixel(0, y);
|
|
||||||
final int rightEdge = optionsLabel.decodePixel(optionsLabel.width - 1, y);
|
|
||||||
if (leftEdge != 0 || rightEdge != 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (leftWidth > 0) {
|
|
||||||
_fillRect320Precise(0, y, leftWidth, y + 1, barColor);
|
|
||||||
}
|
|
||||||
if (rightWidth > 0) {
|
|
||||||
_fillRect320Precise(rightStart, y, 320, y + 1, barColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _fillRect320Precise(
|
|
||||||
int startX320,
|
|
||||||
int startY200,
|
|
||||||
int endX320,
|
|
||||||
int endY200,
|
|
||||||
int color,
|
|
||||||
) {
|
|
||||||
if (endX320 <= startX320 || endY200 <= startY200) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final double scaleX =
|
|
||||||
(_usesTerminalLayout ? projectionWidth : width) / 320.0;
|
|
||||||
final double scaleY =
|
|
||||||
(_usesTerminalLayout ? _terminalPixelHeight : height) / 200.0;
|
|
||||||
|
|
||||||
final int offsetX = _usesTerminalLayout ? projectionOffsetX : 0;
|
|
||||||
final int startX = offsetX + (startX320 * scaleX).floor();
|
|
||||||
final int endX = offsetX + (endX320 * scaleX).ceil();
|
|
||||||
final int startY = (startY200 * scaleY).floor();
|
|
||||||
final int endY = (endY200 * scaleY).ceil();
|
|
||||||
|
|
||||||
if (_usesTerminalLayout) {
|
|
||||||
for (int y = startY; y < endY; y++) {
|
|
||||||
if (y < 0 || y >= _terminalPixelHeight) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (int x = startX; x < endX; x++) {
|
|
||||||
if (x < 0 || x >= _terminalSceneWidth) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
_scenePixels[y][x] = color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int y = startY; y < endY; y++) {
|
|
||||||
if (y < 0 || y >= height) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (int x = startX; x < endX; x++) {
|
|
||||||
if (x < 0 || x >= width) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
_screen[y][x] = ColoredChar(activeTheme.solid, color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- DAMAGE FLASH ---
|
// --- DAMAGE FLASH ---
|
||||||
void _applyDamageFlash() {
|
void _applyDamageFlash() {
|
||||||
for (int y = 0; y < viewHeight; y++) {
|
for (int y = 0; y < viewHeight; y++) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'dart:io';
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:wolf_3d_dart/src/input/cli_input.dart';
|
||||||
import 'package:wolf_3d_dart/src/menu/menu_manager.dart';
|
import 'package:wolf_3d_dart/src/menu/menu_manager.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_engine.dart';
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
@@ -32,9 +33,11 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
static const int _maxRenderWidth = 320;
|
static const int _maxRenderWidth = 320;
|
||||||
static const int _maxRenderHeight = 240;
|
static const int _maxRenderHeight = 240;
|
||||||
static const int _menuFooterBottomMargin = 1;
|
static const int _menuFooterBottomMargin = 1;
|
||||||
|
static const int _headerHeadingY = 24;
|
||||||
static const String _terminalTealBackground = '\x1b[48;2;0;150;136m';
|
static const String _terminalTealBackground = '\x1b[48;2;0;150;136m';
|
||||||
|
|
||||||
late Uint8List _screen;
|
late Uint8List _screen;
|
||||||
|
List<int>? _mainMenuBandFirstColumn;
|
||||||
int _offsetColumns = 0;
|
int _offsetColumns = 0;
|
||||||
int _offsetRows = 0;
|
int _offsetRows = 0;
|
||||||
int _outputWidth = 1;
|
int _outputWidth = 1;
|
||||||
@@ -357,7 +360,9 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void drawMenu(WolfEngine engine) {
|
void drawMenu(WolfEngine engine) {
|
||||||
final int bgColor = _rgbToPaletteIndex(engine.menuBackgroundRgb);
|
final int bgColor = _rgbToPaletteIndex(
|
||||||
|
engine.menuManager.menuBackgroundRgb,
|
||||||
|
);
|
||||||
final int panelColor = _rgbToPaletteIndex(engine.menuPanelRgb);
|
final int panelColor = _rgbToPaletteIndex(engine.menuPanelRgb);
|
||||||
final int headingIndex = WolfMenuPalette.headerTextIndex;
|
final int headingIndex = WolfMenuPalette.headerTextIndex;
|
||||||
final int selectedTextIndex = WolfMenuPalette.selectedTextIndex;
|
final int selectedTextIndex = WolfMenuPalette.selectedTextIndex;
|
||||||
@@ -369,6 +374,10 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final art = WolfClassicMenuArt(engine.data);
|
final art = WolfClassicMenuArt(engine.data);
|
||||||
|
final optionsLabel = art.optionsLabel;
|
||||||
|
if (optionsLabel != null) {
|
||||||
|
_mainMenuBandFirstColumn = _cacheFirstColumn(optionsLabel);
|
||||||
|
}
|
||||||
// Draw footer first so menu panels can clip overlap in the center.
|
// Draw footer first so menu panels can clip overlap in the center.
|
||||||
_drawMenuFooterArt(art);
|
_drawMenuFooterArt(art);
|
||||||
|
|
||||||
@@ -386,7 +395,17 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
_drawMainMenuOptionsSideBars(optionsLabel, optionsX);
|
_drawMainMenuOptionsSideBars(optionsLabel, optionsX);
|
||||||
_blitVgaImage(optionsLabel, optionsX, 0);
|
_blitVgaImage(optionsLabel, optionsX, 0);
|
||||||
} else {
|
} else {
|
||||||
_drawMenuTextCentered('OPTIONS', 24, headingIndex, scale: 2);
|
_drawHeaderBarStack(
|
||||||
|
headingY200: _headerHeadingY,
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
barColor: 0,
|
||||||
|
);
|
||||||
|
_drawMenuTextCentered(
|
||||||
|
'OPTIONS',
|
||||||
|
_headerHeadingY,
|
||||||
|
headingIndex,
|
||||||
|
scale: 2,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final cursor = art.mappedPic(
|
final cursor = art.mappedPic(
|
||||||
@@ -415,8 +434,18 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (engine.menuManager.activeMenu == WolfMenuScreen.gameSelect) {
|
if (engine.menuManager.activeMenu == WolfMenuScreen.gameSelect) {
|
||||||
|
_drawHeaderBarStack(
|
||||||
|
headingY200: _headerHeadingY,
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
barColor: 0,
|
||||||
|
);
|
||||||
_fillRect320(28, 58, 264, 104, panelColor);
|
_fillRect320(28, 58, 264, 104, panelColor);
|
||||||
_drawMenuTextCentered('SELECT GAME', 48, headingIndex, scale: 2);
|
_drawMenuTextCentered(
|
||||||
|
'SELECT GAME',
|
||||||
|
_headerHeadingY,
|
||||||
|
headingIndex,
|
||||||
|
scale: 2,
|
||||||
|
);
|
||||||
final cursor = art.mappedPic(
|
final cursor = art.mappedPic(
|
||||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||||
);
|
);
|
||||||
@@ -440,10 +469,15 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (engine.menuManager.activeMenu == WolfMenuScreen.episodeSelect) {
|
if (engine.menuManager.activeMenu == WolfMenuScreen.episodeSelect) {
|
||||||
|
_drawHeaderBarStack(
|
||||||
|
headingY200: _headerHeadingY,
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
barColor: 0,
|
||||||
|
);
|
||||||
_fillRect320(12, 20, 296, 158, panelColor);
|
_fillRect320(12, 20, 296, 158, panelColor);
|
||||||
_drawMenuTextCentered(
|
_drawMenuTextCentered(
|
||||||
'WHICH EPISODE TO PLAY?',
|
'WHICH EPISODE TO PLAY?',
|
||||||
6,
|
_headerHeadingY,
|
||||||
headingIndex,
|
headingIndex,
|
||||||
scale: 2,
|
scale: 2,
|
||||||
);
|
);
|
||||||
@@ -488,6 +522,11 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
|
|
||||||
final int selectedDifficultyIndex =
|
final int selectedDifficultyIndex =
|
||||||
engine.menuManager.selectedDifficultyIndex;
|
engine.menuManager.selectedDifficultyIndex;
|
||||||
|
_drawHeaderBarStack(
|
||||||
|
headingY200: _headerHeadingY,
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
barColor: 0,
|
||||||
|
);
|
||||||
_fillRect320(28, 70, 264, 82, panelColor);
|
_fillRect320(28, 70, 264, 82, panelColor);
|
||||||
if (_useCompactMenuLayout) {
|
if (_useCompactMenuLayout) {
|
||||||
_drawCompactMenu(selectedDifficultyIndex, headingIndex, panelColor);
|
_drawCompactMenu(selectedDifficultyIndex, headingIndex, panelColor);
|
||||||
@@ -497,7 +536,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
|
|
||||||
_drawMenuTextCentered(
|
_drawMenuTextCentered(
|
||||||
Difficulty.menuText,
|
Difficulty.menuText,
|
||||||
48,
|
_headerHeadingY,
|
||||||
headingIndex,
|
headingIndex,
|
||||||
scale: _menuHeadingScale,
|
scale: _menuHeadingScale,
|
||||||
);
|
);
|
||||||
@@ -567,7 +606,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
WolfIntroSlide.title => art.mappedPic(WolfMenuPic.title),
|
WolfIntroSlide.title => art.mappedPic(WolfMenuPic.title),
|
||||||
};
|
};
|
||||||
|
|
||||||
int splashBg = _rgbToPaletteIndex(engine.menuManager.introBackgroundRgb);
|
int splashBg = _rgbToPaletteIndex(engine.menuManager.menuBackgroundRgb);
|
||||||
if (engine.menuManager.isIntroPg13Slide &&
|
if (engine.menuManager.isIntroPg13Slide &&
|
||||||
image != null &&
|
image != null &&
|
||||||
image.pixels.isNotEmpty) {
|
image.pixels.isNotEmpty) {
|
||||||
@@ -602,11 +641,13 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
const int black = 0;
|
const int black = 0;
|
||||||
const int yellow = 14;
|
const int yellow = 14;
|
||||||
const int white = 15;
|
const int white = 15;
|
||||||
const int lineColor = 4;
|
|
||||||
|
|
||||||
_fillRect320(0, 0, 320, 22, black);
|
_drawHeaderBarStack(
|
||||||
_drawMenuTextCentered('Attention', 6, yellow, scale: 2);
|
headingY200: _headerHeadingY,
|
||||||
_fillRect320(0, 23, 320, 1, lineColor);
|
backgroundColor: backgroundColor,
|
||||||
|
barColor: black,
|
||||||
|
);
|
||||||
|
_drawMenuTextCentered('Attention', _headerHeadingY, yellow, scale: 2);
|
||||||
|
|
||||||
_drawMenuText('This game is NOT shareware.', 40, 56, white, scale: 1);
|
_drawMenuText('This game is NOT shareware.', 40, 56, white, scale: 1);
|
||||||
_drawMenuText('Please do not distribute it.', 40, 68, white, scale: 1);
|
_drawMenuText('Please do not distribute it.', 40, 68, white, scale: 1);
|
||||||
@@ -650,6 +691,56 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _drawHeaderBarStack({
|
||||||
|
required int headingY200,
|
||||||
|
required int backgroundColor,
|
||||||
|
required int barColor,
|
||||||
|
}) {
|
||||||
|
final List<int>? cachedColumn = _mainMenuBandFirstColumn;
|
||||||
|
if (cachedColumn != null && cachedColumn.isNotEmpty) {
|
||||||
|
final int bandHeight = cachedColumn.length.clamp(0, 200);
|
||||||
|
for (int y = 0; y < bandHeight; y++) {
|
||||||
|
final int paletteIndex = cachedColumn[y];
|
||||||
|
final int fillIndex = paletteIndex == 0 ? 0 : paletteIndex;
|
||||||
|
_fillRect320(0, y, 320, 1, fillIndex);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int mainBarTop = (headingY200 - 4).clamp(0, 199);
|
||||||
|
|
||||||
|
_fillRect320(0, mainBarTop, 320, 18, barColor);
|
||||||
|
|
||||||
|
int stripeY = mainBarTop + 18;
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
_fillRect320(
|
||||||
|
0,
|
||||||
|
(stripeY + i).clamp(0, 199),
|
||||||
|
320,
|
||||||
|
1,
|
||||||
|
i.isEven ? barColor : backgroundColor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _drawMainMenuOptionsSideBars(VgaImage optionsLabel, int optionsX320) {
|
||||||
|
_mainMenuBandFirstColumn = _cacheFirstColumn(optionsLabel);
|
||||||
|
final List<int> firstColumn = _mainMenuBandFirstColumn!;
|
||||||
|
for (int y = 0; y < optionsLabel.height; y++) {
|
||||||
|
final int paletteIndex = firstColumn[y];
|
||||||
|
final int fillIndex = paletteIndex == 0 ? 0 : paletteIndex;
|
||||||
|
_fillRect320(0, y, 320, 1, fillIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> _cacheFirstColumn(VgaImage image) {
|
||||||
|
final List<int> column = List<int>.filled(image.height, 0);
|
||||||
|
for (int y = 0; y < image.height; y++) {
|
||||||
|
column[y] = image.decodePixel(0, y);
|
||||||
|
}
|
||||||
|
return column;
|
||||||
|
}
|
||||||
|
|
||||||
bool get _useCompactMenuLayout =>
|
bool get _useCompactMenuLayout =>
|
||||||
width < _compactMenuMinWidthPx || height < _compactMenuMinHeightPx;
|
width < _compactMenuMinWidthPx || height < _compactMenuMinHeightPx;
|
||||||
|
|
||||||
@@ -730,11 +821,52 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
int colorIndex, {
|
int colorIndex, {
|
||||||
int scale = 1,
|
int scale = 1,
|
||||||
}) {
|
}) {
|
||||||
|
if (y == _headerHeadingY) {
|
||||||
|
y = _centerHeaderTitleInBlackBand(defaultY: y, scale: scale);
|
||||||
|
}
|
||||||
final int textWidth = WolfMenuFont.measureTextWidth(text, scale);
|
final int textWidth = WolfMenuFont.measureTextWidth(text, scale);
|
||||||
final int x = ((320 - textWidth) ~/ 2).clamp(0, 319);
|
final int x = ((320 - textWidth) ~/ 2).clamp(0, 319);
|
||||||
_drawMenuText(text, x, y, colorIndex, scale: scale);
|
_drawMenuText(text, x, y, colorIndex, scale: scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int _centerHeaderTitleInBlackBand({
|
||||||
|
required int defaultY,
|
||||||
|
required int scale,
|
||||||
|
}) {
|
||||||
|
final List<int>? column = _mainMenuBandFirstColumn;
|
||||||
|
if (column == null || column.isEmpty) {
|
||||||
|
return defaultY;
|
||||||
|
}
|
||||||
|
|
||||||
|
int bestStart = -1;
|
||||||
|
int bestLength = 0;
|
||||||
|
int runStart = -1;
|
||||||
|
|
||||||
|
for (int i = 0; i <= column.length; i++) {
|
||||||
|
final bool isBlack = i < column.length && column[i] == 0;
|
||||||
|
if (isBlack) {
|
||||||
|
runStart = runStart == -1 ? i : runStart;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (runStart != -1) {
|
||||||
|
final int runLength = i - runStart;
|
||||||
|
if (runLength > bestLength) {
|
||||||
|
bestLength = runLength;
|
||||||
|
bestStart = runStart;
|
||||||
|
}
|
||||||
|
runStart = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestStart == -1 || bestLength == 0) {
|
||||||
|
return defaultY;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int textHeight = 7 * scale;
|
||||||
|
final int centered = bestStart + ((bestLength - textHeight) ~/ 2);
|
||||||
|
return centered.clamp(0, 199 - textHeight);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String finalizeFrame() {
|
String finalizeFrame() {
|
||||||
if (!isSixelSupported) {
|
if (!isSixelSupported) {
|
||||||
@@ -764,7 +896,10 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
const String msg1 = "Terminal does not support Sixel.";
|
const String msg1 = "Terminal does not support Sixel.";
|
||||||
const String msg2 = "Press TAB to switch renderers.";
|
final String shortcutLabel = engine.input is CliInput
|
||||||
|
? (engine.input as CliInput).rendererToggleKeyLabel
|
||||||
|
: 'TAB';
|
||||||
|
final String msg2 = "Press $shortcutLabel to switch renderers.";
|
||||||
|
|
||||||
final int boxWidth = math.max(msg1.length, msg2.length) + 6;
|
final int boxWidth = math.max(msg1.length, msg2.length) + 6;
|
||||||
const int boxHeight = 5;
|
const int boxHeight = 5;
|
||||||
@@ -969,60 +1104,6 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _drawMainMenuOptionsSideBars(VgaImage optionsLabel, int optionsX320) {
|
|
||||||
const int barColor = 0;
|
|
||||||
final int leftWidth = optionsX320.clamp(0, 320);
|
|
||||||
final int rightStart = (optionsX320 + optionsLabel.width).clamp(0, 320);
|
|
||||||
final int rightWidth = (320 - rightStart).clamp(0, 320);
|
|
||||||
|
|
||||||
for (int y = 0; y < optionsLabel.height; y++) {
|
|
||||||
final int leftEdge = optionsLabel.decodePixel(0, y);
|
|
||||||
final int rightEdge = optionsLabel.decodePixel(optionsLabel.width - 1, y);
|
|
||||||
if (leftEdge != 0 || rightEdge != 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (leftWidth > 0) {
|
|
||||||
_fillRect320Precise(0, y, leftWidth, y + 1, barColor);
|
|
||||||
}
|
|
||||||
if (rightWidth > 0) {
|
|
||||||
_fillRect320Precise(rightStart, y, 320, y + 1, barColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _fillRect320Precise(
|
|
||||||
int startX320,
|
|
||||||
int startY200,
|
|
||||||
int endX320,
|
|
||||||
int endY200,
|
|
||||||
int colorIndex,
|
|
||||||
) {
|
|
||||||
if (endX320 <= startX320 || endY200 <= startY200) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final double scaleX = width / 320.0;
|
|
||||||
final double scaleY = height / 200.0;
|
|
||||||
final int startX = (startX320 * scaleX).floor();
|
|
||||||
final int endX = (endX320 * scaleX).ceil();
|
|
||||||
final int startY = (startY200 * scaleY).floor();
|
|
||||||
final int endY = (endY200 * scaleY).ceil();
|
|
||||||
|
|
||||||
for (int y = startY; y < endY; y++) {
|
|
||||||
if (y < 0 || y >= height) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final int rowOffset = y * width;
|
|
||||||
for (int x = startX; x < endX; x++) {
|
|
||||||
if (x < 0 || x >= width) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
_screen[rowOffset + x] = colorIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Maps an RGB color to the nearest VGA palette index.
|
/// Maps an RGB color to the nearest VGA palette index.
|
||||||
int _rgbToPaletteIndex(int rgb) {
|
int _rgbToPaletteIndex(int rgb) {
|
||||||
return ColorPalette.findClosestPaletteIndex(rgb);
|
return ColorPalette.findClosestPaletteIndex(rgb);
|
||||||
|
|||||||
@@ -15,8 +15,10 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
static const int _menuFooterBottomMargin = 1;
|
static const int _menuFooterBottomMargin = 1;
|
||||||
static const int _menuFooterY = 184;
|
static const int _menuFooterY = 184;
|
||||||
static const int _menuFooterHeight = 12;
|
static const int _menuFooterHeight = 12;
|
||||||
|
static const int _headerHeadingY = 24;
|
||||||
|
|
||||||
late FrameBuffer _buffer;
|
late FrameBuffer _buffer;
|
||||||
|
List<int>? _mainMenuBandFirstColumn;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void prepareFrame(WolfEngine engine) {
|
void prepareFrame(WolfEngine engine) {
|
||||||
@@ -143,7 +145,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void drawMenu(WolfEngine engine) {
|
void drawMenu(WolfEngine engine) {
|
||||||
final int bgColor = _rgbToFrameColor(engine.menuBackgroundRgb);
|
final int bgColor = _rgbToFrameColor(engine.menuManager.menuBackgroundRgb);
|
||||||
final int panelColor = _rgbToFrameColor(engine.menuPanelRgb);
|
final int panelColor = _rgbToFrameColor(engine.menuPanelRgb);
|
||||||
final int headingColor = WolfMenuPalette.headerTextColor;
|
final int headingColor = WolfMenuPalette.headerTextColor;
|
||||||
final int selectedTextColor = WolfMenuPalette.selectedTextColor;
|
final int selectedTextColor = WolfMenuPalette.selectedTextColor;
|
||||||
@@ -155,6 +157,10 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final art = WolfClassicMenuArt(engine.data);
|
final art = WolfClassicMenuArt(engine.data);
|
||||||
|
final optionsLabel = art.optionsLabel;
|
||||||
|
if (optionsLabel != null) {
|
||||||
|
_mainMenuBandFirstColumn = _cacheFirstColumn(optionsLabel);
|
||||||
|
}
|
||||||
// Draw footer first so menu panels can clip overlap in the center.
|
// Draw footer first so menu panels can clip overlap in the center.
|
||||||
_drawCenteredMenuFooter(art);
|
_drawCenteredMenuFooter(art);
|
||||||
|
|
||||||
@@ -215,7 +221,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
WolfIntroSlide.title => art.mappedPic(WolfMenuPic.title),
|
WolfIntroSlide.title => art.mappedPic(WolfMenuPic.title),
|
||||||
};
|
};
|
||||||
|
|
||||||
int splashBgColor = _rgbToFrameColor(engine.menuManager.introBackgroundRgb);
|
int splashBgColor = _rgbToFrameColor(engine.menuManager.menuBackgroundRgb);
|
||||||
int? matteIndex;
|
int? matteIndex;
|
||||||
if (engine.menuManager.isIntroPg13Slide &&
|
if (engine.menuManager.isIntroPg13Slide &&
|
||||||
image != null &&
|
image != null &&
|
||||||
@@ -257,11 +263,18 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
final int black = ColorPalette.vga32Bit[0];
|
final int black = ColorPalette.vga32Bit[0];
|
||||||
final int yellow = ColorPalette.vga32Bit[14];
|
final int yellow = ColorPalette.vga32Bit[14];
|
||||||
final int white = ColorPalette.vga32Bit[15];
|
final int white = ColorPalette.vga32Bit[15];
|
||||||
final int lineColor = ColorPalette.vga32Bit[4];
|
|
||||||
|
|
||||||
_fillCanonicalRect(0, 0, 320, 22, black);
|
_drawHeaderBarStack(
|
||||||
_drawCanonicalMenuTextCentered('Attention', 6, yellow, scale: 2);
|
headingY200: _headerHeadingY,
|
||||||
_fillCanonicalRect(0, 23, 320, 1, lineColor);
|
backgroundColor: backgroundColor,
|
||||||
|
barColor: black,
|
||||||
|
);
|
||||||
|
_drawCanonicalMenuTextCentered(
|
||||||
|
'Attention',
|
||||||
|
_headerHeadingY,
|
||||||
|
yellow,
|
||||||
|
scale: 2,
|
||||||
|
);
|
||||||
|
|
||||||
_drawCanonicalMenuText(
|
_drawCanonicalMenuText(
|
||||||
'This game is NOT shareware.',
|
'This game is NOT shareware.',
|
||||||
@@ -358,7 +371,17 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
_drawMainMenuOptionsSideBars(optionsLabel, optionsX);
|
_drawMainMenuOptionsSideBars(optionsLabel, optionsX);
|
||||||
_blitVgaImage(optionsLabel, optionsX, 0);
|
_blitVgaImage(optionsLabel, optionsX, 0);
|
||||||
} else {
|
} else {
|
||||||
_drawCanonicalMenuTextCentered('OPTIONS', 24, headingColor, scale: 2);
|
_drawHeaderBarStack(
|
||||||
|
headingY200: _headerHeadingY,
|
||||||
|
backgroundColor: _rgbToFrameColor(engine.menuManager.menuBackgroundRgb),
|
||||||
|
barColor: ColorPalette.vga32Bit[0],
|
||||||
|
);
|
||||||
|
_drawCanonicalMenuTextCentered(
|
||||||
|
'OPTIONS',
|
||||||
|
_headerHeadingY,
|
||||||
|
headingColor,
|
||||||
|
scale: 2,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final cursor = art.mappedPic(
|
final cursor = art.mappedPic(
|
||||||
@@ -395,13 +418,24 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
int selectedTextColor,
|
int selectedTextColor,
|
||||||
int unselectedTextColor,
|
int unselectedTextColor,
|
||||||
) {
|
) {
|
||||||
|
_drawHeaderBarStack(
|
||||||
|
headingY200: _headerHeadingY,
|
||||||
|
backgroundColor: _rgbToFrameColor(engine.menuManager.menuBackgroundRgb),
|
||||||
|
barColor: ColorPalette.vga32Bit[0],
|
||||||
|
);
|
||||||
|
|
||||||
const int panelX = 28;
|
const int panelX = 28;
|
||||||
const int panelY = 58;
|
const int panelY = 58;
|
||||||
const int panelW = 264;
|
const int panelW = 264;
|
||||||
const int panelH = 104;
|
const int panelH = 104;
|
||||||
_fillCanonicalRect(panelX, panelY, panelW, panelH, panelColor);
|
_fillCanonicalRect(panelX, panelY, panelW, panelH, panelColor);
|
||||||
|
|
||||||
_drawCanonicalMenuTextCentered('SELECT GAME', 38, headingColor, scale: 2);
|
_drawCanonicalMenuTextCentered(
|
||||||
|
'SELECT GAME',
|
||||||
|
_headerHeadingY,
|
||||||
|
headingColor,
|
||||||
|
scale: 2,
|
||||||
|
);
|
||||||
|
|
||||||
final cursor = art.mappedPic(
|
final cursor = art.mappedPic(
|
||||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||||
@@ -435,6 +469,12 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
int selectedTextColor,
|
int selectedTextColor,
|
||||||
int unselectedTextColor,
|
int unselectedTextColor,
|
||||||
) {
|
) {
|
||||||
|
_drawHeaderBarStack(
|
||||||
|
headingY200: _headerHeadingY,
|
||||||
|
backgroundColor: _rgbToFrameColor(engine.menuManager.menuBackgroundRgb),
|
||||||
|
barColor: ColorPalette.vga32Bit[0],
|
||||||
|
);
|
||||||
|
|
||||||
const int panelX = 12;
|
const int panelX = 12;
|
||||||
const int panelY = 20;
|
const int panelY = 20;
|
||||||
const int panelW = 296;
|
const int panelW = 296;
|
||||||
@@ -443,7 +483,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
|
|
||||||
_drawCanonicalMenuTextCentered(
|
_drawCanonicalMenuTextCentered(
|
||||||
'WHICH EPISODE TO PLAY?',
|
'WHICH EPISODE TO PLAY?',
|
||||||
6,
|
_headerHeadingY,
|
||||||
headingColor,
|
headingColor,
|
||||||
scale: 2,
|
scale: 2,
|
||||||
);
|
);
|
||||||
@@ -546,6 +586,12 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
int selectedTextColor,
|
int selectedTextColor,
|
||||||
int unselectedTextColor,
|
int unselectedTextColor,
|
||||||
) {
|
) {
|
||||||
|
_drawHeaderBarStack(
|
||||||
|
headingY200: _headerHeadingY,
|
||||||
|
backgroundColor: _rgbToFrameColor(engine.menuManager.menuBackgroundRgb),
|
||||||
|
barColor: ColorPalette.vga32Bit[0],
|
||||||
|
);
|
||||||
|
|
||||||
final int selectedDifficultyIndex =
|
final int selectedDifficultyIndex =
|
||||||
engine.menuManager.selectedDifficultyIndex;
|
engine.menuManager.selectedDifficultyIndex;
|
||||||
const int panelX = 28;
|
const int panelX = 28;
|
||||||
@@ -556,7 +602,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
|
|
||||||
_drawCanonicalMenuTextCentered(
|
_drawCanonicalMenuTextCentered(
|
||||||
Difficulty.menuText,
|
Difficulty.menuText,
|
||||||
48,
|
_headerHeadingY,
|
||||||
headingColor,
|
headingColor,
|
||||||
scale: 2,
|
scale: 2,
|
||||||
);
|
);
|
||||||
@@ -655,6 +701,38 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
return (0xFF000000) | (b << 16) | (g << 8) | r;
|
return (0xFF000000) | (b << 16) | (g << 8) | r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _drawHeaderBarStack({
|
||||||
|
required int headingY200,
|
||||||
|
required int backgroundColor,
|
||||||
|
required int barColor,
|
||||||
|
}) {
|
||||||
|
final List<int>? cachedColumn = _mainMenuBandFirstColumn;
|
||||||
|
if (cachedColumn != null && cachedColumn.isNotEmpty) {
|
||||||
|
final int bandHeight = cachedColumn.length.clamp(0, 200);
|
||||||
|
for (int y = 0; y < bandHeight; y++) {
|
||||||
|
final int paletteIndex = cachedColumn[y];
|
||||||
|
final int fillIndex = paletteIndex == 0 ? 0 : paletteIndex;
|
||||||
|
_fillCanonicalRect(0, y, 320, 1, ColorPalette.vga32Bit[fillIndex]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int mainBarTop = (headingY200 - 4).clamp(0, 199);
|
||||||
|
|
||||||
|
_fillCanonicalRect(0, mainBarTop, 320, 18, barColor);
|
||||||
|
|
||||||
|
int stripeY = mainBarTop + 18;
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
_fillCanonicalRect(
|
||||||
|
0,
|
||||||
|
(stripeY + i).clamp(0, 199),
|
||||||
|
320,
|
||||||
|
1,
|
||||||
|
i.isEven ? barColor : backgroundColor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
double get _uiScaleX => width / 320.0;
|
double get _uiScaleX => width / 320.0;
|
||||||
|
|
||||||
double get _uiScaleY => height / 200.0;
|
double get _uiScaleY => height / 200.0;
|
||||||
@@ -683,27 +761,23 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _drawMainMenuOptionsSideBars(VgaImage optionsLabel, int optionsX320) {
|
void _drawMainMenuOptionsSideBars(VgaImage optionsLabel, int optionsX320) {
|
||||||
final int barColor = ColorPalette.vga32Bit[0];
|
_mainMenuBandFirstColumn = _cacheFirstColumn(optionsLabel);
|
||||||
final int leftWidth = optionsX320.clamp(0, 320);
|
final List<int> firstColumn = _mainMenuBandFirstColumn!;
|
||||||
final int rightStart = (optionsX320 + optionsLabel.width).clamp(0, 320);
|
|
||||||
final int rightWidth = (320 - rightStart).clamp(0, 320);
|
|
||||||
|
|
||||||
for (int y = 0; y < optionsLabel.height; y++) {
|
for (int y = 0; y < optionsLabel.height; y++) {
|
||||||
final int leftEdge = optionsLabel.decodePixel(0, y);
|
final int paletteIndex = firstColumn[y];
|
||||||
final int rightEdge = optionsLabel.decodePixel(optionsLabel.width - 1, y);
|
final int fillIndex = paletteIndex == 0 ? 0 : paletteIndex;
|
||||||
if (leftEdge != 0 || rightEdge != 0) {
|
_fillCanonicalRect(0, y, 320, 1, ColorPalette.vga32Bit[fillIndex]);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (leftWidth > 0) {
|
|
||||||
_fillCanonicalRect(0, y, leftWidth, 1, barColor);
|
|
||||||
}
|
|
||||||
if (rightWidth > 0) {
|
|
||||||
_fillCanonicalRect(rightStart, y, rightWidth, 1, barColor);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<int> _cacheFirstColumn(VgaImage image) {
|
||||||
|
final List<int> column = List<int>.filled(image.height, 0);
|
||||||
|
for (int y = 0; y < image.height; y++) {
|
||||||
|
column[y] = image.decodePixel(0, y);
|
||||||
|
}
|
||||||
|
return column;
|
||||||
|
}
|
||||||
|
|
||||||
void _drawCanonicalMenuText(
|
void _drawCanonicalMenuText(
|
||||||
String text,
|
String text,
|
||||||
int startX320,
|
int startX320,
|
||||||
@@ -740,11 +814,52 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
int color, {
|
int color, {
|
||||||
int scale = 1,
|
int scale = 1,
|
||||||
}) {
|
}) {
|
||||||
|
if (y200 == _headerHeadingY) {
|
||||||
|
y200 = _centerHeaderTitleInBlackBand(defaultY: y200, scale: scale);
|
||||||
|
}
|
||||||
final int textWidth = WolfMenuFont.measureTextWidth(text, scale);
|
final int textWidth = WolfMenuFont.measureTextWidth(text, scale);
|
||||||
final int x320 = ((320 - textWidth) ~/ 2).clamp(0, 319);
|
final int x320 = ((320 - textWidth) ~/ 2).clamp(0, 319);
|
||||||
_drawCanonicalMenuText(text, x320, y200, color, scale: scale);
|
_drawCanonicalMenuText(text, x320, y200, color, scale: scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int _centerHeaderTitleInBlackBand({
|
||||||
|
required int defaultY,
|
||||||
|
required int scale,
|
||||||
|
}) {
|
||||||
|
final List<int>? column = _mainMenuBandFirstColumn;
|
||||||
|
if (column == null || column.isEmpty) {
|
||||||
|
return defaultY;
|
||||||
|
}
|
||||||
|
|
||||||
|
int bestStart = -1;
|
||||||
|
int bestLength = 0;
|
||||||
|
int runStart = -1;
|
||||||
|
|
||||||
|
for (int i = 0; i <= column.length; i++) {
|
||||||
|
final bool isBlack = i < column.length && column[i] == 0;
|
||||||
|
if (isBlack) {
|
||||||
|
runStart = runStart == -1 ? i : runStart;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (runStart != -1) {
|
||||||
|
final int runLength = i - runStart;
|
||||||
|
if (runLength > bestLength) {
|
||||||
|
bestLength = runLength;
|
||||||
|
bestStart = runStart;
|
||||||
|
}
|
||||||
|
runStart = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestStart == -1 || bestLength == 0) {
|
||||||
|
return defaultY;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int textHeight = 7 * scale;
|
||||||
|
final int centered = bestStart + ((bestLength - textHeight) ~/ 2);
|
||||||
|
return centered.clamp(0, 199 - textHeight);
|
||||||
|
}
|
||||||
|
|
||||||
/// Draws bitmap menu text directly into the framebuffer.
|
/// Draws bitmap menu text directly into the framebuffer.
|
||||||
void _drawMenuText(
|
void _drawMenuText(
|
||||||
String text,
|
String text,
|
||||||
|
|||||||
Reference in New Issue
Block a user