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 1f4e84a..d99b23e 100644 --- a/packages/wolf_3d_dart/lib/src/engine/player/player.dart +++ b/packages/wolf_3d_dart/lib/src/engine/player/player.dart @@ -61,8 +61,11 @@ class Player { // 0.0 is resting, 500.0 is fully off-screen double weaponAnimOffset = 0.0; - // How fast the weapon drops/raises per tick - final double switchSpeed = 30.0; + static const double _weaponSwitchTravel = 500.0; + static const double _weaponSwitchPhaseTics = 6.0; + static const double _ticRate = 70.0; + static const double _weaponSwitchUnitsPerSecond = + (_weaponSwitchTravel * _ticRate) / _weaponSwitchPhaseTics; Player({required this.x, required this.y, required this.angle}) { currentWeapon = weapons[WeaponType.pistol]!; @@ -121,12 +124,18 @@ class Player { } } - updateWeaponSwitch(); + updateWeaponSwitch(elapsed); } // --- Weapon Switching & Animation Logic --- - void updateWeaponSwitch() { + void updateWeaponSwitch(Duration elapsed) { + final double delta = + _weaponSwitchUnitsPerSecond * (elapsed.inMicroseconds / 1000000.0); + if (delta <= 0.0) { + return; + } + if (switchState == WeaponSwitchState.lowering) { // If the map doesn't contain the pending weapon, stop immediately if (weapons[pendingWeaponType] == null) { @@ -134,9 +143,9 @@ class Player { return; } - weaponAnimOffset += switchSpeed; - if (weaponAnimOffset >= 500.0) { - weaponAnimOffset = 500.0; + weaponAnimOffset += delta; + if (weaponAnimOffset >= _weaponSwitchTravel) { + weaponAnimOffset = _weaponSwitchTravel; // We already know it's not null now, but we can keep the // fallback to pistol just to be extra safe. @@ -145,7 +154,7 @@ class Player { switchState = WeaponSwitchState.raising; } } else if (switchState == WeaponSwitchState.raising) { - weaponAnimOffset -= switchSpeed; + weaponAnimOffset -= delta; if (weaponAnimOffset <= 0) { weaponAnimOffset = 0.0; switchState = WeaponSwitchState.idle; @@ -175,8 +184,7 @@ class Player { // 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; + _mutantDeathFaceActive = health <= 0 && attackerType == EnemyType.mutant; if (health <= 0) { log("[PLAYER] Died! Final Score: $score"); 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 483e1d4..73e785a 100644 --- a/packages/wolf_3d_dart/lib/src/rendering/ascii_renderer.dart +++ b/packages/wolf_3d_dart/lib/src/rendering/ascii_renderer.dart @@ -2094,7 +2094,7 @@ class AsciiRenderer extends CliRendererBackend { @override dynamic finalizeFrame() { - if (engine.difficulty != null) { + if (engine.difficulty != null && !engine.isMenuOpen) { if (engine.player.damageFlash > 0.0) { if (_usesTerminalLayout) { _applyDamageFlashToScene(); diff --git a/packages/wolf_3d_dart/lib/src/rendering/sixel_renderer.dart b/packages/wolf_3d_dart/lib/src/rendering/sixel_renderer.dart index 64bf4c2..21783be 100644 --- a/packages/wolf_3d_dart/lib/src/rendering/sixel_renderer.dart +++ b/packages/wolf_3d_dart/lib/src/rendering/sixel_renderer.dart @@ -1336,7 +1336,7 @@ class SixelRenderer extends CliRendererBackend { sb.write('\x1bPq'); sb.write('"1;1;$_outputWidth;$_outputHeight'); - final bool gameplayActive = engine.difficulty != null; + final bool gameplayActive = engine.difficulty != null && !engine.isMenuOpen; final double damageIntensity = gameplayActive ? engine.player.damageFlash : 0.0; diff --git a/packages/wolf_3d_dart/lib/src/rendering/software_renderer.dart b/packages/wolf_3d_dart/lib/src/rendering/software_renderer.dart index aee2d4e..62a5614 100644 --- a/packages/wolf_3d_dart/lib/src/rendering/software_renderer.dart +++ b/packages/wolf_3d_dart/lib/src/rendering/software_renderer.dart @@ -1401,7 +1401,7 @@ class SoftwareRenderer extends RendererBackend { @override FrameBuffer finalizeFrame() { - if (engine.difficulty != null) { + if (engine.difficulty != null && !engine.isMenuOpen) { if (engine.player.damageFlash > 0) { _applyDamageFlash(); } else if (engine.player.bonusFlash > 0) { diff --git a/packages/wolf_3d_dart/test/entities/canonical_numeric_parity_test.dart b/packages/wolf_3d_dart/test/entities/canonical_numeric_parity_test.dart index 6f2f06c..3c33351 100644 --- a/packages/wolf_3d_dart/test/entities/canonical_numeric_parity_test.dart +++ b/packages/wolf_3d_dart/test/entities/canonical_numeric_parity_test.dart @@ -115,5 +115,22 @@ void main() { expect(player.bonusFlash, lessThan(1.0)); expect(player.bonusFlash, greaterThan(0.0)); }); + + test('weapon switch animates at canonical tic pacing', () { + final player = Player(x: 1.5, y: 1.5, angle: 0); + player.weapons[WeaponType.machineGun] = MachineGun(); + + player.requestWeaponSwitch(WeaponType.machineGun); + expect(player.switchState, WeaponSwitchState.lowering); + + player.tick(const Duration(milliseconds: 86)); + expect(player.switchState, WeaponSwitchState.raising); + expect(player.weaponAnimOffset, closeTo(500.0, 0.001)); + expect(player.currentWeapon.type, WeaponType.machineGun); + + player.tick(const Duration(milliseconds: 86)); + expect(player.switchState, WeaponSwitchState.idle); + expect(player.weaponAnimOffset, closeTo(0.0, 0.001)); + }); }); } diff --git a/packages/wolf_3d_dart/test/rendering/map_overlay_renderer_test.dart b/packages/wolf_3d_dart/test/rendering/map_overlay_renderer_test.dart index 2dec58e..4993055 100644 --- a/packages/wolf_3d_dart/test/rendering/map_overlay_renderer_test.dart +++ b/packages/wolf_3d_dart/test/rendering/map_overlay_renderer_test.dart @@ -49,6 +49,27 @@ void main() { expect(mapPixels.contains(ColorPalette.vga32Bit[2]), isTrue); expect(mapPixels[hudProbeIndex], equals(normalPixels[hudProbeIndex])); }); + + test('software renderer does not apply bonus flash while menu is open', () { + final input = _MutableInput(); + final engine = _buildEngine(input: input); + engine.init(); + input.isBack = true; + engine.tick(const Duration(milliseconds: 16)); + input.isBack = false; + + expect(engine.isMenuOpen, isTrue); + + final renderer = SoftwareRenderer(); + + engine.player.bonusFlash = 0.0; + final List menuBaseline = List.from(renderer.render(engine).pixels); + + engine.player.bonusFlash = 1.0; + final List menuWithFlash = List.from(renderer.render(engine).pixels); + + expect(menuWithFlash, equals(menuBaseline)); + }); }); } @@ -69,7 +90,12 @@ class _StaticInput extends Wolf3dInput { } } -WolfEngine _buildEngine() { +class _MutableInput extends Wolf3dInput { + @override + void update() {} +} + +WolfEngine _buildEngine({Wolf3dInput? input}) { final wallGrid = _buildGrid(); final objectGrid = _buildGrid(); @@ -115,7 +141,7 @@ WolfEngine _buildEngine() { difficulty: Difficulty.medium, startingEpisode: 0, frameBuffer: FrameBuffer(96, 96), - input: _StaticInput(), + input: input ?? _StaticInput(), onGameWon: () {}, ); }