diff --git a/apps/wolf_3d_cli/lib/cli_game_loop.dart b/apps/wolf_3d_cli/lib/cli_game_loop.dart index 09d46a8..3385caf 100644 --- a/apps/wolf_3d_cli/lib/cli_game_loop.dart +++ b/apps/wolf_3d_cli/lib/cli_game_loop.dart @@ -25,7 +25,9 @@ class CliGameLoop { 'CliGameLoop requires a CliInput instance.', ), - primaryRasterizer = AsciiRasterizer(isTerminal: true), + primaryRasterizer = AsciiRasterizer( + mode: AsciiRasterizerMode.terminalAnsi, + ), secondaryRasterizer = SixelRasterizer() { _rasterizer = primaryRasterizer; } diff --git a/packages/wolf_3d_dart/lib/src/rasterizer/ascii_rasterizer.dart b/packages/wolf_3d_dart/lib/src/rasterizer/ascii_rasterizer.dart index c332788..78c041c 100644 --- a/packages/wolf_3d_dart/lib/src/rasterizer/ascii_rasterizer.dart +++ b/packages/wolf_3d_dart/lib/src/rasterizer/ascii_rasterizer.dart @@ -61,6 +61,21 @@ class ColoredChar { // Outputs standard AARRGGBB for Flutter's Color(int) constructor int get argb => (0xFF000000) | (r << 16) | (g << 8) | b; + + // Same AABBGGRR → AARRGGBB conversion for the background channel. + int? get backgroundArgb { + final bg = rawBackgroundColor; + if (bg == null) return null; + final int bgR = bg & 0xFF; + final int bgG = (bg >> 8) & 0xFF; + final int bgB = (bg >> 16) & 0xFF; + return 0xFF000000 | (bgR << 16) | (bgG << 8) | bgB; + } +} + +enum AsciiRasterizerMode { + terminalAnsi, + terminalGrid, } class AsciiRasterizer extends CliRasterizer { @@ -71,21 +86,23 @@ class AsciiRasterizer extends CliRasterizer { static const int _simpleHudMinWidth = 84; static const int _simpleHudMinRows = 7; - static const int _menuSelectedTextPaletteIndex = 19; - static const int _menuUnselectedTextPaletteIndex = 23; static const int _menuHintKeyPaletteIndex = 12; static const int _menuHintLabelPaletteIndex = 4; static const int _menuHintBackgroundPaletteIndex = 0; AsciiRasterizer({ this.activeTheme = AsciiThemes.blocks, - this.isTerminal = false, + this.mode = AsciiRasterizerMode.terminalGrid, this.aspectMultiplier = 1.0, this.verticalStretch = 1.0, }); AsciiTheme activeTheme = AsciiThemes.blocks; - final bool isTerminal; + final AsciiRasterizerMode mode; + + bool get _usesTerminalLayout => true; + + bool get _emitAnsi => mode == AsciiRasterizerMode.terminalAnsi; late List> _screen; late List> _scenePixels; @@ -97,7 +114,7 @@ class AsciiRasterizer extends CliRasterizer { final double verticalStretch; @override - int get projectionWidth => isTerminal + int get projectionWidth => _usesTerminalLayout ? math.max( 1, math.min(width, (_terminalPixelHeight * _targetAspectRatio).floor()), @@ -105,14 +122,16 @@ class AsciiRasterizer extends CliRasterizer { : width; @override - int get projectionOffsetX => isTerminal ? (width - projectionWidth) ~/ 2 : 0; + int get projectionOffsetX => + _usesTerminalLayout ? (width - projectionWidth) ~/ 2 : 0; @override - int get projectionViewHeight => isTerminal ? viewHeight * 2 : viewHeight; + int get projectionViewHeight => + _usesTerminalLayout ? viewHeight * 2 : viewHeight; @override bool isTerminalSizeSupported(int columns, int rows) { - if (!isTerminal) { + if (!_usesTerminalLayout) { return true; } return columns >= _minimumTerminalColumns && rows >= _minimumTerminalRows; @@ -123,7 +142,7 @@ class AsciiRasterizer extends CliRasterizer { 'ASCII renderer requires a minimum resolution of ' '${_minimumTerminalColumns}x$_minimumTerminalRows.'; - int get _terminalPixelHeight => isTerminal ? height * 2 : height; + int get _terminalPixelHeight => _usesTerminalLayout ? height * 2 : height; int get _viewportRightX => projectionOffsetX + projectionWidth; @@ -149,7 +168,7 @@ class AsciiRasterizer extends CliRasterizer { // Just grab the raw ints! final int ceilingColor = ColorPalette.vga32Bit[25]; final int floorColor = ColorPalette.vga32Bit[29]; - final int backdropColor = isTerminal + final int backdropColor = _usesTerminalLayout ? _terminalBackdropColor : ColorPalette.vga32Bit[0]; @@ -167,7 +186,7 @@ class AsciiRasterizer extends CliRasterizer { } } - if (!isTerminal) { + if (!_usesTerminalLayout) { for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { if (y < viewHeight / 2) { @@ -206,7 +225,7 @@ class AsciiRasterizer extends CliRasterizer { pixelColor = shadeColor(pixelColor); } - if (isTerminal) { + if (_usesTerminalLayout) { _scenePixels[y][x] = _scaleColor(pixelColor, brightness); } else { final String wallChar = activeTheme.getByBrightness(brightness); @@ -250,7 +269,7 @@ class AsciiRasterizer extends CliRasterizer { int shadedColor = (0xFF000000) | (b << 16) | (g << 8) | r; - if (isTerminal) { + if (_usesTerminalLayout) { _scenePixels[y][stripeX] = shadedColor; } else { _screen[y][stripeX] = ColoredChar(activeTheme.solid, shadedColor); @@ -274,7 +293,7 @@ class AsciiRasterizer extends CliRasterizer { int startY = projectionViewHeight - weaponHeight + - (engine.player.weaponAnimOffset * (isTerminal ? 2 : 1) ~/ 4); + (engine.player.weaponAnimOffset * (_usesTerminalLayout ? 2 : 1) ~/ 4); for (int dy = 0; dy < weaponHeight; dy++) { for (int dx = 0; dx < weaponWidth; dx++) { @@ -288,9 +307,9 @@ class AsciiRasterizer extends CliRasterizer { if (sceneX >= projectionOffsetX && sceneX < _viewportRightX && drawY >= 0) { - if (isTerminal && drawY < projectionViewHeight) { + if (_usesTerminalLayout && drawY < projectionViewHeight) { _scenePixels[drawY][sceneX] = ColorPalette.vga32Bit[colorByte]; - } else if (!isTerminal && drawY < viewHeight) { + } else if (!_usesTerminalLayout && drawY < viewHeight) { _screen[drawY][sceneX] = ColoredChar( activeTheme.solid, ColorPalette.vga32Bit[colorByte], @@ -324,7 +343,7 @@ class AsciiRasterizer extends CliRasterizer { void drawHud(WolfEngine engine) { // If the terminal is at least 160 columns wide and 50 rows tall, // there are enough "pixels" to downscale the VGA image clearly. - int hudWidth = isTerminal ? projectionWidth : width; + int hudWidth = _usesTerminalLayout ? projectionWidth : width; if (hudWidth >= 160 && height >= 50) { _drawFullVgaHud(engine); } else { @@ -338,14 +357,11 @@ class AsciiRasterizer extends CliRasterizer { engine.menuManager.selectedDifficultyIndex; final int bgColor = _rgbToPaletteColor(engine.menuBackgroundRgb); final int panelColor = _rgbToPaletteColor(engine.menuPanelRgb); - // Exact title tint requested for menu heading text (#fff700). - final int headingColor = _rgbToRawColor(0xFFF700); - final int selectedTextColor = - ColorPalette.vga32Bit[_menuSelectedTextPaletteIndex]; - final int unselectedTextColor = - ColorPalette.vga32Bit[_menuUnselectedTextPaletteIndex]; + final int headingColor = WolfMenuPalette.headerTextColor; + final int selectedTextColor = WolfMenuPalette.selectedTextColor; + final int unselectedTextColor = WolfMenuPalette.unselectedTextColor; - if (isTerminal) { + if (_usesTerminalLayout) { _fillTerminalRect(0, 0, width, _terminalPixelHeight, bgColor); } else { _fillRect(0, 0, width, height, activeTheme.solid, bgColor); @@ -464,7 +480,7 @@ class AsciiRasterizer extends CliRasterizer { } int get _fullMenuHeadingScale { - if (!isTerminal) { + if (!_usesTerminalLayout) { return 2; } return projectionWidth < 140 ? 1 : 2; @@ -473,7 +489,8 @@ class AsciiRasterizer extends CliRasterizer { bool get _useMinimalMenuText => _menuGlyphHeightInRows(scale: 1) <= 4; int _menuGlyphHeightInRows({required int scale}) { - final double scaleY = (isTerminal ? _terminalPixelHeight : height) / 200.0; + final double scaleY = + (_usesTerminalLayout ? _terminalPixelHeight : height) / 200.0; // Use pre-compose pixel scale so terminal mode can graduate back to // bitmap menu text at practical window sizes. final double rasterHeight = 7.0 * scale * scaleY; @@ -481,15 +498,17 @@ class AsciiRasterizer extends CliRasterizer { } int _menuY200ToRow(int y200) { - final double scaleY = (isTerminal ? _terminalPixelHeight : height) / 200.0; + final double scaleY = + (_usesTerminalLayout ? _terminalPixelHeight : height) / 200.0; final int pixelY = (y200 * scaleY).toInt(); - final int row = isTerminal ? (pixelY / 2).round() : pixelY; + final int row = _usesTerminalLayout ? (pixelY / 2).round() : pixelY; return row.clamp(0, height - 1); } int _menuX320ToColumn(int x320) { - final double scaleX = (isTerminal ? projectionWidth : width) / 320.0; - final int offsetX = isTerminal ? projectionOffsetX : 0; + final double scaleX = + (_usesTerminalLayout ? projectionWidth : width) / 320.0; + final int offsetX = _usesTerminalLayout ? projectionOffsetX : 0; final int pixelX = offsetX + (x320 * scaleX).toInt(); return pixelX.clamp(0, width - 1); } @@ -517,10 +536,8 @@ class AsciiRasterizer extends CliRasterizer { const int rowStep = 15; for (int i = 0; i < Difficulty.values.length; i++) { final bool isSelected = i == selectedDifficultyIndex; - final int baseRowY = _menuY200ToRow(rowYStart + (i * rowStep)); - final int rowY = i == Difficulty.values.length - 1 - ? (baseRowY + 1).clamp(0, height - 1) - : baseRowY; + final int rowY = _menuY200ToRow(rowYStart + (i * rowStep)); + final String rowText = Difficulty.values[i].title; _writeLeftClipped( rowY, @@ -534,6 +551,11 @@ class AsciiRasterizer extends CliRasterizer { } void _drawCenteredMenuFooter() { + if (_usesTerminalLayout && !_emitAnsi) { + _drawFlutterGridMenuFooter(); + return; + } + final int hintKeyColor = ColorPalette.vga32Bit[_menuHintKeyPaletteIndex]; final int hintLabelColor = ColorPalette.vga32Bit[_menuHintLabelPaletteIndex]; @@ -558,7 +580,7 @@ class AsciiRasterizer extends CliRasterizer { final int boxX = ((width - boxWidth) ~/ 2).clamp(0, width - boxWidth); final int boxY = math.max(0, height - boxHeight - 1); - if (isTerminal) { + if (_usesTerminalLayout) { _fillTerminalRect( boxX, boxY * 2, @@ -587,6 +609,41 @@ class AsciiRasterizer extends CliRasterizer { } } + void _drawFlutterGridMenuFooter() { + final int hintKeyColor = ColorPalette.vga32Bit[_menuHintKeyPaletteIndex]; + final int hintLabelColor = + ColorPalette.vga32Bit[_menuHintLabelPaletteIndex]; + final int hintBackground = + ColorPalette.vga32Bit[_menuHintBackgroundPaletteIndex]; + + final List<(String text, int color)> segments = <(String, int)>[ + ('UP/DN', hintKeyColor), + (' MOVE ', hintLabelColor), + ('RET', hintKeyColor), + (' SELECT ', hintLabelColor), + ('ESC', hintKeyColor), + (' BACK', hintLabelColor), + ]; + + int textWidth = 0; + for (final (text, _) in segments) { + textWidth += WolfMenuFont.measureTextWidth(text, 1); + } + + final int panelWidth = (textWidth + 12).clamp(1, 320); + final int panelHeight = 12; + final int panelX = ((320 - panelWidth) ~/ 2).clamp(0, 319); + const int panelY = 184; + _fillRect320(panelX, panelY, panelWidth, panelHeight, hintBackground); + + int cursorX = panelX + 6; + const int textY = panelY + 2; + for (final (text, color) in segments) { + _drawMenuText(text, cursorX, textY, color, scale: 1); + cursorX += WolfMenuFont.measureTextWidth(text, 1); + } + } + void _writeLeftClipped( int y, String text, @@ -613,15 +670,17 @@ class AsciiRasterizer extends CliRasterizer { } void _plotMenuPixel320(int x320, int y200, int color) { - final double scaleX = (isTerminal ? projectionWidth : width) / 320.0; - final double scaleY = (isTerminal ? _terminalPixelHeight : height) / 200.0; + final double scaleX = + (_usesTerminalLayout ? projectionWidth : width) / 320.0; + final double scaleY = + (_usesTerminalLayout ? _terminalPixelHeight : height) / 200.0; - final int offsetX = isTerminal ? projectionOffsetX : 0; + final int offsetX = _usesTerminalLayout ? projectionOffsetX : 0; final int startX = offsetX + (x320 * scaleX).toInt(); final int startY = (y200 * scaleY).toInt(); final int pixelW = math.max(1, scaleX.ceil()); final int pixelH = math.max(1, scaleY.ceil()); - if (isTerminal) { + if (_usesTerminalLayout) { for (int dy = 0; dy < pixelH; dy++) { final int y = startY + dy; if (y < 0 || y >= _terminalPixelHeight) { @@ -654,7 +713,7 @@ class AsciiRasterizer extends CliRasterizer { } void _drawSimpleHud(WolfEngine engine) { - final int hudWidth = isTerminal ? projectionWidth : width; + final int hudWidth = _usesTerminalLayout ? projectionWidth : width; final int hudRows = height - viewHeight; if (hudWidth < _simpleHudMinWidth || hudRows < _simpleHudMinRows) { _drawMinimalHud(engine); @@ -693,7 +752,7 @@ class AsciiRasterizer extends CliRasterizer { final int baseY = viewHeight + 1; // 3. Clear HUD Base - if (isTerminal) { + if (_usesTerminalLayout) { _fillTerminalRect( projectionOffsetX, viewHeight * 2, @@ -722,7 +781,7 @@ class AsciiRasterizer extends CliRasterizer { // 4. Panel Drawing Helper void drawBorderedPanel(int startX, int startY, int w, int h) { - if (isTerminal) { + if (_usesTerminalLayout) { _fillTerminalRect(startX, startY * 2, w, h * 2, vgaPanelDark); _fillTerminalRect(startX, startY * 2, w, 1, white); _fillTerminalRect(startX, (startY + h) * 2 - 1, w, 1, white); @@ -821,7 +880,7 @@ class AsciiRasterizer extends CliRasterizer { final int red = ColorPalette.vga32Bit[4]; final int hudRows = height - viewHeight; - if (isTerminal) { + if (_usesTerminalLayout) { _fillTerminalRect( projectionOffsetX, viewHeight * 2, @@ -853,8 +912,8 @@ class AsciiRasterizer extends CliRasterizer { final int lineY = viewHeight + 1; if (lineY >= height) return; - final int drawStartX = isTerminal ? projectionOffsetX : 0; - final int drawWidth = isTerminal ? projectionWidth : width; + final int drawStartX = _usesTerminalLayout ? projectionOffsetX : 0; + final int drawWidth = _usesTerminalLayout ? projectionWidth : width; final int maxTextLen = math.max(0, drawWidth - 2); String clipped = hudText; if (clipped.length > maxTextLen) { @@ -965,15 +1024,15 @@ class AsciiRasterizer extends CliRasterizer { @override dynamic finalizeFrame() { if (_engine.difficulty != null && _engine.player.damageFlash > 0.0) { - if (isTerminal) { + if (_usesTerminalLayout) { _applyDamageFlashToScene(); } else { _applyDamageFlash(); } } - if (isTerminal) { + if (_usesTerminalLayout) { _composeTerminalScene(); - return toAnsiString(); + return _emitAnsi ? toAnsiString() : _screen; } return _screen; } @@ -983,14 +1042,16 @@ class AsciiRasterizer extends CliRasterizer { void _blitVgaImageAscii(VgaImage image, int startX_320, int startY_200) { int planeWidth = image.width ~/ 4; int planeSize = planeWidth * image.height; - int maxDrawHeight = isTerminal ? _terminalPixelHeight : height; - int maxDrawWidth = isTerminal ? _viewportRightX : width; + int maxDrawHeight = _usesTerminalLayout ? _terminalPixelHeight : height; + int maxDrawWidth = _usesTerminalLayout ? _viewportRightX : width; - double scaleX = (isTerminal ? projectionWidth : width) / 320.0; - double scaleY = (isTerminal ? _terminalPixelHeight : height) / 200.0; + double scaleX = (_usesTerminalLayout ? projectionWidth : width) / 320.0; + double scaleY = + (_usesTerminalLayout ? _terminalPixelHeight : height) / 200.0; int destStartX = - (isTerminal ? projectionOffsetX : 0) + (startX_320 * scaleX).toInt(); + (_usesTerminalLayout ? projectionOffsetX : 0) + + (startX_320 * scaleX).toInt(); int destStartY = (startY_200 * scaleY).toInt(); int destWidth = (image.width * scaleX).toInt(); int destHeight = (image.height * scaleY).toInt(); @@ -1013,7 +1074,7 @@ class AsciiRasterizer extends CliRasterizer { int colorByte = image.pixels[index]; if (colorByte != 255) { - if (isTerminal) { + if (_usesTerminalLayout) { _scenePixels[drawY][drawX] = ColorPalette.vga32Bit[colorByte]; } else { _screen[drawY][drawX] = ColoredChar( @@ -1046,16 +1107,19 @@ class AsciiRasterizer extends CliRasterizer { int h200, int color, ) { - final double scaleX = (isTerminal ? projectionWidth : width) / 320.0; - final double scaleY = (isTerminal ? _terminalPixelHeight : height) / 200.0; + final double scaleX = + (_usesTerminalLayout ? projectionWidth : width) / 320.0; + final double scaleY = + (_usesTerminalLayout ? _terminalPixelHeight : height) / 200.0; final int startX = - (isTerminal ? projectionOffsetX : 0) + (startX320 * scaleX).toInt(); + (_usesTerminalLayout ? projectionOffsetX : 0) + + (startX320 * scaleX).toInt(); final int startY = (startY200 * scaleY).toInt(); final int w = math.max(1, (w320 * scaleX).toInt()); final int h = math.max(1, (h200 * scaleY).toInt()); - if (isTerminal) { + if (_usesTerminalLayout) { _fillTerminalRect(startX, startY, w, h, color); } else { _fillRect(startX, startY, w, h, activeTheme.solid, color); @@ -1119,7 +1183,11 @@ class AsciiRasterizer extends CliRasterizer { ColoredChar overlay = _screen[y][x]; if (overlay.char != ' ') { - if (overlay.rawBackgroundColor == null) { + // In ANSI terminal mode, inject the bottom scene pixel as background + // so half-block rows pack two pixel rows into one cell. In Flutter + // grid mode any injected background renders as a colored rectangle + // behind the character, so we leave the cell as-is. + if (_emitAnsi && overlay.rawBackgroundColor == null) { _screen[y][x] = ColoredChar( overlay.char, overlay.rawColor, @@ -1129,6 +1197,9 @@ class AsciiRasterizer extends CliRasterizer { continue; } + // Pack two scene rows into one cell for both terminal and Flutter grid + // modes. Overlay characters above keep a null background in Flutter + // mode, so this does not introduce text background artifacts. _screen[y][x] = topColor == bottomColor ? ColoredChar('█', topColor) : ColoredChar('▀', topColor, bottomColor); @@ -1185,13 +1256,6 @@ class AsciiRasterizer extends CliRasterizer { return ColorPalette.vga32Bit[_rgbToPaletteIndex(rgb)]; } - int _rgbToRawColor(int rgb) { - final int r = (rgb >> 16) & 0xFF; - final int g = (rgb >> 8) & 0xFF; - final int b = rgb & 0xFF; - return (0xFF000000) | (b << 16) | (g << 8) | r; - } - int _rgbToPaletteIndex(int rgb) { final int targetR = (rgb >> 16) & 0xFF; final int targetG = (rgb >> 8) & 0xFF; diff --git a/packages/wolf_3d_dart/lib/src/rasterizer/menu_font.dart b/packages/wolf_3d_dart/lib/src/rasterizer/menu_font.dart index 42c2ddf..7969d70 100644 --- a/packages/wolf_3d_dart/lib/src/rasterizer/menu_font.dart +++ b/packages/wolf_3d_dart/lib/src/rasterizer/menu_font.dart @@ -1,6 +1,8 @@ class WolfMenuFont { const WolfMenuFont._(); + static const int _letterSpacing = 2; + static const Map> glyphs = { 'A': ['01110', '10001', '10001', '11111', '10001', '10001', '10001'], 'B': ['11110', '10001', '10001', '11110', '10001', '10001', '11110'], @@ -62,11 +64,11 @@ class WolfMenuFont { case ',': case "'": case ':': - return 4 * scale; + return (4 + _letterSpacing) * scale; case ' ': return 5 * scale; default: - return 6 * scale; + return (6 + _letterSpacing) * scale; } } diff --git a/packages/wolf_3d_dart/lib/src/rasterizer/sixel_rasterizer.dart b/packages/wolf_3d_dart/lib/src/rasterizer/sixel_rasterizer.dart index 2220f1f..f5f8a00 100644 --- a/packages/wolf_3d_dart/lib/src/rasterizer/sixel_rasterizer.dart +++ b/packages/wolf_3d_dart/lib/src/rasterizer/sixel_rasterizer.dart @@ -336,9 +336,9 @@ class SixelRasterizer extends CliRasterizer { engine.menuManager.selectedDifficultyIndex; final int bgColor = _rgbToPaletteIndex(engine.menuBackgroundRgb); final int panelColor = _rgbToPaletteIndex(engine.menuPanelRgb); - const int headingIndex = 119; - const int selectedTextIndex = 19; - const int unselectedTextIndex = 23; + final int headingIndex = WolfMenuPalette.headerTextIndex; + final int selectedTextIndex = WolfMenuPalette.selectedTextIndex; + final int unselectedTextIndex = WolfMenuPalette.unselectedTextIndex; for (int i = 0; i < _screen.length; i++) { _screen[i] = bgColor; @@ -419,7 +419,9 @@ class SixelRasterizer extends CliRasterizer { prefix + Difficulty.values[i].title, 42, rowYStart + (i * rowStep), - isSelected ? 19 : 23, + isSelected + ? WolfMenuPalette.selectedTextIndex + : WolfMenuPalette.unselectedTextIndex, scale: 1, ); } diff --git a/packages/wolf_3d_dart/lib/src/rasterizer/software_rasterizer.dart b/packages/wolf_3d_dart/lib/src/rasterizer/software_rasterizer.dart index 817606f..5345a62 100644 --- a/packages/wolf_3d_dart/lib/src/rasterizer/software_rasterizer.dart +++ b/packages/wolf_3d_dart/lib/src/rasterizer/software_rasterizer.dart @@ -182,9 +182,9 @@ class SoftwareRasterizer extends Rasterizer { engine.menuManager.selectedDifficultyIndex; final int bgColor = _rgbToFrameColor(engine.menuBackgroundRgb); final int panelColor = _rgbToFrameColor(engine.menuPanelRgb); - final int headingColor = ColorPalette.vga32Bit[119]; - final int selectedTextColor = ColorPalette.vga32Bit[19]; - final int unselectedTextColor = ColorPalette.vga32Bit[23]; + final int headingColor = WolfMenuPalette.headerTextColor; + final int selectedTextColor = WolfMenuPalette.selectedTextColor; + final int unselectedTextColor = WolfMenuPalette.unselectedTextColor; for (int i = 0; i < _buffer.pixels.length; i++) { _buffer.pixels[i] = bgColor; diff --git a/packages/wolf_3d_dart/lib/wolf_3d_menu.dart b/packages/wolf_3d_dart/lib/wolf_3d_menu.dart index 47a7ec9..e408080 100644 --- a/packages/wolf_3d_dart/lib/wolf_3d_menu.dart +++ b/packages/wolf_3d_dart/lib/wolf_3d_menu.dart @@ -43,6 +43,55 @@ abstract class WolfMenuPic { ]; } +/// 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; diff --git a/packages/wolf_3d_dart/lib/wolf_3d_rasterizer.dart b/packages/wolf_3d_dart/lib/wolf_3d_rasterizer.dart index 654e592..30e9e5f 100644 --- a/packages/wolf_3d_dart/lib/wolf_3d_rasterizer.dart +++ b/packages/wolf_3d_dart/lib/wolf_3d_rasterizer.dart @@ -1,6 +1,7 @@ library; -export 'src/rasterizer/ascii_rasterizer.dart' show AsciiRasterizer, ColoredChar; +export 'src/rasterizer/ascii_rasterizer.dart' + show AsciiRasterizer, AsciiRasterizerMode, ColoredChar; export 'src/rasterizer/cli_rasterizer.dart'; export 'src/rasterizer/rasterizer.dart'; export 'src/rasterizer/sixel_rasterizer.dart'; diff --git a/packages/wolf_3d_renderer/lib/wolf_3d_ascii_renderer.dart b/packages/wolf_3d_renderer/lib/wolf_3d_ascii_renderer.dart index b0ed657..46f0c85 100644 --- a/packages/wolf_3d_renderer/lib/wolf_3d_ascii_renderer.dart +++ b/packages/wolf_3d_renderer/lib/wolf_3d_ascii_renderer.dart @@ -22,7 +22,9 @@ class _WolfAsciiRendererState extends BaseWolfRendererState { static const int _renderHeight = 100; List> _asciiFrame = []; - final AsciiRasterizer _asciiRasterizer = AsciiRasterizer(); + final AsciiRasterizer _asciiRasterizer = AsciiRasterizer( + mode: AsciiRasterizerMode.terminalGrid, + ); @override void initState() { @@ -84,22 +86,16 @@ class AsciiFrameWidget extends StatelessWidget { // Merge adjacent cells with the same color to keep the rich // text tree smaller and reduce per-frame layout overhead. Color currentColor = Color(row[0].argb); - Color? currentBackground = row[0].rawBackgroundColor == null + Color? currentBackground = row[0].backgroundArgb == null ? null - : Color( - 0xFF000000 | (row[0].rawBackgroundColor! & 0x00FFFFFF), - ); + : Color(row[0].backgroundArgb!); StringBuffer currentSegment = StringBuffer(row[0].char); for (int i = 1; i < row.length; i++) { final Color nextColor = Color(row[i].argb); - final Color? nextBackground = - row[i].rawBackgroundColor == null + final Color? nextBackground = row[i].backgroundArgb == null ? null - : Color( - 0xFF000000 | - (row[i].rawBackgroundColor! & 0x00FFFFFF), - ); + : Color(row[i].backgroundArgb!); if (nextColor == currentColor && nextBackground == currentBackground) { currentSegment.write(row[i].char);