goonscape/game/game.go

530 lines
14 KiB
Go

package game
import (
"log"
"sync"
"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 {
// Component-based architecture
PlayerManager *PlayerManager
AssetManager *AssetManager
UIManager *UIManager
// Core game state
Camera rl.Camera3D
quitChan chan struct{}
cleanupOnce sync.Once
frameCounter int // For periodic logging
// Legacy fields for backward compatibility
Player *types.Player // Use PlayerManager.LocalPlayer instead
OtherPlayers map[int32]*types.Player // Use PlayerManager.OtherPlayers instead
Models []types.ModelAsset // Use AssetManager.Models instead
Music rl.Music // Use AssetManager.Music instead
Chat *Chat // Use UIManager.Chat instead
MenuOpen bool // Use UIManager.MenuOpen instead
loginScreen *LoginScreen // Use UIManager.LoginScreen instead
isLoggedIn bool // Use UIManager.IsLoggedIn instead
}
func New() *Game {
// Create managers
playerManager := NewPlayerManager()
assetManager := NewAssetManager()
uiManager := NewUIManager()
g := &Game{
PlayerManager: playerManager,
AssetManager: assetManager,
UIManager: uiManager,
Camera: rl.Camera3D{
Position: rl.NewVector3(0.0, 20.0, 0.0),
Target: rl.NewVector3(0.0, 0.0, 0.0),
Up: rl.NewVector3(0.0, 1.0, 0.0),
Fovy: 45.0,
Projection: rl.CameraPerspective,
},
quitChan: make(chan struct{}),
}
// Initialize legacy fields (for backward compatibility)
g.Player = g.PlayerManager.LocalPlayer
g.OtherPlayers = g.PlayerManager.OtherPlayers
g.Models = g.AssetManager.Models
g.Music = g.AssetManager.Music
g.Chat = g.UIManager.Chat
g.MenuOpen = g.UIManager.MenuOpen
g.loginScreen = g.UIManager.LoginScreen
g.isLoggedIn = g.UIManager.IsLoggedIn
// Set up inter-component references
g.Chat.userData = g // Pass game instance to chat for callbacks
// Initialize world
InitWorld()
return g
}
func (g *Game) LoadAssets() error {
return SafeExecute(func() error {
// Load models
var err error
models, err := assets.LoadModels()
if err != nil {
log.Printf("Warning: Failed to load models: %v", err)
}
g.AssetManager.Models = models
// Update legacy field
g.Models = models
// Try to load music
music, err := assets.LoadMusic("resources/audio/music.mp3")
if err != nil {
log.Printf("Warning: Failed to load music: %v", err)
} else {
g.AssetManager.Music = music
// Update legacy field
g.Music = music
}
return nil
})
}
func (g *Game) Update(deltaTime float32) {
// Legacy code to maintain compatibility
if !g.UIManager.IsLoggedIn {
// Handle login
username, password, isRegistering, doAuth := g.UIManager.LoginScreen.Update()
// Update legacy fields
g.isLoggedIn = g.UIManager.IsLoggedIn
if doAuth {
conn, playerID, err := network.ConnectToServer(username, password, isRegistering)
if err != nil {
g.UIManager.LoginScreen.SetError(err.Error())
return
}
g.PlayerManager.LocalPlayer = &types.Player{
Speed: 50.0,
TargetPath: []types.Tile{},
UserData: g,
QuitDone: make(chan struct{}),
ID: playerID,
}
g.AssignModelToPlayer(g.PlayerManager.LocalPlayer)
go network.HandleServerCommunication(conn, playerID, g.PlayerManager.LocalPlayer, g.PlayerManager.OtherPlayers, g.quitChan)
g.UIManager.IsLoggedIn = true
}
return
}
// Handle ESC for menu
if rl.IsKeyPressed(rl.KeyEscape) {
g.UIManager.MenuOpen = !g.UIManager.MenuOpen
return
}
// Don't process other inputs if menu is open
if g.UIManager.MenuOpen {
return
}
if message, sent := g.UIManager.Chat.Update(); sent {
g.PlayerManager.LocalPlayer.Lock()
g.PlayerManager.LocalPlayer.ActionQueue = append(g.PlayerManager.LocalPlayer.ActionQueue, &pb.Action{
Type: pb.Action_CHAT,
ChatMessage: message,
PlayerId: g.PlayerManager.LocalPlayer.ID,
})
g.PlayerManager.LocalPlayer.Unlock()
}
g.HandleInput()
if len(g.PlayerManager.LocalPlayer.TargetPath) > 0 {
g.PlayerManager.LocalPlayer.MoveTowards(g.PlayerManager.LocalPlayer.TargetPath[0], deltaTime, GetMapGrid())
}
// Periodically log information about other players
g.frameCounter++
if g.frameCounter%300 == 0 {
rl.TraceLog(rl.LogInfo, "There are %d other players", len(g.PlayerManager.OtherPlayers))
for id, other := range g.PlayerManager.OtherPlayers {
rl.TraceLog(rl.LogInfo, "Other player ID: %d, Position: (%f, %f, %f), Has model: %v",
id, other.PosActual.X, other.PosActual.Y, other.PosActual.Z, other.Model.Meshes != nil)
}
}
// Process other players
for _, other := range g.PlayerManager.OtherPlayers {
if other == nil {
continue
}
// Make sure other players have models assigned
if other.Model.Meshes == nil {
g.AssignModelToPlayer(other)
}
// Update other player movement
if len(other.TargetPath) > 0 {
other.MoveTowards(other.TargetPath[0], deltaTime, GetMapGrid())
}
}
UpdateCamera(&g.Camera, g.PlayerManager.LocalPlayer.PosActual, deltaTime)
// Update music if available
if g.AssetManager.Music.Stream.Buffer != nil {
rl.UpdateMusicStream(g.AssetManager.Music)
}
// Update legacy fields
g.Player = g.PlayerManager.LocalPlayer
g.OtherPlayers = g.PlayerManager.OtherPlayers
g.Models = g.AssetManager.Models
g.Music = g.AssetManager.Music
g.MenuOpen = g.UIManager.MenuOpen
}
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) {
// No need for lock in rendering, we'll use a "take snapshot" approach
// This avoids potential deadlocks and makes the rendering more consistent
// Check for invalid model
if model.Meshes == nil || model.Meshes.VertexCount <= 0 {
// Don't try to draw invalid models
return
}
grid := GetMapGrid()
modelIndex := int(player.ID) % len(g.Models)
if modelIndex < 0 || modelIndex >= len(g.Models) {
// Prevent out of bounds access
modelIndex = 0
}
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
if anim.FrameCount > 0 {
currentFrame := player.AnimationFrame % anim.FrameCount
rl.UpdateModelAnimation(model, anim, currentFrame)
}
} else if len(modelAsset.Animations.Idle) > 0 {
anim := modelAsset.Animations.Idle[0] // Use first idle animation
if anim.FrameCount > 0 {
currentFrame := player.AnimationFrame % anim.FrameCount
rl.UpdateModelAnimation(model, anim, currentFrame)
}
}
}
// Use placeholder color if it's set, otherwise use white
var drawColor rl.Color = rl.White
if player.PlaceholderColor.A > 0 {
drawColor = player.PlaceholderColor
}
rl.DrawModel(model, playerPos, 16, drawColor)
// 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()
defer func() {
// This defer will catch any panics that might occur during rendering
// and ensure EndDrawing gets called to maintain proper graphics state
if r := recover(); r != nil {
rl.TraceLog(rl.LogError, "Panic during rendering: %v", r)
}
rl.EndDrawing()
}()
rl.ClearBackground(rl.RayWhite)
if !g.UIManager.IsLoggedIn {
g.UIManager.LoginScreen.Draw()
return
}
rl.BeginMode3D(g.Camera)
g.DrawMap()
// Draw player only if valid
if g.PlayerManager.LocalPlayer != nil && g.PlayerManager.LocalPlayer.Model.Meshes != nil {
g.DrawPlayer(g.PlayerManager.LocalPlayer, g.PlayerManager.LocalPlayer.Model)
}
// Draw other players with defensive checks
for _, other := range g.PlayerManager.OtherPlayers {
if other == nil {
continue
}
// Make sure model is assigned
if other.Model.Meshes == nil {
g.AssignModelToPlayer(other)
// Skip this frame if assignment failed
if other.Model.Meshes == nil {
continue
}
}
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.PlayerManager.LocalPlayer != nil && g.PlayerManager.LocalPlayer.FloatingMessage != nil {
drawFloatingMessage(g.PlayerManager.LocalPlayer.FloatingMessage)
}
for _, other := range g.PlayerManager.OtherPlayers {
if other != nil && other.FloatingMessage != nil {
drawFloatingMessage(other.FloatingMessage)
}
}
// Draw menu if open
if g.UIManager.MenuOpen {
g.DrawMenu()
}
// Only draw chat if menu is not open
if !g.UIManager.MenuOpen && g.UIManager.Chat != nil {
g.UIManager.Chat.Draw(int32(rl.GetScreenWidth()), int32(rl.GetScreenHeight()))
}
rl.DrawFPS(10, 10)
}
func (g *Game) Cleanup() {
g.cleanupOnce.Do(func() {
// Cleanup models
for _, model := range g.AssetManager.Models {
rl.UnloadModel(model.Model)
if model.Texture.ID > 0 {
rl.UnloadTexture(model.Texture)
}
}
// Unload music
if g.AssetManager.Music.Stream.Buffer != nil {
rl.UnloadMusicStream(g.AssetManager.Music)
}
// Only close the channel if it hasn't been closed yet
select {
case <-g.quitChan:
// Channel already closed, do nothing
default:
close(g.quitChan)
}
})
}
func (g *Game) HandleInput() {
clickedTile, clicked := g.GetTileAtMouse()
if clicked {
path := FindPath(GetTile(g.PlayerManager.LocalPlayer.PosTile.X, g.PlayerManager.LocalPlayer.PosTile.Y), clickedTile)
if len(path) > 1 {
g.PlayerManager.LocalPlayer.Lock()
g.PlayerManager.LocalPlayer.TargetPath = path[1:]
g.PlayerManager.LocalPlayer.ActionQueue = append(g.PlayerManager.LocalPlayer.ActionQueue, &pb.Action{
Type: pb.Action_MOVE,
X: int32(clickedTile.X),
Y: int32(clickedTile.Y),
PlayerId: g.PlayerManager.LocalPlayer.ID,
})
g.PlayerManager.LocalPlayer.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.UIManager.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() {
// Use the cleanup method which has channel-closing safety
g.Cleanup()
}
func (g *Game) HandleServerMessages(messages []*pb.ChatMessage) {
g.UIManager.Chat.HandleServerMessages(messages)
}
func (g *Game) AssignModelToPlayer(player *types.Player) {
if player == nil {
return
}
modelAsset, found := g.AssetManager.GetModelForPlayer(player.ID)
if !found {
return
}
player.Model = modelAsset.Model
player.PlaceholderColor = modelAsset.PlaceholderColor
// Initialize animations if available
if len(modelAsset.Animations.Idle) > 0 || len(modelAsset.Animations.Walk) > 0 {
player.InitializeAnimations(modelAsset.Animations)
}
}
func (g *Game) QuitChan() <-chan struct{} {
return g.quitChan
}