diff --git a/packages/shared/src/shooter-game.ts b/packages/shared/src/shooter-game.ts index 7b33254..055013a 100644 --- a/packages/shared/src/shooter-game.ts +++ b/packages/shared/src/shooter-game.ts @@ -125,7 +125,14 @@ const shooterDefinition = defineNetworkedGame< if (!player.alive) continue; simulateMovement(player, deltaSeconds); emitFootstep(state, player, tick, emit); - if (player.bot) tryFireWeapon(state, player, emit); + const repeatingAutomaticFire = + player.kind === "human" && + player.firing && + player.lastFirePressed && + WEAPONS[player.weapon].automatic; + if (player.bot || repeatingAutomaticFire) { + tryFireWeapon(state, player, emit); + } collectNearbyPickup(state, player, emit); player.lastFirePressed = player.firing; } diff --git a/packages/shared/test/shooter.test.mjs b/packages/shared/test/shooter.test.mjs index d95a74d..ae32cbe 100644 --- a/packages/shared/test/shooter.test.mjs +++ b/packages/shared/test/shooter.test.mjs @@ -4,6 +4,7 @@ import { BOT_COUNT, BOT_ID_BASE, Weapon, + createShooterGame, shooterGame, } from "../dist/index.js"; @@ -61,6 +62,43 @@ test("backend simulation is deterministic", () => { assert.deepEqual(runHeadlessMatch(2_400), runHeadlessMatch(2_400)); }); +test("held automatic fire follows server ticks while semi-auto remains edge-triggered", () => { + const game = createShooterGame({ botCount: 0 }); + + const runHeldFire = (weapon, ticks) => { + const server = game.createServer(); + server.addPlayer(1); + server.currentState.players.get(1).weapon = weapon; + const packet = { + sequence: 1, + targetTick: 1, + observedTick: 0, + input: { + strafe: 0, + forward: 0, + yaw: 0, + pitch: 0, + fire: true, + sprint: false, + reload: false, + weapon, + }, + }; + assert.deepEqual(server.submitInput(1, packet), { accepted: true }); + let shots = 0; + for (let tick = 0; tick < ticks; tick += 1) { + if (tick > 0 && tick % 10 === 0) { + assert.deepEqual(server.submitInput(1, packet), { accepted: true }); + } + shots += server.step().events.filter((event) => event.type === "shot").length; + } + return shots; + }; + + assert.equal(runHeldFire(Weapon.PulseRifle, 60), 10); + assert.equal(runHeldFire(Weapon.Scattergun, 100), 1); +}); + test("human hitscan damages the historical target without rewinding live positions", () => { const server = shooterGame.createServer(); server.addPlayer(1);