Archived
1
0
Fork 0

feat: Implement player input replay, bullet grace period, and graze logic separation

This commit is contained in:
Sebastian Benjamin (aider) 2025-06-03 19:19:14 -07:00
parent 8586180dbc
commit 7718a68991
2 changed files with 62 additions and 20 deletions

View file

@ -38,23 +38,8 @@ func RespondToInput(lobbyState *MatchState, messages []runtime.MatchData, logger
continue
}
// Apply latest player input
lobbyState.presences[msg.GetSessionId()].stageState.BoundsCheckedMove(update.X, update.Y)
// Check if the input is within the grace window
if update.Tick < tick && tick-update.Tick <= GRACE_WINDOW_TICKS {
// Check the player's collision state for all subsequent ticks
playerSurvives := true
for t := update.Tick; t < tick; t++ {
if lobbyState.presences[msg.GetSessionId()].stageState.CheckCollisionState(t) == PLAYER_DEAD {
playerSurvives = false
}
}
// Set a flag to cancel death if the player survives all ticks
if playerSurvives {
lobbyState.presences[msg.GetSessionId()].stageState.cancelDeath = true
}
}
// Store the input in the player's stage state
lobbyState.presences[msg.GetSessionId()].stageState.playerInputs = append(lobbyState.presences[msg.GetSessionId()].stageState.playerInputs, update)
}
}
@ -131,6 +116,11 @@ func (m *Match) MatchLoop(ctx context.Context, logger runtime.Logger, db *sql.DB
}
RespondToInput(lobbyState, messages, &logger, tick, &dispatcher)
for _, playerState := range lobbyState.presences {
playerState.stageState.ProcessPlayerInputs(tick)
playerState.stageState.CleanupBullets(tick)
}
BroadcastToPresences(tick, lobbyState, &logger, &dispatcher)
return lobbyState

View file

@ -10,13 +10,52 @@ import (
type PlayerStageState struct {
hitCol *ffi.Circle
grazeCol *ffi.Circle
bullets []*ffi.Bullet
bullets []*Bullet
updatePlayerPos bool
health int
graze int
score int
deathTimer int
cancelDeath bool
playerInputs []ClientUpdate
lastInput *ClientUpdate
}
func (s *PlayerStageState) ProcessPlayerInputs(tick int64) {
// Sort inputs by tick
slices.SortFunc(s.playerInputs, func(a, b ClientUpdate) bool {
return a.Tick < b.Tick
})
// Replay each tick within the grace window
for t := tick - GRACE_WINDOW_TICKS; t < tick; t++ {
// Find the input for the current tick
var currentInput *ClientUpdate
for _, input := range s.playerInputs {
if input.Tick == t {
currentInput = &input
break
}
}
// Apply the correct movement or no movement
if currentInput != nil {
s.BoundsCheckedMove(currentInput.X, currentInput.Y)
s.lastInput = currentInput
} else if s.lastInput != nil {
s.BoundsCheckedMove(s.lastInput.X, s.lastInput.Y)
}
if s.CheckCollisionState(t) == PLAYER_DEAD {
s.cancelDeath = false
break
}
}
// Clean up inputs outside the grace window
s.playerInputs = slices.DeleteFunc(s.playerInputs, func(input ClientUpdate) bool {
return tick-input.Tick > GRACE_WINDOW_TICKS
})
}
func NewPlayerStage() *PlayerStageState {
@ -55,7 +94,15 @@ func (s *PlayerStageState) BoundsCheckedMove(x float64, y float64) {
func (s *PlayerStageState) DeleteBulletsBeyondKillBoundary(tick int64) {
s.bullets = slices.DeleteFunc(s.bullets, func(b *ffi.Bullet) bool {
if b.BeyondKillBoundary(tick) {
if b.BeyondKillBoundary(tick) && b.deletionTick == 0 {
b.deletionTick = tick
}
})
}
func (s *PlayerStageState) CleanupBullets(tick int64) {
s.bullets = slices.DeleteFunc(s.bullets, func(b *Bullet) bool {
if b.deletionTick > 0 && tick-b.deletionTick > GRACE_WINDOW_TICKS {
ffi.DestroyBullet(b)
return true
}
@ -87,7 +134,12 @@ func (s *PlayerStageState) CheckCollisionState(tick int64) int {
return b.CollidesWith(s.hitCol, tick)
}) {
return PLAYER_DEAD
} else if slices.ContainsFunc(s.bullets, func(b *ffi.Bullet) bool {
}
return PLAYER_ALIVE
}
func (s *PlayerStageState) UpdateGrazeMultiplier(tick int64) {
if slices.ContainsFunc(s.bullets, func(b *Bullet) bool {
return b.CollidesWith(s.grazeCol, tick)
}) {
s.graze += GRAZE_ADDITION_MULTIPLIER