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 7691041..c3e7816 100644 --- a/packages/wolf_3d_dart/lib/src/rendering/ascii_renderer.dart +++ b/packages/wolf_3d_dart/lib/src/rendering/ascii_renderer.dart @@ -1951,7 +1951,13 @@ class AsciiRenderer extends CliRendererBackend { drawBorderedPanel(cursorX, baseY + 1, livesW, 4); _writeString(cursorX + 2, baseY + 2, "LIV", white, vgaPanelDark); - _writeString(cursorX + 3, baseY + 3, "3", white, vgaPanelDark); + _writeString( + cursorX + 3, + baseY + 3, + engine.player.lives.toString(), + white, + vgaPanelDark, + ); cursorX += livesW + gap; drawBorderedPanel(cursorX, baseY, faceW, 5); 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 591da10..58792fa 100644 --- a/packages/wolf_3d_dart/lib/src/rendering/renderer_backend.dart +++ b/packages/wolf_3d_dart/lib/src/rendering/renderer_backend.dart @@ -179,7 +179,7 @@ abstract class RendererBackend _drawHudNumber(engine, vgaImages, 1, 32, 176); _drawHudNumber(engine, vgaImages, engine.player.score, 96, 176); - _drawHudNumber(engine, vgaImages, 3, 120, 176); + _drawHudNumber(engine, vgaImages, engine.player.lives, 120, 176); _drawHudNumber(engine, vgaImages, engine.player.health, 192, 176); _drawHudNumber(engine, vgaImages, engine.player.ammo, 232, 176); _drawHudFace(engine, vgaImages); diff --git a/packages/wolf_3d_dart/test/engine/level_state_and_pause_menu_test.dart b/packages/wolf_3d_dart/test/engine/level_state_and_pause_menu_test.dart index 0b80111..9dd9a0b 100644 --- a/packages/wolf_3d_dart/test/engine/level_state_and_pause_menu_test.dart +++ b/packages/wolf_3d_dart/test/engine/level_state_and_pause_menu_test.dart @@ -38,12 +38,75 @@ void main() { expect(engine.player.lives, 5); expect(engine.player.hasMachineGun, isTrue); expect(engine.player.hasChainGun, isTrue); + expect(engine.player.weapons[WeaponType.machineGun], isNotNull); + expect(engine.player.weapons[WeaponType.chainGun], isNotNull); expect(engine.player.currentWeapon.type, WeaponType.chainGun); expect(engine.player.hasGoldKey, isFalse); expect(engine.player.hasSilverKey, isFalse); expect(engine.player.x, closeTo(4.5, 0.001)); expect(engine.player.y, closeTo(4.5, 0.001)); }); + + test('secret-exit transitions clear keys but preserve other player state', () { + final input = _TestInput(); + final engine = WolfEngine( + data: _buildSecretExitTestData(), + difficulty: Difficulty.hard, + startingEpisode: 0, + frameBuffer: FrameBuffer(64, 64), + input: input, + engineAudio: _SilentAudio(), + onGameWon: () {}, + ); + + engine.init(); + engine.player + ..health = 61 + ..ammo = 42 + ..score = 777 + ..lives = 6 + ..hasMachineGun = true + ..hasChainGun = true + ..hasGoldKey = true + ..hasSilverKey = true; + engine.player.weapons[WeaponType.machineGun] = MachineGun(); + engine.player.weapons[WeaponType.chainGun] = ChainGun(); + engine.player.currentWeapon = engine.player.weapons[WeaponType.chainGun]!; + + input.isInteracting = true; + engine.tick(const Duration(milliseconds: 16)); + input.isInteracting = false; + + expect(engine.activeLevel.name, 'Level 10'); + expect(engine.player.health, 61); + expect(engine.player.ammo, 42); + expect(engine.player.score, 777); + expect(engine.player.lives, 6); + expect(engine.player.hasMachineGun, isTrue); + expect(engine.player.hasChainGun, isTrue); + expect(engine.player.currentWeapon.type, WeaponType.chainGun); + expect(engine.player.hasGoldKey, isFalse); + expect(engine.player.hasSilverKey, isFalse); + + engine.player + ..hasGoldKey = true + ..hasSilverKey = true; + + input.isInteracting = true; + engine.tick(const Duration(milliseconds: 16)); + input.isInteracting = false; + + expect(engine.activeLevel.name, 'Level 2'); + expect(engine.player.health, 61); + expect(engine.player.ammo, 42); + expect(engine.player.score, 777); + expect(engine.player.lives, 6); + expect(engine.player.hasMachineGun, isTrue); + expect(engine.player.hasChainGun, isTrue); + expect(engine.player.currentWeapon.type, WeaponType.chainGun); + expect(engine.player.hasGoldKey, isFalse); + expect(engine.player.hasSilverKey, isFalse); + }); }); group('Pause and main menu', () { @@ -434,6 +497,49 @@ WolfensteinData _buildTestData({required GameVersion gameVersion}) { ); } +WolfensteinData _buildSecretExitTestData() { + final List levels = []; + + for (int i = 0; i < 10; i++) { + final walls = _buildGrid(); + final objects = _buildGrid(); + _fillBoundaries(walls, 2); + + objects[2][2] = MapObject.playerEast; + + if (i == 0) { + walls[2][3] = MapObject.secretElevatorSwitch; + } else if (i == 9) { + walls[2][3] = MapObject.normalElevatorSwitch; + } + + levels.add( + WolfLevel( + name: 'Level ${i + 1}', + wallGrid: walls, + areaGrid: List.generate(64, (_) => List.filled(64, -1)), + objectGrid: objects, + music: Music.level01, + ), + ); + } + + return WolfensteinData( + version: GameVersion.retail, + dataVersion: DataVersion.unknown, + registry: RetailAssetRegistry(), + walls: [_solidSprite(1), _solidSprite(1), _solidSprite(2), _solidSprite(2)], + sprites: List.generate(436, (_) => _solidSprite(255)), + sounds: List.generate(200, (_) => PcmSound(Uint8List(1))), + adLibSounds: const [], + music: const [], + vgaImages: const [], + episodes: [ + Episode(name: 'Episode 1', levels: levels), + ], + ); +} + class _TestInput extends Wolf3dInput { @override void update() {} diff --git a/packages/wolf_3d_dart/test/rendering/hud_lives_rendering_test.dart b/packages/wolf_3d_dart/test/rendering/hud_lives_rendering_test.dart new file mode 100644 index 0000000..3ee7dcc --- /dev/null +++ b/packages/wolf_3d_dart/test/rendering/hud_lives_rendering_test.dart @@ -0,0 +1,173 @@ +import 'dart:typed_data'; + +import 'package:test/test.dart'; +import 'package:wolf_3d_dart/src/rendering/renderer_backend.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() { + test('standard VGA HUD renders current player lives value', () { + final engine = _buildEngine(); + engine.init(); + engine.player.lives = 7; + + final renderer = _HudProbeRenderer(vgaImages: engine.data.vgaImages); + renderer.drawHudForTest(engine); + + final expectedDigitIndex = engine.data.registry.hud + .resolve(HudKey.digit7) + ?.vgaIndex; + expect(expectedDigitIndex, isNotNull); + + final livesDigitCall = renderer.drawCalls.firstWhere( + (call) => call.startY == 176 && call.startX == 112, + orElse: () => throw StateError('Lives digit was not rendered.'), + ); + + expect(livesDigitCall.imageIndex, expectedDigitIndex); + }); +} + +class _HudProbeRenderer extends RendererBackend { + final List vgaImages; + final List<_HudDrawCall> drawCalls = <_HudDrawCall>[]; + + _HudProbeRenderer({required this.vgaImages}); + + void drawHudForTest(WolfEngine engine) { + drawStandardVgaHud(engine); + } + + @override + void blitHudVgaImage(VgaImage image, int startX320, int startY200) { + drawCalls.add( + _HudDrawCall( + imageIndex: vgaImages.indexOf(image), + startX: startX320, + startY: startY200, + ), + ); + } + + @override + int finalizeFrame() => 0; + + @override + void drawHud(WolfEngine engine) {} + + @override + void drawSpriteStripe( + int stripeX, + int drawStartY, + int drawEndY, + int spriteHeight, + Sprite texture, + int texX, + double transformY, + ) {} + + @override + void drawWallColumn( + int x, + int drawStart, + int drawEnd, + int columnHeight, + Sprite texture, + int texX, + double perpWallDist, + int side, + ) {} + + @override + void drawWeapon(WolfEngine engine) {} + + @override + void prepareFrame(WolfEngine engine) {} +} + +class _HudDrawCall { + final int imageIndex; + final int startX; + final int startY; + + const _HudDrawCall({ + required this.imageIndex, + required this.startX, + required this.startY, + }); +} + +WolfEngine _buildEngine() { + final wallGrid = _buildGrid(); + final objectGrid = _buildGrid(); + + _fillBoundaries(wallGrid, 2); + objectGrid[2][2] = MapObject.playerEast; + + return WolfEngine( + data: WolfensteinData( + version: GameVersion.retail, + 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: List.generate(220, (_) => _vgaStub()), + 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: () {}, + ); +} + +class _StaticInput extends Wolf3dInput { + @override + void update() {} +} + +VgaImage _vgaStub() { + return VgaImage( + width: 4, + height: 1, + pixels: Uint8List.fromList([0, 0, 0, 0]), + ); +} + +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))); +}