feat: Implement player input replay, bullet grace period, and graze logic separation
This commit is contained in:
parent
8586180dbc
commit
7718a68991
2 changed files with 62 additions and 20 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Reference in a new issue