feat: Implement chaingun pickup face animation and update HUD rendering logic

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-23 12:23:10 +01:00
parent 8ed460b03e
commit 400ce4f680
9 changed files with 193 additions and 6 deletions
@@ -4,6 +4,7 @@ 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_entities.dart';
import 'package:wolf_3d_dart/wolf_3d_input.dart';
void main() {
@@ -78,6 +79,89 @@ void main() {
expect(faceCall.imageIndex, expectedFaceIndex);
});
test('chaingun pickup shows GOTGATLING face then returns to health face', () {
final engine = _buildEngine();
engine.init();
engine.player.health = 100;
const int chainGunSfxId = 38;
const int chainGunDurationMs = 350;
engine.data.sounds[chainGunSfxId] = PcmSound(
Uint8List(chainGunDurationMs * 7),
);
final chainGun = WeaponCollectible(
x: engine.player.x,
y: engine.player.y,
mapId: MapObject.chainGun,
);
engine.entities.add(chainGun);
engine.tick(const Duration(milliseconds: 16));
final renderer = _HudProbeRenderer(vgaImages: engine.data.vgaImages);
renderer.drawHudForTest(engine);
final expectedGotGatlingIndex = engine.data.registry.hud
.resolve(HudKey.faceGotGatling)
?.vgaIndex;
expect(expectedGotGatlingIndex, isNotNull);
final gotGatlingFaceCall = renderer.drawCalls.firstWhere(
(call) => call.startY == 164 && call.startX == 136,
orElse: () => throw StateError('Face slot was not rendered.'),
);
expect(gotGatlingFaceCall.imageIndex, expectedGotGatlingIndex);
engine.player.tick(Duration(milliseconds: chainGunDurationMs + 50));
final rendererAfterTimeout = _HudProbeRenderer(
vgaImages: engine.data.vgaImages,
);
rendererAfterTimeout.drawHudForTest(engine);
final expectedHealthFaceIndex = engine.data.registry.hud
.faceForHealth(engine.player.health)
?.vgaIndex;
expect(expectedHealthFaceIndex, isNotNull);
final healthFaceCall = rendererAfterTimeout.drawCalls.firstWhere(
(call) => call.startY == 164 && call.startX == 136,
orElse: () =>
throw StateError('Face slot was not rendered after timeout.'),
);
expect(healthFaceCall.imageIndex, expectedHealthFaceIndex);
});
test(
'standard VGA HUD face animates between health-band frames over time',
() {
final engine = _buildEngine();
engine.init();
engine.player.health = 100;
final renderer = _HudProbeRenderer(vgaImages: engine.data.vgaImages);
renderer.drawHudForTest(engine);
final initialFaceCall = renderer.drawCalls.firstWhere(
(call) => call.startY == 164 && call.startX == 136,
orElse: () => throw StateError('Initial face slot was not rendered.'),
);
engine.player.tick(const Duration(milliseconds: 4500));
final rendererAfter = _HudProbeRenderer(vgaImages: engine.data.vgaImages);
rendererAfter.drawHudForTest(engine);
final animatedFaceCall = rendererAfter.drawCalls.firstWhere(
(call) => call.startY == 164 && call.startX == 136,
orElse: () => throw StateError('Animated face slot was not rendered.'),
);
expect(
animatedFaceCall.imageIndex,
isNot(equals(initialFaceCall.imageIndex)),
);
},
);
}
class _HudProbeRenderer extends RendererBackend<int> {
@@ -168,7 +252,7 @@ WolfEngine _buildEngine() {
_solidSprite(2),
],
sprites: List.generate(436, (_) => _solidSprite(255)),
sounds: const [],
sounds: List.generate(200, (_) => PcmSound(Uint8List(1))),
adLibSounds: const [],
music: const [],
vgaImages: List.generate(220, (_) => _vgaStub()),