/// 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 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; } }