feat: Enhance weapon switching logic and add tests for animation pacing and menu behavior
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -61,8 +61,11 @@ class Player {
|
|||||||
// 0.0 is resting, 500.0 is fully off-screen
|
// 0.0 is resting, 500.0 is fully off-screen
|
||||||
double weaponAnimOffset = 0.0;
|
double weaponAnimOffset = 0.0;
|
||||||
|
|
||||||
// How fast the weapon drops/raises per tick
|
static const double _weaponSwitchTravel = 500.0;
|
||||||
final double switchSpeed = 30.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}) {
|
Player({required this.x, required this.y, required this.angle}) {
|
||||||
currentWeapon = weapons[WeaponType.pistol]!;
|
currentWeapon = weapons[WeaponType.pistol]!;
|
||||||
@@ -121,12 +124,18 @@ class Player {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateWeaponSwitch();
|
updateWeaponSwitch(elapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Weapon Switching & Animation Logic ---
|
// --- 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 (switchState == WeaponSwitchState.lowering) {
|
||||||
// If the map doesn't contain the pending weapon, stop immediately
|
// If the map doesn't contain the pending weapon, stop immediately
|
||||||
if (weapons[pendingWeaponType] == null) {
|
if (weapons[pendingWeaponType] == null) {
|
||||||
@@ -134,9 +143,9 @@ class Player {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
weaponAnimOffset += switchSpeed;
|
weaponAnimOffset += delta;
|
||||||
if (weaponAnimOffset >= 500.0) {
|
if (weaponAnimOffset >= _weaponSwitchTravel) {
|
||||||
weaponAnimOffset = 500.0;
|
weaponAnimOffset = _weaponSwitchTravel;
|
||||||
|
|
||||||
// We already know it's not null now, but we can keep the
|
// We already know it's not null now, but we can keep the
|
||||||
// fallback to pistol just to be extra safe.
|
// fallback to pistol just to be extra safe.
|
||||||
@@ -145,7 +154,7 @@ class Player {
|
|||||||
switchState = WeaponSwitchState.raising;
|
switchState = WeaponSwitchState.raising;
|
||||||
}
|
}
|
||||||
} else if (switchState == WeaponSwitchState.raising) {
|
} else if (switchState == WeaponSwitchState.raising) {
|
||||||
weaponAnimOffset -= switchSpeed;
|
weaponAnimOffset -= delta;
|
||||||
if (weaponAnimOffset <= 0) {
|
if (weaponAnimOffset <= 0) {
|
||||||
weaponAnimOffset = 0.0;
|
weaponAnimOffset = 0.0;
|
||||||
switchState = WeaponSwitchState.idle;
|
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
|
// 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 =
|
_mutantDeathFaceActive = health <= 0 && attackerType == EnemyType.mutant;
|
||||||
health <= 0 && attackerType == EnemyType.mutant;
|
|
||||||
|
|
||||||
if (health <= 0) {
|
if (health <= 0) {
|
||||||
log("[PLAYER] Died! Final Score: $score");
|
log("[PLAYER] Died! Final Score: $score");
|
||||||
|
|||||||
@@ -2094,7 +2094,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
dynamic finalizeFrame() {
|
dynamic finalizeFrame() {
|
||||||
if (engine.difficulty != null) {
|
if (engine.difficulty != null && !engine.isMenuOpen) {
|
||||||
if (engine.player.damageFlash > 0.0) {
|
if (engine.player.damageFlash > 0.0) {
|
||||||
if (_usesTerminalLayout) {
|
if (_usesTerminalLayout) {
|
||||||
_applyDamageFlashToScene();
|
_applyDamageFlashToScene();
|
||||||
|
|||||||
@@ -1336,7 +1336,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
sb.write('\x1bPq');
|
sb.write('\x1bPq');
|
||||||
sb.write('"1;1;$_outputWidth;$_outputHeight');
|
sb.write('"1;1;$_outputWidth;$_outputHeight');
|
||||||
|
|
||||||
final bool gameplayActive = engine.difficulty != null;
|
final bool gameplayActive = engine.difficulty != null && !engine.isMenuOpen;
|
||||||
final double damageIntensity = gameplayActive
|
final double damageIntensity = gameplayActive
|
||||||
? engine.player.damageFlash
|
? engine.player.damageFlash
|
||||||
: 0.0;
|
: 0.0;
|
||||||
|
|||||||
@@ -1401,7 +1401,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
FrameBuffer finalizeFrame() {
|
FrameBuffer finalizeFrame() {
|
||||||
if (engine.difficulty != null) {
|
if (engine.difficulty != null && !engine.isMenuOpen) {
|
||||||
if (engine.player.damageFlash > 0) {
|
if (engine.player.damageFlash > 0) {
|
||||||
_applyDamageFlash();
|
_applyDamageFlash();
|
||||||
} else if (engine.player.bonusFlash > 0) {
|
} else if (engine.player.bonusFlash > 0) {
|
||||||
|
|||||||
@@ -115,5 +115,22 @@ void main() {
|
|||||||
expect(player.bonusFlash, lessThan(1.0));
|
expect(player.bonusFlash, lessThan(1.0));
|
||||||
expect(player.bonusFlash, greaterThan(0.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));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,27 @@ void main() {
|
|||||||
expect(mapPixels.contains(ColorPalette.vga32Bit[2]), isTrue);
|
expect(mapPixels.contains(ColorPalette.vga32Bit[2]), isTrue);
|
||||||
expect(mapPixels[hudProbeIndex], equals(normalPixels[hudProbeIndex]));
|
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<int> menuBaseline = List<int>.from(renderer.render(engine).pixels);
|
||||||
|
|
||||||
|
engine.player.bonusFlash = 1.0;
|
||||||
|
final List<int> menuWithFlash = List<int>.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 wallGrid = _buildGrid();
|
||||||
final objectGrid = _buildGrid();
|
final objectGrid = _buildGrid();
|
||||||
|
|
||||||
@@ -115,7 +141,7 @@ WolfEngine _buildEngine() {
|
|||||||
difficulty: Difficulty.medium,
|
difficulty: Difficulty.medium,
|
||||||
startingEpisode: 0,
|
startingEpisode: 0,
|
||||||
frameBuffer: FrameBuffer(96, 96),
|
frameBuffer: FrameBuffer(96, 96),
|
||||||
input: _StaticInput(),
|
input: input ?? _StaticInput(),
|
||||||
onGameWon: () {},
|
onGameWon: () {},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user