package game import ( "os" "time" "gitea.boner.be/bdnugget/goonscape/assets" "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" ) type Game struct { Player *types.Player OtherPlayers map[int32]*types.Player Camera rl.Camera3D Models []types.ModelAsset Music rl.Music Chat *Chat MenuOpen bool QuitChan chan struct{} // Channel to signal shutdown loginScreen *LoginScreen isLoggedIn bool } func New() *Game { InitWorld() game := &Game{ OtherPlayers: make(map[int32]*types.Player), 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 { var err error g.Models, err = assets.LoadModels() if err != nil { return err } g.Music, err = assets.LoadMusic("resources/audio/GoonScape2.mp3") if err != nil { return err } return nil } func (g *Game) Update(deltaTime float32) { if !g.isLoggedIn { 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 = true return } g.loginScreen.Draw() return } // Handle ESC for menu if rl.IsKeyPressed(rl.KeyEscape) { g.MenuOpen = !g.MenuOpen return } // Don't process other inputs if menu is open if g.MenuOpen { 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.MoveTowards(g.Player.TargetPath[0], deltaTime, GetMapGrid()) } for _, other := range g.OtherPlayers { if len(other.TargetPath) > 0 { other.MoveTowards(other.TargetPath[0], deltaTime, GetMapGrid()) } } 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, model rl.Model) { player.Lock() defer player.Unlock() grid := GetMapGrid() playerPos := rl.Vector3{ X: player.PosActual.X, Y: grid[player.PosTile.X][player.PosTile.Y].Height*types.TileHeight + 16.0, Z: player.PosActual.Z, } if player.ID%int32(len(g.Models)) == 0 { modelAsset := g.Models[0] // 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(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(model, anim, player.AnimationFrame) } } } rl.DrawModel(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() { rl.BeginDrawing() rl.ClearBackground(rl.RayWhite) if !g.isLoggedIn { g.loginScreen.Draw() rl.EndDrawing() return } rl.BeginMode3D(g.Camera) g.DrawMap() g.DrawPlayer(g.Player, g.Player.Model) for _, other := range g.OtherPlayers { if other.Model.Meshes == nil { g.AssignModelToPlayer(other) } g.DrawPlayer(other, other.Model) } 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) } for _, other := range g.OtherPlayers { drawFloatingMessage(other.FloatingMessage) } // Draw menu if open if g.MenuOpen { g.DrawMenu() } // Only draw chat if menu is not open if !g.MenuOpen { g.Chat.Draw(int32(rl.GetScreenWidth()), int32(rl.GetScreenHeight())) } rl.DrawFPS(10, 10) rl.EndDrawing() } func (g *Game) Cleanup() { assets.UnloadModels(g.Models) assets.UnloadMusic(g.Music) } 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 = 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] // Just use the original model - don't try to copy it player.Model = modelAsset.Model player.Texture = modelAsset.Texture }