diff --git a/packages/frontend/src/game/player/controls/shoot.ts b/packages/frontend/src/game/player/controls/shoot.ts index 492a927..86112f8 100644 --- a/packages/frontend/src/game/player/controls/shoot.ts +++ b/packages/frontend/src/game/player/controls/shoot.ts @@ -11,43 +11,67 @@ type Shoot = Pick; interface ShootProps extends Shoot { clock: Clock; gameStateRef: GameStateRef; + lastFrameTime: React.MutableRefObject; lastShotTime: React.MutableRefObject; + sprintEndedTimer: React.MutableRefObject; wasShootPressed: React.MutableRefObject; + wasSprinting: React.MutableRefObject; } export const shoot = (props: ShootProps) => { - const currentWeapon = props.playerStateRef.current.currentWeapon; + const { + clock, + gameStateRef, + lastFrameTime, + lastShotTime, + wasShootPressed, + sprintEndedTimer, + playerStateRef, + wasSprinting, + } = props; + const currentWeapon = playerStateRef.current.currentWeapon; if (props.gamepad == null || currentWeapon == null) return; - const activeWeaponId = getActiveWeaponId( - getPlayerId(), - props.gameStateRef.current.weapon.weapons, - ); + const activeWeaponId = getActiveWeaponId(getPlayerId(), gameStateRef.current.weapon.weapons); if (activeWeaponId == null) return; const activeWeapon = - props.gameStateRef.current.weapon.weapons[activeWeaponId] ?? raise("Weapon not found"); - - const { lastShotTime, wasShootPressed } = props; + gameStateRef.current.weapon.weapons[activeWeaponId] ?? raise("Weapon not found"); const isShootPressed = !!props.gamepad.buttons[7]?.pressed; const firingMode = WEAPONS[currentWeapon].firingMode; - const fireRateMs = WEAPONS[currentWeapon].fireRateMs / 1000; // Convert to seconds for getElapsedTime() + const fireRate = WEAPONS[currentWeapon].fireRateMs / 1000; // Convert to seconds for getElapsedTime() + const sprintToFireTime = WEAPONS[currentWeapon].sprintToFireTimeMs / 1000; // Convert to seconds + const currentTime = clock.getElapsedTime(); + + // Sprint to fire logic + if (!playerStateRef.current.isSprinting && wasSprinting.current) { + sprintEndedTimer.current = 0; + } + wasSprinting.current = playerStateRef.current.isSprinting; + + const delta = currentTime - lastFrameTime.current; + lastFrameTime.current = currentTime; + sprintEndedTimer.current += delta; + if (sprintEndedTimer.current < sprintToFireTime) { + return; + } - const currentTime = props.clock.getElapsedTime(); + sprintEndedTimer.current = Infinity; // Reset timer after sprint to fire time is reached + if (playerStateRef.current.isSprinting) return; // TODO: Bug, isShooting state should be handled on server https://app.clickup.com/t/86b5v1jnn if (firingMode === "Auto") { // Auto fire (only shoot on button press AND weapon fire rate interval) if ( isShootPressed && - currentTime - lastShotTime.current >= fireRateMs && + currentTime - lastShotTime.current >= fireRate && activeWeapon.currentAmmoInMagazine > 0 ) { - props.playerStateRef.current.isShooting = true; + playerStateRef.current.isShooting = true; lastShotTime.current = currentTime; } else { - props.playerStateRef.current.isShooting = false; + playerStateRef.current.isShooting = false; } } @@ -58,10 +82,10 @@ export const shoot = (props: ShootProps) => { if (firingMode === "Single") { if (isShootPressed && !wasShootPressed.current) { - props.playerStateRef.current.isShooting = true; + playerStateRef.current.isShooting = true; lastShotTime.current = currentTime; } else { - props.playerStateRef.current.isShooting = false; + playerStateRef.current.isShooting = false; } } diff --git a/packages/frontend/src/game/player/hooks/useControls.ts b/packages/frontend/src/game/player/hooks/useControls.ts index 7195a30..2e8294d 100644 --- a/packages/frontend/src/game/player/hooks/useControls.ts +++ b/packages/frontend/src/game/player/hooks/useControls.ts @@ -36,6 +36,11 @@ export const useControls = (props: UseControlsProps) => { const wasShootPressed = React.useRef(false); const lastShotTime = React.useRef(0); + // Movement state + const sprintEndedTimer = React.useRef(Infinity); + const wasSprinting = React.useRef(false); + const lastFrameTime = React.useRef(0); + useFrame(({ clock }, delta) => { // ! Player cannot perform this action if they are not alive if (!props.playerStateRef.current.isAlive) return; @@ -80,9 +85,12 @@ export const useControls = (props: UseControlsProps) => { clock, gameStateRef: props.gameStateRef, gamepad, + lastFrameTime, lastShotTime, playerStateRef: props.playerStateRef, + sprintEndedTimer, wasShootPressed, + wasSprinting, }); }); }; diff --git a/packages/lib/src/objects/weapon.ts b/packages/lib/src/objects/weapon.ts index 52f0e2b..0b0dbfa 100644 --- a/packages/lib/src/objects/weapon.ts +++ b/packages/lib/src/objects/weapon.ts @@ -37,6 +37,7 @@ export interface WeaponStats { reloadTimeMs: number; // The time it takes to reload the weapon in milliseconds reserveAmmo: number; // Recommended to be a multiple of bulletsPerMagazine shotsPerBurst?: number; // Only set if firingMode is "Burst" + sprintToFireTimeMs: number; // The time from when the player stops sprinting to when they can fire the weapon weaponClass: WeaponClass; // The class of the weapon weaponType: WeaponType; // The type of weapon weight: number; // Determines how fast the player can move with the weapon @@ -61,6 +62,7 @@ export const WEAPONS = { }, reloadTimeMs: 3000, reserveAmmo: 150, + sprintToFireTimeMs: 250, weaponClass: "Assault Rifle", weaponType: "Primary", weight: 1, @@ -82,6 +84,7 @@ export const WEAPONS = { }, reloadTimeMs: 3000, reserveAmmo: 150, + sprintToFireTimeMs: 250, weaponClass: "Assault Rifle", weaponType: "Primary", weight: 1, @@ -105,6 +108,7 @@ export const WEAPONS = { }, reloadTimeMs: 3000, reserveAmmo: 150, + sprintToFireTimeMs: 250, weaponClass: "Assault Rifle", weaponType: "Primary", weight: 1, @@ -126,6 +130,7 @@ export const WEAPONS = { }, reloadTimeMs: 3000, reserveAmmo: 150, + sprintToFireTimeMs: 250, weaponClass: "Assault Rifle", weaponType: "Primary", weight: 1,