From 827b8c779e69a9880a5a05bf64ebaa29a5e126c8 Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Mon, 23 Mar 2026 12:29:56 +0100 Subject: [PATCH] feat: Add mutant death and god mode face animations, update HUD rendering and player damage handling Signed-off-by: Hans Kokx --- .../lib/src/engine/player/player.dart | 17 ++++- .../lib/src/engine/wolf_3d_engine_base.dart | 5 +- .../registry/built_in/retail_hud_module.dart | 1 + .../built_in/shareware_hud_module.dart | 1 + .../built_in/spear_demo_hud_module.dart | 1 + .../lib/src/registry/keys/hud_key.dart | 2 + .../lib/src/rendering/renderer_backend.dart | 27 +++++++ .../rendering/hud_lives_rendering_test.dart | 75 ++++++++++++++++++- 8 files changed, 124 insertions(+), 5 deletions(-) diff --git a/packages/wolf_3d_dart/lib/src/engine/player/player.dart b/packages/wolf_3d_dart/lib/src/engine/player/player.dart index 986e628..1f4e84a 100644 --- a/packages/wolf_3d_dart/lib/src/engine/player/player.dart +++ b/packages/wolf_3d_dart/lib/src/engine/player/player.dart @@ -30,6 +30,10 @@ class Player { int _chaingunPickupFaceMsRemaining = 0; static const int _chaingunPickupFaceDurationMs = 900; + // Additional classic face states. + bool _mutantDeathFaceActive = false; + bool _godModeFaceEnabled = false; + // Classic face animation (UpdateFace/FACETICS random glance frames) math.Random _faceRng = math.Random(0); int _faceFrame = 0; @@ -69,6 +73,8 @@ class Player { Coordinate2D get position => Coordinate2D(x, y); bool get isChaingunPickupFaceActive => _chaingunPickupFaceMsRemaining > 0; + bool get isMutantDeathFaceActive => _mutantDeathFaceActive; + bool get isGodModeFaceEnabled => _godModeFaceEnabled; int get hudFaceFrame => _faceFrame; void setHudFaceAnimationSeed(int seed) { @@ -76,6 +82,12 @@ class Player { _faceFrame = 0; _faceCountTics = 0.0; _nextFaceChangeThreshold = _faceRng.nextInt(256); + _mutantDeathFaceActive = false; + _godModeFaceEnabled = false; + } + + void setGodModeFaceEnabled(bool enabled) { + _godModeFaceEnabled = enabled; } // --- General Update --- @@ -156,13 +168,15 @@ class Player { // --- Health & Damage --- - void takeDamage(int damage) { + void takeDamage(int damage, {EnemyType? attackerType}) { health = math.max(0, health - damage); // Spike the damage flash based on how much damage was taken // A 10 damage hit gives a 0.5 flash, a 20 damage hit maxes it out at 1.0 damageFlash = math.min(1.0, damageFlash + (damage * 0.05)); _chaingunPickupFaceMsRemaining = 0; + _mutantDeathFaceActive = + health <= 0 && attackerType == EnemyType.mutant; if (health <= 0) { log("[PLAYER] Died! Final Score: $score"); @@ -178,6 +192,7 @@ class Player { } health = newHealth; _chaingunPickupFaceMsRemaining = 0; + _mutantDeathFaceActive = false; } void triggerBonusFlash() { 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 eef8aa6..b511e50 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 @@ -1032,7 +1032,10 @@ class WolfEngine { tryOpenDoor: doorManager.tryOpenDoor, onDamagePlayer: (int damage) { final difficultyMode = difficulty ?? Difficulty.medium; - player.takeDamage(difficultyMode.scaleIncomingEnemyDamage(damage)); + player.takeDamage( + difficultyMode.scaleIncomingEnemyDamage(damage), + attackerType: entity.type, + ); }, onPlaySound: audio.playSoundEffect, ); diff --git a/packages/wolf_3d_dart/lib/src/registry/built_in/retail_hud_module.dart b/packages/wolf_3d_dart/lib/src/registry/built_in/retail_hud_module.dart index 2b33363..83840d1 100644 --- a/packages/wolf_3d_dart/lib/src/registry/built_in/retail_hud_module.dart +++ b/packages/wolf_3d_dart/lib/src/registry/built_in/retail_hud_module.dart @@ -45,6 +45,7 @@ class RetailHudModule extends HudModule { HudKey.faceNearDeath: 124, HudKey.faceDead: 127, HudKey.faceGotGatling: 128, + HudKey.faceMutantDeath: 129, // Weapon icons. HudKey.pistolIcon: 89, HudKey.machineGunIcon: 90, diff --git a/packages/wolf_3d_dart/lib/src/registry/built_in/shareware_hud_module.dart b/packages/wolf_3d_dart/lib/src/registry/built_in/shareware_hud_module.dart index 14c09c3..ab43164 100644 --- a/packages/wolf_3d_dart/lib/src/registry/built_in/shareware_hud_module.dart +++ b/packages/wolf_3d_dart/lib/src/registry/built_in/shareware_hud_module.dart @@ -45,6 +45,7 @@ class SharewareHudModule extends HudModule { HudKey.faceNearDeath: 124, HudKey.faceDead: 127, HudKey.faceGotGatling: 128, + HudKey.faceMutantDeath: 129, HudKey.pistolIcon: 89, HudKey.machineGunIcon: 90, HudKey.chainGunIcon: 91, diff --git a/packages/wolf_3d_dart/lib/src/registry/built_in/spear_demo_hud_module.dart b/packages/wolf_3d_dart/lib/src/registry/built_in/spear_demo_hud_module.dart index 3d4813d..bc5c0a3 100644 --- a/packages/wolf_3d_dart/lib/src/registry/built_in/spear_demo_hud_module.dart +++ b/packages/wolf_3d_dart/lib/src/registry/built_in/spear_demo_hud_module.dart @@ -38,6 +38,7 @@ class SpearDemoHudModule extends HudModule { HudKey.faceNearDeath: 112, HudKey.faceDead: 122, // BJOUCHPIC HudKey.faceGotGatling: 116, // GOTGATLINGPIC + HudKey.faceGodMode: 117, // GODMODEFACE1PIC HudKey.pistolIcon: 77, // GUNPIC HudKey.machineGunIcon: 78, // MACHINEGUNPIC HudKey.chainGunIcon: 79, // GATLINGGUNPIC diff --git a/packages/wolf_3d_dart/lib/src/registry/keys/hud_key.dart b/packages/wolf_3d_dart/lib/src/registry/keys/hud_key.dart index 3f9b487..bbfdf5f 100644 --- a/packages/wolf_3d_dart/lib/src/registry/keys/hud_key.dart +++ b/packages/wolf_3d_dart/lib/src/registry/keys/hud_key.dart @@ -25,6 +25,8 @@ enum HudKey { faceNearDeath('faceNearDeath'), // health 1-4 faceDead('faceDead'), // health <= 0 faceGotGatling('faceGotGatling'), + faceMutantDeath('faceMutantDeath'), + faceGodMode('faceGodMode'), // --- Weapon icons --- pistolIcon('pistolIcon'), 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 4dc37dc..0557204 100644 --- a/packages/wolf_3d_dart/lib/src/rendering/renderer_backend.dart +++ b/packages/wolf_3d_dart/lib/src/rendering/renderer_backend.dart @@ -228,6 +228,20 @@ abstract class RendererBackend faceIndex = engine.data.registry.hud.resolve(HudKey.faceGotGatling)?.vgaIndex ?? -1; + } else if (engine.player.isMutantDeathFaceActive) { + faceIndex = + engine.data.registry.hud.resolve(HudKey.faceMutantDeath)?.vgaIndex ?? + -1; + if (faceIndex < 0) { + faceIndex = + engine.data.registry.hud.resolve(HudKey.faceDead)?.vgaIndex ?? -1; + } + } else if (engine.player.isGodModeFaceEnabled) { + final int baseGodFace = + engine.data.registry.hud.resolve(HudKey.faceGodMode)?.vgaIndex ?? -1; + if (baseGodFace >= 0) { + faceIndex = baseGodFace + engine.player.hudFaceFrame; + } } else { final HudKey faceKey = engine.data.registry.hud.faceKeyForHealth( engine.player.health, @@ -241,6 +255,19 @@ abstract class RendererBackend } } + if (faceIndex < 0) { + final HudKey fallbackKey = engine.data.registry.hud.faceKeyForHealth( + engine.player.health, + ); + final int fallbackBase = + engine.data.registry.hud.resolve(fallbackKey)?.vgaIndex ?? -1; + if (fallbackBase >= 0) { + faceIndex = fallbackKey == HudKey.faceDead + ? fallbackBase + : fallbackBase + engine.player.hudFaceFrame; + } + } + if (faceIndex >= 0 && faceIndex < vgaImages.length) { blitHudVgaImage(vgaImages[faceIndex], 136, 164); } 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 index ae1f210..2ca641b 100644 --- a/packages/wolf_3d_dart/test/rendering/hud_lives_rendering_test.dart +++ b/packages/wolf_3d_dart/test/rendering/hud_lives_rendering_test.dart @@ -162,6 +162,72 @@ void main() { ); }, ); + + test('fatal mutant hit shows mutant death face', () { + final engine = _buildEngine(); + engine.init(); + engine.player.health = 10; + engine.player.takeDamage(20, attackerType: EnemyType.mutant); + + final renderer = _HudProbeRenderer(vgaImages: engine.data.vgaImages); + renderer.drawHudForTest(engine); + + final expectedMutantDeathFaceIndex = engine.data.registry.hud + .resolve(HudKey.faceMutantDeath) + ?.vgaIndex; + expect(expectedMutantDeathFaceIndex, isNotNull); + + final faceCall = renderer.drawCalls.firstWhere( + (call) => call.startY == 164 && call.startX == 136, + orElse: () => throw StateError('Face slot was not rendered.'), + ); + expect(faceCall.imageIndex, expectedMutantDeathFaceIndex); + }); + + test('god mode face renders in spear demo HUD mapping', () { + final engine = _buildEngine( + version: GameVersion.spearOfDestinyDemo, + registry: SpearDemoAssetRegistry(), + ); + engine.init(); + engine.player.health = 90; + engine.player.setGodModeFaceEnabled(true); + + final renderer = _HudProbeRenderer(vgaImages: engine.data.vgaImages); + renderer.drawHudForTest(engine); + + final expectedGodModeFaceIndex = engine.data.registry.hud + .resolve(HudKey.faceGodMode) + ?.vgaIndex; + expect(expectedGodModeFaceIndex, isNotNull); + + final faceCall = renderer.drawCalls.firstWhere( + (call) => call.startY == 164 && call.startX == 136, + orElse: () => throw StateError('Face slot was not rendered.'), + ); + expect(faceCall.imageIndex, expectedGodModeFaceIndex); + }); + + test('god mode face falls back to health face when unmapped', () { + final engine = _buildEngine(); + engine.init(); + engine.player.health = 90; + engine.player.setGodModeFaceEnabled(true); + + final renderer = _HudProbeRenderer(vgaImages: engine.data.vgaImages); + renderer.drawHudForTest(engine); + + final expectedHealthFaceIndex = engine.data.registry.hud + .faceForHealth(engine.player.health) + ?.vgaIndex; + expect(expectedHealthFaceIndex, isNotNull); + + final faceCall = renderer.drawCalls.firstWhere( + (call) => call.startY == 164 && call.startX == 136, + orElse: () => throw StateError('Face slot was not rendered.'), + ); + expect(faceCall.imageIndex, expectedHealthFaceIndex); + }); } class _HudProbeRenderer extends RendererBackend { @@ -233,7 +299,10 @@ class _HudDrawCall { }); } -WolfEngine _buildEngine() { +WolfEngine _buildEngine({ + GameVersion version = GameVersion.retail, + AssetRegistry? registry, +}) { final wallGrid = _buildGrid(); final objectGrid = _buildGrid(); @@ -242,9 +311,9 @@ WolfEngine _buildEngine() { return WolfEngine( data: WolfensteinData( - version: GameVersion.retail, + version: version, dataVersion: DataVersion.unknown, - registry: RetailAssetRegistry(), + registry: registry ?? RetailAssetRegistry(), walls: [ _solidSprite(1), _solidSprite(1),