feat: Add mutant death and god mode face animations, update HUD rendering and player damage handling
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -228,6 +228,20 @@ abstract class RendererBackend<T>
|
||||
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<T>
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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<int> {
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user