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;
|
int _chaingunPickupFaceMsRemaining = 0;
|
||||||
static const int _chaingunPickupFaceDurationMs = 900;
|
static const int _chaingunPickupFaceDurationMs = 900;
|
||||||
|
|
||||||
|
// Additional classic face states.
|
||||||
|
bool _mutantDeathFaceActive = false;
|
||||||
|
bool _godModeFaceEnabled = false;
|
||||||
|
|
||||||
// Classic face animation (UpdateFace/FACETICS random glance frames)
|
// Classic face animation (UpdateFace/FACETICS random glance frames)
|
||||||
math.Random _faceRng = math.Random(0);
|
math.Random _faceRng = math.Random(0);
|
||||||
int _faceFrame = 0;
|
int _faceFrame = 0;
|
||||||
@@ -69,6 +73,8 @@ class Player {
|
|||||||
Coordinate2D get position => Coordinate2D(x, y);
|
Coordinate2D get position => Coordinate2D(x, y);
|
||||||
|
|
||||||
bool get isChaingunPickupFaceActive => _chaingunPickupFaceMsRemaining > 0;
|
bool get isChaingunPickupFaceActive => _chaingunPickupFaceMsRemaining > 0;
|
||||||
|
bool get isMutantDeathFaceActive => _mutantDeathFaceActive;
|
||||||
|
bool get isGodModeFaceEnabled => _godModeFaceEnabled;
|
||||||
int get hudFaceFrame => _faceFrame;
|
int get hudFaceFrame => _faceFrame;
|
||||||
|
|
||||||
void setHudFaceAnimationSeed(int seed) {
|
void setHudFaceAnimationSeed(int seed) {
|
||||||
@@ -76,6 +82,12 @@ class Player {
|
|||||||
_faceFrame = 0;
|
_faceFrame = 0;
|
||||||
_faceCountTics = 0.0;
|
_faceCountTics = 0.0;
|
||||||
_nextFaceChangeThreshold = _faceRng.nextInt(256);
|
_nextFaceChangeThreshold = _faceRng.nextInt(256);
|
||||||
|
_mutantDeathFaceActive = false;
|
||||||
|
_godModeFaceEnabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setGodModeFaceEnabled(bool enabled) {
|
||||||
|
_godModeFaceEnabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- General Update ---
|
// --- General Update ---
|
||||||
@@ -156,13 +168,15 @@ class Player {
|
|||||||
|
|
||||||
// --- Health & Damage ---
|
// --- Health & Damage ---
|
||||||
|
|
||||||
void takeDamage(int damage) {
|
void takeDamage(int damage, {EnemyType? attackerType}) {
|
||||||
health = math.max(0, health - damage);
|
health = math.max(0, health - damage);
|
||||||
|
|
||||||
// Spike the damage flash based on how much damage was taken
|
// 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
|
// 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));
|
damageFlash = math.min(1.0, damageFlash + (damage * 0.05));
|
||||||
_chaingunPickupFaceMsRemaining = 0;
|
_chaingunPickupFaceMsRemaining = 0;
|
||||||
|
_mutantDeathFaceActive =
|
||||||
|
health <= 0 && attackerType == EnemyType.mutant;
|
||||||
|
|
||||||
if (health <= 0) {
|
if (health <= 0) {
|
||||||
log("[PLAYER] Died! Final Score: $score");
|
log("[PLAYER] Died! Final Score: $score");
|
||||||
@@ -178,6 +192,7 @@ class Player {
|
|||||||
}
|
}
|
||||||
health = newHealth;
|
health = newHealth;
|
||||||
_chaingunPickupFaceMsRemaining = 0;
|
_chaingunPickupFaceMsRemaining = 0;
|
||||||
|
_mutantDeathFaceActive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void triggerBonusFlash() {
|
void triggerBonusFlash() {
|
||||||
|
|||||||
@@ -1032,7 +1032,10 @@ class WolfEngine {
|
|||||||
tryOpenDoor: doorManager.tryOpenDoor,
|
tryOpenDoor: doorManager.tryOpenDoor,
|
||||||
onDamagePlayer: (int damage) {
|
onDamagePlayer: (int damage) {
|
||||||
final difficultyMode = difficulty ?? Difficulty.medium;
|
final difficultyMode = difficulty ?? Difficulty.medium;
|
||||||
player.takeDamage(difficultyMode.scaleIncomingEnemyDamage(damage));
|
player.takeDamage(
|
||||||
|
difficultyMode.scaleIncomingEnemyDamage(damage),
|
||||||
|
attackerType: entity.type,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onPlaySound: audio.playSoundEffect,
|
onPlaySound: audio.playSoundEffect,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class RetailHudModule extends HudModule {
|
|||||||
HudKey.faceNearDeath: 124,
|
HudKey.faceNearDeath: 124,
|
||||||
HudKey.faceDead: 127,
|
HudKey.faceDead: 127,
|
||||||
HudKey.faceGotGatling: 128,
|
HudKey.faceGotGatling: 128,
|
||||||
|
HudKey.faceMutantDeath: 129,
|
||||||
// Weapon icons.
|
// Weapon icons.
|
||||||
HudKey.pistolIcon: 89,
|
HudKey.pistolIcon: 89,
|
||||||
HudKey.machineGunIcon: 90,
|
HudKey.machineGunIcon: 90,
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class SharewareHudModule extends HudModule {
|
|||||||
HudKey.faceNearDeath: 124,
|
HudKey.faceNearDeath: 124,
|
||||||
HudKey.faceDead: 127,
|
HudKey.faceDead: 127,
|
||||||
HudKey.faceGotGatling: 128,
|
HudKey.faceGotGatling: 128,
|
||||||
|
HudKey.faceMutantDeath: 129,
|
||||||
HudKey.pistolIcon: 89,
|
HudKey.pistolIcon: 89,
|
||||||
HudKey.machineGunIcon: 90,
|
HudKey.machineGunIcon: 90,
|
||||||
HudKey.chainGunIcon: 91,
|
HudKey.chainGunIcon: 91,
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class SpearDemoHudModule extends HudModule {
|
|||||||
HudKey.faceNearDeath: 112,
|
HudKey.faceNearDeath: 112,
|
||||||
HudKey.faceDead: 122, // BJOUCHPIC
|
HudKey.faceDead: 122, // BJOUCHPIC
|
||||||
HudKey.faceGotGatling: 116, // GOTGATLINGPIC
|
HudKey.faceGotGatling: 116, // GOTGATLINGPIC
|
||||||
|
HudKey.faceGodMode: 117, // GODMODEFACE1PIC
|
||||||
HudKey.pistolIcon: 77, // GUNPIC
|
HudKey.pistolIcon: 77, // GUNPIC
|
||||||
HudKey.machineGunIcon: 78, // MACHINEGUNPIC
|
HudKey.machineGunIcon: 78, // MACHINEGUNPIC
|
||||||
HudKey.chainGunIcon: 79, // GATLINGGUNPIC
|
HudKey.chainGunIcon: 79, // GATLINGGUNPIC
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ enum HudKey {
|
|||||||
faceNearDeath('faceNearDeath'), // health 1-4
|
faceNearDeath('faceNearDeath'), // health 1-4
|
||||||
faceDead('faceDead'), // health <= 0
|
faceDead('faceDead'), // health <= 0
|
||||||
faceGotGatling('faceGotGatling'),
|
faceGotGatling('faceGotGatling'),
|
||||||
|
faceMutantDeath('faceMutantDeath'),
|
||||||
|
faceGodMode('faceGodMode'),
|
||||||
|
|
||||||
// --- Weapon icons ---
|
// --- Weapon icons ---
|
||||||
pistolIcon('pistolIcon'),
|
pistolIcon('pistolIcon'),
|
||||||
|
|||||||
@@ -228,6 +228,20 @@ abstract class RendererBackend<T>
|
|||||||
faceIndex =
|
faceIndex =
|
||||||
engine.data.registry.hud.resolve(HudKey.faceGotGatling)?.vgaIndex ??
|
engine.data.registry.hud.resolve(HudKey.faceGotGatling)?.vgaIndex ??
|
||||||
-1;
|
-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 {
|
} else {
|
||||||
final HudKey faceKey = engine.data.registry.hud.faceKeyForHealth(
|
final HudKey faceKey = engine.data.registry.hud.faceKeyForHealth(
|
||||||
engine.player.health,
|
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) {
|
if (faceIndex >= 0 && faceIndex < vgaImages.length) {
|
||||||
blitHudVgaImage(vgaImages[faceIndex], 136, 164);
|
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> {
|
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 wallGrid = _buildGrid();
|
||||||
final objectGrid = _buildGrid();
|
final objectGrid = _buildGrid();
|
||||||
|
|
||||||
@@ -242,9 +311,9 @@ WolfEngine _buildEngine() {
|
|||||||
|
|
||||||
return WolfEngine(
|
return WolfEngine(
|
||||||
data: WolfensteinData(
|
data: WolfensteinData(
|
||||||
version: GameVersion.retail,
|
version: version,
|
||||||
dataVersion: DataVersion.unknown,
|
dataVersion: DataVersion.unknown,
|
||||||
registry: RetailAssetRegistry(),
|
registry: registry ?? RetailAssetRegistry(),
|
||||||
walls: [
|
walls: [
|
||||||
_solidSprite(1),
|
_solidSprite(1),
|
||||||
_solidSprite(1),
|
_solidSprite(1),
|
||||||
|
|||||||
Reference in New Issue
Block a user