big ol refactor
This commit is contained in:
parent
b866ac879e
commit
9d60d5e9cd
131
assets/assets.go
131
assets/assets.go
@ -8,6 +8,72 @@ import (
|
||||
rl "github.com/gen2brain/raylib-go/raylib"
|
||||
)
|
||||
|
||||
// ModelLoader handles loading and fallback for 3D models
|
||||
type ModelLoader struct {
|
||||
safeMode bool
|
||||
}
|
||||
|
||||
// NewModelLoader creates a new model loader instance
|
||||
func NewModelLoader() *ModelLoader {
|
||||
return &ModelLoader{
|
||||
safeMode: os.Getenv("GOONSCAPE_SAFE_MODE") == "1",
|
||||
}
|
||||
}
|
||||
|
||||
// IsSafeMode returns if we should avoid loading external models
|
||||
func (ml *ModelLoader) IsSafeMode() bool {
|
||||
return ml.safeMode || os.Getenv("GOONSCAPE_SAFE_MODE") == "1"
|
||||
}
|
||||
|
||||
// LoadModel attempts to load a model, returning a placeholder if it fails
|
||||
func (ml *ModelLoader) LoadModel(fileName string, fallbackShape int, fallbackColor rl.Color) (rl.Model, bool, rl.Color) {
|
||||
// Don't even try to load external models in safe mode
|
||||
if ml.IsSafeMode() {
|
||||
rl.TraceLog(rl.LogInfo, "Safe mode enabled, using primitive shape instead of %s", fileName)
|
||||
return ml.createPrimitiveShape(fallbackShape), false, fallbackColor
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// Recover from any panics during model loading
|
||||
if r := recover(); r != nil {
|
||||
rl.TraceLog(rl.LogError, "Panic in LoadModel: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
// Try to load the model
|
||||
model := rl.LoadModel(fileName)
|
||||
|
||||
// Check if the model is valid
|
||||
if model.Meshes == nil || model.Meshes.VertexCount <= 0 {
|
||||
rl.TraceLog(rl.LogWarning, "Failed to load model %s, using placeholder", fileName)
|
||||
return ml.createPrimitiveShape(fallbackShape), false, fallbackColor
|
||||
}
|
||||
|
||||
// For real models, return zero color since we don't need it
|
||||
return model, true, rl.Color{}
|
||||
}
|
||||
|
||||
// createPrimitiveShape creates a simple shape without loading external models
|
||||
func (ml *ModelLoader) createPrimitiveShape(shapeType int) rl.Model {
|
||||
var mesh rl.Mesh
|
||||
|
||||
switch shapeType {
|
||||
case 0: // Cube
|
||||
mesh = rl.GenMeshCube(1.0, 2.0, 1.0)
|
||||
case 1: // Sphere
|
||||
mesh = rl.GenMeshSphere(1.0, 8, 8)
|
||||
case 2: // Cylinder
|
||||
mesh = rl.GenMeshCylinder(0.8, 2.0, 8)
|
||||
case 3: // Cone
|
||||
mesh = rl.GenMeshCone(1.0, 2.0, 8)
|
||||
default: // Default to cube
|
||||
mesh = rl.GenMeshCube(1.0, 2.0, 1.0)
|
||||
}
|
||||
|
||||
model := rl.LoadModelFromMesh(mesh)
|
||||
return model
|
||||
}
|
||||
|
||||
// Helper function to load animations for a model
|
||||
func loadModelAnimations(animPaths map[string]string) (types.AnimationSet, error) {
|
||||
var animSet types.AnimationSet
|
||||
@ -58,51 +124,8 @@ func CompletelyAvoidExternalModels() bool {
|
||||
|
||||
// SafeLoadModel attempts to load a model, returning a placeholder if it fails
|
||||
func SafeLoadModel(fileName string, fallbackShape int, fallbackColor rl.Color) (rl.Model, bool, rl.Color) {
|
||||
// Don't even try to load external models in safe mode
|
||||
if CompletelyAvoidExternalModels() {
|
||||
rl.TraceLog(rl.LogInfo, "Safe mode enabled, using primitive shape instead of %s", fileName)
|
||||
return createPrimitiveShape(fallbackShape), false, fallbackColor
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// Recover from any panics during model loading
|
||||
if r := recover(); r != nil {
|
||||
rl.TraceLog(rl.LogError, "Panic in SafeLoadModel: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
// Try to load the model
|
||||
model := rl.LoadModel(fileName)
|
||||
|
||||
// Check if the model is valid
|
||||
if model.Meshes == nil || model.Meshes.VertexCount <= 0 {
|
||||
rl.TraceLog(rl.LogWarning, "Failed to load model %s, using placeholder", fileName)
|
||||
return createPrimitiveShape(fallbackShape), false, fallbackColor
|
||||
}
|
||||
|
||||
// For real models, return zero color since we don't need it
|
||||
return model, true, rl.Color{}
|
||||
}
|
||||
|
||||
// createPrimitiveShape creates a simple shape without loading external models
|
||||
func createPrimitiveShape(shapeType int) rl.Model {
|
||||
var mesh rl.Mesh
|
||||
|
||||
switch shapeType {
|
||||
case 0: // Cube
|
||||
mesh = rl.GenMeshCube(1.0, 2.0, 1.0)
|
||||
case 1: // Sphere
|
||||
mesh = rl.GenMeshSphere(1.0, 8, 8)
|
||||
case 2: // Cylinder
|
||||
mesh = rl.GenMeshCylinder(0.8, 2.0, 8)
|
||||
case 3: // Cone
|
||||
mesh = rl.GenMeshCone(1.0, 2.0, 8)
|
||||
default: // Default to cube
|
||||
mesh = rl.GenMeshCube(1.0, 2.0, 1.0)
|
||||
}
|
||||
|
||||
model := rl.LoadModelFromMesh(mesh)
|
||||
return model
|
||||
loader := NewModelLoader()
|
||||
return loader.LoadModel(fileName, fallbackShape, fallbackColor)
|
||||
}
|
||||
|
||||
func LoadModels() ([]types.ModelAsset, error) {
|
||||
@ -110,9 +133,7 @@ func LoadModels() ([]types.ModelAsset, error) {
|
||||
os.Setenv("GOONSCAPE_SAFE_MODE", "1")
|
||||
|
||||
models := make([]types.ModelAsset, 0, 3)
|
||||
|
||||
// Use environment variable to completely disable model loading
|
||||
safeMode := CompletelyAvoidExternalModels()
|
||||
modelLoader := NewModelLoader()
|
||||
|
||||
// Colors for the different models
|
||||
goonerColor := rl.Color{R: 255, G: 200, B: 200, A: 255} // Pinkish
|
||||
@ -120,9 +141,9 @@ func LoadModels() ([]types.ModelAsset, error) {
|
||||
shrekeColor := rl.Color{R: 180, G: 255, B: 180, A: 255} // Light green
|
||||
|
||||
// If in safe mode, create all models directly without loading
|
||||
if safeMode {
|
||||
if modelLoader.IsSafeMode() {
|
||||
// Gooner model (cube)
|
||||
cube := createPrimitiveShape(0)
|
||||
cube := modelLoader.createPrimitiveShape(0)
|
||||
models = append(models, types.ModelAsset{
|
||||
Model: cube,
|
||||
YOffset: 0.0,
|
||||
@ -130,7 +151,7 @@ func LoadModels() ([]types.ModelAsset, error) {
|
||||
})
|
||||
|
||||
// Coomer model (sphere)
|
||||
sphere := createPrimitiveShape(1)
|
||||
sphere := modelLoader.createPrimitiveShape(1)
|
||||
models = append(models, types.ModelAsset{
|
||||
Model: sphere,
|
||||
YOffset: -4.0,
|
||||
@ -138,7 +159,7 @@ func LoadModels() ([]types.ModelAsset, error) {
|
||||
})
|
||||
|
||||
// Shreke model (cylinder)
|
||||
cylinder := createPrimitiveShape(2)
|
||||
cylinder := modelLoader.createPrimitiveShape(2)
|
||||
models = append(models, types.ModelAsset{
|
||||
Model: cylinder,
|
||||
YOffset: 0.0,
|
||||
@ -154,7 +175,7 @@ func LoadModels() ([]types.ModelAsset, error) {
|
||||
var success bool
|
||||
var modelColor rl.Color
|
||||
|
||||
goonerModel, success, modelColor = SafeLoadModel("resources/models/gooner/walk_no_y_transform.glb", 0, goonerColor)
|
||||
goonerModel, success, modelColor = modelLoader.LoadModel("resources/models/gooner/walk_no_y_transform.glb", 0, goonerColor)
|
||||
|
||||
// Create animations only if model was loaded successfully
|
||||
var goonerAnims types.AnimationSet
|
||||
@ -184,7 +205,7 @@ func LoadModels() ([]types.ModelAsset, error) {
|
||||
|
||||
// Coomer model with safe loading - using a sphere shape
|
||||
var coomerModel rl.Model
|
||||
coomerModel, success, modelColor = SafeLoadModel("resources/models/coomer/idle_notransy.glb", 1, coomerColor)
|
||||
coomerModel, success, modelColor = modelLoader.LoadModel("resources/models/coomer/idle_notransy.glb", 1, coomerColor)
|
||||
|
||||
if success {
|
||||
// Only load animations if the model loaded successfully
|
||||
@ -219,7 +240,7 @@ func LoadModels() ([]types.ModelAsset, error) {
|
||||
|
||||
// Shreke model with safe loading - using a cylinder shape
|
||||
var shrekeModel rl.Model
|
||||
shrekeModel, success, modelColor = SafeLoadModel("resources/models/shreke.obj", 2, shrekeColor)
|
||||
shrekeModel, success, modelColor = modelLoader.LoadModel("resources/models/shreke.obj", 2, shrekeColor)
|
||||
|
||||
if success {
|
||||
// Only proceed with texture if model loaded
|
||||
|
22
constants.go
22
constants.go
@ -9,4 +9,26 @@ const (
|
||||
ClientTickRate = 50 * time.Millisecond // Client runs at higher rate for smooth rendering
|
||||
MaxTickDesync = 5 // Max ticks behind before forcing resync
|
||||
DefaultPort = "6969" // Default server port
|
||||
|
||||
// Map constants
|
||||
MapWidth = 50
|
||||
MapHeight = 50
|
||||
TileSize = 32
|
||||
TileHeight = 2.0
|
||||
)
|
||||
|
||||
// UI constants
|
||||
const (
|
||||
ChatMargin = 10
|
||||
ChatHeight = 200
|
||||
MessageHeight = 20
|
||||
InputHeight = 30
|
||||
MaxMessages = 50
|
||||
)
|
||||
|
||||
// Environment variable names
|
||||
const (
|
||||
EnvSafeMode = "GOONSCAPE_SAFE_MODE"
|
||||
EnvDisableAnimations = "GOONSCAPE_DISABLE_ANIMATIONS"
|
||||
EnvDisableAudio = "GOONSCAPE_DISABLE_AUDIO"
|
||||
)
|
||||
|
34
game/chat.go
34
game/chat.go
@ -11,13 +11,9 @@ import (
|
||||
rl "github.com/gen2brain/raylib-go/raylib"
|
||||
)
|
||||
|
||||
// Local UI constants (these could be moved to a centralized constants package later)
|
||||
const (
|
||||
maxMessages = 50
|
||||
chatMargin = 10 // Margin from screen edges
|
||||
chatHeight = 200
|
||||
messageHeight = 20
|
||||
inputHeight = 30
|
||||
runeLimit = 256
|
||||
runeLimit = 256
|
||||
)
|
||||
|
||||
type Chat struct {
|
||||
@ -32,7 +28,7 @@ type Chat struct {
|
||||
|
||||
func NewChat() *Chat {
|
||||
return &Chat{
|
||||
messages: make([]types.ChatMessage, 0, maxMessages),
|
||||
messages: make([]types.ChatMessage, 0, types.MaxChatMessages),
|
||||
inputBuffer: make([]rune, 0, runeLimit),
|
||||
}
|
||||
}
|
||||
@ -44,7 +40,7 @@ func (c *Chat) AddMessage(playerID int32, content string) {
|
||||
Time: time.Now(),
|
||||
}
|
||||
|
||||
if len(c.messages) >= maxMessages {
|
||||
if len(c.messages) >= types.MaxChatMessages {
|
||||
c.messages = c.messages[1:]
|
||||
}
|
||||
c.messages = append(c.messages, msg)
|
||||
@ -72,14 +68,14 @@ func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
|
||||
|
||||
// Only add if it's not already in our history
|
||||
if len(c.messages) == 0 || c.messages[len(c.messages)-1].Time.UnixNano() < msg.Timestamp {
|
||||
if len(c.messages) >= maxMessages {
|
||||
if len(c.messages) >= types.MaxChatMessages {
|
||||
c.messages = c.messages[1:]
|
||||
}
|
||||
c.messages = append(c.messages, localMsg)
|
||||
log.Printf("Added chat message from %s: %s", msg.Username, msg.Content)
|
||||
|
||||
// Scroll to latest message if it's not already visible
|
||||
visibleMessages := int((chatHeight - inputHeight) / messageHeight)
|
||||
visibleMessages := int((types.ChatHeight - types.InputHeight) / types.MessageHeight)
|
||||
if len(c.messages) > visibleMessages {
|
||||
c.scrollOffset = len(c.messages) - visibleMessages
|
||||
}
|
||||
@ -114,16 +110,16 @@ func (c *Chat) Draw(screenWidth, screenHeight int32) {
|
||||
defer c.mutex.RUnlock()
|
||||
|
||||
// Calculate chat window width based on screen width
|
||||
chatWindowWidth := screenWidth - (chatMargin * 2)
|
||||
chatWindowWidth := screenWidth - (types.ChatMargin * 2)
|
||||
|
||||
// Draw chat window background
|
||||
chatX := float32(chatMargin)
|
||||
chatY := float32(screenHeight - chatHeight - chatMargin)
|
||||
rl.DrawRectangle(int32(chatX), int32(chatY), chatWindowWidth, chatHeight, rl.ColorAlpha(rl.Black, 0.5))
|
||||
chatX := float32(types.ChatMargin)
|
||||
chatY := float32(screenHeight - types.ChatHeight - types.ChatMargin)
|
||||
rl.DrawRectangle(int32(chatX), int32(chatY), chatWindowWidth, types.ChatHeight, rl.ColorAlpha(rl.Black, 0.5))
|
||||
|
||||
// Draw messages from oldest to newest
|
||||
messageY := chatY + 5
|
||||
visibleMessages := int((chatHeight - inputHeight) / messageHeight)
|
||||
visibleMessages := int((types.ChatHeight - types.InputHeight) / types.MessageHeight)
|
||||
|
||||
// Auto-scroll to bottom if no manual scrolling has occurred
|
||||
if c.scrollOffset == 0 {
|
||||
@ -145,12 +141,12 @@ func (c *Chat) Draw(screenWidth, screenHeight int32) {
|
||||
}
|
||||
text := fmt.Sprintf("%s: %s", msg.Username, msg.Content)
|
||||
rl.DrawText(text, int32(chatX)+5, int32(messageY), 20, color)
|
||||
messageY += messageHeight
|
||||
messageY += types.MessageHeight
|
||||
}
|
||||
|
||||
// Draw input field
|
||||
inputY := chatY + float32(chatHeight-inputHeight)
|
||||
rl.DrawRectangle(int32(chatX), int32(inputY), chatWindowWidth, inputHeight, rl.ColorAlpha(rl.White, 0.3))
|
||||
inputY := chatY + float32(types.ChatHeight-types.InputHeight)
|
||||
rl.DrawRectangle(int32(chatX), int32(inputY), chatWindowWidth, types.InputHeight, rl.ColorAlpha(rl.White, 0.3))
|
||||
if c.isTyping {
|
||||
inputText := string(c.inputBuffer)
|
||||
rl.DrawText(inputText, int32(chatX)+5, int32(inputY)+5, 20, rl.White)
|
||||
@ -168,7 +164,7 @@ func (c *Chat) Update() (string, bool) {
|
||||
if !c.isTyping {
|
||||
wheelMove := rl.GetMouseWheelMove()
|
||||
if wheelMove != 0 {
|
||||
maxScroll := max(0, len(c.messages)-int((chatHeight-inputHeight)/messageHeight))
|
||||
maxScroll := max(0, len(c.messages)-int((types.ChatHeight-types.InputHeight)/types.MessageHeight))
|
||||
c.scrollOffset = clamp(c.scrollOffset-int(wheelMove), 0, maxScroll)
|
||||
}
|
||||
|
||||
|
107
game/components.go
Normal file
107
game/components.go
Normal file
@ -0,0 +1,107 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"gitea.boner.be/bdnugget/goonscape/types"
|
||||
rl "github.com/gen2brain/raylib-go/raylib"
|
||||
)
|
||||
|
||||
// PlayerManager handles all player-related operations
|
||||
type PlayerManager struct {
|
||||
LocalPlayer *types.Player
|
||||
OtherPlayers map[int32]*types.Player
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewPlayerManager creates a new player manager
|
||||
func NewPlayerManager() *PlayerManager {
|
||||
return &PlayerManager{
|
||||
OtherPlayers: make(map[int32]*types.Player),
|
||||
}
|
||||
}
|
||||
|
||||
// GetPlayer returns the player with the given ID, or the local player if ID matches
|
||||
func (pm *PlayerManager) GetPlayer(id int32) *types.Player {
|
||||
pm.mutex.RLock()
|
||||
defer pm.mutex.RUnlock()
|
||||
|
||||
if pm.LocalPlayer != nil && pm.LocalPlayer.ID == id {
|
||||
return pm.LocalPlayer
|
||||
}
|
||||
|
||||
return pm.OtherPlayers[id]
|
||||
}
|
||||
|
||||
// AddPlayer adds a player to the manager
|
||||
func (pm *PlayerManager) AddPlayer(player *types.Player) {
|
||||
pm.mutex.Lock()
|
||||
defer pm.mutex.Unlock()
|
||||
|
||||
pm.OtherPlayers[player.ID] = player
|
||||
}
|
||||
|
||||
// RemovePlayer removes a player from the manager
|
||||
func (pm *PlayerManager) RemovePlayer(id int32) {
|
||||
pm.mutex.Lock()
|
||||
defer pm.mutex.Unlock()
|
||||
|
||||
delete(pm.OtherPlayers, id)
|
||||
}
|
||||
|
||||
// AssetManager handles all game assets
|
||||
type AssetManager struct {
|
||||
Models []types.ModelAsset
|
||||
Music rl.Music
|
||||
}
|
||||
|
||||
// NewAssetManager creates a new asset manager
|
||||
func NewAssetManager() *AssetManager {
|
||||
return &AssetManager{}
|
||||
}
|
||||
|
||||
// GetModelForPlayer returns the appropriate model for a player
|
||||
func (am *AssetManager) GetModelForPlayer(playerID int32) (types.ModelAsset, bool) {
|
||||
if len(am.Models) == 0 {
|
||||
return types.ModelAsset{}, false
|
||||
}
|
||||
|
||||
// Simple model assignment based on player ID
|
||||
modelIndex := int(playerID) % len(am.Models)
|
||||
return am.Models[modelIndex], true
|
||||
}
|
||||
|
||||
// UIManager manages all user interface components
|
||||
type UIManager struct {
|
||||
Chat *Chat
|
||||
LoginScreen *LoginScreen
|
||||
IsLoggedIn bool
|
||||
MenuOpen bool
|
||||
}
|
||||
|
||||
// NewUIManager creates a new UI manager
|
||||
func NewUIManager() *UIManager {
|
||||
return &UIManager{
|
||||
Chat: NewChat(),
|
||||
LoginScreen: NewLoginScreen(),
|
||||
}
|
||||
}
|
||||
|
||||
// HandleChatInput processes chat input and returns messages to send
|
||||
func (ui *UIManager) HandleChatInput() (string, bool) {
|
||||
return ui.Chat.Update()
|
||||
}
|
||||
|
||||
// DrawUI renders all UI components
|
||||
func (ui *UIManager) DrawUI(screenWidth, screenHeight int32) {
|
||||
if !ui.IsLoggedIn {
|
||||
ui.LoginScreen.Draw()
|
||||
} else {
|
||||
if ui.MenuOpen {
|
||||
// Draw menu
|
||||
}
|
||||
|
||||
// Draw chat always when logged in
|
||||
ui.Chat.Draw(screenWidth, screenHeight)
|
||||
}
|
||||
}
|
254
game/game.go
254
game/game.go
@ -1,7 +1,7 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@ -13,135 +13,163 @@ import (
|
||||
)
|
||||
|
||||
type Game struct {
|
||||
Player *types.Player
|
||||
OtherPlayers map[int32]*types.Player
|
||||
// Component-based architecture
|
||||
PlayerManager *PlayerManager
|
||||
AssetManager *AssetManager
|
||||
UIManager *UIManager
|
||||
|
||||
// Core game state
|
||||
Camera rl.Camera3D
|
||||
Models []types.ModelAsset
|
||||
Music rl.Music
|
||||
Chat *Chat
|
||||
MenuOpen bool
|
||||
quitChan chan struct{}
|
||||
loginScreen *LoginScreen
|
||||
isLoggedIn bool
|
||||
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 {
|
||||
InitWorld()
|
||||
game := &Game{
|
||||
OtherPlayers: make(map[int32]*types.Player),
|
||||
// Create managers
|
||||
playerManager := NewPlayerManager()
|
||||
assetManager := NewAssetManager()
|
||||
uiManager := NewUIManager()
|
||||
|
||||
g := &Game{
|
||||
PlayerManager: playerManager,
|
||||
AssetManager: assetManager,
|
||||
UIManager: uiManager,
|
||||
Camera: rl.Camera3D{
|
||||
Position: rl.NewVector3(0, 10, 10),
|
||||
Target: rl.NewVector3(0, 0, 0),
|
||||
Up: rl.NewVector3(0, 1, 0),
|
||||
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,
|
||||
},
|
||||
Chat: NewChat(),
|
||||
quitChan: make(chan struct{}),
|
||||
loginScreen: NewLoginScreen(),
|
||||
quitChan: make(chan struct{}),
|
||||
}
|
||||
game.Chat.userData = game
|
||||
return game
|
||||
|
||||
// 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 {
|
||||
var loadErr error
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
loadErr = fmt.Errorf("panic during asset loading: %v", r)
|
||||
// Cleanup any partially loaded assets
|
||||
g.Cleanup()
|
||||
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
|
||||
|
||||
// Load models with better error handling
|
||||
g.Models, loadErr = assets.LoadModels()
|
||||
if loadErr != nil {
|
||||
return fmt.Errorf("failed to load models: %v", loadErr)
|
||||
}
|
||||
// Update legacy field
|
||||
g.Models = models
|
||||
|
||||
// Verify model loading
|
||||
for i, model := range g.Models {
|
||||
if model.Model.Meshes == nil {
|
||||
return fmt.Errorf("model %d failed to load properly", i)
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// Load music with better error handling
|
||||
g.Music, loadErr = assets.LoadMusic("resources/audio/GoonScape2.mp3")
|
||||
if loadErr != nil {
|
||||
return fmt.Errorf("failed to load music: %v", loadErr)
|
||||
}
|
||||
|
||||
return nil
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (g *Game) Update(deltaTime float32) {
|
||||
if !g.isLoggedIn {
|
||||
username, password, isRegistering, submitted := g.loginScreen.Update()
|
||||
if submitted {
|
||||
// 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.loginScreen.SetError(err.Error())
|
||||
g.UIManager.LoginScreen.SetError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
g.Player = &types.Player{
|
||||
g.PlayerManager.LocalPlayer = &types.Player{
|
||||
Speed: 50.0,
|
||||
TargetPath: []types.Tile{},
|
||||
UserData: g,
|
||||
QuitDone: make(chan struct{}),
|
||||
ID: playerID,
|
||||
}
|
||||
g.AssignModelToPlayer(g.Player)
|
||||
g.AssignModelToPlayer(g.PlayerManager.LocalPlayer)
|
||||
|
||||
go network.HandleServerCommunication(conn, playerID, g.Player, g.OtherPlayers, g.quitChan)
|
||||
g.isLoggedIn = true
|
||||
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.MenuOpen = !g.MenuOpen
|
||||
g.UIManager.MenuOpen = !g.UIManager.MenuOpen
|
||||
return
|
||||
}
|
||||
|
||||
// Don't process other inputs if menu is open
|
||||
if g.MenuOpen {
|
||||
if g.UIManager.MenuOpen {
|
||||
return
|
||||
}
|
||||
|
||||
if message, sent := g.Chat.Update(); sent {
|
||||
g.Player.Lock()
|
||||
g.Player.ActionQueue = append(g.Player.ActionQueue, &pb.Action{
|
||||
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.Player.ID,
|
||||
PlayerId: g.PlayerManager.LocalPlayer.ID,
|
||||
})
|
||||
g.Player.Unlock()
|
||||
g.PlayerManager.LocalPlayer.Unlock()
|
||||
}
|
||||
|
||||
g.HandleInput()
|
||||
|
||||
if len(g.Player.TargetPath) > 0 {
|
||||
g.Player.MoveTowards(g.Player.TargetPath[0], deltaTime, GetMapGrid())
|
||||
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.OtherPlayers))
|
||||
for id, other := range g.OtherPlayers {
|
||||
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.OtherPlayers {
|
||||
for _, other := range g.PlayerManager.OtherPlayers {
|
||||
if other == nil {
|
||||
continue
|
||||
}
|
||||
@ -157,7 +185,19 @@ func (g *Game) Update(deltaTime float32) {
|
||||
}
|
||||
}
|
||||
|
||||
UpdateCamera(&g.Camera, g.Player.PosActual, deltaTime)
|
||||
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() {
|
||||
@ -280,8 +320,8 @@ func (g *Game) Render() {
|
||||
|
||||
rl.ClearBackground(rl.RayWhite)
|
||||
|
||||
if !g.isLoggedIn {
|
||||
g.loginScreen.Draw()
|
||||
if !g.UIManager.IsLoggedIn {
|
||||
g.UIManager.LoginScreen.Draw()
|
||||
return
|
||||
}
|
||||
|
||||
@ -289,12 +329,12 @@ func (g *Game) Render() {
|
||||
g.DrawMap()
|
||||
|
||||
// Draw player only if valid
|
||||
if g.Player != nil && g.Player.Model.Meshes != nil {
|
||||
g.DrawPlayer(g.Player, g.Player.Model)
|
||||
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.OtherPlayers {
|
||||
for _, other := range g.PlayerManager.OtherPlayers {
|
||||
if other == nil {
|
||||
continue
|
||||
}
|
||||
@ -332,24 +372,24 @@ func (g *Game) Render() {
|
||||
rl.DrawText(text, int32(pos.X)-textWidth/2, int32(pos.Y), 20, rl.Yellow)
|
||||
}
|
||||
|
||||
if g.Player != nil && g.Player.FloatingMessage != nil {
|
||||
drawFloatingMessage(g.Player.FloatingMessage)
|
||||
if g.PlayerManager.LocalPlayer != nil && g.PlayerManager.LocalPlayer.FloatingMessage != nil {
|
||||
drawFloatingMessage(g.PlayerManager.LocalPlayer.FloatingMessage)
|
||||
}
|
||||
|
||||
for _, other := range g.OtherPlayers {
|
||||
for _, other := range g.PlayerManager.OtherPlayers {
|
||||
if other != nil && other.FloatingMessage != nil {
|
||||
drawFloatingMessage(other.FloatingMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// Draw menu if open
|
||||
if g.MenuOpen {
|
||||
if g.UIManager.MenuOpen {
|
||||
g.DrawMenu()
|
||||
}
|
||||
|
||||
// Only draw chat if menu is not open
|
||||
if !g.MenuOpen && g.Chat != nil {
|
||||
g.Chat.Draw(int32(rl.GetScreenWidth()), int32(rl.GetScreenHeight()))
|
||||
if !g.UIManager.MenuOpen && g.UIManager.Chat != nil {
|
||||
g.UIManager.Chat.Draw(int32(rl.GetScreenWidth()), int32(rl.GetScreenHeight()))
|
||||
}
|
||||
|
||||
rl.DrawFPS(10, 10)
|
||||
@ -357,35 +397,37 @@ func (g *Game) Render() {
|
||||
|
||||
func (g *Game) Cleanup() {
|
||||
g.cleanupOnce.Do(func() {
|
||||
// Stop music first
|
||||
if g.Music.Stream.Buffer != nil {
|
||||
rl.StopMusicStream(g.Music)
|
||||
rl.UnloadMusicStream(g.Music)
|
||||
}
|
||||
|
||||
// Unload textures
|
||||
for _, model := range g.Models {
|
||||
// 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)
|
||||
}
|
||||
|
||||
close(g.quitChan)
|
||||
})
|
||||
}
|
||||
|
||||
func (g *Game) HandleInput() {
|
||||
clickedTile, clicked := g.GetTileAtMouse()
|
||||
if clicked {
|
||||
path := FindPath(GetTile(g.Player.PosTile.X, g.Player.PosTile.Y), clickedTile)
|
||||
path := FindPath(GetTile(g.PlayerManager.LocalPlayer.PosTile.X, g.PlayerManager.LocalPlayer.PosTile.Y), clickedTile)
|
||||
if len(path) > 1 {
|
||||
g.Player.Lock()
|
||||
g.Player.TargetPath = path[1:]
|
||||
g.Player.ActionQueue = append(g.Player.ActionQueue, &pb.Action{
|
||||
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.Player.ID,
|
||||
PlayerId: g.PlayerManager.LocalPlayer.ID,
|
||||
})
|
||||
g.Player.Unlock()
|
||||
g.PlayerManager.LocalPlayer.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -428,7 +470,7 @@ func (g *Game) DrawMenu() {
|
||||
if rl.IsMouseButtonPressed(rl.MouseLeftButton) {
|
||||
switch item {
|
||||
case "Resume":
|
||||
g.MenuOpen = false
|
||||
g.UIManager.MenuOpen = false
|
||||
case "Settings":
|
||||
// TODO: Implement settings
|
||||
case "Exit Game":
|
||||
@ -453,7 +495,7 @@ func (g *Game) Shutdown() {
|
||||
}
|
||||
|
||||
func (g *Game) HandleServerMessages(messages []*pb.ChatMessage) {
|
||||
g.Chat.HandleServerMessages(messages)
|
||||
g.UIManager.Chat.HandleServerMessages(messages)
|
||||
}
|
||||
|
||||
func (g *Game) AssignModelToPlayer(player *types.Player) {
|
||||
@ -461,32 +503,18 @@ func (g *Game) AssignModelToPlayer(player *types.Player) {
|
||||
return
|
||||
}
|
||||
|
||||
// Defensive check for empty models array
|
||||
if len(g.Models) == 0 {
|
||||
rl.TraceLog(rl.LogWarning, "No models available to assign to player")
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure model index is positive for consistent player appearances
|
||||
// Use abs value of ID to ensure consistent appearance for negative IDs
|
||||
modelIndex := abs(int(player.ID)) % len(g.Models)
|
||||
if modelIndex < 0 || modelIndex >= len(g.Models) {
|
||||
// Prevent out of bounds access
|
||||
modelIndex = 0
|
||||
}
|
||||
|
||||
rl.TraceLog(rl.LogInfo, "Assigning model %d to player %d", modelIndex, player.ID)
|
||||
modelAsset := g.Models[modelIndex]
|
||||
|
||||
// Validate model before assigning
|
||||
if modelAsset.Model.Meshes == nil {
|
||||
rl.TraceLog(rl.LogWarning, "Trying to assign invalid model to player %d", player.ID)
|
||||
modelAsset, found := g.AssetManager.GetModelForPlayer(player.ID)
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
|
||||
player.Model = modelAsset.Model
|
||||
player.Texture = modelAsset.Texture
|
||||
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{} {
|
||||
|
@ -1,91 +1,157 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"fmt"
|
||||
|
||||
"gitea.boner.be/bdnugget/goonscape/types"
|
||||
)
|
||||
|
||||
// Node represents a node in the A* pathfinding algorithm
|
||||
type Node struct {
|
||||
Tile types.Tile
|
||||
Parent *Node
|
||||
G, H, F float32
|
||||
G, H, F float32 // G = cost from start, H = heuristic to goal, F = G + H
|
||||
}
|
||||
|
||||
// PriorityQueue implements a min-heap for nodes ordered by F value
|
||||
type PriorityQueue []*Node
|
||||
|
||||
// Implement the heap.Interface for PriorityQueue
|
||||
func (pq PriorityQueue) Len() int { return len(pq) }
|
||||
|
||||
func (pq PriorityQueue) Less(i, j int) bool {
|
||||
return pq[i].F < pq[j].F
|
||||
}
|
||||
|
||||
func (pq PriorityQueue) Swap(i, j int) {
|
||||
pq[i], pq[j] = pq[j], pq[i]
|
||||
}
|
||||
|
||||
func (pq *PriorityQueue) Push(x interface{}) {
|
||||
item := x.(*Node)
|
||||
*pq = append(*pq, item)
|
||||
}
|
||||
|
||||
func (pq *PriorityQueue) Pop() interface{} {
|
||||
old := *pq
|
||||
n := len(old)
|
||||
item := old[n-1]
|
||||
*pq = old[0 : n-1]
|
||||
return item
|
||||
}
|
||||
|
||||
// Helper to check if tile is in priority queue
|
||||
func isInQueue(queue *PriorityQueue, tile types.Tile) (bool, *Node) {
|
||||
for _, node := range *queue {
|
||||
if node.Tile.X == tile.X && node.Tile.Y == tile.Y {
|
||||
return true, node
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// FindPath implements A* pathfinding algorithm with a priority queue
|
||||
func FindPath(start, end types.Tile) []types.Tile {
|
||||
openList := []*Node{}
|
||||
closedList := make(map[[2]int]bool)
|
||||
// Initialize open and closed sets
|
||||
openSet := &PriorityQueue{}
|
||||
heap.Init(openSet)
|
||||
|
||||
startNode := &Node{Tile: start, G: 0, H: heuristic(start, end)}
|
||||
closedSet := make(map[[2]int]bool)
|
||||
|
||||
// Create start node and add to open set
|
||||
startNode := &Node{
|
||||
Tile: start,
|
||||
Parent: nil,
|
||||
G: 0,
|
||||
H: heuristic(start, end),
|
||||
}
|
||||
startNode.F = startNode.G + startNode.H
|
||||
openList = append(openList, startNode)
|
||||
heap.Push(openSet, startNode)
|
||||
|
||||
for len(openList) > 0 {
|
||||
current := openList[0]
|
||||
currentIndex := 0
|
||||
for i, node := range openList {
|
||||
if node.F < current.F {
|
||||
current = node
|
||||
currentIndex = i
|
||||
}
|
||||
}
|
||||
|
||||
openList = append(openList[:currentIndex], openList[currentIndex+1:]...)
|
||||
closedList[[2]int{current.Tile.X, current.Tile.Y}] = true
|
||||
// Main search loop
|
||||
for openSet.Len() > 0 {
|
||||
// Get node with lowest F score
|
||||
current := heap.Pop(openSet).(*Node)
|
||||
|
||||
// If we reached the goal, reconstruct and return the path
|
||||
if current.Tile.X == end.X && current.Tile.Y == end.Y {
|
||||
path := []types.Tile{}
|
||||
node := current
|
||||
for node != nil {
|
||||
path = append([]types.Tile{node.Tile}, path...)
|
||||
node = node.Parent
|
||||
}
|
||||
fmt.Printf("Path found: %v\n", path)
|
||||
return path
|
||||
return reconstructPath(current)
|
||||
}
|
||||
|
||||
neighbors := GetNeighbors(current.Tile)
|
||||
for _, neighbor := range neighbors {
|
||||
if !neighbor.Walkable || closedList[[2]int{neighbor.X, neighbor.Y}] {
|
||||
// Add current to closed set
|
||||
closedSet[[2]int{current.Tile.X, current.Tile.Y}] = true
|
||||
|
||||
// Check all neighbors
|
||||
for _, neighbor := range GetNeighbors(current.Tile) {
|
||||
// Skip if in closed set or not walkable
|
||||
if !neighbor.Walkable || closedSet[[2]int{neighbor.X, neighbor.Y}] {
|
||||
continue
|
||||
}
|
||||
|
||||
// Calculate tentative G score
|
||||
tentativeG := current.G + distance(current.Tile, neighbor)
|
||||
inOpen := false
|
||||
var existingNode *Node
|
||||
for _, node := range openList {
|
||||
if node.Tile.X == neighbor.X && node.Tile.Y == neighbor.Y {
|
||||
existingNode = node
|
||||
inOpen = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Check if in open set
|
||||
inOpen, existingNode := isInQueue(openSet, neighbor)
|
||||
|
||||
// If not in open set or better path found
|
||||
if !inOpen || tentativeG < existingNode.G {
|
||||
newNode := &Node{
|
||||
Tile: neighbor,
|
||||
Parent: current,
|
||||
G: tentativeG,
|
||||
H: heuristic(neighbor, end),
|
||||
// Create or update the node
|
||||
var neighborNode *Node
|
||||
if inOpen {
|
||||
neighborNode = existingNode
|
||||
} else {
|
||||
neighborNode = &Node{
|
||||
Tile: neighbor,
|
||||
Parent: current,
|
||||
}
|
||||
}
|
||||
newNode.F = newNode.G + newNode.H
|
||||
|
||||
// Update scores
|
||||
neighborNode.G = tentativeG
|
||||
neighborNode.H = heuristic(neighbor, end)
|
||||
neighborNode.F = neighborNode.G + neighborNode.H
|
||||
neighborNode.Parent = current
|
||||
|
||||
// Add to open set if not already there
|
||||
if !inOpen {
|
||||
openList = append(openList, newNode)
|
||||
heap.Push(openSet, neighborNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No path found
|
||||
return nil
|
||||
}
|
||||
|
||||
// reconstructPath builds the path from goal node to start
|
||||
func reconstructPath(node *Node) []types.Tile {
|
||||
path := []types.Tile{}
|
||||
current := node
|
||||
|
||||
// Follow parent pointers back to start
|
||||
for current != nil {
|
||||
path = append([]types.Tile{current.Tile}, path...)
|
||||
current = current.Parent
|
||||
}
|
||||
|
||||
fmt.Printf("Path found: %v\n", path)
|
||||
return path
|
||||
}
|
||||
|
||||
// heuristic estimates cost from current to goal (Manhattan distance)
|
||||
func heuristic(a, b types.Tile) float32 {
|
||||
return float32(abs(a.X-b.X) + abs(a.Y-b.Y))
|
||||
}
|
||||
|
||||
// distance calculates cost between adjacent tiles
|
||||
func distance(a, b types.Tile) float32 {
|
||||
return 1.0 // uniform cost for now
|
||||
}
|
||||
|
||||
// GetNeighbors returns walkable tiles adjacent to the given tile
|
||||
func GetNeighbors(tile types.Tile) []types.Tile {
|
||||
directions := [][2]int{
|
||||
{1, 0}, {-1, 0}, {0, 1}, {0, -1},
|
||||
@ -104,6 +170,7 @@ func GetNeighbors(tile types.Tile) []types.Tile {
|
||||
return neighbors
|
||||
}
|
||||
|
||||
// abs returns the absolute value of x
|
||||
func abs(x int) int {
|
||||
if x < 0 {
|
||||
return -x
|
||||
|
@ -1,9 +1,36 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"runtime/debug"
|
||||
|
||||
rl "github.com/gen2brain/raylib-go/raylib"
|
||||
)
|
||||
|
||||
// SafeExecute runs a function and recovers from panics
|
||||
func SafeExecute(action func() error) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
stack := debug.Stack()
|
||||
log.Printf("Recovered from panic: %v\nStack trace:\n%s", r, stack)
|
||||
err = fmt.Errorf("recovered from panic: %v", r)
|
||||
}
|
||||
}()
|
||||
return action()
|
||||
}
|
||||
|
||||
// SafeExecuteVoid runs a void function and recovers from panics
|
||||
func SafeExecuteVoid(action func()) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
stack := debug.Stack()
|
||||
log.Printf("Recovered from panic: %v\nStack trace:\n%s", r, stack)
|
||||
}
|
||||
}()
|
||||
action()
|
||||
}
|
||||
|
||||
func RayIntersectsBox(ray rl.Ray, boxMin, boxMax rl.Vector3) bool {
|
||||
tmin := (boxMin.X - ray.Position.X) / ray.Direction.X
|
||||
tmax := (boxMax.X - ray.Position.X) / ray.Direction.X
|
||||
|
@ -26,6 +26,138 @@ func SetServerAddr(addr string) {
|
||||
log.Printf("Server address set to: %s", serverAddr)
|
||||
}
|
||||
|
||||
// MessageHandler handles reading and writing protobuf messages
|
||||
type MessageHandler struct {
|
||||
conn net.Conn
|
||||
reader *bufio.Reader
|
||||
}
|
||||
|
||||
// NewMessageHandler creates a new message handler
|
||||
func NewMessageHandler(conn net.Conn) *MessageHandler {
|
||||
return &MessageHandler{
|
||||
conn: conn,
|
||||
reader: bufio.NewReader(conn),
|
||||
}
|
||||
}
|
||||
|
||||
// ReadMessage reads a single message from the network
|
||||
func (mh *MessageHandler) ReadMessage() (*pb.ServerMessage, error) {
|
||||
// Read message length
|
||||
lengthBuf := make([]byte, 4)
|
||||
if _, err := io.ReadFull(mh.reader, lengthBuf); err != nil {
|
||||
return nil, fmt.Errorf("failed to read message length: %v", err)
|
||||
}
|
||||
|
||||
messageLength := binary.BigEndian.Uint32(lengthBuf)
|
||||
|
||||
// Sanity check message size
|
||||
if messageLength > 1024*1024 { // 1MB max message size
|
||||
return nil, fmt.Errorf("message size too large: %d bytes", messageLength)
|
||||
}
|
||||
|
||||
// Read message body
|
||||
messageBuf := make([]byte, messageLength)
|
||||
if _, err := io.ReadFull(mh.reader, messageBuf); err != nil {
|
||||
return nil, fmt.Errorf("failed to read message body: %v", err)
|
||||
}
|
||||
|
||||
// Unmarshal the message
|
||||
var message pb.ServerMessage
|
||||
if err := proto.Unmarshal(messageBuf, &message); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal message: %v", err)
|
||||
}
|
||||
|
||||
return &message, nil
|
||||
}
|
||||
|
||||
// WriteMessage writes a protobuf message to the network
|
||||
func (mh *MessageHandler) WriteMessage(msg proto.Message) error {
|
||||
data, err := proto.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write length prefix
|
||||
lengthBuf := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(lengthBuf, uint32(len(data)))
|
||||
if _, err := mh.conn.Write(lengthBuf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write message body
|
||||
_, err = mh.conn.Write(data)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateGameState processes a server message and updates game state
|
||||
func UpdateGameState(serverMessage *pb.ServerMessage, player *types.Player, otherPlayers map[int32]*types.Player) {
|
||||
playerID := player.ID
|
||||
|
||||
player.Lock()
|
||||
player.CurrentTick = serverMessage.CurrentTick
|
||||
|
||||
tickDiff := serverMessage.CurrentTick - player.CurrentTick
|
||||
if tickDiff > types.MaxTickDesync {
|
||||
for _, state := range serverMessage.Players {
|
||||
if state.PlayerId == playerID {
|
||||
player.ForceResync(state)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
player.Unlock()
|
||||
|
||||
// Process player states
|
||||
validPlayerIds := make(map[int32]bool)
|
||||
for _, state := range serverMessage.Players {
|
||||
validPlayerIds[state.PlayerId] = true
|
||||
|
||||
if state.PlayerId == playerID {
|
||||
player.Lock()
|
||||
// Update initial position if not set
|
||||
if player.PosActual.X == 0 && player.PosActual.Z == 0 {
|
||||
player.PosActual = rl.Vector3{
|
||||
X: float32(state.X * types.TileSize),
|
||||
Y: 0,
|
||||
Z: float32(state.Y * types.TileSize),
|
||||
}
|
||||
player.PosTile = types.Tile{X: int(state.X), Y: int(state.Y)}
|
||||
}
|
||||
player.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
// Update or create other players
|
||||
if otherPlayer, exists := otherPlayers[state.PlayerId]; exists {
|
||||
otherPlayer.UpdatePosition(state, types.ServerTickRate)
|
||||
} else {
|
||||
log.Printf("Creating new player with ID: %d", state.PlayerId)
|
||||
otherPlayers[state.PlayerId] = types.NewPlayer(state)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove players no longer in the server state
|
||||
for id := range otherPlayers {
|
||||
if id != playerID && !validPlayerIds[id] {
|
||||
log.Printf("Removing player with ID: %d", id)
|
||||
delete(otherPlayers, id)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle chat messages
|
||||
if handler, ok := player.UserData.(types.ChatMessageHandler); ok && len(serverMessage.ChatMessages) > 0 {
|
||||
log.Printf("Received %d chat messages from server", len(serverMessage.ChatMessages))
|
||||
handler.HandleServerMessages(serverMessage.ChatMessages)
|
||||
|
||||
// Update the last seen message timestamp to the most recent message
|
||||
if len(serverMessage.ChatMessages) > 0 {
|
||||
lastMsg := serverMessage.ChatMessages[len(serverMessage.ChatMessages)-1]
|
||||
lastSeenMessageTimestamp = lastMsg.Timestamp
|
||||
log.Printf("Updated last seen message timestamp to %d", lastSeenMessageTimestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ConnectToServer(username, password string, isRegistering bool) (net.Conn, int32, error) {
|
||||
log.Printf("Connecting to server at %s...", serverAddr)
|
||||
|
||||
@ -57,6 +189,9 @@ func ConnectToServer(username, password string, isRegistering bool) (net.Conn, i
|
||||
|
||||
log.Println("Connected to server. Authenticating...")
|
||||
|
||||
// Create a message handler
|
||||
msgHandler := NewMessageHandler(conn)
|
||||
|
||||
// Send auth message
|
||||
authAction := &pb.Action{
|
||||
Type: pb.Action_LOGIN,
|
||||
@ -72,45 +207,24 @@ func ConnectToServer(username, password string, isRegistering bool) (net.Conn, i
|
||||
ProtocolVersion: protoVersion,
|
||||
}
|
||||
|
||||
if err := writeMessage(conn, authBatch); err != nil {
|
||||
if err := msgHandler.WriteMessage(authBatch); err != nil {
|
||||
conn.Close()
|
||||
return nil, 0, fmt.Errorf("failed to send auth: %v", err)
|
||||
}
|
||||
|
||||
// Read server response with timeout
|
||||
reader := bufio.NewReader(conn)
|
||||
|
||||
// Set a read deadline for authentication
|
||||
conn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||
|
||||
lengthBuf := make([]byte, 4)
|
||||
if _, err := io.ReadFull(reader, lengthBuf); err != nil {
|
||||
// Read server response
|
||||
response, err := msgHandler.ReadMessage()
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, 0, fmt.Errorf("failed to read auth response: %v", err)
|
||||
}
|
||||
messageLength := binary.BigEndian.Uint32(lengthBuf)
|
||||
|
||||
// Sanity check message size
|
||||
if messageLength > 1024*1024 { // 1MB max message size
|
||||
conn.Close()
|
||||
return nil, 0, fmt.Errorf("authentication response too large: %d bytes", messageLength)
|
||||
}
|
||||
|
||||
messageBuf := make([]byte, messageLength)
|
||||
if _, err := io.ReadFull(reader, messageBuf); err != nil {
|
||||
conn.Close()
|
||||
return nil, 0, fmt.Errorf("failed to read auth response body: %v", err)
|
||||
}
|
||||
|
||||
// Clear read deadline after authentication
|
||||
conn.SetReadDeadline(time.Time{})
|
||||
|
||||
var response pb.ServerMessage
|
||||
if err := proto.Unmarshal(messageBuf, &response); err != nil {
|
||||
conn.Close()
|
||||
return nil, 0, fmt.Errorf("failed to unmarshal auth response: %v", err)
|
||||
}
|
||||
|
||||
if response.ProtocolVersion > protoVersion {
|
||||
conn.Close()
|
||||
return nil, 0, fmt.Errorf("server requires newer protocol version (server: %d, client: %d)",
|
||||
@ -132,7 +246,8 @@ func ConnectToServer(username, password string, isRegistering bool) (net.Conn, i
|
||||
}
|
||||
|
||||
func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Player, otherPlayers map[int32]*types.Player, quitChan <-chan struct{}) {
|
||||
reader := bufio.NewReader(conn)
|
||||
msgHandler := NewMessageHandler(conn)
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("Recovered from panic in HandleServerCommunication: %v", r)
|
||||
@ -176,7 +291,7 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
|
||||
PlayerId: playerID,
|
||||
}},
|
||||
}
|
||||
writeMessage(conn, disconnectMsg)
|
||||
msgHandler.WriteMessage(disconnectMsg)
|
||||
done <- struct{}{}
|
||||
return
|
||||
case <-done:
|
||||
@ -190,7 +305,7 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
|
||||
PlayerId: playerID,
|
||||
LastSeenMessageTimestamp: lastSeenMessageTimestamp,
|
||||
}
|
||||
if err := writeMessage(conn, emptyBatch); err != nil {
|
||||
if err := msgHandler.WriteMessage(emptyBatch); err != nil {
|
||||
log.Printf("Failed to send heartbeat: %v", err)
|
||||
errChan <- err
|
||||
return
|
||||
@ -211,7 +326,7 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
|
||||
player.ActionQueue = player.ActionQueue[:0]
|
||||
player.Unlock()
|
||||
|
||||
if err := writeMessage(conn, batch); err != nil {
|
||||
if err := msgHandler.WriteMessage(batch); err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
@ -237,101 +352,21 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
|
||||
case <-quitChan:
|
||||
return
|
||||
default:
|
||||
lengthBuf := make([]byte, 4)
|
||||
if _, err := io.ReadFull(reader, lengthBuf); err != nil {
|
||||
if err != io.EOF {
|
||||
serverMessage, err := msgHandler.ReadMessage()
|
||||
if err != nil {
|
||||
if err, ok := err.(net.Error); ok && err.Timeout() {
|
||||
log.Printf("Network timeout: %v", err)
|
||||
} else if err != io.EOF {
|
||||
log.Printf("Network read error: %v", err)
|
||||
errChan <- fmt.Errorf("failed to read message length: %v", err)
|
||||
errChan <- err
|
||||
} else {
|
||||
log.Printf("Connection closed by server")
|
||||
}
|
||||
return
|
||||
}
|
||||
messageLength := binary.BigEndian.Uint32(lengthBuf)
|
||||
|
||||
// Sanity check message size to prevent potential memory issues
|
||||
if messageLength > 1024*1024 { // 1MB max message size
|
||||
log.Printf("Message size too large: %d bytes", messageLength)
|
||||
errChan <- fmt.Errorf("message size too large: %d bytes", messageLength)
|
||||
return
|
||||
}
|
||||
|
||||
messageBuf := make([]byte, messageLength)
|
||||
if _, err := io.ReadFull(reader, messageBuf); err != nil {
|
||||
log.Printf("Failed to read message body: %v", err)
|
||||
errChan <- fmt.Errorf("failed to read message body: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var serverMessage pb.ServerMessage
|
||||
if err := proto.Unmarshal(messageBuf, &serverMessage); err != nil {
|
||||
log.Printf("Failed to unmarshal server message: %v", err)
|
||||
continue // Skip this message but don't quit
|
||||
}
|
||||
|
||||
player.Lock()
|
||||
player.CurrentTick = serverMessage.CurrentTick
|
||||
|
||||
tickDiff := serverMessage.CurrentTick - player.CurrentTick
|
||||
if tickDiff > types.MaxTickDesync {
|
||||
for _, state := range serverMessage.Players {
|
||||
if state.PlayerId == playerID {
|
||||
player.ForceResync(state)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
player.Unlock()
|
||||
|
||||
// Process player states
|
||||
validPlayerIds := make(map[int32]bool)
|
||||
for _, state := range serverMessage.Players {
|
||||
validPlayerIds[state.PlayerId] = true
|
||||
|
||||
if state.PlayerId == playerID {
|
||||
player.Lock()
|
||||
// Update initial position if not set
|
||||
if player.PosActual.X == 0 && player.PosActual.Z == 0 {
|
||||
player.PosActual = rl.Vector3{
|
||||
X: float32(state.X * types.TileSize),
|
||||
Y: 0,
|
||||
Z: float32(state.Y * types.TileSize),
|
||||
}
|
||||
player.PosTile = types.Tile{X: int(state.X), Y: int(state.Y)}
|
||||
}
|
||||
player.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
// Update or create other players
|
||||
if otherPlayer, exists := otherPlayers[state.PlayerId]; exists {
|
||||
otherPlayer.UpdatePosition(state, types.ServerTickRate)
|
||||
} else {
|
||||
log.Printf("Creating new player with ID: %d", state.PlayerId)
|
||||
otherPlayers[state.PlayerId] = types.NewPlayer(state)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove players no longer in the server state
|
||||
for id := range otherPlayers {
|
||||
if id != playerID && !validPlayerIds[id] {
|
||||
log.Printf("Removing player with ID: %d", id)
|
||||
delete(otherPlayers, id)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle chat messages
|
||||
if handler, ok := player.UserData.(types.ChatMessageHandler); ok && len(serverMessage.ChatMessages) > 0 {
|
||||
log.Printf("Received %d chat messages from server", len(serverMessage.ChatMessages))
|
||||
handler.HandleServerMessages(serverMessage.ChatMessages)
|
||||
|
||||
// Update the last seen message timestamp to the most recent message
|
||||
if len(serverMessage.ChatMessages) > 0 {
|
||||
lastMsg := serverMessage.ChatMessages[len(serverMessage.ChatMessages)-1]
|
||||
lastSeenMessageTimestamp = lastMsg.Timestamp
|
||||
log.Printf("Updated last seen message timestamp to %d", lastSeenMessageTimestamp)
|
||||
}
|
||||
}
|
||||
// Process the server message
|
||||
UpdateGameState(serverMessage, player, otherPlayers)
|
||||
}
|
||||
}
|
||||
}()
|
||||
@ -348,7 +383,7 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
|
||||
PlayerId: playerID,
|
||||
}},
|
||||
}
|
||||
writeMessage(conn, disconnectMsg)
|
||||
msgHandler.WriteMessage(disconnectMsg)
|
||||
close(done)
|
||||
case err := <-errChan:
|
||||
log.Printf("Network error: %v", err)
|
||||
@ -358,21 +393,8 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
|
||||
|
||||
// Helper function to write length-prefixed messages
|
||||
func writeMessage(conn net.Conn, msg proto.Message) error {
|
||||
data, err := proto.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write length prefix
|
||||
lengthBuf := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(lengthBuf, uint32(len(data)))
|
||||
if _, err := conn.Write(lengthBuf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write message body
|
||||
_, err = conn.Write(data)
|
||||
return err
|
||||
msgHandler := NewMessageHandler(conn)
|
||||
return msgHandler.WriteMessage(msg)
|
||||
}
|
||||
|
||||
type Connection struct {
|
||||
|
102
types/player.go
102
types/player.go
@ -8,6 +8,66 @@ import (
|
||||
rl "github.com/gen2brain/raylib-go/raylib"
|
||||
)
|
||||
|
||||
// AnimationController manages animation state and updates
|
||||
type AnimationController struct {
|
||||
animations AnimationSet
|
||||
currentAnimation string // "idle" or "walk"
|
||||
frame int32
|
||||
lastUpdate time.Time
|
||||
frameCount int32
|
||||
}
|
||||
|
||||
// NewAnimationController creates a new animation controller
|
||||
func NewAnimationController(animations AnimationSet) *AnimationController {
|
||||
return &AnimationController{
|
||||
animations: animations,
|
||||
currentAnimation: "idle",
|
||||
frame: 0,
|
||||
lastUpdate: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// Update updates the animation state based on movement
|
||||
func (ac *AnimationController) Update(deltaTime float32, isMoving bool) {
|
||||
// Set the current animation based on movement
|
||||
newAnimation := "idle"
|
||||
if isMoving {
|
||||
newAnimation = "walk"
|
||||
}
|
||||
|
||||
// Reset frame counter when animation changes
|
||||
if ac.currentAnimation != newAnimation {
|
||||
ac.frame = 0
|
||||
ac.currentAnimation = newAnimation
|
||||
}
|
||||
|
||||
// Update the frame
|
||||
ac.frame += int32(deltaTime * 60)
|
||||
|
||||
// Determine which animation set to use
|
||||
var frames []rl.ModelAnimation
|
||||
if ac.currentAnimation == "walk" && len(ac.animations.Walk) > 0 {
|
||||
frames = ac.animations.Walk
|
||||
} else if len(ac.animations.Idle) > 0 {
|
||||
frames = ac.animations.Idle
|
||||
}
|
||||
|
||||
// If we have frames, ensure we loop properly
|
||||
if len(frames) > 0 && frames[0].FrameCount > 0 {
|
||||
ac.frame = ac.frame % frames[0].FrameCount
|
||||
}
|
||||
}
|
||||
|
||||
// GetAnimFrame returns the current animation frame
|
||||
func (ac *AnimationController) GetAnimFrame() int32 {
|
||||
return ac.frame
|
||||
}
|
||||
|
||||
// GetCurrentAnimation returns the current animation type
|
||||
func (ac *AnimationController) GetCurrentAnimation() string {
|
||||
return ac.currentAnimation
|
||||
}
|
||||
|
||||
type Player struct {
|
||||
sync.RWMutex // Keep this for network operations
|
||||
Model rl.Model
|
||||
@ -28,6 +88,7 @@ type Player struct {
|
||||
LastUpdateTime time.Time
|
||||
InterpolationProgress float32
|
||||
PlaceholderColor rl.Color
|
||||
AnimController *AnimationController
|
||||
}
|
||||
|
||||
func (p *Player) MoveTowards(target Tile, deltaTime float32, mapGrid [][]Tile) {
|
||||
@ -42,29 +103,29 @@ func (p *Player) MoveTowards(target Tile, deltaTime float32, mapGrid [][]Tile) {
|
||||
distance := rl.Vector3Length(direction)
|
||||
|
||||
if distance > 1.0 {
|
||||
wasMoving := p.IsMoving
|
||||
p.IsMoving = true
|
||||
|
||||
if !wasMoving {
|
||||
p.AnimationFrame = 0
|
||||
}
|
||||
|
||||
oldFrame := p.AnimationFrame
|
||||
p.AnimationFrame += int32(deltaTime * 60)
|
||||
rl.TraceLog(rl.LogDebug, "Walk frame update: %d -> %d (delta: %f)",
|
||||
oldFrame, p.AnimationFrame, deltaTime)
|
||||
} else {
|
||||
wasMoving := p.IsMoving
|
||||
p.IsMoving = false
|
||||
}
|
||||
|
||||
if wasMoving {
|
||||
p.AnimationFrame = 0
|
||||
// Update animation if controller exists
|
||||
if p.AnimController != nil {
|
||||
p.AnimController.Update(deltaTime, p.IsMoving)
|
||||
p.AnimationFrame = p.AnimController.GetAnimFrame()
|
||||
} else {
|
||||
// Legacy animation update for backward compatibility
|
||||
if p.IsMoving {
|
||||
if !p.IsMoving {
|
||||
p.AnimationFrame = 0
|
||||
}
|
||||
p.AnimationFrame += int32(deltaTime * 60)
|
||||
} else {
|
||||
wasMoving := p.IsMoving
|
||||
if wasMoving {
|
||||
p.AnimationFrame = 0
|
||||
}
|
||||
p.AnimationFrame += int32(deltaTime * 60)
|
||||
}
|
||||
|
||||
oldFrame := p.AnimationFrame
|
||||
p.AnimationFrame += int32(deltaTime * 60)
|
||||
rl.TraceLog(rl.LogDebug, "Idle frame update: %d -> %d (delta: %f)",
|
||||
oldFrame, p.AnimationFrame, deltaTime)
|
||||
}
|
||||
|
||||
if distance > 0 {
|
||||
@ -100,6 +161,11 @@ func NewPlayer(state *pb.PlayerState) *Player {
|
||||
}
|
||||
}
|
||||
|
||||
// InitializeAnimations sets up the animation controller for the player
|
||||
func (p *Player) InitializeAnimations(animations AnimationSet) {
|
||||
p.AnimController = NewAnimationController(animations)
|
||||
}
|
||||
|
||||
func (p *Player) UpdatePosition(state *pb.PlayerState, tickRate time.Duration) {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
@ -59,3 +59,12 @@ const (
|
||||
ClientTickRate = 50 * time.Millisecond
|
||||
MaxTickDesync = 5
|
||||
)
|
||||
|
||||
// UI constants
|
||||
const (
|
||||
ChatMargin = 10
|
||||
ChatHeight = 200
|
||||
MessageHeight = 20
|
||||
InputHeight = 30
|
||||
MaxChatMessages = 50
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user