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 continue
} }
// Apply latest player input // Store the input in the player's stage state
lobbyState.presences[msg.GetSessionId()].stageState.BoundsCheckedMove(update.X, update.Y) lobbyState.presences[msg.GetSessionId()].stageState.playerInputs = append(lobbyState.presences[msg.GetSessionId()].stageState.playerInputs, update)
// 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
}
}
} }
} }
@ -131,6 +116,11 @@ func (m *Match) MatchLoop(ctx context.Context, logger runtime.Logger, db *sql.DB
} }
RespondToInput(lobbyState, messages, &logger, tick, &dispatcher) RespondToInput(lobbyState, messages, &logger, tick, &dispatcher)
for _, playerState := range lobbyState.presences {
playerState.stageState.ProcessPlayerInputs(tick)
playerState.stageState.CleanupBullets(tick)
}
BroadcastToPresences(tick, lobbyState, &logger, &dispatcher) BroadcastToPresences(tick, lobbyState, &logger, &dispatcher)
return lobbyState return lobbyState

View file

@ -10,13 +10,52 @@ import (
type PlayerStageState struct { type PlayerStageState struct {
hitCol *ffi.Circle hitCol *ffi.Circle
grazeCol *ffi.Circle grazeCol *ffi.Circle
bullets []*ffi.Bullet bullets []*Bullet
updatePlayerPos bool updatePlayerPos bool
health int health int
graze int graze int
score int score int
deathTimer int deathTimer int
cancelDeath bool 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 { func NewPlayerStage() *PlayerStageState {
@ -55,7 +94,15 @@ func (s *PlayerStageState) BoundsCheckedMove(x float64, y float64) {
func (s *PlayerStageState) DeleteBulletsBeyondKillBoundary(tick int64) { func (s *PlayerStageState) DeleteBulletsBeyondKillBoundary(tick int64) {
s.bullets = slices.DeleteFunc(s.bullets, func(b *ffi.Bullet) bool { 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) ffi.DestroyBullet(b)
return true return true
} }
@ -87,7 +134,12 @@ func (s *PlayerStageState) CheckCollisionState(tick int64) int {
return b.CollidesWith(s.hitCol, tick) return b.CollidesWith(s.hitCol, tick)
}) { }) {
return PLAYER_DEAD 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) return b.CollidesWith(s.grazeCol, tick)
}) { }) {
s.graze += GRAZE_ADDITION_MULTIPLIER s.graze += GRAZE_ADDITION_MULTIPLIER