feat: Implement map overlay toggle functionality and rendering across input and rendering systems

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-23 11:08:43 +01:00
parent d63b316f1b
commit 1165e0bc44
11 changed files with 600 additions and 0 deletions
@@ -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)));
}
@@ -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<int> normalPixels = List<int>.from(frameNormal.pixels);
engine.isMapOverlayVisible = true;
final frameMap = renderer.render(engine);
final List<int> mapPixels = List<int>.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)));
}