217 lines
6.4 KiB
Dart
217 lines
6.4 KiB
Dart
/// Shared menu helpers for Wolf3D hosts.
|
|
library;
|
|
|
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
|
|
|
/// Known VGA picture indexes used by the original Wolf3D control-panel menus.
|
|
///
|
|
/// Values below are picture-table indexes (not raw chunk ids).
|
|
/// For example, `C_CONTROLPIC` is chunk 26 in `GFXV_WL6.H`, so its picture
|
|
/// index is `26 - STARTPICS(3) = 23`.
|
|
abstract class WolfMenuPic {
|
|
static const int hBj = 0; // H_BJPIC
|
|
static const int hTopWindow = 3; // H_TOPWINDOWPIC
|
|
static const int cOptions = 7; // C_OPTIONSPIC
|
|
static const int cCursor1 = 8; // C_CURSOR1PIC
|
|
static const int cCursor2 = 9; // C_CURSOR2PIC
|
|
static const int cNotSelected = 10; // C_NOTSELECTEDPIC
|
|
static const int cSelected = 11; // C_SELECTEDPIC
|
|
static const int cBabyMode = 16; // C_BABYMODEPIC
|
|
static const int cEasy = 17; // C_EASYPIC
|
|
static const int cNormal = 18; // C_NORMALPIC
|
|
static const int cHard = 19; // C_HARDPIC
|
|
static const int cControl = 23; // C_CONTROLPIC
|
|
static const int cEpisode1 = 27; // C_EPISODE1PIC
|
|
static const int cEpisode2 = 28; // C_EPISODE2PIC
|
|
static const int cEpisode3 = 29; // C_EPISODE3PIC
|
|
static const int cEpisode4 = 30; // C_EPISODE4PIC
|
|
static const int cEpisode5 = 31; // C_EPISODE5PIC
|
|
static const int cEpisode6 = 32; // C_EPISODE6PIC
|
|
static const int statusBar = 83; // STATUSBARPIC
|
|
static const int title = 84; // TITLEPIC
|
|
static const int pg13 = 85; // PG13PIC
|
|
static const int credits = 86; // CREDITSPIC
|
|
static const int highScores = 87; // HIGHSCORESPIC
|
|
|
|
static const List<int> episodePics = [
|
|
cEpisode1,
|
|
cEpisode2,
|
|
cEpisode3,
|
|
cEpisode4,
|
|
cEpisode5,
|
|
cEpisode6,
|
|
];
|
|
}
|
|
|
|
/// Shared menu text colors resolved from the VGA palette.
|
|
///
|
|
/// Keep menu color choices centralized so renderers don't duplicate
|
|
/// hard-coded palette slots or RGB conversion logic.
|
|
abstract class WolfMenuPalette {
|
|
static const int selectedTextIndex = 19;
|
|
static const int unselectedTextIndex = 23;
|
|
static const int _headerTargetRgb = 0xFFF700;
|
|
|
|
static int? _cachedHeaderTextIndex;
|
|
|
|
static int get headerTextIndex =>
|
|
_cachedHeaderTextIndex ??= _nearestPaletteIndex(_headerTargetRgb);
|
|
|
|
static int get selectedTextColor => ColorPalette.vga32Bit[selectedTextIndex];
|
|
|
|
static int get unselectedTextColor =>
|
|
ColorPalette.vga32Bit[unselectedTextIndex];
|
|
|
|
static int get headerTextColor => ColorPalette.vga32Bit[headerTextIndex];
|
|
|
|
static int _nearestPaletteIndex(int rgb) {
|
|
final int targetR = (rgb >> 16) & 0xFF;
|
|
final int targetG = (rgb >> 8) & 0xFF;
|
|
final int targetB = rgb & 0xFF;
|
|
|
|
int bestIndex = 0;
|
|
int bestDistance = 1 << 30;
|
|
|
|
for (int i = 0; i < 256; i++) {
|
|
final int color = ColorPalette.vga32Bit[i];
|
|
final int r = color & 0xFF;
|
|
final int g = (color >> 8) & 0xFF;
|
|
final int b = (color >> 16) & 0xFF;
|
|
|
|
final int dr = targetR - r;
|
|
final int dg = targetG - g;
|
|
final int db = targetB - b;
|
|
final int distance = (dr * dr) + (dg * dg) + (db * db);
|
|
if (distance < bestDistance) {
|
|
bestDistance = distance;
|
|
bestIndex = i;
|
|
}
|
|
}
|
|
|
|
return bestIndex;
|
|
}
|
|
}
|
|
|
|
/// Structured accessors for classic Wolf3D menu art.
|
|
class WolfClassicMenuArt {
|
|
final WolfensteinData data;
|
|
|
|
WolfClassicMenuArt(this.data);
|
|
|
|
int? _resolvedIndexOffset;
|
|
|
|
VgaImage? get controlBackground {
|
|
final preferred = mappedPic(WolfMenuPic.cControl);
|
|
if (_looksLikeMenuBackdrop(preferred)) {
|
|
return preferred;
|
|
}
|
|
|
|
// Older data layouts may shift/control-panel art around nearby indices.
|
|
for (int delta = -4; delta <= 4; delta++) {
|
|
final candidate = mappedPic(WolfMenuPic.cControl + delta);
|
|
if (_looksLikeMenuBackdrop(candidate)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
|
|
return preferred;
|
|
}
|
|
|
|
VgaImage? get title => mappedPic(WolfMenuPic.title);
|
|
|
|
VgaImage? get heading => mappedPic(WolfMenuPic.hTopWindow);
|
|
|
|
VgaImage? get selectedMarker => mappedPic(WolfMenuPic.cSelected);
|
|
|
|
VgaImage? get unselectedMarker => mappedPic(WolfMenuPic.cNotSelected);
|
|
|
|
VgaImage? get optionsLabel => mappedPic(WolfMenuPic.cOptions);
|
|
|
|
VgaImage? get credits => mappedPic(WolfMenuPic.credits);
|
|
|
|
VgaImage? episodeOption(int episodeIndex) {
|
|
if (episodeIndex < 0 || episodeIndex >= WolfMenuPic.episodePics.length) {
|
|
return null;
|
|
}
|
|
return mappedPic(WolfMenuPic.episodePics[episodeIndex]);
|
|
}
|
|
|
|
VgaImage? difficultyOption(Difficulty difficulty) {
|
|
switch (difficulty) {
|
|
case Difficulty.baby:
|
|
return mappedPic(WolfMenuPic.cBabyMode);
|
|
case Difficulty.easy:
|
|
return mappedPic(WolfMenuPic.cEasy);
|
|
case Difficulty.medium:
|
|
return mappedPic(WolfMenuPic.cNormal);
|
|
case Difficulty.hard:
|
|
return mappedPic(WolfMenuPic.cHard);
|
|
}
|
|
}
|
|
|
|
/// Returns [index] after applying a detected version/layout offset.
|
|
VgaImage? mappedPic(int index) {
|
|
return pic(index + _indexOffset);
|
|
}
|
|
|
|
int get _indexOffset {
|
|
if (_resolvedIndexOffset != null) {
|
|
return _resolvedIndexOffset!;
|
|
}
|
|
|
|
// Retail and shareware generally place STATUSBAR/TITLE/PG13/CREDITS as a
|
|
// contiguous block. If files are from a different release, infer a shift.
|
|
for (int i = 0; i < data.vgaImages.length - 3; i++) {
|
|
final status = data.vgaImages[i];
|
|
if (!_looksLikeStatusBar(status)) {
|
|
continue;
|
|
}
|
|
|
|
final title = data.vgaImages[i + 1];
|
|
final pg13 = data.vgaImages[i + 2];
|
|
final credits = data.vgaImages[i + 3];
|
|
if (_looksLikeFullScreen(title) &&
|
|
_looksLikeFullScreen(pg13) &&
|
|
_looksLikeFullScreen(credits)) {
|
|
_resolvedIndexOffset = i - WolfMenuPic.statusBar;
|
|
return _resolvedIndexOffset!;
|
|
}
|
|
}
|
|
|
|
_resolvedIndexOffset = 0;
|
|
return 0;
|
|
}
|
|
|
|
bool _looksLikeStatusBar(VgaImage image) {
|
|
return image.width >= 280 && image.height >= 24 && image.height <= 64;
|
|
}
|
|
|
|
bool _looksLikeFullScreen(VgaImage image) {
|
|
return image.width >= 280 && image.height >= 140;
|
|
}
|
|
|
|
bool _looksLikeMenuBackdrop(VgaImage? image) {
|
|
if (image == null) {
|
|
return false;
|
|
}
|
|
return image.width >= 180 && image.height >= 100;
|
|
}
|
|
|
|
VgaImage? pic(int index) {
|
|
if (index < 0 || index >= data.vgaImages.length) {
|
|
return null;
|
|
}
|
|
final image = data.vgaImages[index];
|
|
|
|
// Ignore known gameplay HUD art in menu composition.
|
|
if (index == WolfMenuPic.statusBar + _indexOffset) {
|
|
return null;
|
|
}
|
|
if (image.width <= 0 || image.height <= 0) {
|
|
return null;
|
|
}
|
|
|
|
return image;
|
|
}
|
|
}
|