Compare commits
8 Commits
v1.1.1
...
555b8118f2
Author | SHA1 | Date | |
---|---|---|---|
555b8118f2 | |||
5bf962a18d | |||
220a451475 | |||
417bf4ea63 | |||
84d63ba4c1 | |||
49b84c8540 | |||
0e509ad752 | |||
bcd63efd7b |
2
.gitignore
vendored
2
.gitignore
vendored
@ -11,3 +11,5 @@ goonscape.exe
|
|||||||
# OS files
|
# OS files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
resources/models/old_and_test/
|
279
assets/assets.go
279
assets/assets.go
@ -1,41 +1,274 @@
|
|||||||
package assets
|
package assets
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/types"
|
"gitea.boner.be/bdnugget/goonscape/types"
|
||||||
rl "github.com/gen2brain/raylib-go/raylib"
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Helper function to load animations for a model
|
||||||
|
func loadModelAnimations(animPaths map[string]string) (types.AnimationSet, error) {
|
||||||
|
var animSet types.AnimationSet
|
||||||
|
|
||||||
|
// Only try to load animations if environment variable isn't set
|
||||||
|
if os.Getenv("GOONSCAPE_DISABLE_ANIMATIONS") == "1" {
|
||||||
|
return animSet, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load idle animations if specified
|
||||||
|
if idlePath, ok := animPaths["idle"]; ok {
|
||||||
|
idleAnims := rl.LoadModelAnimations(idlePath)
|
||||||
|
if len(idleAnims) > 0 {
|
||||||
|
animSet.Idle = idleAnims
|
||||||
|
rl.TraceLog(rl.LogInfo, "Loaded idle animation: %s (%d frames, %f seconds)",
|
||||||
|
idlePath, idleAnims[0].FrameCount, float32(idleAnims[0].FrameCount)/60.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load walk animations if specified
|
||||||
|
if walkPath, ok := animPaths["walk"]; ok {
|
||||||
|
walkAnims := rl.LoadModelAnimations(walkPath)
|
||||||
|
if len(walkAnims) > 0 {
|
||||||
|
animSet.Walk = walkAnims
|
||||||
|
rl.TraceLog(rl.LogInfo, "Loaded walk animation: %s (%d frames, %f seconds)",
|
||||||
|
walkPath, walkAnims[0].FrameCount, float32(walkAnims[0].FrameCount)/60.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return animSet, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateModel checks if a model is valid and properly loaded
|
||||||
|
func ValidateModel(model rl.Model) error {
|
||||||
|
if model.Meshes == nil {
|
||||||
|
return fmt.Errorf("model has nil meshes")
|
||||||
|
}
|
||||||
|
if model.Meshes.VertexCount <= 0 {
|
||||||
|
return fmt.Errorf("model has invalid vertex count")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompletelyAvoidExternalModels determines if we should avoid loading external models
|
||||||
|
func CompletelyAvoidExternalModels() bool {
|
||||||
|
return os.Getenv("GOONSCAPE_SAFE_MODE") == "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
func LoadModels() ([]types.ModelAsset, error) {
|
func LoadModels() ([]types.ModelAsset, error) {
|
||||||
goonerModel := rl.LoadModel("resources/models/goonion.obj")
|
// Force safe mode for now until we fix the segfault
|
||||||
goonerTexture := rl.LoadTexture("resources/models/goonion.png")
|
os.Setenv("GOONSCAPE_SAFE_MODE", "1")
|
||||||
rl.SetMaterialTexture(goonerModel.Materials, rl.MapDiffuse, goonerTexture)
|
|
||||||
|
|
||||||
coomerModel := rl.LoadModel("resources/models/coomer.obj")
|
models := make([]types.ModelAsset, 0, 3)
|
||||||
coomerTexture := rl.LoadTexture("resources/models/coomer.png")
|
|
||||||
rl.SetMaterialTexture(coomerModel.Materials, rl.MapDiffuse, coomerTexture)
|
|
||||||
|
|
||||||
shrekeModel := rl.LoadModel("resources/models/shreke.obj")
|
// Use environment variable to completely disable model loading
|
||||||
shrekeTexture := rl.LoadTexture("resources/models/shreke.png")
|
safeMode := CompletelyAvoidExternalModels()
|
||||||
rl.SetMaterialTexture(shrekeModel.Materials, rl.MapDiffuse, shrekeTexture)
|
|
||||||
|
|
||||||
return []types.ModelAsset{
|
// Colors for the different models
|
||||||
{Model: goonerModel, Texture: goonerTexture},
|
goonerColor := rl.Color{R: 255, G: 200, B: 200, A: 255} // Pinkish
|
||||||
{Model: coomerModel, Texture: coomerTexture},
|
coomerColor := rl.Color{R: 200, G: 230, B: 255, A: 255} // Light blue
|
||||||
{Model: shrekeModel, Texture: shrekeTexture},
|
shrekeColor := rl.Color{R: 180, G: 255, B: 180, A: 255} // Light green
|
||||||
}, nil
|
|
||||||
|
// If in safe mode, create all models directly without loading
|
||||||
|
if safeMode {
|
||||||
|
// Gooner model (cube)
|
||||||
|
cube := createPrimitiveShape(0)
|
||||||
|
models = append(models, types.ModelAsset{
|
||||||
|
Model: cube,
|
||||||
|
YOffset: 0.0,
|
||||||
|
PlaceholderColor: goonerColor,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Coomer model (sphere)
|
||||||
|
sphere := createPrimitiveShape(1)
|
||||||
|
models = append(models, types.ModelAsset{
|
||||||
|
Model: sphere,
|
||||||
|
YOffset: -4.0,
|
||||||
|
PlaceholderColor: coomerColor,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Shreke model (cylinder)
|
||||||
|
cylinder := createPrimitiveShape(2)
|
||||||
|
models = append(models, types.ModelAsset{
|
||||||
|
Model: cylinder,
|
||||||
|
YOffset: 0.0,
|
||||||
|
PlaceholderColor: shrekeColor,
|
||||||
|
})
|
||||||
|
|
||||||
|
return models, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// The rest of the function with normal model loading
|
||||||
|
// Load Goonion model with error handling
|
||||||
|
var goonerModel rl.Model
|
||||||
|
var success bool
|
||||||
|
var modelColor rl.Color
|
||||||
|
|
||||||
|
goonerModel, success, modelColor = SafeLoadModel("resources/models/gooner/walk_no_y_transform.glb", 0, goonerColor)
|
||||||
|
|
||||||
|
// Create animations only if model was loaded successfully
|
||||||
|
var goonerAnims types.AnimationSet
|
||||||
|
if success {
|
||||||
|
goonerAnims, _ = loadModelAnimations(map[string]string{
|
||||||
|
"idle": "resources/models/gooner/idle_no_y_transform.glb",
|
||||||
|
"walk": "resources/models/gooner/walk_no_y_transform.glb",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Apply transformations
|
||||||
|
transform := rl.MatrixIdentity()
|
||||||
|
transform = rl.MatrixMultiply(transform, rl.MatrixRotateY(180*rl.Deg2rad))
|
||||||
|
transform = rl.MatrixMultiply(transform, rl.MatrixRotateX(-90*rl.Deg2rad))
|
||||||
|
transform = rl.MatrixMultiply(transform, rl.MatrixScale(1.0, 1.0, 1.0))
|
||||||
|
goonerModel.Transform = transform
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always add a model (real or placeholder)
|
||||||
|
models = append(models, types.ModelAsset{
|
||||||
|
Model: goonerModel,
|
||||||
|
Animation: append(goonerAnims.Idle, goonerAnims.Walk...),
|
||||||
|
AnimFrames: int32(len(goonerAnims.Idle) + len(goonerAnims.Walk)),
|
||||||
|
Animations: goonerAnims,
|
||||||
|
YOffset: 0.0,
|
||||||
|
PlaceholderColor: modelColor,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
if success {
|
||||||
|
// Only load animations if the model loaded successfully
|
||||||
|
coomerAnims, _ := loadModelAnimations(map[string]string{
|
||||||
|
"idle": "resources/models/coomer/idle_notransy.glb",
|
||||||
|
"walk": "resources/models/coomer/unsteadywalk_notransy.glb",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Apply transformations
|
||||||
|
transform := rl.MatrixIdentity()
|
||||||
|
transform = rl.MatrixMultiply(transform, rl.MatrixRotateY(180*rl.Deg2rad))
|
||||||
|
transform = rl.MatrixMultiply(transform, rl.MatrixRotateX(-90*rl.Deg2rad))
|
||||||
|
transform = rl.MatrixMultiply(transform, rl.MatrixScale(1.0, 1.0, 1.0))
|
||||||
|
coomerModel.Transform = transform
|
||||||
|
|
||||||
|
models = append(models, types.ModelAsset{
|
||||||
|
Model: coomerModel,
|
||||||
|
Animation: append(coomerAnims.Idle, coomerAnims.Walk...),
|
||||||
|
AnimFrames: int32(len(coomerAnims.Idle) + len(coomerAnims.Walk)),
|
||||||
|
Animations: coomerAnims,
|
||||||
|
YOffset: -4.0,
|
||||||
|
PlaceholderColor: rl.Color{}, // Not a placeholder
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Add a placeholder with different shape/color
|
||||||
|
models = append(models, types.ModelAsset{
|
||||||
|
Model: coomerModel,
|
||||||
|
YOffset: -4.0,
|
||||||
|
PlaceholderColor: modelColor,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shreke model with safe loading - using a cylinder shape
|
||||||
|
var shrekeModel rl.Model
|
||||||
|
shrekeModel, success, modelColor = SafeLoadModel("resources/models/shreke.obj", 2, shrekeColor)
|
||||||
|
|
||||||
|
if success {
|
||||||
|
// Only proceed with texture if model loaded
|
||||||
|
shrekeTexture := rl.LoadTexture("resources/models/shreke.png")
|
||||||
|
if shrekeTexture.ID <= 0 {
|
||||||
|
rl.TraceLog(rl.LogWarning, "Failed to load shreke texture")
|
||||||
|
} else {
|
||||||
|
rl.SetMaterialTexture(shrekeModel.Materials, rl.MapDiffuse, shrekeTexture)
|
||||||
|
|
||||||
|
models = append(models, types.ModelAsset{
|
||||||
|
Model: shrekeModel,
|
||||||
|
Texture: shrekeTexture,
|
||||||
|
YOffset: 0.0,
|
||||||
|
PlaceholderColor: rl.Color{}, // Not a placeholder
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Add another placeholder with different shape/color
|
||||||
|
models = append(models, types.ModelAsset{
|
||||||
|
Model: shrekeModel,
|
||||||
|
YOffset: 0.0,
|
||||||
|
PlaceholderColor: modelColor,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(models) == 0 {
|
||||||
|
return nil, fmt.Errorf("failed to load any models")
|
||||||
|
}
|
||||||
|
|
||||||
|
return models, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadMusic(filename string) (rl.Music, error) {
|
func LoadMusic(filename string) (rl.Music, error) {
|
||||||
return rl.LoadMusicStream(filename), nil
|
defer func() {
|
||||||
}
|
// Recover from any panics during music loading
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
rl.TraceLog(rl.LogError, "Panic in LoadMusic: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
func UnloadModels(models []types.ModelAsset) {
|
// Skip loading music if environment variable is set
|
||||||
for _, model := range models {
|
if os.Getenv("GOONSCAPE_DISABLE_AUDIO") == "1" {
|
||||||
rl.UnloadModel(model.Model)
|
rl.TraceLog(rl.LogInfo, "Audio disabled, skipping music loading")
|
||||||
rl.UnloadTexture(model.Texture)
|
return rl.Music{}, fmt.Errorf("audio disabled")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func UnloadMusic(music rl.Music) {
|
music := rl.LoadMusicStream(filename)
|
||||||
rl.UnloadMusicStream(music)
|
if music.Stream.Buffer == nil {
|
||||||
|
return music, fmt.Errorf("failed to load music: %s", filename)
|
||||||
|
}
|
||||||
|
return music, nil
|
||||||
}
|
}
|
||||||
|
21
constants.go
21
constants.go
@ -2,20 +2,11 @@ package main
|
|||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
|
// Game world constants
|
||||||
const (
|
const (
|
||||||
MapWidth = 50
|
// Server-related constants
|
||||||
MapHeight = 50
|
ServerTickRate = 600 * time.Millisecond // RuneScape-style tick rate
|
||||||
TileSize = 32
|
ClientTickRate = 50 * time.Millisecond // Client runs at higher rate for smooth rendering
|
||||||
TileHeight = 2.0
|
MaxTickDesync = 5 // Max ticks behind before forcing resync
|
||||||
TickRate = 600 * time.Millisecond // Server tick rate (600ms)
|
DefaultPort = "6969" // Default server port
|
||||||
serverAddr = "localhost:6969"
|
|
||||||
|
|
||||||
// RuneScape-style tick rate (600ms)
|
|
||||||
ServerTickRate = 600 * time.Millisecond
|
|
||||||
|
|
||||||
// Client might run at a higher tick rate for smooth rendering
|
|
||||||
ClientTickRate = 50 * time.Millisecond
|
|
||||||
|
|
||||||
// Maximum number of ticks we can get behind before forcing a resync
|
|
||||||
MaxTickDesync = 5
|
|
||||||
)
|
)
|
||||||
|
@ -10,6 +10,7 @@ var (
|
|||||||
cameraDistance = float32(20.0)
|
cameraDistance = float32(20.0)
|
||||||
cameraYaw = float32(145.0)
|
cameraYaw = float32(145.0)
|
||||||
cameraPitch = float32(45.0)
|
cameraPitch = float32(45.0)
|
||||||
|
lastMousePos rl.Vector2 // Add this to track mouse movement
|
||||||
)
|
)
|
||||||
|
|
||||||
func UpdateCamera(camera *rl.Camera3D, player rl.Vector3, deltaTime float32) {
|
func UpdateCamera(camera *rl.Camera3D, player rl.Vector3, deltaTime float32) {
|
||||||
@ -32,6 +33,34 @@ func UpdateCamera(camera *rl.Camera3D, player rl.Vector3, deltaTime float32) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle middle mouse camera rotation
|
||||||
|
if rl.IsMouseButtonDown(rl.MouseMiddleButton) {
|
||||||
|
currentMousePos := rl.GetMousePosition()
|
||||||
|
|
||||||
|
// If we just started holding the button, initialize last position
|
||||||
|
if !rl.IsMouseButtonPressed(rl.MouseMiddleButton) {
|
||||||
|
mouseDelta := rl.Vector2{
|
||||||
|
X: currentMousePos.X - lastMousePos.X,
|
||||||
|
Y: currentMousePos.Y - lastMousePos.Y,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust rotation speed as needed
|
||||||
|
cameraYaw += mouseDelta.X * 0.5 * deltaTime * 60
|
||||||
|
cameraPitch += mouseDelta.Y * 0.5 * deltaTime * 60
|
||||||
|
|
||||||
|
// Clamp pitch to prevent camera flipping
|
||||||
|
if cameraPitch < 20 {
|
||||||
|
cameraPitch = 20
|
||||||
|
}
|
||||||
|
if cameraPitch > 85 {
|
||||||
|
cameraPitch = 85
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastMousePos = currentMousePos
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the keyboard controls too
|
||||||
if rl.IsKeyDown(rl.KeyRight) {
|
if rl.IsKeyDown(rl.KeyRight) {
|
||||||
cameraYaw += 100 * deltaTime
|
cameraYaw += 100 * deltaTime
|
||||||
}
|
}
|
||||||
|
19
game/chat.go
19
game/chat.go
@ -2,6 +2,8 @@ package game
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/types"
|
"gitea.boner.be/bdnugget/goonscape/types"
|
||||||
@ -25,6 +27,7 @@ type Chat struct {
|
|||||||
cursorPos int
|
cursorPos int
|
||||||
scrollOffset int
|
scrollOffset int
|
||||||
userData interface{}
|
userData interface{}
|
||||||
|
mutex sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewChat() *Chat {
|
func NewChat() *Chat {
|
||||||
@ -49,6 +52,15 @@ func (c *Chat) AddMessage(playerID int32, content string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
|
func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
if len(messages) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Processing %d chat messages", len(messages))
|
||||||
|
|
||||||
// Convert protobuf messages to our local type
|
// Convert protobuf messages to our local type
|
||||||
for _, msg := range messages {
|
for _, msg := range messages {
|
||||||
localMsg := types.ChatMessage{
|
localMsg := types.ChatMessage{
|
||||||
@ -64,6 +76,7 @@ func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
|
|||||||
c.messages = c.messages[1:]
|
c.messages = c.messages[1:]
|
||||||
}
|
}
|
||||||
c.messages = append(c.messages, localMsg)
|
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
|
// Scroll to latest message if it's not already visible
|
||||||
visibleMessages := int((chatHeight - inputHeight) / messageHeight)
|
visibleMessages := int((chatHeight - inputHeight) / messageHeight)
|
||||||
@ -87,6 +100,9 @@ func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
|
|||||||
ExpireTime: time.Now().Add(6 * time.Second),
|
ExpireTime: time.Now().Add(6 * time.Second),
|
||||||
}
|
}
|
||||||
otherPlayer.Unlock()
|
otherPlayer.Unlock()
|
||||||
|
log.Printf("Added floating message to other player %d", msg.PlayerId)
|
||||||
|
} else {
|
||||||
|
log.Printf("Could not find other player %d to add floating message", msg.PlayerId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -94,6 +110,9 @@ func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Chat) Draw(screenWidth, screenHeight int32) {
|
func (c *Chat) Draw(screenWidth, screenHeight int32) {
|
||||||
|
c.mutex.RLock()
|
||||||
|
defer c.mutex.RUnlock()
|
||||||
|
|
||||||
// Calculate chat window width based on screen width
|
// Calculate chat window width based on screen width
|
||||||
chatWindowWidth := screenWidth - (chatMargin * 2)
|
chatWindowWidth := screenWidth - (chatMargin * 2)
|
||||||
|
|
||||||
|
216
game/game.go
216
game/game.go
@ -1,7 +1,8 @@
|
|||||||
package game
|
package game
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"fmt"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/assets"
|
"gitea.boner.be/bdnugget/goonscape/assets"
|
||||||
@ -19,9 +20,11 @@ type Game struct {
|
|||||||
Music rl.Music
|
Music rl.Music
|
||||||
Chat *Chat
|
Chat *Chat
|
||||||
MenuOpen bool
|
MenuOpen bool
|
||||||
QuitChan chan struct{} // Channel to signal shutdown
|
quitChan chan struct{}
|
||||||
loginScreen *LoginScreen
|
loginScreen *LoginScreen
|
||||||
isLoggedIn bool
|
isLoggedIn bool
|
||||||
|
cleanupOnce sync.Once
|
||||||
|
frameCounter int // For periodic logging
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() *Game {
|
func New() *Game {
|
||||||
@ -36,7 +39,7 @@ func New() *Game {
|
|||||||
Projection: rl.CameraPerspective,
|
Projection: rl.CameraPerspective,
|
||||||
},
|
},
|
||||||
Chat: NewChat(),
|
Chat: NewChat(),
|
||||||
QuitChan: make(chan struct{}),
|
quitChan: make(chan struct{}),
|
||||||
loginScreen: NewLoginScreen(),
|
loginScreen: NewLoginScreen(),
|
||||||
}
|
}
|
||||||
game.Chat.userData = game
|
game.Chat.userData = game
|
||||||
@ -44,15 +47,32 @@ func New() *Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) LoadAssets() error {
|
func (g *Game) LoadAssets() error {
|
||||||
var err error
|
var loadErr error
|
||||||
g.Models, err = assets.LoadModels()
|
defer func() {
|
||||||
if err != nil {
|
if r := recover(); r != nil {
|
||||||
return err
|
loadErr = fmt.Errorf("panic during asset loading: %v", r)
|
||||||
|
// Cleanup any partially loaded assets
|
||||||
|
g.Cleanup()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Load models with better error handling
|
||||||
|
g.Models, loadErr = assets.LoadModels()
|
||||||
|
if loadErr != nil {
|
||||||
|
return fmt.Errorf("failed to load models: %v", loadErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
g.Music, err = assets.LoadMusic("resources/audio/GoonScape2.mp3")
|
// Verify model loading
|
||||||
if err != nil {
|
for i, model := range g.Models {
|
||||||
return err
|
if model.Model.Meshes == nil {
|
||||||
|
return fmt.Errorf("model %d failed to load properly", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
@ -68,23 +88,18 @@ func (g *Game) Update(deltaTime float32) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign model based on player ID
|
|
||||||
modelIndex := int(playerID) % len(g.Models)
|
|
||||||
g.Player = &types.Player{
|
g.Player = &types.Player{
|
||||||
Speed: 50.0,
|
Speed: 50.0,
|
||||||
TargetPath: []types.Tile{},
|
TargetPath: []types.Tile{},
|
||||||
UserData: g,
|
UserData: g,
|
||||||
QuitDone: make(chan struct{}),
|
QuitDone: make(chan struct{}),
|
||||||
ID: playerID,
|
ID: playerID,
|
||||||
Model: g.Models[modelIndex].Model,
|
|
||||||
Texture: g.Models[modelIndex].Texture,
|
|
||||||
}
|
}
|
||||||
|
g.AssignModelToPlayer(g.Player)
|
||||||
|
|
||||||
go network.HandleServerCommunication(conn, playerID, g.Player, g.OtherPlayers, g.QuitChan)
|
go network.HandleServerCommunication(conn, playerID, g.Player, g.OtherPlayers, g.quitChan)
|
||||||
g.isLoggedIn = true
|
g.isLoggedIn = true
|
||||||
return
|
|
||||||
}
|
}
|
||||||
g.loginScreen.Draw()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,7 +130,28 @@ func (g *Game) Update(deltaTime float32) {
|
|||||||
g.Player.MoveTowards(g.Player.TargetPath[0], deltaTime, GetMapGrid())
|
g.Player.MoveTowards(g.Player.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, "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.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 {
|
if len(other.TargetPath) > 0 {
|
||||||
other.MoveTowards(other.TargetPath[0], deltaTime, GetMapGrid())
|
other.MoveTowards(other.TargetPath[0], deltaTime, GetMapGrid())
|
||||||
}
|
}
|
||||||
@ -153,18 +189,55 @@ func (g *Game) DrawMap() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) DrawPlayer(player *types.Player, model rl.Model) {
|
func (g *Game) DrawPlayer(player *types.Player, model rl.Model) {
|
||||||
player.Lock()
|
// No need for lock in rendering, we'll use a "take snapshot" approach
|
||||||
defer player.Unlock()
|
// 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()
|
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{
|
playerPos := rl.Vector3{
|
||||||
X: player.PosActual.X,
|
X: player.PosActual.X,
|
||||||
Y: grid[player.PosTile.X][player.PosTile.Y].Height*types.TileHeight + 16.0,
|
Y: grid[player.PosTile.X][player.PosTile.Y].Height*types.TileHeight + defaultHeight + modelAsset.YOffset,
|
||||||
Z: player.PosActual.Z,
|
Z: player.PosActual.Z,
|
||||||
}
|
}
|
||||||
|
|
||||||
rl.DrawModel(model, playerPos, 16, rl.White)
|
// 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 {
|
if player.FloatingMessage != nil {
|
||||||
screenPos := rl.GetWorldToScreen(rl.Vector3{
|
screenPos := rl.GetWorldToScreen(rl.Vector3{
|
||||||
X: playerPos.X,
|
X: playerPos.X,
|
||||||
@ -196,23 +269,43 @@ func (g *Game) DrawPlayer(player *types.Player, model rl.Model) {
|
|||||||
|
|
||||||
func (g *Game) Render() {
|
func (g *Game) Render() {
|
||||||
rl.BeginDrawing()
|
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)
|
rl.ClearBackground(rl.RayWhite)
|
||||||
|
|
||||||
if !g.isLoggedIn {
|
if !g.isLoggedIn {
|
||||||
g.loginScreen.Draw()
|
g.loginScreen.Draw()
|
||||||
rl.EndDrawing()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rl.BeginMode3D(g.Camera)
|
rl.BeginMode3D(g.Camera)
|
||||||
g.DrawMap()
|
g.DrawMap()
|
||||||
g.DrawPlayer(g.Player, g.Player.Model)
|
|
||||||
for id, other := range g.OtherPlayers {
|
// Draw player only if valid
|
||||||
|
if g.Player != nil && g.Player.Model.Meshes != nil {
|
||||||
|
g.DrawPlayer(g.Player, g.Player.Model)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw other players with defensive checks
|
||||||
|
for _, other := range g.OtherPlayers {
|
||||||
|
if other == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure model is assigned
|
||||||
if other.Model.Meshes == nil {
|
if other.Model.Meshes == nil {
|
||||||
// Assign model based on player ID for consistency
|
g.AssignModelToPlayer(other)
|
||||||
modelIndex := int(id) % len(g.Models)
|
// Skip this frame if assignment failed
|
||||||
other.Model = g.Models[modelIndex].Model
|
if other.Model.Meshes == nil {
|
||||||
other.Texture = g.Models[modelIndex].Texture
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
g.DrawPlayer(other, other.Model)
|
g.DrawPlayer(other, other.Model)
|
||||||
}
|
}
|
||||||
@ -239,12 +332,14 @@ func (g *Game) Render() {
|
|||||||
rl.DrawText(text, int32(pos.X)-textWidth/2, int32(pos.Y), 20, rl.Yellow)
|
rl.DrawText(text, int32(pos.X)-textWidth/2, int32(pos.Y), 20, rl.Yellow)
|
||||||
}
|
}
|
||||||
|
|
||||||
if g.Player.FloatingMessage != nil {
|
if g.Player != nil && g.Player.FloatingMessage != nil {
|
||||||
drawFloatingMessage(g.Player.FloatingMessage)
|
drawFloatingMessage(g.Player.FloatingMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, other := range g.OtherPlayers {
|
for _, other := range g.OtherPlayers {
|
||||||
drawFloatingMessage(other.FloatingMessage)
|
if other != nil && other.FloatingMessage != nil {
|
||||||
|
drawFloatingMessage(other.FloatingMessage)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw menu if open
|
// Draw menu if open
|
||||||
@ -253,17 +348,28 @@ func (g *Game) Render() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only draw chat if menu is not open
|
// Only draw chat if menu is not open
|
||||||
if !g.MenuOpen {
|
if !g.MenuOpen && g.Chat != nil {
|
||||||
g.Chat.Draw(int32(rl.GetScreenWidth()), int32(rl.GetScreenHeight()))
|
g.Chat.Draw(int32(rl.GetScreenWidth()), int32(rl.GetScreenHeight()))
|
||||||
}
|
}
|
||||||
|
|
||||||
rl.DrawFPS(10, 10)
|
rl.DrawFPS(10, 10)
|
||||||
rl.EndDrawing()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) Cleanup() {
|
func (g *Game) Cleanup() {
|
||||||
assets.UnloadModels(g.Models)
|
g.cleanupOnce.Do(func() {
|
||||||
assets.UnloadMusic(g.Music)
|
// Stop music first
|
||||||
|
if g.Music.Stream.Buffer != nil {
|
||||||
|
rl.StopMusicStream(g.Music)
|
||||||
|
rl.UnloadMusicStream(g.Music)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unload textures
|
||||||
|
for _, model := range g.Models {
|
||||||
|
if model.Texture.ID > 0 {
|
||||||
|
rl.UnloadTexture(model.Texture)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) HandleInput() {
|
func (g *Game) HandleInput() {
|
||||||
@ -343,12 +449,46 @@ func (g *Game) DrawMenu() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) Shutdown() {
|
func (g *Game) Shutdown() {
|
||||||
close(g.QuitChan)
|
close(g.quitChan)
|
||||||
<-g.Player.QuitDone
|
|
||||||
rl.CloseWindow()
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) HandleServerMessages(messages []*pb.ChatMessage) {
|
func (g *Game) HandleServerMessages(messages []*pb.ChatMessage) {
|
||||||
g.Chat.HandleServerMessages(messages)
|
g.Chat.HandleServerMessages(messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *Game) AssignModelToPlayer(player *types.Player) {
|
||||||
|
if player == nil {
|
||||||
|
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)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
player.Model = modelAsset.Model
|
||||||
|
player.Texture = modelAsset.Texture
|
||||||
|
player.PlaceholderColor = modelAsset.PlaceholderColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) QuitChan() <-chan struct{} {
|
||||||
|
return g.quitChan
|
||||||
|
}
|
||||||
|
125
main.go
125
main.go
@ -3,7 +3,11 @@ package main
|
|||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/game"
|
"gitea.boner.be/bdnugget/goonscape/game"
|
||||||
"gitea.boner.be/bdnugget/goonscape/network"
|
"gitea.boner.be/bdnugget/goonscape/network"
|
||||||
@ -11,11 +15,27 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// Set up panic recovery at the top level
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Printf("Recovered from fatal panic in main: %v", r)
|
||||||
|
// Give the user a chance to see the error
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Parse command line flags
|
// Parse command line flags
|
||||||
|
verbose := flag.Bool("v", false, "Also show info logs (spammy)")
|
||||||
local := flag.Bool("local", false, "Connect to local server")
|
local := flag.Bool("local", false, "Connect to local server")
|
||||||
addr := flag.String("addr", "", "Server address (host or host:port)")
|
addr := flag.String("addr", "", "Server address (host or host:port)")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
if *verbose {
|
||||||
|
rl.SetTraceLogLevel(rl.LogTrace)
|
||||||
|
} else {
|
||||||
|
rl.SetTraceLogLevel(rl.LogWarning)
|
||||||
|
}
|
||||||
|
|
||||||
// Set server address based on flags
|
// Set server address based on flags
|
||||||
if *local {
|
if *local {
|
||||||
if *addr != "" {
|
if *addr != "" {
|
||||||
@ -30,31 +50,104 @@ func main() {
|
|||||||
network.SetServerAddr(*addr)
|
network.SetServerAddr(*addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize window with error handling
|
||||||
|
rl.SetConfigFlags(rl.FlagMsaa4xHint | rl.FlagWindowResizable) // Enable MSAA and make window resizable
|
||||||
rl.InitWindow(1024, 768, "GoonScape")
|
rl.InitWindow(1024, 768, "GoonScape")
|
||||||
rl.SetExitKey(0)
|
|
||||||
defer rl.CloseWindow()
|
|
||||||
|
|
||||||
rl.InitAudioDevice()
|
rl.SetExitKey(0)
|
||||||
defer rl.CloseAudioDevice()
|
|
||||||
|
// Initialize audio with error handling
|
||||||
|
if !rl.IsAudioDeviceReady() {
|
||||||
|
rl.InitAudioDevice()
|
||||||
|
if !rl.IsAudioDeviceReady() {
|
||||||
|
log.Println("Warning: Failed to initialize audio device, continuing without audio")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a maximum of 3 attempts to load assets
|
||||||
|
var gameInstance *game.Game
|
||||||
|
var loadErr error
|
||||||
|
maxAttempts := 3
|
||||||
|
|
||||||
|
for attempt := 1; attempt <= maxAttempts; attempt++ {
|
||||||
|
gameInstance = game.New()
|
||||||
|
loadErr = gameInstance.LoadAssets()
|
||||||
|
if loadErr == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Attempt %d/%d: Failed to load assets: %v", attempt, maxAttempts, loadErr)
|
||||||
|
if attempt < maxAttempts {
|
||||||
|
log.Println("Retrying...")
|
||||||
|
gameInstance.Cleanup() // Cleanup before retrying
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if loadErr != nil {
|
||||||
|
log.Printf("Failed to load assets after %d attempts. Starting with default assets.", maxAttempts)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if gameInstance != nil {
|
||||||
|
gameInstance.Cleanup()
|
||||||
|
}
|
||||||
|
rl.CloseWindow()
|
||||||
|
if rl.IsAudioDeviceReady() {
|
||||||
|
rl.CloseAudioDevice()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
rl.SetTargetFPS(60)
|
rl.SetTargetFPS(60)
|
||||||
|
|
||||||
game := game.New()
|
// Play music if available
|
||||||
if err := game.LoadAssets(); err != nil {
|
if gameInstance.Music.Stream.Buffer != nil {
|
||||||
log.Fatalf("Failed to load assets: %v", err)
|
rl.PlayMusicStream(gameInstance.Music)
|
||||||
|
rl.SetMusicVolume(gameInstance.Music, 0.5)
|
||||||
}
|
}
|
||||||
defer game.Cleanup()
|
|
||||||
|
|
||||||
rl.PlayMusicStream(game.Music)
|
// Handle OS signals for clean shutdown
|
||||||
rl.SetMusicVolume(game.Music, 0.5)
|
sigChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
<-sigChan
|
||||||
|
if gameInstance != nil {
|
||||||
|
gameInstance.Shutdown()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Keep game loop in main thread for Raylib
|
||||||
for !rl.WindowShouldClose() {
|
for !rl.WindowShouldClose() {
|
||||||
deltaTime := rl.GetFrameTime()
|
deltaTime := rl.GetFrameTime()
|
||||||
rl.UpdateMusicStream(game.Music)
|
|
||||||
game.Update(deltaTime)
|
|
||||||
game.Render()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for clean shutdown
|
// Update music if available
|
||||||
<-game.QuitChan
|
if gameInstance.Music.Stream.Buffer != nil {
|
||||||
|
rl.UpdateMusicStream(gameInstance.Music)
|
||||||
|
}
|
||||||
|
|
||||||
|
func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Printf("Recovered from panic in game update: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
gameInstance.Update(deltaTime)
|
||||||
|
}()
|
||||||
|
|
||||||
|
func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Printf("Recovered from panic in game render: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
gameInstance.Render()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Check if game requested shutdown
|
||||||
|
select {
|
||||||
|
case <-gameInstance.QuitChan():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/types"
|
"gitea.boner.be/bdnugget/goonscape/types"
|
||||||
@ -17,10 +18,11 @@ import (
|
|||||||
|
|
||||||
const protoVersion = 1
|
const protoVersion = 1
|
||||||
|
|
||||||
var serverAddr = "boner.be:6969"
|
var serverAddr = "boner.be:6969" // Default server address
|
||||||
|
|
||||||
func SetServerAddr(addr string) {
|
func SetServerAddr(addr string) {
|
||||||
serverAddr = addr
|
serverAddr = addr
|
||||||
|
log.Printf("Server address set to: %s", serverAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConnectToServer(username, password string, isRegistering bool) (net.Conn, int32, error) {
|
func ConnectToServer(username, password string, isRegistering bool) (net.Conn, int32, error) {
|
||||||
@ -91,19 +93,32 @@ 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{}) {
|
func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Player, otherPlayers map[int32]*types.Player, quitChan <-chan struct{}) {
|
||||||
reader := bufio.NewReader(conn)
|
reader := bufio.NewReader(conn)
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Printf("Recovered from panic in HandleServerCommunication: %v", r)
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
if player.QuitDone != nil {
|
||||||
|
close(player.QuitDone)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
actionTicker := time.NewTicker(types.ClientTickRate)
|
actionTicker := time.NewTicker(types.ClientTickRate)
|
||||||
defer actionTicker.Stop()
|
defer actionTicker.Stop()
|
||||||
defer conn.Close()
|
|
||||||
defer close(player.QuitDone)
|
|
||||||
|
|
||||||
// Create a channel to signal when goroutines are done
|
// Create error channel for goroutine communication
|
||||||
|
errChan := make(chan error, 1)
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
|
|
||||||
// Create a set of current players to track disconnects
|
// Start message sending goroutine
|
||||||
currentPlayers := make(map[int32]bool)
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Printf("Recovered from panic in message sender: %v", r)
|
||||||
|
errChan <- fmt.Errorf("message sender panic: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-quitChan:
|
case <-quitChan:
|
||||||
@ -118,23 +133,23 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
|
|||||||
writeMessage(conn, disconnectMsg)
|
writeMessage(conn, disconnectMsg)
|
||||||
done <- struct{}{}
|
done <- struct{}{}
|
||||||
return
|
return
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
case <-actionTicker.C:
|
case <-actionTicker.C:
|
||||||
player.Lock()
|
player.Lock()
|
||||||
if len(player.ActionQueue) > 0 {
|
if len(player.ActionQueue) > 0 {
|
||||||
actions := make([]*pb.Action, len(player.ActionQueue))
|
actions := make([]*pb.Action, len(player.ActionQueue))
|
||||||
copy(actions, player.ActionQueue)
|
copy(actions, player.ActionQueue)
|
||||||
|
|
||||||
batch := &pb.ActionBatch{
|
batch := &pb.ActionBatch{
|
||||||
PlayerId: playerID,
|
PlayerId: playerID,
|
||||||
Actions: actions,
|
Actions: actions,
|
||||||
Tick: player.CurrentTick,
|
Tick: player.CurrentTick,
|
||||||
}
|
}
|
||||||
|
|
||||||
player.ActionQueue = player.ActionQueue[:0]
|
player.ActionQueue = player.ActionQueue[:0]
|
||||||
player.Unlock()
|
player.Unlock()
|
||||||
|
|
||||||
if err := writeMessage(conn, batch); err != nil {
|
if err := writeMessage(conn, batch); err != nil {
|
||||||
log.Printf("Failed to send actions to server: %v", err)
|
errChan <- err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -144,93 +159,126 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for {
|
// Main message receiving loop
|
||||||
select {
|
go func() {
|
||||||
case <-quitChan:
|
defer func() {
|
||||||
done := make(chan struct{})
|
if r := recover(); r != nil {
|
||||||
go func() {
|
log.Printf("Recovered from panic in message receiver: %v", r)
|
||||||
<-done
|
errChan <- fmt.Errorf("message receiver panic: %v", r)
|
||||||
close(player.QuitDone)
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
select {
|
select {
|
||||||
case <-done:
|
case <-quitChan:
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
case <-time.After(1 * time.Second):
|
|
||||||
log.Println("Shutdown timed out")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
// Read message length (4 bytes)
|
|
||||||
lengthBuf := make([]byte, 4)
|
|
||||||
if _, err := io.ReadFull(reader, lengthBuf); err != nil {
|
|
||||||
log.Printf("Failed to read message length: %v", err)
|
|
||||||
return
|
return
|
||||||
}
|
default:
|
||||||
messageLength := binary.BigEndian.Uint32(lengthBuf)
|
lengthBuf := make([]byte, 4)
|
||||||
|
if _, err := io.ReadFull(reader, lengthBuf); err != nil {
|
||||||
// Read the full message
|
if err != io.EOF {
|
||||||
messageBuf := make([]byte, messageLength)
|
log.Printf("Network read error: %v", err)
|
||||||
if _, err := io.ReadFull(reader, messageBuf); err != nil {
|
errChan <- fmt.Errorf("failed to read message length: %v", err)
|
||||||
log.Printf("Failed to read message body: %v", err)
|
} else {
|
||||||
return
|
log.Printf("Connection closed by server")
|
||||||
}
|
|
||||||
|
|
||||||
var serverMessage pb.ServerMessage
|
|
||||||
if err := proto.Unmarshal(messageBuf, &serverMessage); err != nil {
|
|
||||||
log.Printf("Failed to unmarshal server message: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
messageLength := binary.BigEndian.Uint32(lengthBuf)
|
||||||
player.Unlock()
|
|
||||||
|
|
||||||
for _, state := range serverMessage.Players {
|
// Sanity check message size to prevent potential memory issues
|
||||||
currentPlayers[state.PlayerId] = true
|
if messageLength > 1024*1024 { // 1MB max message size
|
||||||
if state.PlayerId == playerID {
|
log.Printf("Message size too large: %d bytes", messageLength)
|
||||||
player.Lock()
|
errChan <- fmt.Errorf("message size too large: %d bytes", messageLength)
|
||||||
// Update initial position if not set
|
return
|
||||||
if player.PosActual.X == 0 && player.PosActual.Z == 0 {
|
}
|
||||||
player.PosActual = rl.Vector3{
|
|
||||||
X: float32(state.X * types.TileSize),
|
messageBuf := make([]byte, messageLength)
|
||||||
Y: 0,
|
if _, err := io.ReadFull(reader, messageBuf); err != nil {
|
||||||
Z: float32(state.Y * types.TileSize),
|
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.PosTile = types.Tile{X: int(state.X), Y: int(state.Y)}
|
|
||||||
}
|
}
|
||||||
player.Unlock()
|
}
|
||||||
continue
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if otherPlayer, exists := otherPlayers[state.PlayerId]; exists {
|
// Remove players no longer in the server state
|
||||||
otherPlayer.UpdatePosition(state, types.ServerTickRate)
|
for id := range otherPlayers {
|
||||||
} else {
|
if id != playerID && !validPlayerIds[id] {
|
||||||
otherPlayers[state.PlayerId] = types.NewPlayer(state)
|
log.Printf("Removing player with ID: %d", id)
|
||||||
|
delete(otherPlayers, id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Remove players that are no longer in the server state
|
// Handle chat messages
|
||||||
for id := range otherPlayers {
|
if handler, ok := player.UserData.(types.ChatMessageHandler); ok && len(serverMessage.ChatMessages) > 0 {
|
||||||
if !currentPlayers[id] {
|
log.Printf("Received %d chat messages from server", len(serverMessage.ChatMessages))
|
||||||
delete(otherPlayers, id)
|
handler.HandleServerMessages(serverMessage.ChatMessages)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if handler, ok := player.UserData.(types.ChatMessageHandler); ok && len(serverMessage.ChatMessages) > 0 {
|
|
||||||
handler.HandleServerMessages(serverMessage.ChatMessages)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for error or quit signal
|
||||||
|
select {
|
||||||
|
case <-quitChan:
|
||||||
|
// Send disconnect message
|
||||||
|
disconnectMsg := &pb.ActionBatch{
|
||||||
|
PlayerId: playerID,
|
||||||
|
Actions: []*pb.Action{{
|
||||||
|
Type: pb.Action_DISCONNECT,
|
||||||
|
PlayerId: playerID,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
writeMessage(conn, disconnectMsg)
|
||||||
|
case err := <-errChan:
|
||||||
|
log.Printf("Network error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,3 +300,50 @@ func writeMessage(conn net.Conn, msg proto.Message) error {
|
|||||||
_, err = conn.Write(data)
|
_, err = conn.Write(data)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Connection struct {
|
||||||
|
conn net.Conn
|
||||||
|
playerID int32
|
||||||
|
quitChan chan struct{}
|
||||||
|
quitDone chan struct{}
|
||||||
|
closeOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConnection(username, password string, isRegistering bool) (*Connection, error) {
|
||||||
|
conn, playerID, err := ConnectToServer(username, password, isRegistering)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Connection{
|
||||||
|
conn: conn,
|
||||||
|
playerID: playerID,
|
||||||
|
quitChan: make(chan struct{}),
|
||||||
|
quitDone: make(chan struct{}),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Connection) Close() {
|
||||||
|
c.closeOnce.Do(func() {
|
||||||
|
close(c.quitChan)
|
||||||
|
// Wait with timeout for network cleanup
|
||||||
|
select {
|
||||||
|
case <-c.quitDone:
|
||||||
|
// Clean shutdown completed
|
||||||
|
case <-time.After(500 * time.Millisecond):
|
||||||
|
log.Println("Network cleanup timed out")
|
||||||
|
}
|
||||||
|
c.conn.Close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Connection) PlayerID() int32 {
|
||||||
|
return c.playerID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Connection) Start(player *types.Player, otherPlayers map[int32]*types.Player) {
|
||||||
|
go HandleServerCommunication(c.conn, c.playerID, player, otherPlayers, c.quitChan)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Connection) QuitChan() <-chan struct{} {
|
||||||
|
return c.quitChan
|
||||||
|
}
|
||||||
|
BIN
resources/models/coomer/Animation_Confused_Scratch_withSkin.glb
Normal file
BIN
resources/models/coomer/Animation_Confused_Scratch_withSkin.glb
Normal file
Binary file not shown.
BIN
resources/models/coomer/Animation_Idle_withSkin.glb
Normal file
BIN
resources/models/coomer/Animation_Idle_withSkin.glb
Normal file
Binary file not shown.
BIN
resources/models/coomer/Animation_Running_withSkin.glb
Normal file
BIN
resources/models/coomer/Animation_Running_withSkin.glb
Normal file
Binary file not shown.
BIN
resources/models/coomer/Animation_Unsteady_Walk_withSkin.glb
Normal file
BIN
resources/models/coomer/Animation_Unsteady_Walk_withSkin.glb
Normal file
Binary file not shown.
BIN
resources/models/coomer/Animation_Walking_withSkin.glb
Normal file
BIN
resources/models/coomer/Animation_Walking_withSkin.glb
Normal file
Binary file not shown.
BIN
resources/models/coomer/idle_notransy.glb
Normal file
BIN
resources/models/coomer/idle_notransy.glb
Normal file
Binary file not shown.
BIN
resources/models/coomer/unsteadywalk_notransy.glb
Normal file
BIN
resources/models/coomer/unsteadywalk_notransy.glb
Normal file
Binary file not shown.
BIN
resources/models/coomerAnim.zip
Normal file
BIN
resources/models/coomerAnim.zip
Normal file
Binary file not shown.
BIN
resources/models/gooner/idle_no_y_transform.glb
Normal file
BIN
resources/models/gooner/idle_no_y_transform.glb
Normal file
Binary file not shown.
BIN
resources/models/gooner/walk_no_y_transform.glb
Normal file
BIN
resources/models/gooner/walk_no_y_transform.glb
Normal file
Binary file not shown.
@ -1,16 +1,37 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
||||||
rl "github.com/gen2brain/raylib-go/raylib"
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *Player) MoveTowards(target Tile, deltaTime float32, mapGrid [][]Tile) {
|
type Player struct {
|
||||||
p.Lock()
|
sync.RWMutex // Keep this for network operations
|
||||||
defer p.Unlock()
|
Model rl.Model
|
||||||
|
Texture rl.Texture2D
|
||||||
|
PosActual rl.Vector3
|
||||||
|
PosTile Tile
|
||||||
|
TargetPath []Tile
|
||||||
|
Speed float32
|
||||||
|
ActionQueue []*pb.Action
|
||||||
|
ID int32
|
||||||
|
QuitDone chan struct{}
|
||||||
|
CurrentTick int64
|
||||||
|
UserData interface{}
|
||||||
|
FloatingMessage *FloatingMessage
|
||||||
|
IsMoving bool
|
||||||
|
AnimationFrame int32
|
||||||
|
LastAnimUpdate time.Time
|
||||||
|
LastUpdateTime time.Time
|
||||||
|
InterpolationProgress float32
|
||||||
|
PlaceholderColor rl.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Player) MoveTowards(target Tile, deltaTime float32, mapGrid [][]Tile) {
|
||||||
|
// No need for lock here as this is called from a single thread (game loop)
|
||||||
targetPos := rl.Vector3{
|
targetPos := rl.Vector3{
|
||||||
X: float32(target.X * TileSize),
|
X: float32(target.X * TileSize),
|
||||||
Y: mapGrid[target.X][target.Y].Height * TileHeight,
|
Y: mapGrid[target.X][target.Y].Height * TileHeight,
|
||||||
@ -19,6 +40,33 @@ func (p *Player) MoveTowards(target Tile, deltaTime float32, mapGrid [][]Tile) {
|
|||||||
|
|
||||||
direction := rl.Vector3Subtract(targetPos, p.PosActual)
|
direction := rl.Vector3Subtract(targetPos, p.PosActual)
|
||||||
distance := rl.Vector3Length(direction)
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
if distance > 0 {
|
||||||
direction = rl.Vector3Scale(direction, p.Speed*deltaTime/distance)
|
direction = rl.Vector3Scale(direction, p.Speed*deltaTime/distance)
|
||||||
}
|
}
|
||||||
@ -41,9 +89,14 @@ func NewPlayer(state *pb.PlayerState) *Player {
|
|||||||
Y: float32(state.Y * TileHeight),
|
Y: float32(state.Y * TileHeight),
|
||||||
Z: float32(state.Y * TileSize),
|
Z: float32(state.Y * TileSize),
|
||||||
},
|
},
|
||||||
PosTile: Tile{X: int(state.X), Y: int(state.Y)},
|
PosTile: Tile{X: int(state.X), Y: int(state.Y)},
|
||||||
Speed: 50.0,
|
Speed: 50.0,
|
||||||
ID: state.PlayerId,
|
ID: state.PlayerId,
|
||||||
|
IsMoving: false,
|
||||||
|
AnimationFrame: 0,
|
||||||
|
LastAnimUpdate: time.Now(),
|
||||||
|
LastUpdateTime: time.Now(),
|
||||||
|
InterpolationProgress: 1.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,6 +114,7 @@ func (p *Player) UpdatePosition(state *pb.PlayerState, tickRate time.Duration) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Player) ForceResync(state *pb.PlayerState) {
|
func (p *Player) ForceResync(state *pb.PlayerState) {
|
||||||
|
// Keep this lock since it's called from the network goroutine
|
||||||
p.Lock()
|
p.Lock()
|
||||||
defer p.Unlock()
|
defer p.Unlock()
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
||||||
@ -14,27 +13,22 @@ type Tile struct {
|
|||||||
Walkable bool
|
Walkable bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Player struct {
|
type AnimationSet struct {
|
||||||
sync.Mutex
|
Idle []rl.ModelAnimation
|
||||||
PosActual rl.Vector3
|
Walk []rl.ModelAnimation
|
||||||
PosTile Tile
|
// Can add more animation types later like:
|
||||||
TargetPath []Tile
|
// Attack []ModelAnimation
|
||||||
ActionQueue []*pb.Action
|
// Jump []ModelAnimation
|
||||||
Speed float32
|
|
||||||
Model rl.Model
|
|
||||||
Texture rl.Texture2D
|
|
||||||
ID int32
|
|
||||||
CurrentTick int64
|
|
||||||
LastUpdateTime time.Time
|
|
||||||
InterpolationProgress float32
|
|
||||||
UserData interface{}
|
|
||||||
FloatingMessage *FloatingMessage
|
|
||||||
QuitDone chan struct{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModelAsset struct {
|
type ModelAsset struct {
|
||||||
Model rl.Model
|
Model rl.Model
|
||||||
Texture rl.Texture2D
|
Texture rl.Texture2D
|
||||||
|
Animation []rl.ModelAnimation // Keep this for compatibility
|
||||||
|
AnimFrames int32 // Keep this for compatibility
|
||||||
|
Animations AnimationSet // New field for organized animations
|
||||||
|
YOffset float32 // Additional height offset (added to default 8.0)
|
||||||
|
PlaceholderColor rl.Color
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChatMessage struct {
|
type ChatMessage struct {
|
||||||
|
Reference in New Issue
Block a user