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 }