diff --git a/apps/wolf_3d_gui/lib/screens/game_screen.dart b/apps/wolf_3d_gui/lib/screens/game_screen.dart index 2b6358a..d0ab008 100644 --- a/apps/wolf_3d_gui/lib/screens/game_screen.dart +++ b/apps/wolf_3d_gui/lib/screens/game_screen.dart @@ -37,13 +37,12 @@ class _GameScreenState extends State { @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - if (_engine.isDifficultySelectionPending) { + return PopScope( + canPop: _engine.difficulty != null, + onPopInvokedWithResult: (didPop, result) { + if (!didPop && _engine.difficulty == null) { widget.wolf3d.input.queueBackAction(); - return false; } - return true; }, child: Scaffold( body: LayoutBuilder( @@ -55,7 +54,7 @@ class _GameScreenState extends State { return Listener( onPointerDown: (event) { widget.wolf3d.input.onPointerDown(event); - if (_engine.isDifficultySelectionPending && + if (_engine.difficulty == null && viewportRect.width > 0 && viewportRect.height > 0 && viewportRect.contains(event.localPosition)) { diff --git a/packages/wolf_3d_dart/lib/src/engine/rasterizer/software_rasterizer.dart b/packages/wolf_3d_dart/lib/src/engine/rasterizer/software_rasterizer.dart deleted file mode 100644 index 5442f6b..0000000 --- a/packages/wolf_3d_dart/lib/src/engine/rasterizer/software_rasterizer.dart +++ /dev/null @@ -1,412 +0,0 @@ -import 'dart:math' as math; - -import 'package:wolf_3d_dart/src/rasterizer/rasterizer.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_menu.dart'; - -class SoftwareRasterizer extends Rasterizer { - static const Map> _menuFont = { - 'A': ['01110', '10001', '10001', '11111', '10001', '10001', '10001'], - 'B': ['11110', '10001', '10001', '11110', '10001', '10001', '11110'], - 'C': ['01110', '10001', '10000', '10000', '10000', '10001', '01110'], - 'D': ['11110', '10001', '10001', '10001', '10001', '10001', '11110'], - 'E': ['11111', '10000', '10000', '11110', '10000', '10000', '11111'], - 'F': ['11111', '10000', '10000', '11110', '10000', '10000', '10000'], - 'G': ['01110', '10001', '10000', '10111', '10001', '10001', '01111'], - 'H': ['10001', '10001', '10001', '11111', '10001', '10001', '10001'], - 'I': ['11111', '00100', '00100', '00100', '00100', '00100', '11111'], - 'K': ['10001', '10010', '10100', '11000', '10100', '10010', '10001'], - 'L': ['10000', '10000', '10000', '10000', '10000', '10000', '11111'], - 'M': ['10001', '11011', '10101', '10101', '10001', '10001', '10001'], - 'N': ['10001', '10001', '11001', '10101', '10011', '10001', '10001'], - 'O': ['01110', '10001', '10001', '10001', '10001', '10001', '01110'], - 'P': ['11110', '10001', '10001', '11110', '10000', '10000', '10000'], - 'R': ['11110', '10001', '10001', '11110', '10100', '10010', '10001'], - 'S': ['01111', '10000', '10000', '01110', '00001', '00001', '11110'], - 'T': ['11111', '00100', '00100', '00100', '00100', '00100', '00100'], - 'U': ['10001', '10001', '10001', '10001', '10001', '10001', '01110'], - 'W': ['10001', '10001', '10001', '10101', '10101', '11011', '10001'], - 'Y': ['10001', '10001', '01010', '00100', '00100', '00100', '00100'], - '?': ['01110', '10001', '00001', '00010', '00100', '00000', '00100'], - '!': ['00100', '00100', '00100', '00100', '00100', '00000', '00100'], - ',': ['00000', '00000', '00000', '00000', '00110', '00100', '01000'], - '.': ['00000', '00000', '00000', '00000', '00000', '00110', '00110'], - "'": ['00100', '00100', '00100', '00000', '00000', '00000', '00000'], - ' ': ['00000', '00000', '00000', '00000', '00000', '00000', '00000'], - }; - - late FrameBuffer _buffer; - late WolfEngine _engine; - - // Intercept the base render call to store our references - @override - FrameBuffer render(WolfEngine engine) { - _engine = engine; - _buffer = engine.frameBuffer; - return super.render(engine); - } - - @override - void prepareFrame(WolfEngine engine) { - // Top half is ceiling color (25), bottom half is floor color (29) - int ceilingColor = ColorPalette.vga32Bit[25]; - int floorColor = ColorPalette.vga32Bit[29]; - - for (int y = 0; y < viewHeight; y++) { - int color = (y < viewHeight / 2) ? ceilingColor : floorColor; - for (int x = 0; x < width; x++) { - _buffer.pixels[y * width + x] = color; - } - } - } - - @override - void drawWallColumn( - int x, - int drawStart, - int drawEnd, - int columnHeight, - Sprite texture, - int texX, - double perpWallDist, - int side, - ) { - for (int y = drawStart; y < drawEnd; y++) { - // Calculate which Y pixel of the texture to sample - double relativeY = - (y - (-columnHeight ~/ 2 + viewHeight ~/ 2)) / columnHeight; - int texY = (relativeY * 64).toInt().clamp(0, 63); - - int colorByte = texture.pixels[texX * 64 + texY]; - int pixelColor = ColorPalette.vga32Bit[colorByte]; - - // Darken Y-side walls for faux directional lighting - if (side == 1) { - pixelColor = shadeColor(pixelColor); - } - - _buffer.pixels[y * width + x] = pixelColor; - } - } - - @override - void drawSpriteStripe( - int stripeX, - int drawStartY, - int drawEndY, - int spriteHeight, - Sprite texture, - int texX, - double transformY, - ) { - for ( - int y = math.max(0, drawStartY); - y < math.min(viewHeight, drawEndY); - y++ - ) { - double relativeY = (y - drawStartY) / spriteHeight; - int texY = (relativeY * 64).toInt().clamp(0, 63); - - int colorByte = texture.pixels[texX * 64 + texY]; - - // 255 is the "transparent" color index in VGA Wolfenstein - if (colorByte != 255) { - _buffer.pixels[y * width + stripeX] = ColorPalette.vga32Bit[colorByte]; - } - } - } - - @override - void drawWeapon(WolfEngine engine) { - int spriteIndex = engine.player.currentWeapon.getCurrentSpriteIndex( - engine.data.sprites.length, - ); - Sprite weaponSprite = engine.data.sprites[spriteIndex]; - - int weaponWidth = (width * 0.5).toInt(); - int weaponHeight = (viewHeight * 0.8).toInt(); - - int startX = (width ~/ 2) - (weaponWidth ~/ 2); - int startY = - viewHeight - weaponHeight + (engine.player.weaponAnimOffset ~/ 4); - - for (int dy = 0; dy < weaponHeight; dy++) { - for (int dx = 0; dx < weaponWidth; dx++) { - int texX = (dx * 64 ~/ weaponWidth).clamp(0, 63); - int texY = (dy * 64 ~/ weaponHeight).clamp(0, 63); - - int colorByte = weaponSprite.pixels[texX * 64 + texY]; - if (colorByte != 255) { - int drawX = startX + dx; - int drawY = startY + dy; - if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < viewHeight) { - _buffer.pixels[drawY * width + drawX] = - ColorPalette.vga32Bit[colorByte]; - } - } - } - } - } - - @override - void drawHud(WolfEngine engine) { - int statusBarIndex = engine.data.vgaImages.indexWhere( - (img) => img.width == 320 && img.height == 40, - ); - if (statusBarIndex == -1) return; - - // 1. Draw Background - _blitVgaImage(engine.data.vgaImages[statusBarIndex], 0, 160); - - // 2. Draw Stats (100% mathematically accurate right-aligned coordinates) - _drawNumber(1, 32, 176, engine.data.vgaImages); // Floor - _drawNumber(engine.player.score, 96, 176, engine.data.vgaImages); // Score - _drawNumber(3, 120, 176, engine.data.vgaImages); // Lives - _drawNumber( - engine.player.health, - 192, - 176, - engine.data.vgaImages, - ); // Health - _drawNumber(engine.player.ammo, 232, 176, engine.data.vgaImages); // Ammo - - // 3. Draw BJ's Face & Current Weapon - _drawFace(engine); - _drawWeaponIcon(engine); - } - - @override - void drawMenu(WolfEngine engine) { - final int bgColor = ColorPalette.vga32Bit[153]; - final int panelColor = ColorPalette.vga32Bit[157]; - final int headingColor = ColorPalette.vga32Bit[119]; - final int selectedTextColor = ColorPalette.vga32Bit[19]; - final int unselectedTextColor = ColorPalette.vga32Bit[23]; - - for (int i = 0; i < _buffer.pixels.length; i++) { - _buffer.pixels[i] = bgColor; - } - - const panelX = 28; - const panelY = 70; - const panelW = 264; - const panelH = 82; - - for (int y = panelY; y < panelY + panelH; y++) { - if (y < 0 || y >= height) continue; - final rowStart = y * width; - for (int x = panelX; x < panelX + panelW; x++) { - if (x >= 0 && x < width) { - _buffer.pixels[rowStart + x] = panelColor; - } - } - } - - final art = WolfClassicMenuArt(engine.data); - _drawMenuTextCentered('HOW TOUGH ARE YOU?', 48, headingColor, scale: 2); - - final bottom = art.pic(15); - if (bottom != null) { - final x = (width - bottom.width) ~/ 2; - final y = height - bottom.height - 8; - _blitVgaImage(bottom, x, y); - } - - final face = art.difficultyOption( - Difficulty.values[engine.menuSelectedDifficultyIndex], - ); - if (face != null) { - _blitVgaImage(face, panelX + panelW - face.width - 10, panelY + 22); - } - - final cursor = art.pic(engine.isMenuCursorAltFrame ? 9 : 8); - const rowYStart = panelY + 16; - const rowStep = 15; - const textX = panelX + 42; - const labels = [ - 'CAN I PLAY, DADDY?', - "DON'T HURT ME.", - "BRING 'EM ON!", - 'I AM DEATH INCARNATE!', - ]; - - for (int i = 0; i < Difficulty.values.length; i++) { - final y = rowYStart + (i * rowStep); - final isSelected = i == engine.menuSelectedDifficultyIndex; - - if (isSelected && cursor != null) { - _blitVgaImage(cursor, panelX + 10, y - 2); - } - - _drawMenuText( - labels[i], - textX, - y, - isSelected ? selectedTextColor : unselectedTextColor, - ); - } - } - - void _drawMenuText( - String text, - int startX, - int startY, - int color, { - int scale = 1, - }) { - int x = startX; - for (final rune in text.runes) { - final char = String.fromCharCode(rune).toUpperCase(); - final pattern = _menuFont[char] ?? _menuFont[' ']!; - - for (int row = 0; row < pattern.length; row++) { - final bits = pattern[row]; - for (int col = 0; col < bits.length; col++) { - if (bits[col] != '1') continue; - for (int sy = 0; sy < scale; sy++) { - for (int sx = 0; sx < scale; sx++) { - final drawX = x + (col * scale) + sx; - final drawY = startY + (row * scale) + sy; - if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < height) { - _buffer.pixels[drawY * width + drawX] = color; - } - } - } - } - } - - x += (6 * scale); - } - } - - void _drawMenuTextCentered( - String text, - int y, - int color, { - int scale = 1, - }) { - final textWidth = text.length * 6 * scale; - final x = ((width - textWidth) ~/ 2).clamp(0, width - 1); - _drawMenuText(text, x, y, color, scale: scale); - } - - @override - FrameBuffer finalizeFrame() { - // If the player took damage, overlay a red tint across the 3D view - if (!_engine.isDifficultySelectionPending && - _engine.player.damageFlash > 0) { - _applyDamageFlash(); - } - return _buffer; // Return the fully painted pixel array - } - - // =========================================================================== - // PRIVATE HELPER METHODS - // =========================================================================== - - /// Maps the planar VGA image data directly to 32-bit pixels. - /// (Assuming a 1:1 scale, which is standard for the 320x200 software renderer). - void _blitVgaImage(VgaImage image, int startX, int startY) { - int planeWidth = image.width ~/ 4; - int planeSize = planeWidth * image.height; - - for (int dy = 0; dy < image.height; dy++) { - for (int dx = 0; dx < image.width; dx++) { - int drawX = startX + dx; - int drawY = startY + dy; - - if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < height) { - int srcX = dx.clamp(0, image.width - 1); - int srcY = dy.clamp(0, image.height - 1); - - int plane = srcX % 4; - int sx = srcX ~/ 4; - int index = (plane * planeSize) + (srcY * planeWidth) + sx; - - int colorByte = image.pixels[index]; - if (colorByte != 255) { - _buffer.pixels[drawY * width + drawX] = - ColorPalette.vga32Bit[colorByte]; - } - } - } - } - } - - void _drawNumber( - int value, - int rightAlignX, - int startY, - List vgaImages, - ) { - const int zeroIndex = 96; - String numStr = value.toString(); - int currentX = rightAlignX - (numStr.length * 8); - - for (int i = 0; i < numStr.length; i++) { - int digit = int.parse(numStr[i]); - if (zeroIndex + digit < vgaImages.length) { - _blitVgaImage(vgaImages[zeroIndex + digit], currentX, startY); - } - currentX += 8; - } - } - - void _drawFace(WolfEngine engine) { - int health = engine.player.health; - int faceIndex; - - if (health <= 0) { - faceIndex = 127; // Dead face - } else { - int healthTier = ((100 - health) ~/ 16).clamp(0, 6); - faceIndex = 106 + (healthTier * 3); - } - - if (faceIndex < engine.data.vgaImages.length) { - _blitVgaImage(engine.data.vgaImages[faceIndex], 136, 164); - } - } - - void _drawWeaponIcon(WolfEngine engine) { - int weaponIndex = 89; // Default to Pistol - - if (engine.player.hasChainGun) { - weaponIndex = 91; - } else if (engine.player.hasMachineGun) { - weaponIndex = 90; - } - - if (weaponIndex < engine.data.vgaImages.length) { - _blitVgaImage(engine.data.vgaImages[weaponIndex], 256, 164); - } - } - - /// Tints the top 80% of the screen red based on player.damageFlash intensity - void _applyDamageFlash() { - // Grab the intensity (0.0 to 1.0) - double intensity = _engine.player.damageFlash; - - // Calculate how much to boost red and drop green/blue - int redBoost = (150 * intensity).toInt(); - double colorDrop = 1.0 - (0.5 * intensity); - - for (int y = 0; y < viewHeight; y++) { - for (int x = 0; x < width; x++) { - int index = y * width + x; - int color = _buffer.pixels[index]; - - int r = color & 0xFF; - int g = (color >> 8) & 0xFF; - int b = (color >> 16) & 0xFF; - - r = (r + redBoost).clamp(0, 255); - g = (g * colorDrop).toInt(); - b = (b * colorDrop).toInt(); - - _buffer.pixels[index] = (0xFF000000) | (b << 16) | (g << 8) | r; - } - } - } -} diff --git a/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart b/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart index fa652b5..ba333f6 100644 --- a/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart +++ b/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart @@ -45,17 +45,11 @@ class WolfEngine { /// The active difficulty level, affecting enemy spawning and behavior. Difficulty? difficulty; - /// Whether the engine is waiting on player difficulty selection. - bool get isDifficultySelectionPending => difficulty == null; - /// Menu state owner for difficulty-selection navigation and edge detection. final MenuManager menuManager = MenuManager(); - /// Cursor index used by renderer-side difficulty menus. - int get menuSelectedDifficultyIndex => menuManager.selectedDifficultyIndex; - - /// Cursor blink phase used by renderer-side difficulty menus. - bool get isMenuCursorAltFrame => menuManager.isCursorAltFrame(_timeAliveMs); + /// Elapsed engine lifetime in milliseconds. + int get timeAliveMs => _timeAliveMs; /// The episode index where the game session begins. final int startingEpisode; @@ -119,7 +113,7 @@ class WolfEngine { menuManager.beginDifficultySelection(initialDifficulty: difficulty); - if (!isDifficultySelectionPending) { + if (difficulty != null) { _loadLevel(); } @@ -151,7 +145,7 @@ class WolfEngine { input.update(); final currentInput = input.currentInput; - if (isDifficultySelectionPending) { + if (difficulty == null) { _tickDifficultyMenu(currentInput); return; } 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 748978b..c9e40a8 100644 --- a/packages/wolf_3d_dart/lib/src/rasterizer/ascii_rasterizer.dart +++ b/packages/wolf_3d_dart/lib/src/rasterizer/ascii_rasterizer.dart @@ -63,6 +63,55 @@ class ColoredChar { } class AsciiRasterizer extends CliRasterizer { + static const int _unsetMenuSubPixel = -1; + static const Map _quadrantGlyphByMask = { + 0x0: ' ', + 0x1: '▘', + 0x2: '▝', + 0x3: '▀', + 0x4: '▖', + 0x5: '▌', + 0x6: '▞', + 0x7: '▛', + 0x8: '▗', + 0x9: '▚', + 0xA: '▐', + 0xB: '▜', + 0xC: '▄', + 0xD: '▙', + 0xE: '▟', + 0xF: '█', + }; + static const Map> _menuFont = { + 'A': ['01110', '10001', '10001', '11111', '10001', '10001', '10001'], + 'B': ['11110', '10001', '10001', '11110', '10001', '10001', '11110'], + 'C': ['01110', '10001', '10000', '10000', '10000', '10001', '01110'], + 'D': ['11110', '10001', '10001', '10001', '10001', '10001', '11110'], + 'E': ['11111', '10000', '10000', '11110', '10000', '10000', '11111'], + 'F': ['11111', '10000', '10000', '11110', '10000', '10000', '10000'], + 'G': ['01110', '10001', '10000', '10111', '10001', '10001', '01111'], + 'H': ['10001', '10001', '10001', '11111', '10001', '10001', '10001'], + 'I': ['11111', '00100', '00100', '00100', '00100', '00100', '11111'], + 'K': ['10001', '10010', '10100', '11000', '10100', '10010', '10001'], + 'L': ['10000', '10000', '10000', '10000', '10000', '10000', '11111'], + 'M': ['10001', '11011', '10101', '10101', '10001', '10001', '10001'], + 'N': ['10001', '10001', '11001', '10101', '10011', '10001', '10001'], + 'O': ['01110', '10001', '10001', '10001', '10001', '10001', '01110'], + 'P': ['11110', '10001', '10001', '11110', '10000', '10000', '10000'], + 'R': ['11110', '10001', '10001', '11110', '10100', '10010', '10001'], + 'S': ['01111', '10000', '10000', '01110', '00001', '00001', '11110'], + 'T': ['11111', '00100', '00100', '00100', '00100', '00100', '00100'], + 'U': ['10001', '10001', '10001', '10001', '10001', '10001', '01110'], + 'W': ['10001', '10001', '10001', '10101', '10101', '11011', '10001'], + 'Y': ['10001', '10001', '01010', '00100', '00100', '00100', '00100'], + '?': ['01110', '10001', '00001', '00010', '00100', '00000', '00100'], + '!': ['00100', '00100', '00100', '00100', '00100', '00000', '00100'], + ',': ['00000', '00000', '00000', '00000', '00110', '00100', '01000'], + '.': ['00000', '00000', '00000', '00000', '00000', '00110', '00110'], + "'": ['00100', '00100', '00100', '00000', '00000', '00000', '00000'], + ' ': ['00000', '00000', '00000', '00000', '00000', '00000', '00000'], + }; + static const double _targetAspectRatio = 4 / 3; static const int _terminalBackdropArgb = 0xFF009688; static const int _minimumTerminalColumns = 80; @@ -83,6 +132,7 @@ class AsciiRasterizer extends CliRasterizer { late List> _screen; late List> _scenePixels; late WolfEngine _engine; + List>? _menuTextSubPixels; @override final double aspectMultiplier; @@ -327,6 +377,8 @@ class AsciiRasterizer extends CliRasterizer { @override void drawMenu(WolfEngine engine) { + final int selectedDifficultyIndex = + engine.menuManager.selectedDifficultyIndex; final int bgColor = _rgbToRawColor(engine.menuBackgroundRgb); final int panelColor = _rgbToRawColor(engine.menuPanelRgb); final int headingColor = ColorPalette.vga32Bit[119]; @@ -337,53 +389,51 @@ class AsciiRasterizer extends CliRasterizer { _fillTerminalRect(0, 0, width, _terminalPixelHeight, bgColor); } else { _fillRect(0, 0, width, height, activeTheme.solid, bgColor); + _menuTextSubPixels = List.generate( + height * 2, + (_) => List.filled(width * 2, _unsetMenuSubPixel), + ); } _fillRect320(28, 70, 264, 82, panelColor); - const heading = 'HOW TOUGH ARE YOU?'; - final headingY = ((48 / 200) * height).toInt().clamp(0, height - 1); - final headingX = ((width - heading.length) ~/ 2).clamp(0, width - 1); - _writeString(headingX, headingY, heading, headingColor, bgColor); + _drawMenuTextCentered('HOW TOUGH ARE YOU?', 48, headingColor, scale: 2); final art = WolfClassicMenuArt(engine.data); final face = art.difficultyOption( - Difficulty.values[engine.menuSelectedDifficultyIndex], + Difficulty.values[selectedDifficultyIndex], ); if (face != null) { _blitVgaImageAscii(face, 28 + 264 - face.width - 10, 92); } - final cursor = art.pic(engine.isMenuCursorAltFrame ? 9 : 8); + final cursor = art.pic( + engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8, + ); const rowYStart = 86; const rowStep = 15; - const labels = [ - 'CAN I PLAY, DADDY?', - "DON'T HURT ME.", - "BRING 'EM ON!", - 'I AM DEATH INCARNATE!', - ]; for (int i = 0; i < Difficulty.values.length; i++) { final y = rowYStart + (i * rowStep); - final isSelected = i == engine.menuSelectedDifficultyIndex; - + final isSelected = i == selectedDifficultyIndex; if (isSelected && cursor != null) { _blitVgaImageAscii(cursor, 38, y - 2); } - final textY = ((y / 200) * height).toInt().clamp(0, height - 1); - final textX = ((70 / 320) * width).toInt().clamp(0, width - 1); - _writeString( - textX, - textY, - labels[i], + _drawMenuText( + _difficultyLabel(Difficulty.values[i]), + 70, + y, isSelected ? selectedTextColor : unselectedTextColor, - panelColor, ); } + if (!isTerminal) { + _composeMenuTextSubPixels(); + _menuTextSubPixels = null; + } + final int hintKeyColor = _rgbToRawColor(0xFF5555); final int hintLabelColor = _rgbToRawColor(0x900303); final int hintBackground = _rgbToRawColor(0x000000); @@ -442,6 +492,190 @@ class AsciiRasterizer extends CliRasterizer { ); } + String _difficultyLabel(Difficulty difficulty) { + switch (difficulty) { + case Difficulty.baby: + return 'CAN I PLAY, DADDY?'; + case Difficulty.easy: + return "DON'T HURT ME."; + case Difficulty.medium: + return "BRING 'EM ON!"; + case Difficulty.hard: + return 'I AM DEATH INCARNATE!'; + } + } + + void _drawMenuText( + String text, + int startX320, + int startY200, + int color, { + int scale = 1, + }) { + int x320 = startX320; + for (final rune in text.runes) { + final String char = String.fromCharCode(rune).toUpperCase(); + final List pattern = _menuFont[char] ?? _menuFont[' ']!; + + for (int row = 0; row < pattern.length; row++) { + final String bits = pattern[row]; + for (int col = 0; col < bits.length; col++) { + if (bits[col] != '1') { + continue; + } + for (int sy = 0; sy < scale; sy++) { + for (int sx = 0; sx < scale; sx++) { + _plotMenuPixel320( + x320 + (col * scale) + sx, + startY200 + (row * scale) + sy, + color, + ); + } + } + } + } + + x320 += _menuGlyphAdvance(char, scale); + } + } + + void _drawMenuTextCentered( + String text, + int y200, + int color, { + int scale = 1, + }) { + final int textWidth = _measureMenuTextWidth(text, scale); + final int x320 = ((320 - textWidth) ~/ 2).clamp(0, 319); + _drawMenuText(text, x320, y200, color, scale: scale); + } + + int _measureMenuTextWidth(String text, int scale) { + int width320 = 0; + for (final rune in text.runes) { + final char = String.fromCharCode(rune).toUpperCase(); + width320 += _menuGlyphAdvance(char, scale); + } + return width320; + } + + int _menuGlyphAdvance(String char, int scale) { + switch (char) { + case 'I': + case '!': + case '.': + case ',': + case "'": + return 4 * scale; + case ' ': + return 5 * scale; + default: + return 6 * scale; + } + } + + 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 int offsetX = isTerminal ? projectionOffsetX : 0; + final int startX = offsetX + (x320 * scaleX).toInt(); + final int pixelW = math.max(1, scaleX.ceil()); + if (isTerminal) { + final int startY = (y200 * scaleY).toInt(); + final int pixelH = math.max(1, scaleY.ceil()); + + for (int dy = 0; dy < pixelH; dy++) { + final int y = startY + dy; + if (y < 0 || y >= _terminalPixelHeight) { + continue; + } + for (int dx = 0; dx < pixelW; dx++) { + final int x = startX + dx; + if (x < 0 || x >= width) { + continue; + } + _scenePixels[y][x] = color; + } + } + return; + } + + final overlay = _menuTextSubPixels; + if (overlay == null) { + return; + } + + final double subScaleX = (width * 2) / 320.0; + final double subScaleY = (height * 2) / 200.0; + final int startXSub = (x320 * subScaleX).toInt(); + final int startYSub = (y200 * subScaleY).toInt(); + final int pixelWSub = math.max(1, subScaleX.ceil()); + final int pixelHSub = math.max(1, subScaleY.ceil()); + + for (int dy = 0; dy < pixelHSub; dy++) { + final int y = startYSub + dy; + if (y < 0 || y >= overlay.length) { + continue; + } + for (int dx = 0; dx < pixelWSub; dx++) { + final int x = startXSub + dx; + if (x < 0 || x >= (width * 2)) { + continue; + } + overlay[y][x] = color; + } + } + } + + void _composeMenuTextSubPixels() { + final overlay = _menuTextSubPixels; + if (overlay == null) { + return; + } + + for (int y = 0; y < height; y++) { + final int topY = y * 2; + final int bottomY = math.min(topY + 1, overlay.length - 1); + + for (int x = 0; x < width; x++) { + final int leftX = x * 2; + final int rightX = leftX + 1; + + final int tl = overlay[topY][leftX]; + final int tr = overlay[topY][rightX]; + final int bl = overlay[bottomY][leftX]; + final int br = overlay[bottomY][rightX]; + + int mask = 0; + if (tl != _unsetMenuSubPixel) mask |= 0x1; + if (tr != _unsetMenuSubPixel) mask |= 0x2; + if (bl != _unsetMenuSubPixel) mask |= 0x4; + if (br != _unsetMenuSubPixel) mask |= 0x8; + + if (mask == 0) { + continue; + } + + final int baseColor = _screen[y][x].rawColor; + final int fgColor = tl != _unsetMenuSubPixel + ? tl + : tr != _unsetMenuSubPixel + ? tr + : bl != _unsetMenuSubPixel + ? bl + : br; + final String glyph = _quadrantGlyphByMask[mask] ?? '█'; + + if (mask == 0xF) { + _screen[y][x] = ColoredChar('█', fgColor); + } else { + _screen[y][x] = ColoredChar(glyph, fgColor, baseColor); + } + } + } + } + void _drawSimpleHud(WolfEngine engine) { final int hudWidth = isTerminal ? projectionWidth : width; final int hudRows = height - viewHeight; @@ -753,8 +987,7 @@ class AsciiRasterizer extends CliRasterizer { @override dynamic finalizeFrame() { - if (!_engine.isDifficultySelectionPending && - _engine.player.damageFlash > 0.0) { + if (_engine.difficulty != null && _engine.player.damageFlash > 0.0) { if (isTerminal) { _applyDamageFlashToScene(); } else { diff --git a/packages/wolf_3d_dart/lib/src/rasterizer/rasterizer.dart b/packages/wolf_3d_dart/lib/src/rasterizer/rasterizer.dart index 6f166d1..2f5afc2 100644 --- a/packages/wolf_3d_dart/lib/src/rasterizer/rasterizer.dart +++ b/packages/wolf_3d_dart/lib/src/rasterizer/rasterizer.dart @@ -50,7 +50,7 @@ abstract class Rasterizer { // 1. Setup the frame (clear screen, draw floor/ceiling) prepareFrame(engine); - if (engine.isDifficultySelectionPending) { + if (engine.difficulty == null) { drawMenu(engine); return finalizeFrame(); } 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 9691a9b..5bef064 100644 --- a/packages/wolf_3d_dart/lib/src/rasterizer/sixel_rasterizer.dart +++ b/packages/wolf_3d_dart/lib/src/rasterizer/sixel_rasterizer.dart @@ -359,6 +359,8 @@ class SixelRasterizer extends CliRasterizer { @override void drawMenu(WolfEngine engine) { + final int selectedDifficultyIndex = + engine.menuManager.selectedDifficultyIndex; final int bgColor = _rgbToPaletteIndex(engine.menuBackgroundRgb); final int panelColor = _rgbToPaletteIndex(engine.menuPanelRgb); const int headingIndex = 119; @@ -380,13 +382,15 @@ class SixelRasterizer extends CliRasterizer { } final face = art.difficultyOption( - Difficulty.values[engine.menuSelectedDifficultyIndex], + Difficulty.values[selectedDifficultyIndex], ); if (face != null) { _blitVgaImage(face, 28 + 264 - face.width - 10, 92); } - final cursor = art.pic(engine.isMenuCursorAltFrame ? 9 : 8); + final cursor = art.pic( + engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8, + ); const rowYStart = 86; const rowStep = 15; const textX = 70; @@ -398,7 +402,7 @@ class SixelRasterizer extends CliRasterizer { ]; for (int i = 0; i < Difficulty.values.length; i++) { final y = rowYStart + (i * rowStep); - final isSelected = i == engine.menuSelectedDifficultyIndex; + final isSelected = i == selectedDifficultyIndex; if (isSelected && cursor != null) { _blitVgaImage(cursor, 38, y - 2); @@ -532,7 +536,7 @@ class SixelRasterizer extends CliRasterizer { StringBuffer sb = StringBuffer(); sb.write('\x1bPq'); - double damageIntensity = _engine.isDifficultySelectionPending + double damageIntensity = _engine.difficulty == null ? 0.0 : _engine.player.damageFlash; int redBoost = (150 * damageIntensity).toInt(); 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 b698877..c744f38 100644 --- a/packages/wolf_3d_dart/lib/src/rasterizer/software_rasterizer.dart +++ b/packages/wolf_3d_dart/lib/src/rasterizer/software_rasterizer.dart @@ -178,6 +178,8 @@ class SoftwareRasterizer extends Rasterizer { @override void drawMenu(WolfEngine engine) { + final int selectedDifficultyIndex = + engine.menuManager.selectedDifficultyIndex; final int bgColor = _rgbToFrameColor(engine.menuBackgroundRgb); final int panelColor = _rgbToFrameColor(engine.menuPanelRgb); final int headingColor = ColorPalette.vga32Bit[119]; @@ -214,13 +216,15 @@ class SoftwareRasterizer extends Rasterizer { } final face = art.difficultyOption( - Difficulty.values[engine.menuSelectedDifficultyIndex], + Difficulty.values[selectedDifficultyIndex], ); if (face != null) { _blitVgaImage(face, panelX + panelW - face.width - 10, panelY + 22); } - final cursor = art.pic(engine.isMenuCursorAltFrame ? 9 : 8); + final cursor = art.pic( + engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8, + ); const rowYStart = panelY + 16; const rowStep = 15; const textX = panelX + 42; @@ -233,7 +237,7 @@ class SoftwareRasterizer extends Rasterizer { for (int i = 0; i < Difficulty.values.length; i++) { final y = rowYStart + (i * rowStep); - final isSelected = i == engine.menuSelectedDifficultyIndex; + final isSelected = i == selectedDifficultyIndex; if (isSelected && cursor != null) { _blitVgaImage(cursor, panelX + 10, y - 2); @@ -303,8 +307,7 @@ class SoftwareRasterizer extends Rasterizer { @override FrameBuffer finalizeFrame() { // If the player took damage, overlay a red tint across the 3D view - if (!_engine.isDifficultySelectionPending && - _engine.player.damageFlash > 0) { + if (_engine.difficulty != null && _engine.player.damageFlash > 0) { _applyDamageFlash(); } return _buffer; // Return the fully painted pixel array 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 b240b7a..5f40939 100644 --- a/packages/wolf_3d_renderer/lib/wolf_3d_ascii_renderer.dart +++ b/packages/wolf_3d_renderer/lib/wolf_3d_ascii_renderer.dart @@ -36,7 +36,7 @@ class _WolfAsciiRendererState extends BaseWolfRendererState { } @override - Color get scaffoldColor => widget.engine.isDifficultySelectionPending + Color get scaffoldColor => widget.engine.difficulty == null ? _colorFromRgb(widget.engine.menuBackgroundRgb) : const Color.fromARGB(255, 4, 64, 64); diff --git a/packages/wolf_3d_renderer/lib/wolf_3d_flutter_renderer.dart b/packages/wolf_3d_renderer/lib/wolf_3d_flutter_renderer.dart index 5336cf3..30bfb6c 100644 --- a/packages/wolf_3d_renderer/lib/wolf_3d_flutter_renderer.dart +++ b/packages/wolf_3d_renderer/lib/wolf_3d_flutter_renderer.dart @@ -41,7 +41,7 @@ class _WolfFlutterRendererState } @override - Color get scaffoldColor => widget.engine.isDifficultySelectionPending + Color get scaffoldColor => widget.engine.difficulty == null ? _colorFromRgb(widget.engine.menuBackgroundRgb) : const Color.fromARGB(255, 4, 64, 64);