package game import ( "fmt" "os" "sync" "time" "sync/atomic" "gitea.boner.be/bdnugget/goonscape/assets" "gitea.boner.be/bdnugget/goonscape/config" "gitea.boner.be/bdnugget/goonscape/logging" "gitea.boner.be/bdnugget/goonscape/network" "gitea.boner.be/bdnugget/goonscape/types" pb "gitea.boner.be/bdnugget/goonserver/actions" rl "github.com/gen2brain/raylib-go/raylib" ) var audioMutex sync.Mutex var audioInitOnce sync.Once type Game struct { ctx *GameContext Player *types.Player OtherPlayers sync.Map // Using sync.Map for concurrent access Camera rl.Camera3D Models []types.ModelAsset Music rl.Music Chat *Chat MenuOpen atomic.Bool QuitChan chan struct{} // Channel to signal shutdown loginScreen *LoginScreen isLoggedIn atomic.Bool } func New() *Game { InitWorld() game := &Game{ ctx: NewGameContext(), OtherPlayers: sync.Map{}, Camera: rl.Camera3D{ Position: rl.NewVector3(0, 10, 10), Target: rl.NewVector3(0, 0, 0), Up: rl.NewVector3(0, 1, 0), Fovy: 45.0, Projection: rl.CameraPerspective, }, Chat: NewChat(), QuitChan: make(chan struct{}), loginScreen: NewLoginScreen(), } game.Chat.userData = game return game } func (g *Game) LoadAssets() error { audioMutex.Lock() defer audioMutex.Unlock() logging.Info.Println("Loading game assets") var err error // Load models first g.Models, err = assets.LoadModels() if err != nil { logging.Error.Printf("Failed to load models: %v", err) return err } // Load music only if enabled if config.Current.PlayMusic { logging.Info.Println("Loading music stream") g.Music = rl.LoadMusicStream("resources/audio/GoonScape2.mp3") if g.Music.CtxType == 0 { logging.Error.Println("Failed to load music stream") return fmt.Errorf("failed to load music stream") } logging.Info.Println("Music stream loaded successfully") } else { logging.Info.Println("Music disabled by config") } logging.Info.Println("Assets loaded successfully") return nil } func (g *Game) Update(deltaTime float32) { if !g.isLoggedIn.Load() { username, password, isRegistering, submitted := g.loginScreen.Update() if submitted { conn, playerID, err := network.ConnectToServer(username, password, isRegistering) if err != nil { g.loginScreen.SetError(err.Error()) return } g.Player = &types.Player{ Speed: 50.0, TargetPath: []types.Tile{}, UserData: g, QuitDone: make(chan struct{}), ID: playerID, } g.AssignModelToPlayer(g.Player) go network.HandleServerCommunication(conn, playerID, g.Player, &g.OtherPlayers, g.QuitChan) g.isLoggedIn.Store(true) return } g.loginScreen.Draw() return } // Handle ESC for menu if rl.IsKeyPressed(rl.KeyEscape) { g.MenuOpen.Store(!g.MenuOpen.Load()) return } // Don't process other inputs if menu is open if g.MenuOpen.Load() { return } if message, sent := g.Chat.Update(); sent { g.Player.Lock() g.Player.ActionQueue = append(g.Player.ActionQueue, &pb.Action{ Type: pb.Action_CHAT, ChatMessage: message, PlayerId: g.Player.ID, }) g.Player.Unlock() } g.HandleInput() if len(g.Player.TargetPath) > 0 { g.Player.Lock() if len(g.Player.TargetPath) > 0 { g.Player.MoveTowards(g.Player.TargetPath[0], deltaTime, GetMapGrid()) } g.Player.Unlock() } g.OtherPlayers.Range(func(key, value any) bool { other := value.(*types.Player) if len(other.TargetPath) > 0 { other.MoveTowards(other.TargetPath[0], deltaTime, GetMapGrid()) } return true }) UpdateCamera(&g.Camera, g.Player.PosActual, deltaTime) } func (g *Game) DrawMap() { for x := 0; x < types.MapWidth; x++ { for y := 0; y < types.MapHeight; y++ { height := GetTileHeight(x, y) // Interpolate height for smoother landscape if x > 0 { height += GetTileHeight(x-1, y) } if y > 0 { height += GetTileHeight(x, y-1) } if x > 0 && y > 0 { height += GetTileHeight(x-1, y-1) } height /= 4.0 tilePos := rl.Vector3{ X: float32(x * types.TileSize), Y: height * types.TileHeight, Z: float32(y * types.TileSize), } color := rl.Color{R: uint8(height * 25), G: 100, B: 100, A: 64} rl.DrawCube(tilePos, types.TileSize, types.TileHeight, types.TileSize, color) } } } func (g *Game) DrawPlayer(player *types.Player) { player.Lock() defer player.Unlock() if player.Model.Meshes == nil { logging.Error.Println("Player model not initialized") return } grid := GetMapGrid() modelIndex := int(player.ID) % len(g.Models) modelAsset := g.Models[modelIndex] const defaultHeight = 8.0 // Default height above tile, fine tune per model in types.ModelAsset playerPos := rl.Vector3{ X: player.PosActual.X, Y: grid[player.PosTile.X][player.PosTile.Y].Height*types.TileHeight + defaultHeight + modelAsset.YOffset, Z: player.PosActual.Z, } // Check if model has animations if modelAsset.Animations.Idle != nil || modelAsset.Animations.Walk != nil { if player.IsMoving && len(modelAsset.Animations.Walk) > 0 { anim := modelAsset.Animations.Walk[0] // Use first walk animation player.AnimationFrame = player.AnimationFrame % anim.FrameCount rl.UpdateModelAnimation(player.Model, anim, player.AnimationFrame) } else if len(modelAsset.Animations.Idle) > 0 { anim := modelAsset.Animations.Idle[0] // Use first idle animation player.AnimationFrame = player.AnimationFrame % anim.FrameCount rl.UpdateModelAnimation(player.Model, anim, player.AnimationFrame) } } rl.DrawModel(player.Model, playerPos, 16, rl.White) // Draw floating messages and path indicators if player.FloatingMessage != nil { screenPos := rl.GetWorldToScreen(rl.Vector3{ X: playerPos.X, Y: playerPos.Y + 24.0, Z: playerPos.Z, }, g.Camera) player.FloatingMessage.ScreenPos = screenPos } if len(player.TargetPath) > 0 { targetTile := player.TargetPath[len(player.TargetPath)-1] targetPos := rl.Vector3{ X: float32(targetTile.X * types.TileSize), Y: grid[targetTile.X][targetTile.Y].Height * types.TileHeight, Z: float32(targetTile.Y * types.TileSize), } rl.DrawCubeWires(targetPos, types.TileSize, types.TileHeight, types.TileSize, rl.Green) nextTile := player.TargetPath[0] nextPos := rl.Vector3{ X: float32(nextTile.X * types.TileSize), Y: grid[nextTile.X][nextTile.Y].Height * types.TileHeight, Z: float32(nextTile.Y * types.TileSize), } rl.DrawCubeWires(nextPos, types.TileSize, types.TileHeight, types.TileSize, rl.Yellow) } } func (g *Game) Render() { if !rl.IsWindowReady() { logging.Error.Println("Window not ready for rendering") return } rl.BeginDrawing() defer func() { if rl.IsWindowReady() { rl.EndDrawing() } }() if !g.isLoggedIn.Load() { g.loginScreen.Draw() return } rl.BeginMode3D(g.Camera) g.DrawMap() g.DrawPlayer(g.Player) g.OtherPlayers.Range(func(key, value any) bool { other := value.(*types.Player) if other.Model.Meshes == nil { g.AssignModelToPlayer(other) } g.DrawPlayer(other) return true }) rl.EndMode3D() // Draw floating messages drawFloatingMessage := func(msg *types.FloatingMessage) { if msg == nil || time.Now().After(msg.ExpireTime) { return } pos := msg.ScreenPos text := msg.Content textWidth := rl.MeasureText(text, 20) for offsetX := -2; offsetX <= 2; offsetX++ { for offsetY := -2; offsetY <= 2; offsetY++ { rl.DrawText(text, int32(pos.X)-textWidth/2+int32(offsetX), int32(pos.Y)+int32(offsetY), 20, rl.Black) } } rl.DrawText(text, int32(pos.X)-textWidth/2, int32(pos.Y), 20, rl.Yellow) } if g.Player.FloatingMessage != nil { drawFloatingMessage(g.Player.FloatingMessage) } g.OtherPlayers.Range(func(key, value any) bool { other := value.(*types.Player) drawFloatingMessage(other.FloatingMessage) return true }) // Draw menu if open if g.MenuOpen.Load() { g.DrawMenu() } // Only draw chat if menu is not open if !g.MenuOpen.Load() { g.Chat.Draw(int32(rl.GetScreenWidth()), int32(rl.GetScreenHeight())) } rl.DrawFPS(10, 10) } func (g *Game) Cleanup() { // Unload models if g.Models != nil { assets.UnloadModels(g.Models) } // Stop and unload music if enabled if config.Current.PlayMusic && g.Music.CtxType != 0 { rl.StopMusicStream(g.Music) rl.UnloadMusicStream(g.Music) } // Close audio device if it's ready if rl.IsAudioDeviceReady() { rl.CloseAudioDevice() } } func (g *Game) HandleInput() { clickedTile, clicked := g.GetTileAtMouse() if clicked { path := FindPath(GetTile(g.Player.PosTile.X, g.Player.PosTile.Y), clickedTile) if len(path) > 1 { g.Player.Lock() g.Player.TargetPath = path[1:] g.Player.ActionQueue = append(g.Player.ActionQueue, &pb.Action{ Type: pb.Action_MOVE, X: int32(clickedTile.X), Y: int32(clickedTile.Y), PlayerId: g.Player.ID, }) g.Player.Unlock() } } } func (g *Game) DrawMenu() { screenWidth := float32(rl.GetScreenWidth()) screenHeight := float32(rl.GetScreenHeight()) // Semi-transparent background rl.DrawRectangle(0, 0, int32(screenWidth), int32(screenHeight), rl.ColorAlpha(rl.Black, 0.7)) // Menu title title := "Menu" titleSize := int32(40) titleWidth := rl.MeasureText(title, titleSize) rl.DrawText(title, int32(screenWidth/2)-titleWidth/2, 100, titleSize, rl.White) // Menu buttons buttonWidth := float32(200) buttonHeight := float32(40) buttonY := float32(200) buttonSpacing := float32(60) menuItems := []string{"Resume", "Settings", "Exit Game"} for _, item := range menuItems { buttonRect := rl.Rectangle{ X: screenWidth/2 - buttonWidth/2, Y: buttonY, Width: buttonWidth, Height: buttonHeight, } // Check mouse hover mousePoint := rl.GetMousePosition() mouseHover := rl.CheckCollisionPointRec(mousePoint, buttonRect) // Draw button if mouseHover { rl.DrawRectangleRec(buttonRect, rl.ColorAlpha(rl.White, 0.3)) if rl.IsMouseButtonPressed(rl.MouseLeftButton) { switch item { case "Resume": g.MenuOpen.Store(false) case "Settings": // TODO: Implement settings case "Exit Game": g.Shutdown() } } } // Draw button text textSize := int32(20) textWidth := rl.MeasureText(item, textSize) textX := int32(buttonRect.X+buttonRect.Width/2) - textWidth/2 textY := int32(buttonRect.Y + buttonRect.Height/2 - float32(textSize)/2) rl.DrawText(item, textX, textY, textSize, rl.White) buttonY += buttonSpacing } } func (g *Game) Shutdown() { close(g.QuitChan) <-g.Player.QuitDone rl.CloseWindow() os.Exit(0) } func (g *Game) HandleServerMessages(messages []*pb.ChatMessage) { g.Chat.HandleServerMessages(messages) } func (g *Game) AssignModelToPlayer(player *types.Player) { modelIndex := int(player.ID) % len(g.Models) modelAsset := g.Models[modelIndex] player.Model = modelAsset.Model player.Texture = modelAsset.Texture player.AnimationFrame = 0 } func (g *Game) Run() { if config.Current.PlayMusic { audioInitOnce.Do(func() { logging.Info.Println("Initializing audio device") rl.InitAudioDevice() if !rl.IsAudioDeviceReady() { logging.Error.Println("Failed to initialize audio device") } }) defer func() { logging.Info.Println("Closing audio device") rl.CloseAudioDevice() }() } logging.Info.Println("Starting game loop") for !rl.WindowShouldClose() { deltaTime := rl.GetFrameTime() if config.Current.PlayMusic { rl.UpdateMusicStream(g.Music) } g.Update(deltaTime) g.Render() } logging.Info.Println("Game loop ended") logging.Info.Println("Closing quit channel") close(g.QuitChan) }