From 1165e0bc440766e29629e71076ac717413064d4f Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Mon, 23 Mar 2026 11:08:43 +0100 Subject: [PATCH] feat: Implement map overlay toggle functionality and rendering across input and rendering systems Signed-off-by: Hans Kokx --- .../lib/src/engine/input/engine_input.dart | 2 + .../lib/src/engine/wolf_3d_engine_base.dart | 11 ++ .../wolf_3d_dart/lib/src/input/cli_input.dart | 9 ++ .../lib/src/input/wolf_3d_input.dart | 3 + .../lib/src/rendering/ascii_renderer.dart | 129 ++++++++++++++++ .../lib/src/rendering/renderer_backend.dart | 6 + .../lib/src/rendering/sixel_renderer.dart | 96 ++++++++++++ .../lib/src/rendering/software_renderer.dart | 86 +++++++++++ .../test/engine/map_overlay_toggle_test.dart | 142 ++++++++++++++++++ .../rendering/map_overlay_renderer_test.dart | 114 ++++++++++++++ .../lib/wolf_3d_input_flutter.dart | 2 + 11 files changed, 600 insertions(+) create mode 100644 packages/wolf_3d_dart/test/engine/map_overlay_toggle_test.dart create mode 100644 packages/wolf_3d_dart/test/rendering/map_overlay_renderer_test.dart diff --git a/packages/wolf_3d_dart/lib/src/engine/input/engine_input.dart b/packages/wolf_3d_dart/lib/src/engine/input/engine_input.dart index e960474..1e7f95c 100644 --- a/packages/wolf_3d_dart/lib/src/engine/input/engine_input.dart +++ b/packages/wolf_3d_dart/lib/src/engine/input/engine_input.dart @@ -6,6 +6,7 @@ class EngineInput { final bool isMovingBackward; final bool isTurningLeft; final bool isTurningRight; + final bool isMapToggle; final bool isFiring; final bool isInteracting; final bool isBack; @@ -18,6 +19,7 @@ class EngineInput { this.isMovingBackward = false, this.isTurningLeft = false, this.isTurningRight = false, + this.isMapToggle = false, this.isFiring = false, this.isInteracting = false, this.isBack = false, 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 d7ca72f..f5ce7a3 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 @@ -99,6 +99,9 @@ class WolfEngine { /// Whether renderers should draw the FPS counter overlay. bool showFpsCounter = false; + /// Whether gameplay renderers should draw the fullscreen map overlay. + bool isMapOverlayVisible = false; + /// The episode index where the game session begins. final int? startingEpisode; @@ -463,6 +466,10 @@ class WolfEngine { input.update(); final currentInput = input.currentInput; + if (!isMenuOpen && currentInput.isMapToggle) { + isMapOverlayVisible = !isMapOverlayVisible; + } + if (difficulty != null && !_isMenuOverlayVisible && currentInput.isBack) { _openPauseMenu(); menuManager.absorbInputState(currentInput); @@ -711,6 +718,7 @@ class WolfEngine { if (!_hasActiveSession) { return; } + isMapOverlayVisible = false; _isMenuOverlayVisible = true; menuManager.showMainMenu(hasResumableGame: true); } @@ -724,6 +732,7 @@ class WolfEngine { difficulty = selectedDifficulty; _currentLevelIndex = 0; _returnLevelIndex = null; + isMapOverlayVisible = false; _isMenuOverlayVisible = false; _loadLevel(preservePlayerState: false); _hasActiveSession = true; @@ -731,6 +740,7 @@ class WolfEngine { void _endCurrentGame() { difficulty = null; + isMapOverlayVisible = false; _isMenuOverlayVisible = false; _hasActiveSession = false; _returnLevelIndex = null; @@ -756,6 +766,7 @@ class WolfEngine { /// Wipes the current world state and builds a new floor from map data. void _loadLevel({required bool preservePlayerState}) { + isMapOverlayVisible = false; entities.clear(); _lastPatrolTileByEnemy.clear(); diff --git a/packages/wolf_3d_dart/lib/src/input/cli_input.dart b/packages/wolf_3d_dart/lib/src/input/cli_input.dart index 2588225..feed119 100644 --- a/packages/wolf_3d_dart/lib/src/input/cli_input.dart +++ b/packages/wolf_3d_dart/lib/src/input/cli_input.dart @@ -69,6 +69,7 @@ class CliInput extends Wolf3dInput { bool _pFire = false; bool _pInteract = false; bool _pBack = false; + bool _pMapToggle = false; WeaponType? _pWeapon; /// Queues a raw terminal key sequence for the next engine frame. @@ -94,6 +95,12 @@ class CliInput extends Wolf3dInput { return; } + // Tab toggles fullscreen map overlay. + if (bytes.length == 1 && bytes[0] == 9) { + _pMapToggle = true; + return; + } + String char = String.fromCharCodes(bytes).toLowerCase(); if (char == 'w') _pForward = true; @@ -119,6 +126,7 @@ class CliInput extends Wolf3dInput { isMovingBackward = _pBackward; isTurningLeft = _pLeft; isTurningRight = _pRight; + isMapToggle = _pMapToggle; isFiring = _pFire; isInteracting = _pInteract; isBack = _pBack; @@ -127,6 +135,7 @@ class CliInput extends Wolf3dInput { // Reset the pending buffer so each keypress behaves like a frame impulse. _pForward = _pBackward = _pLeft = _pRight = _pFire = _pInteract = false; _pBack = false; + _pMapToggle = false; _pWeapon = null; } } diff --git a/packages/wolf_3d_dart/lib/src/input/wolf_3d_input.dart b/packages/wolf_3d_dart/lib/src/input/wolf_3d_input.dart index 79ea9e2..cb171aa 100644 --- a/packages/wolf_3d_dart/lib/src/input/wolf_3d_input.dart +++ b/packages/wolf_3d_dart/lib/src/input/wolf_3d_input.dart @@ -6,6 +6,7 @@ abstract class Wolf3dInput { bool isMovingBackward = false; bool isTurningLeft = false; bool isTurningRight = false; + bool isMapToggle = false; bool isInteracting = false; bool isBack = false; double? menuTapX; @@ -23,6 +24,7 @@ abstract class Wolf3dInput { isMovingBackward: isMovingBackward, isTurningLeft: isTurningLeft, isTurningRight: isTurningRight, + isMapToggle: isMapToggle, isFiring: isFiring, isInteracting: isInteracting, isBack: isBack, @@ -37,6 +39,7 @@ enum WolfInputAction { backward, turnLeft, turnRight, + mapToggle, fire, interact, back, diff --git a/packages/wolf_3d_dart/lib/src/rendering/ascii_renderer.dart b/packages/wolf_3d_dart/lib/src/rendering/ascii_renderer.dart index 57936fb..d0031d4 100644 --- a/packages/wolf_3d_dart/lib/src/rendering/ascii_renderer.dart +++ b/packages/wolf_3d_dart/lib/src/rendering/ascii_renderer.dart @@ -406,6 +406,93 @@ class AsciiRenderer extends CliRendererBackend { _writeString(1, 0, ' ${fpsLabel(engine)} ', textColor, bgColor); } + @override + void drawGameplayOverlay(WolfEngine engine) { + if (!engine.isMapOverlayVisible) { + return; + } + + final int mapBgColor = ColorPalette.vga32Bit[0]; + final int floorColor = ColorPalette.vga32Bit[8]; + final int wallColor = ColorPalette.vga32Bit[7]; + final int doorColor = ColorPalette.vga32Bit[14]; + final int playerColor = ColorPalette.vga32Bit[10]; + final int facingColor = ColorPalette.vga32Bit[12]; + + if (_usesTerminalLayout) { + _fillTerminalRect( + projectionOffsetX, + 0, + projectionWidth, + _terminalPixelHeight, + mapBgColor, + ); + } else { + _fillRect(0, 0, width, height, activeTheme.solid, mapBgColor); + } + + final int viewportX = _usesTerminalLayout ? projectionOffsetX : 0; + final int viewportWidth = _usesTerminalLayout ? projectionWidth : width; + final int viewportHeight = _usesTerminalLayout + ? _terminalPixelHeight + : height; + final int viewportPadding = math.max( + 3, + math.min(viewportWidth, viewportHeight) ~/ 24, + ); + final int availableWidth = math.max( + 1, + viewportWidth - (viewportPadding * 2), + ); + final int availableHeight = math.max( + 1, + viewportHeight - (viewportPadding * 2), + ); + final int tileSize = math.max( + 1, + math.min(availableWidth, availableHeight) ~/ 64, + ); + final int mapPixelWidth = tileSize * 64; + final int mapPixelHeight = tileSize * 64; + final int mapStartX = viewportX + ((viewportWidth - mapPixelWidth) ~/ 2); + final int mapStartY = (viewportHeight - mapPixelHeight) ~/ 2; + + for (int y = 0; y < 64; y++) { + for (int x = 0; x < 64; x++) { + final int tileId = engine.currentLevel[y][x]; + final int color = tileId == 0 + ? floorColor + : (tileId >= 90 ? doorColor : wallColor); + _fillMapRect( + mapStartX + (x * tileSize), + mapStartY + (y * tileSize), + tileSize, + tileSize, + color, + ); + } + } + + final int playerX = (mapStartX + (engine.player.x * tileSize)).floor(); + final int playerY = (mapStartY + (engine.player.y * tileSize)).floor(); + final int markerRadius = math.max(1, tileSize ~/ 2); + final int markerSize = (markerRadius * 2) + 1; + _fillMapRect( + playerX - markerRadius, + playerY - markerRadius, + markerSize, + markerSize, + playerColor, + ); + + final int facingLength = math.max(4, (tileSize * 3) ~/ 2); + final int facingEndX = + (playerX + (math.cos(engine.player.angle) * facingLength)).round(); + final int facingEndY = + (playerY + (math.sin(engine.player.angle) * facingLength)).round(); + _drawMapLine(playerX, playerY, facingEndX, facingEndY, facingColor); + } + @override void drawMenu(WolfEngine engine) { final int bgColor = _rgbToPaletteColor( @@ -2053,6 +2140,48 @@ class AsciiRenderer extends CliRendererBackend { } } + void _fillMapRect(int startX, int startY, int w, int h, int color) { + for (int y = startY; y < startY + h; y++) { + for (int x = startX; x < startX + w; x++) { + _setMapPixel(x, y, color); + } + } + } + + void _drawMapLine(int x0, int y0, int x1, int y1, int color) { + final int dx = x1 - x0; + final int dy = y1 - y0; + final int steps = math.max(dx.abs(), dy.abs()); + if (steps == 0) { + _setMapPixel(x0, y0, color); + return; + } + for (int i = 0; i <= steps; i++) { + final double t = i / steps; + final int x = (x0 + (dx * t)).round(); + final int y = (y0 + (dy * t)).round(); + _setMapPixel(x, y, color); + } + } + + void _setMapPixel(int x, int y, int color) { + if (_usesTerminalLayout) { + if (x < projectionOffsetX || x >= _viewportRightX) { + return; + } + if (y < 0 || y >= _terminalPixelHeight) { + return; + } + _scenePixels[y][x] = color; + return; + } + + if (x < 0 || x >= width || y < 0 || y >= height) { + return; + } + _screen[y][x] = ColoredChar(activeTheme.solid, color); + } + // --- DAMAGE FLASH --- void _applyDamageFlash() { for (int y = 0; y < viewHeight; y++) { diff --git a/packages/wolf_3d_dart/lib/src/rendering/renderer_backend.dart b/packages/wolf_3d_dart/lib/src/rendering/renderer_backend.dart index 5259df9..591da10 100644 --- a/packages/wolf_3d_dart/lib/src/rendering/renderer_backend.dart +++ b/packages/wolf_3d_dart/lib/src/rendering/renderer_backend.dart @@ -91,6 +91,7 @@ abstract class RendererBackend // 3. Draw 2D overlays. drawWeapon(engine); drawHud(engine); + drawGameplayOverlay(engine); if (engine.showFpsCounter) { drawFpsOverlay(engine); } @@ -140,6 +141,11 @@ abstract class RendererBackend /// Return the finished frame (e.g., the FrameBuffer itself, or an ASCII list). T finalizeFrame(); + /// Draws gameplay overlays that should appear above world/HUD content. + /// + /// Default implementation is a no-op. + void drawGameplayOverlay(WolfEngine engine) {} + /// Draws a non-world menu frame when the engine is awaiting configuration. /// /// Default implementation is a no-op for backends that don't support menus. diff --git a/packages/wolf_3d_dart/lib/src/rendering/sixel_renderer.dart b/packages/wolf_3d_dart/lib/src/rendering/sixel_renderer.dart index a31f97a..aa19cd0 100644 --- a/packages/wolf_3d_dart/lib/src/rendering/sixel_renderer.dart +++ b/packages/wolf_3d_dart/lib/src/rendering/sixel_renderer.dart @@ -360,6 +360,71 @@ class SixelRenderer extends CliRendererBackend { _drawMenuText(label, 4, 3, textColor, scale: 1); } + @override + void drawGameplayOverlay(WolfEngine engine) { + if (!engine.isMapOverlayVisible) { + return; + } + + const int mapBgColor = 0; + const int floorColor = 8; + const int wallColor = 7; + const int doorColor = 14; + const int playerColor = 10; + const int facingColor = 12; + + for (int i = 0; i < _screen.length; i++) { + _screen[i] = mapBgColor; + } + + final int viewportPadding = math.max(3, math.min(width, height) ~/ 24); + final int availableWidth = math.max(1, width - (viewportPadding * 2)); + final int availableHeight = math.max(1, height - (viewportPadding * 2)); + final int tileSize = math.max( + 1, + math.min(availableWidth, availableHeight) ~/ 64, + ); + final int mapPixelWidth = tileSize * 64; + final int mapPixelHeight = tileSize * 64; + final int mapStartX = (width - mapPixelWidth) ~/ 2; + final int mapStartY = (height - mapPixelHeight) ~/ 2; + + for (int y = 0; y < 64; y++) { + for (int x = 0; x < 64; x++) { + final int tileId = engine.currentLevel[y][x]; + final int color = tileId == 0 + ? floorColor + : (tileId >= 90 ? doorColor : wallColor); + _fillMapRect( + mapStartX + (x * tileSize), + mapStartY + (y * tileSize), + tileSize, + tileSize, + color, + ); + } + } + + final int playerX = (mapStartX + (engine.player.x * tileSize)).floor(); + final int playerY = (mapStartY + (engine.player.y * tileSize)).floor(); + final int markerRadius = math.max(1, tileSize ~/ 2); + final int markerSize = (markerRadius * 2) + 1; + _fillMapRect( + playerX - markerRadius, + playerY - markerRadius, + markerSize, + markerSize, + playerColor, + ); + + final int facingLength = math.max(4, (tileSize * 3) ~/ 2); + final int facingEndX = + (playerX + (math.cos(engine.player.angle) * facingLength)).round(); + final int facingEndY = + (playerY + (math.sin(engine.player.angle) * facingLength)).round(); + _drawMapLine(playerX, playerY, facingEndX, facingEndY, facingColor); + } + @override /// Blits a VGA image into the Sixel index buffer HUD space (320x200). void blitHudVgaImage(VgaImage image, int startX320, int startY200) { @@ -1392,6 +1457,37 @@ class SixelRenderer extends CliRendererBackend { } } + void _fillMapRect(int startX, int startY, int w, int h, int colorIndex) { + for (int y = startY; y < startY + h; y++) { + for (int x = startX; x < startX + w; x++) { + _setMapPixel(x, y, colorIndex); + } + } + } + + void _drawMapLine(int x0, int y0, int x1, int y1, int colorIndex) { + final int dx = x1 - x0; + final int dy = y1 - y0; + final int steps = math.max(dx.abs(), dy.abs()); + if (steps == 0) { + _setMapPixel(x0, y0, colorIndex); + return; + } + for (int i = 0; i <= steps; i++) { + final double t = i / steps; + final int x = (x0 + (dx * t)).round(); + final int y = (y0 + (dy * t)).round(); + _setMapPixel(x, y, colorIndex); + } + } + + void _setMapPixel(int x, int y, int colorIndex) { + if (x < 0 || x >= width || y < 0 || y >= height) { + return; + } + _screen[(y * width) + x] = colorIndex; + } + /// Maps an RGB color to the nearest VGA palette index. int _rgbToPaletteIndex(int rgb) { return ColorPalette.findClosestPaletteIndex(rgb); diff --git a/packages/wolf_3d_dart/lib/src/rendering/software_renderer.dart b/packages/wolf_3d_dart/lib/src/rendering/software_renderer.dart index 6925aff..eb36a1f 100644 --- a/packages/wolf_3d_dart/lib/src/rendering/software_renderer.dart +++ b/packages/wolf_3d_dart/lib/src/rendering/software_renderer.dart @@ -140,6 +140,69 @@ class SoftwareRenderer extends RendererBackend { _drawMenuText(fpsLabel(engine), panelX + 3, panelY + 1, textColor); } + @override + void drawGameplayOverlay(WolfEngine engine) { + if (!engine.isMapOverlayVisible) { + return; + } + + final int mapBgColor = ColorPalette.vga32Bit[0]; + final int floorColor = ColorPalette.vga32Bit[8]; + final int wallColor = ColorPalette.vga32Bit[7]; + final int doorColor = ColorPalette.vga32Bit[14]; + final int playerColor = ColorPalette.vga32Bit[10]; + final int facingColor = ColorPalette.vga32Bit[12]; + + _fillMenuPanel(0, 0, width, height, mapBgColor); + + final int viewportPadding = math.max(6, math.min(width, height) ~/ 24); + final int availableWidth = math.max(1, width - (viewportPadding * 2)); + final int availableHeight = math.max(1, height - (viewportPadding * 2)); + final int tileSize = math.max( + 1, + math.min(availableWidth, availableHeight) ~/ 64, + ); + final int mapPixelWidth = tileSize * 64; + final int mapPixelHeight = tileSize * 64; + final int mapStartX = (width - mapPixelWidth) ~/ 2; + final int mapStartY = (height - mapPixelHeight) ~/ 2; + + for (int y = 0; y < 64; y++) { + for (int x = 0; x < 64; x++) { + final int tileId = engine.currentLevel[y][x]; + final int color = tileId == 0 + ? floorColor + : (tileId >= 90 ? doorColor : wallColor); + _fillMenuPanel( + mapStartX + (x * tileSize), + mapStartY + (y * tileSize), + tileSize, + tileSize, + color, + ); + } + } + + final int playerX = (mapStartX + (engine.player.x * tileSize)).floor(); + final int playerY = (mapStartY + (engine.player.y * tileSize)).floor(); + final int markerRadius = math.max(1, tileSize ~/ 2); + final int markerSize = (markerRadius * 2) + 1; + _fillMenuPanel( + playerX - markerRadius, + playerY - markerRadius, + markerSize, + markerSize, + playerColor, + ); + + final int facingLength = math.max(4, (tileSize * 3) ~/ 2); + final int facingEndX = + (playerX + (math.cos(engine.player.angle) * facingLength)).round(); + final int facingEndY = + (playerY + (math.sin(engine.player.angle) * facingLength)).round(); + _drawMapLine(playerX, playerY, facingEndX, facingEndY, facingColor); + } + @override /// Blits a VGA image into the software framebuffer HUD space (320x200). void blitHudVgaImage(VgaImage image, int startX320, int startY200) { @@ -925,6 +988,29 @@ class SoftwareRenderer extends RendererBackend { } } + void _drawMapLine(int x0, int y0, int x1, int y1, int color) { + final int dx = x1 - x0; + final int dy = y1 - y0; + final int steps = math.max(dx.abs(), dy.abs()); + if (steps == 0) { + _setMapPixel(x0, y0, color); + return; + } + for (int i = 0; i <= steps; i++) { + final double t = i / steps; + final int x = (x0 + (dx * t)).round(); + final int y = (y0 + (dy * t)).round(); + _setMapPixel(x, y, color); + } + } + + void _setMapPixel(int x, int y, int color) { + if (x < 0 || x >= width || y < 0 || y >= height) { + return; + } + _buffer.pixels[(y * width) + x] = color; + } + String _gameTitle(GameVersion version) { switch (version) { case GameVersion.shareware: diff --git a/packages/wolf_3d_dart/test/engine/map_overlay_toggle_test.dart b/packages/wolf_3d_dart/test/engine/map_overlay_toggle_test.dart new file mode 100644 index 0000000..b53bd30 --- /dev/null +++ b/packages/wolf_3d_dart/test/engine/map_overlay_toggle_test.dart @@ -0,0 +1,142 @@ +import 'dart:typed_data'; + +import 'package:test/test.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_input.dart'; + +void main() { + group('Map overlay toggle', () { + test('toggles in gameplay on map toggle pulse', () { + final input = _PulseInput(); + final engine = _buildEngine(input: input, difficulty: Difficulty.medium); + + engine.init(); + expect(engine.isMapOverlayVisible, isFalse); + + input.queueMapToggle = true; + engine.tick(const Duration(milliseconds: 16)); + expect(engine.isMapOverlayVisible, isTrue); + + input.queueMapToggle = true; + engine.tick(const Duration(milliseconds: 16)); + expect(engine.isMapOverlayVisible, isFalse); + }); + + test('does not toggle while menu is open', () { + final input = _PulseInput(); + final engine = _buildEngine(input: input, difficulty: null); + + engine.init(); + expect(engine.isMenuOpen, isTrue); + expect(engine.isMapOverlayVisible, isFalse); + + input.queueMapToggle = true; + engine.tick(const Duration(milliseconds: 16)); + expect(engine.isMapOverlayVisible, isFalse); + }); + + test('clears map overlay when pause menu opens', () { + final input = _PulseInput(); + final engine = _buildEngine(input: input, difficulty: Difficulty.medium); + + engine.init(); + + input.queueMapToggle = true; + engine.tick(const Duration(milliseconds: 16)); + expect(engine.isMapOverlayVisible, isTrue); + + input.queueBack = true; + engine.tick(const Duration(milliseconds: 16)); + expect(engine.isMenuOpen, isTrue); + expect(engine.isMapOverlayVisible, isFalse); + }); + }); +} + +class _PulseInput extends Wolf3dInput { + bool queueMapToggle = false; + bool queueBack = false; + + @override + void update() { + isMapToggle = queueMapToggle; + isBack = queueBack; + + queueMapToggle = false; + queueBack = false; + + isMovingForward = false; + isMovingBackward = false; + isTurningLeft = false; + isTurningRight = false; + isInteracting = false; + isFiring = false; + requestedWeapon = null; + menuTapX = null; + menuTapY = null; + } +} + +WolfEngine _buildEngine({ + required Wolf3dInput input, + required Difficulty? difficulty, +}) { + final wallGrid = _buildGrid(); + final objectGrid = _buildGrid(); + _fillBoundaries(wallGrid, 2); + objectGrid[2][2] = MapObject.playerEast; + + return WolfEngine( + data: WolfensteinData( + version: GameVersion.shareware, + dataVersion: DataVersion.unknown, + registry: RetailAssetRegistry(), + walls: [ + _solidSprite(1), + _solidSprite(1), + _solidSprite(2), + _solidSprite(2), + ], + sprites: List.generate(436, (_) => _solidSprite(255)), + sounds: const [], + adLibSounds: const [], + music: const [], + vgaImages: const [], + episodes: [ + Episode( + name: 'Episode 1', + levels: [ + WolfLevel( + name: 'Level 1', + wallGrid: wallGrid, + areaGrid: List.generate(64, (_) => List.filled(64, -1)), + objectGrid: objectGrid, + music: Music.level01, + ), + ], + ), + ], + ), + difficulty: difficulty, + startingEpisode: 0, + frameBuffer: FrameBuffer(64, 64), + input: input, + onGameWon: () {}, + ); +} + +SpriteMap _buildGrid() => List.generate(64, (_) => List.filled(64, 0)); + +void _fillBoundaries(SpriteMap grid, int wallId) { + for (int i = 0; i < 64; i++) { + grid[0][i] = wallId; + grid[63][i] = wallId; + grid[i][0] = wallId; + grid[i][63] = wallId; + } +} + +Sprite _solidSprite(int colorIndex) { + return Sprite(Uint8List.fromList(List.filled(64 * 64, colorIndex))); +} diff --git a/packages/wolf_3d_dart/test/rendering/map_overlay_renderer_test.dart b/packages/wolf_3d_dart/test/rendering/map_overlay_renderer_test.dart new file mode 100644 index 0000000..51281a4 --- /dev/null +++ b/packages/wolf_3d_dart/test/rendering/map_overlay_renderer_test.dart @@ -0,0 +1,114 @@ +import 'dart:typed_data'; + +import 'package:test/test.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_input.dart'; +import 'package:wolf_3d_dart/wolf_3d_renderer.dart'; + +void main() { + group('Map overlay rendering', () { + test('software renderer draws fullscreen map overlay when enabled', () { + final engine = _buildEngine(); + engine.init(); + + final renderer = SoftwareRenderer(); + final frameNormal = renderer.render(engine); + final List normalPixels = List.from(frameNormal.pixels); + + engine.isMapOverlayVisible = true; + final frameMap = renderer.render(engine); + final List mapPixels = List.from(frameMap.pixels); + + int changedPixels = 0; + for (int i = 0; i < mapPixels.length; i++) { + if (normalPixels[i] != mapPixels[i]) { + changedPixels++; + } + } + + expect(changedPixels, greaterThan(mapPixels.length ~/ 5)); + expect(mapPixels.contains(ColorPalette.vga32Bit[10]), isTrue); + }); + }); +} + +class _StaticInput extends Wolf3dInput { + @override + void update() { + isMovingForward = false; + isMovingBackward = false; + isTurningLeft = false; + isTurningRight = false; + isInteracting = false; + isBack = false; + isMapToggle = false; + isFiring = false; + requestedWeapon = null; + menuTapX = null; + menuTapY = null; + } +} + +WolfEngine _buildEngine() { + final wallGrid = _buildGrid(); + final objectGrid = _buildGrid(); + + _fillBoundaries(wallGrid, 2); + wallGrid[2][4] = 1; + wallGrid[2][5] = 90; + objectGrid[2][2] = MapObject.playerEast; + + return WolfEngine( + data: WolfensteinData( + version: GameVersion.shareware, + dataVersion: DataVersion.unknown, + registry: RetailAssetRegistry(), + walls: [ + _solidSprite(1), + _solidSprite(1), + _solidSprite(2), + _solidSprite(2), + ], + sprites: List.generate(436, (_) => _solidSprite(255)), + sounds: const [], + adLibSounds: const [], + music: const [], + vgaImages: const [], + episodes: [ + Episode( + name: 'Episode 1', + levels: [ + WolfLevel( + name: 'Level 1', + wallGrid: wallGrid, + areaGrid: List.generate(64, (_) => List.filled(64, -1)), + objectGrid: objectGrid, + music: Music.level01, + ), + ], + ), + ], + ), + difficulty: Difficulty.medium, + startingEpisode: 0, + frameBuffer: FrameBuffer(96, 96), + input: _StaticInput(), + onGameWon: () {}, + ); +} + +SpriteMap _buildGrid() => List.generate(64, (_) => List.filled(64, 0)); + +void _fillBoundaries(SpriteMap grid, int wallId) { + for (int i = 0; i < 64; i++) { + grid[0][i] = wallId; + grid[63][i] = wallId; + grid[i][0] = wallId; + grid[i][63] = wallId; + } +} + +Sprite _solidSprite(int colorIndex) { + return Sprite(Uint8List.fromList(List.filled(64 * 64, colorIndex))); +} diff --git a/packages/wolf_3d_flutter/lib/wolf_3d_input_flutter.dart b/packages/wolf_3d_flutter/lib/wolf_3d_input_flutter.dart index 343ffb0..c94209c 100644 --- a/packages/wolf_3d_flutter/lib/wolf_3d_input_flutter.dart +++ b/packages/wolf_3d_flutter/lib/wolf_3d_input_flutter.dart @@ -70,6 +70,7 @@ class Wolf3dFlutterInput extends Wolf3dInput { LogicalKeyboardKey.keyD, LogicalKeyboardKey.arrowRight, }, + WolfInputAction.mapToggle: {LogicalKeyboardKey.tab}, WolfInputAction.fire: { LogicalKeyboardKey.controlLeft, LogicalKeyboardKey.controlRight, @@ -209,6 +210,7 @@ class Wolf3dFlutterInput extends Wolf3dInput { kbBackward || (_isMouseLookEnabled && _mouseDeltaY > 1.5); isTurningLeft = kbLeft || (_isMouseLookEnabled && _mouseDeltaX < -1.5); isTurningRight = kbRight || (_isMouseLookEnabled && _mouseDeltaX > 1.5); + isMapToggle = _isNewlyPressed(WolfInputAction.mapToggle, newlyPressedKeys); // Deltas are one-frame impulses, so consume them immediately after use. _mouseDeltaX = 0.0;