Compare commits

..

No commits in common. "master" and "refactor/less-complexity" have entirely different histories.

2 changed files with 198 additions and 109 deletions

View File

@ -23,6 +23,16 @@ type Game struct {
quitChan chan struct{} quitChan chan struct{}
cleanupOnce sync.Once cleanupOnce sync.Once
frameCounter int // For periodic logging 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 { func New() *Game {
@ -43,10 +53,22 @@ func New() *Game {
Projection: rl.CameraPerspective, Projection: rl.CameraPerspective,
}, },
quitChan: make(chan struct{}), quitChan: make(chan struct{}),
// Initialize empty maps to avoid nil references
OtherPlayers: make(map[int32]*types.Player),
} }
// 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 // Set up inter-component references
g.UIManager.Chat.userData = g // Pass game instance to chat for callbacks g.Chat.userData = g // Pass game instance to chat for callbacks
// Initialize world // Initialize world
InitWorld() InitWorld()
@ -64,12 +86,17 @@ func (g *Game) LoadAssets() error {
} }
g.AssetManager.Models = models g.AssetManager.Models = models
// Update legacy field
g.Models = models
// Try to load music // Try to load music
music, err := assets.LoadMusic("resources/audio/GoonScape1.mp3") music, err := assets.LoadMusic("resources/audio/music.mp3")
if err != nil { if err != nil {
log.Printf("Warning: Failed to load music: %v", err) log.Printf("Warning: Failed to load music: %v", err)
} else { } else {
g.AssetManager.Music = music g.AssetManager.Music = music
// Update legacy field
g.Music = music
} }
return nil return nil
@ -77,10 +104,12 @@ func (g *Game) LoadAssets() error {
} }
func (g *Game) Update(deltaTime float32) { func (g *Game) Update(deltaTime float32) {
// Handle login screen if not logged in // Legacy code to maintain compatibility
if !g.UIManager.IsLoggedIn { if !g.UIManager.IsLoggedIn {
// Handle login // Handle login
username, password, isRegistering, doAuth := g.UIManager.LoginScreen.Update() username, password, isRegistering, doAuth := g.UIManager.LoginScreen.Update()
// Update legacy fields
g.isLoggedIn = g.UIManager.IsLoggedIn
if doAuth { if doAuth {
conn, playerID, err := network.ConnectToServer(username, password, isRegistering) conn, playerID, err := network.ConnectToServer(username, password, isRegistering)
@ -98,6 +127,9 @@ func (g *Game) Update(deltaTime float32) {
} }
g.AssignModelToPlayer(g.PlayerManager.LocalPlayer) g.AssignModelToPlayer(g.PlayerManager.LocalPlayer)
// Update the legacy Player field
g.Player = g.PlayerManager.LocalPlayer
// Set user data to allow chat message handling // Set user data to allow chat message handling
g.PlayerManager.LocalPlayer.UserData = g g.PlayerManager.LocalPlayer.UserData = g
@ -149,13 +181,8 @@ func (g *Game) Update(deltaTime float32) {
rl.TraceLog(rl.LogInfo, "There are %d other players", len(g.PlayerManager.OtherPlayers)) rl.TraceLog(rl.LogInfo, "There are %d other players", len(g.PlayerManager.OtherPlayers))
for id, other := range g.PlayerManager.OtherPlayers { for id, other := range g.PlayerManager.OtherPlayers {
if other != nil { if other != nil {
// Calculate tile coordinates from absolute position rl.TraceLog(rl.LogInfo, "Other player ID: %d, Position: (%f, %f, %f), Has model: %v",
tileX := int(other.PosActual.X / float32(types.TileSize)) id, other.PosActual.X, other.PosActual.Y, other.PosActual.Z, other.Model.Meshes != nil)
tileY := int(other.PosActual.Z / float32(types.TileSize))
rl.TraceLog(rl.LogInfo, "Other player ID: %d, Position: (%f, %f, %f), Tile: (%d, %d), Has model: %v",
id, other.PosActual.X, other.PosActual.Y, other.PosActual.Z,
tileX, tileY, other.Model.Meshes != nil)
} else { } else {
rl.TraceLog(rl.LogInfo, "Other player ID: %d is nil", id) rl.TraceLog(rl.LogInfo, "Other player ID: %d is nil", id)
} }
@ -186,6 +213,13 @@ func (g *Game) Update(deltaTime float32) {
if g.AssetManager.Music.Stream.Buffer != nil { if g.AssetManager.Music.Stream.Buffer != nil {
rl.UpdateMusicStream(g.AssetManager.Music) 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() { func (g *Game) DrawMap() {
@ -217,84 +251,94 @@ func (g *Game) DrawMap() {
} }
func (g *Game) DrawPlayer(player *types.Player, model rl.Model) { func (g *Game) DrawPlayer(player *types.Player, model rl.Model) {
if player == nil { // 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 return
} }
// Get necessary data grid := GetMapGrid()
modelIndex := int(player.ID) % len(g.AssetManager.Models) modelIndex := int(player.ID) % len(g.Models)
if modelIndex < 0 || modelIndex >= len(g.AssetManager.Models) { if modelIndex < 0 || modelIndex >= len(g.Models) {
// Prevent out of bounds access
modelIndex = 0 modelIndex = 0
} }
modelAsset := g.AssetManager.Models[modelIndex] modelAsset := g.Models[modelIndex]
// Calculate position const defaultHeight = 8.0 // Default height above tile, fine tune per model in types.ModelAsset
const defaultHeight = 8.0
playerPos := rl.Vector3{ playerPos := rl.Vector3{
X: player.PosActual.X, X: player.PosActual.X,
Y: player.PosActual.Y + defaultHeight + modelAsset.YOffset, Y: grid[player.PosTile.X][player.PosTile.Y].Height*types.TileHeight + defaultHeight + modelAsset.YOffset,
Z: player.PosActual.Z, Z: player.PosActual.Z,
} }
// Simple drawing with scale parameter // 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 var drawColor rl.Color = rl.White
if player.PlaceholderColor.A > 0 { if player.PlaceholderColor.A > 0 {
drawColor = player.PlaceholderColor drawColor = player.PlaceholderColor
} }
rl.DrawModel(model, playerPos, 16, drawColor)
// Draw the model at normal scale (16.0) // Draw floating messages and path indicators
rl.DrawModel(model, playerPos, 16.0, drawColor)
// Update floating message position
if player.FloatingMessage != nil { if player.FloatingMessage != nil {
worldPos := rl.Vector3{ screenPos := rl.GetWorldToScreen(rl.Vector3{
X: playerPos.X, X: playerPos.X,
Y: playerPos.Y + 24.0, // Position above head Y: playerPos.Y + 24.0,
Z: playerPos.Z, Z: playerPos.Z,
} }, g.Camera)
player.FloatingMessage.ScreenPos = rl.GetWorldToScreen(worldPos, g.Camera)
} player.FloatingMessage.ScreenPos = screenPos
} }
func (g *Game) DrawFloatingMessages() { if len(player.TargetPath) > 0 {
var drawFloatingMessage = func(msg *types.FloatingMessage) { targetTile := player.TargetPath[len(player.TargetPath)-1]
if msg == nil || time.Now().After(msg.ExpireTime) { targetPos := rl.Vector3{
return 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)
// Draw the message with RuneScape-style coloring (black outline with yellow text) nextTile := player.TargetPath[0]
text := msg.Content nextPos := rl.Vector3{
textWidth := rl.MeasureText(text, 20) X: float32(nextTile.X * types.TileSize),
Y: grid[nextTile.X][nextTile.Y].Height * types.TileHeight,
// Draw black outline by offsetting the text slightly in all directions Z: float32(nextTile.Y * types.TileSize),
for offsetX := -2; offsetX <= 2; offsetX++ {
for offsetY := -2; offsetY <= 2; offsetY++ {
rl.DrawText(text,
int32(msg.ScreenPos.X)-textWidth/2+int32(offsetX),
int32(msg.ScreenPos.Y)+int32(offsetY),
20,
rl.Black)
}
}
// Draw the yellow text on top
rl.DrawText(text, int32(msg.ScreenPos.X)-textWidth/2, int32(msg.ScreenPos.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)
} }
rl.DrawCubeWires(nextPos, types.TileSize, types.TileHeight, types.TileSize, rl.Yellow)
} }
} }
func (g *Game) Render() { func (g *Game) Render() {
rl.BeginDrawing() rl.BeginDrawing()
defer rl.EndDrawing() 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) rl.ClearBackground(rl.RayWhite)
@ -303,7 +347,6 @@ func (g *Game) Render() {
return return
} }
// Draw 3D elements
rl.BeginMode3D(g.Camera) rl.BeginMode3D(g.Camera)
g.DrawMap() g.DrawMap()
@ -318,14 +361,48 @@ func (g *Game) Render() {
continue continue
} }
if other.Model.Meshes != nil { // Make sure model is assigned
g.DrawPlayer(other, other.Model) 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() rl.EndMode3D()
// Draw floating messages with RuneScape style // Draw floating messages
g.DrawFloatingMessages() 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 // Draw menu if open
if g.UIManager.MenuOpen { if g.UIManager.MenuOpen {
@ -337,7 +414,6 @@ func (g *Game) Render() {
g.UIManager.Chat.Draw(int32(rl.GetScreenWidth()), int32(rl.GetScreenHeight())) g.UIManager.Chat.Draw(int32(rl.GetScreenWidth()), int32(rl.GetScreenHeight()))
} }
// Draw FPS counter
rl.DrawFPS(10, 10) rl.DrawFPS(10, 10)
} }
@ -388,45 +464,63 @@ func (g *Game) DrawMenu() {
screenWidth := float32(rl.GetScreenWidth()) screenWidth := float32(rl.GetScreenWidth())
screenHeight := float32(rl.GetScreenHeight()) screenHeight := float32(rl.GetScreenHeight())
// Draw semi-transparent background // Semi-transparent background
rl.DrawRectangle(0, 0, int32(screenWidth), int32(screenHeight), rl.ColorAlpha(rl.Black, 0.7)) rl.DrawRectangle(0, 0, int32(screenWidth), int32(screenHeight), rl.ColorAlpha(rl.Black, 0.7))
// Draw menu items // Menu title
menuItems := []string{"Resume", "Settings", "Quit"} title := "Menu"
menuY := screenHeight/2 - float32(len(menuItems)*40)/2 titleSize := int32(40)
titleWidth := rl.MeasureText(title, titleSize)
rl.DrawText(title, int32(screenWidth/2)-titleWidth/2, 100, titleSize, rl.White)
for i, item := range menuItems { // Menu buttons
itemY := menuY + float32(i*40) buttonWidth := float32(200)
mousePoint := rl.GetMousePosition() buttonHeight := float32(40)
itemRect := rl.Rectangle{X: screenWidth/2 - 100, Y: itemY, Width: 200, Height: 36} buttonY := float32(200)
buttonSpacing := float32(60)
// Check for hover menuItems := []string{"Resume", "Settings", "Exit Game"}
isHover := rl.CheckCollisionPointRec(mousePoint, itemRect) for _, item := range menuItems {
buttonRect := rl.Rectangle{
// Draw button background X: screenWidth/2 - buttonWidth/2,
if isHover { Y: buttonY,
rl.DrawRectangleRec(itemRect, rl.ColorAlpha(rl.White, 0.3)) Width: buttonWidth,
} else { Height: buttonHeight,
rl.DrawRectangleRec(itemRect, rl.ColorAlpha(rl.White, 0.1))
} }
// Draw button text // Check mouse hover
textWidth := rl.MeasureText(item, 20) mousePoint := rl.GetMousePosition()
rl.DrawText(item, int32(itemRect.X+(itemRect.Width-float32(textWidth))/2), int32(itemRect.Y+8), 20, rl.White) mouseHover := rl.CheckCollisionPointRec(mousePoint, buttonRect)
// Handle click // Draw button
if isHover && rl.IsMouseButtonReleased(rl.MouseLeftButton) { if mouseHover {
rl.DrawRectangleRec(buttonRect, rl.ColorAlpha(rl.White, 0.3))
if rl.IsMouseButtonPressed(rl.MouseLeftButton) {
switch item { switch item {
case "Resume": case "Resume":
g.UIManager.MenuOpen = false g.UIManager.MenuOpen = false
case "Settings": case "Settings":
// TODO: Implement settings // TODO: Implement settings
case "Quit": case "Exit Game":
g.Shutdown() g.Shutdown()
rl.CloseWindow()
} }
} }
} }
// 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) { func (g *Game) HandleServerMessages(messages []*pb.ChatMessage) {
@ -438,15 +532,6 @@ func (g *Game) HandleServerMessages(messages []*pb.ChatMessage) {
} }
} }
func (g *Game) QuitChan() <-chan struct{} {
return g.quitChan
}
func (g *Game) Shutdown() {
// Use the cleanup method which has channel-closing safety
g.Cleanup()
}
func (g *Game) AssignModelToPlayer(player *types.Player) { func (g *Game) AssignModelToPlayer(player *types.Player) {
if player == nil { if player == nil {
return return
@ -465,3 +550,7 @@ func (g *Game) AssignModelToPlayer(player *types.Player) {
player.InitializeAnimations(modelAsset.Animations) player.InitializeAnimations(modelAsset.Animations)
} }
} }
func (g *Game) QuitChan() <-chan struct{} {
return g.quitChan
}

10
main.go
View File

@ -101,9 +101,9 @@ func main() {
rl.SetTargetFPS(60) rl.SetTargetFPS(60)
// Play music if available // Play music if available
if gameInstance.AssetManager.Music.Stream.Buffer != nil { if gameInstance.Music.Stream.Buffer != nil {
rl.PlayMusicStream(gameInstance.AssetManager.Music) rl.PlayMusicStream(gameInstance.Music)
rl.SetMusicVolume(gameInstance.AssetManager.Music, 0.5) rl.SetMusicVolume(gameInstance.Music, 0.5)
} }
// Handle OS signals for clean shutdown // Handle OS signals for clean shutdown
@ -121,8 +121,8 @@ func main() {
deltaTime := rl.GetFrameTime() deltaTime := rl.GetFrameTime()
// Update music if available // Update music if available
if gameInstance.AssetManager.Music.Stream.Buffer != nil { if gameInstance.Music.Stream.Buffer != nil {
rl.UpdateMusicStream(gameInstance.AssetManager.Music) rl.UpdateMusicStream(gameInstance.Music)
} }
func() { func() {