139 lines
4.1 KiB
Go
139 lines
4.1 KiB
Go
package battleroyale
|
|
|
|
import (
|
|
"context"
|
|
"danmaku/ffi"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"github.com/heroiclabs/nakama-common/runtime"
|
|
"math"
|
|
)
|
|
|
|
func CheckMatchTerminate(lobbyState *MatchState, logger *runtime.Logger) bool {
|
|
// If we have no presences in the match according to the match state, increment the empty ticks count
|
|
if len(lobbyState.presences) == 0 {
|
|
lobbyState.emptyTicks++
|
|
}
|
|
|
|
// If the match has been empty for more than 100 ticks, end the match by returning nil
|
|
if lobbyState.emptyTicks > 100 {
|
|
(*logger).Info("Match terminated due to empty lobby.")
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func RespondToInput(lobbyState *MatchState, messages []runtime.MatchData, logger *runtime.Logger, tick int64, dispatcher *runtime.MatchDispatcher) {
|
|
for _, msg := range messages {
|
|
_, exists := lobbyState.presences[msg.GetSessionId()]
|
|
if !exists {
|
|
(*logger).Warn("Received input for non-existent player session ID: %v", msg.GetSessionId())
|
|
continue
|
|
}
|
|
|
|
var update ClientUpdate
|
|
if err := json.Unmarshal(msg.GetData(), &update); err != nil {
|
|
(*logger).Warn("Failed to parse input: %v", err)
|
|
continue
|
|
}
|
|
|
|
// Apply the input to the tick where it occurred
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFireBullets(tick int64) []*ffi.Bullet {
|
|
var bullets = []*ffi.Bullet{}
|
|
if tick%30 == 0 {
|
|
numBullets := 20
|
|
spreadAngle := 60.0
|
|
startAngle := 90 - (spreadAngle / 2)
|
|
bulletSpeed := STAGE_WIDTH / float64(TICK_RATE) / 3
|
|
bulletRadius := 0.01 * STAGE_WIDTH
|
|
|
|
// Define a single spawn point near the top of the screen
|
|
spawnX := STAGE_WIDTH / 2 // Centered horizontally
|
|
spawnY := STAGE_HEIGHT * 0.1 // 10% from the top
|
|
|
|
for i := range numBullets {
|
|
angle := startAngle + (spreadAngle/float64(numBullets-1))*float64(i)
|
|
angleRad := angle * (math.Pi / 180.0)
|
|
|
|
velx := bulletSpeed * math.Cos(angleRad)
|
|
vely := bulletSpeed * math.Sin(angleRad)
|
|
|
|
bullet := ffi.NewLinearBullet(
|
|
tick,
|
|
spawnX,
|
|
spawnY,
|
|
bulletRadius,
|
|
velx,
|
|
vely,
|
|
)
|
|
bullets = append(bullets, bullet)
|
|
}
|
|
}
|
|
return bullets
|
|
}
|
|
|
|
func BroadcastToPresences(tick int64, lobbyState *MatchState, logger *runtime.Logger, dispatcher *runtime.MatchDispatcher) {
|
|
for _, v := range lobbyState.presences {
|
|
v.stageState.DeleteBulletsBeyondKillBoundary(tick)
|
|
|
|
var newBulletsToBroadcast = []map[string]any{}
|
|
if v.stageState.CheckCollisionState(tick) == PLAYER_ALIVE {
|
|
newBullets := TestFireBullets(tick)
|
|
|
|
for _, bullet := range newBullets {
|
|
v.stageState.AddBullet(bullet)
|
|
newBulletsToBroadcast = append(newBulletsToBroadcast, bullet.Serialize(tick))
|
|
}
|
|
}
|
|
|
|
v.stageState.UpdateDeathTimer(tick)
|
|
|
|
var tickData = v.stageState.MakeServerTick(tick, newBulletsToBroadcast)
|
|
|
|
data, err := json.Marshal(tickData)
|
|
if err != nil {
|
|
(*logger).Error("Error marshalling bullet data", err)
|
|
} else {
|
|
reliable := false
|
|
(*dispatcher).BroadcastMessage(STATE_UPDATE, data, nil, nil, reliable)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Main game loop, executed at tickRate per second specified in MatchInit
|
|
func (m *Match) MatchLoop(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state any, messages []runtime.MatchData) any {
|
|
lobbyState, ok := state.(*MatchState)
|
|
if !ok {
|
|
logger.Error("State is not a valid lobby state object for MatchLoop.")
|
|
return nil
|
|
}
|
|
|
|
if CheckMatchTerminate(lobbyState, &logger) {
|
|
return nil
|
|
}
|
|
|
|
RespondToInput(lobbyState, messages, &logger, tick, &dispatcher)
|
|
BroadcastToPresences(tick, lobbyState, &logger, &dispatcher)
|
|
|
|
return lobbyState
|
|
}
|