Compare commits
13 Commits
feature/ma
...
master
Author | SHA1 | Date | |
---|---|---|---|
53cc9bca6b | |||
a1aeb71512 | |||
ef3732c53c | |||
7be859d58f | |||
fde07e3e48 | |||
f966d538d3 | |||
220a451475 | |||
417bf4ea63 | |||
84d63ba4c1 | |||
49b84c8540 | |||
0e509ad752 | |||
bcd63efd7b | |||
944c33ce3b |
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/
|
5
2
Normal file
5
2
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
INFO: 2025/02/10 14:51:58 main.go:15: Starting GoonScape client
|
||||||
|
INFO: 2025/02/10 14:51:58 main.go:38: Initializing window
|
||||||
|
INFO: 2025/02/10 14:51:58 main.go:55: Loading game assets
|
||||||
|
INFO: 2025/02/10 14:51:58 game.go:54: Loading game assets
|
||||||
|
INFO: 2025/02/10 14:51:58 assets.go:51: Loading models
|
24
LICENSE
24
LICENSE
@ -1,11 +1,21 @@
|
|||||||
“Commons Clause” License Condition v1.0
|
MIT License
|
||||||
|
|
||||||
The Software is provided to you by the Licensor under the License, as defined below, subject to the following condition.
|
Copyright (c) 2025 bdnugget
|
||||||
|
|
||||||
Without limiting other conditions in the License, the grant of rights under the License will not include, and the License does not grant to you, right to Sell the Software.
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
For purposes of the foregoing, “Sell” means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration (including without limitation fees for hosting or consulting/ support services related to the Software), a product or service whose value derives, entirely or substantially, from the functionality of the Software. Any license notice or attribution required by the License must also include this Commons Cause License Condition notice.
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
Software: GoonScape
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
License: Commons Clause v1.0
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
Licensor: bdnugget
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
12
README.md
12
README.md
@ -122,3 +122,15 @@ The project uses Protocol Buffers for network communication. If you modify the `
|
|||||||
```bash
|
```bash
|
||||||
protoc --go_out=. goonserver/actions/actions.proto
|
protoc --go_out=. goonserver/actions/actions.proto
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Fixing the models
|
||||||
|
This is a note/reminder to myself. The models and animations are generated with [meshy.ai (referral link, give me credits pls)](https://www.meshy.ai/?utm_source=referral-program&utm_medium=link&utm_content=TUZLRK). Somehow the bones armature things are not in the same root as the model and it becomes like a weird pudding when using the animation. I had to manually fix this in Blender by doing:
|
||||||
|
- Selected the char1 mesh thing
|
||||||
|
- Alt + P (the right Alt kek) => Clear Parent and Keep Transform
|
||||||
|
- Tried to do the same for Armature, they should both be in the Scene collection as siblings, so straight from root
|
||||||
|
- Select char1 and press Ctrl + A => All Transforms
|
||||||
|
- Repeat for Armature
|
||||||
|
- Select char1 and make sure it still has the Armature Modifier attached (via purple wrench icon), I remove it and reapply it just to be sure
|
||||||
|
|
||||||
|
When exporting it, I make sure Transform +Y is up direction is turned off, otherwise my gooner walks on the walls instead of the floor lmao
|
||||||
|
|
||||||
|
164
assets/assets.go
164
assets/assets.go
@ -1,38 +1,172 @@
|
|||||||
package assets
|
package assets
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"gitea.boner.be/bdnugget/goonscape/logging"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
assetMutex sync.RWMutex
|
||||||
|
loadedModels map[string]types.ModelAsset
|
||||||
|
audioMutex sync.Mutex
|
||||||
|
audioInitialized bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
loadedModels = make(map[string]types.ModelAsset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to load animations for a model
|
||||||
|
func loadModelAnimations(animPaths map[string]string) (types.AnimationSet, error) {
|
||||||
|
var animSet types.AnimationSet
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
func LoadModels() ([]types.ModelAsset, error) {
|
func LoadModels() ([]types.ModelAsset, error) {
|
||||||
goonerModel := rl.LoadModel("resources/models/goonion.obj")
|
logging.Info.Println("Loading models")
|
||||||
goonerTexture := rl.LoadTexture("resources/models/goonion.png")
|
assetMutex.Lock()
|
||||||
rl.SetMaterialTexture(goonerModel.Materials, rl.MapDiffuse, goonerTexture)
|
defer assetMutex.Unlock()
|
||||||
|
|
||||||
coomerModel := rl.LoadModel("resources/models/coomer.obj")
|
if len(loadedModels) > 0 {
|
||||||
coomerTexture := rl.LoadTexture("resources/models/coomer.png")
|
logging.Info.Println("Returning cached models")
|
||||||
rl.SetMaterialTexture(coomerModel.Materials, rl.MapDiffuse, coomerTexture)
|
models := make([]types.ModelAsset, 0, len(loadedModels))
|
||||||
|
for _, model := range loadedModels {
|
||||||
|
models = append(models, model)
|
||||||
|
}
|
||||||
|
return models, nil
|
||||||
|
}
|
||||||
|
|
||||||
shrekeModel := rl.LoadModel("resources/models/shreke.obj")
|
// Goonion model and animations
|
||||||
shrekeTexture := rl.LoadTexture("resources/models/shreke.png")
|
goonerModel := rl.LoadModel("resources/models/gooner/walk_no_y_transform.glb")
|
||||||
rl.SetMaterialTexture(shrekeModel.Materials, rl.MapDiffuse, shrekeTexture)
|
goonerAnims, err := loadModelAnimations(map[string]string{
|
||||||
|
"idle": "resources/models/gooner/idle_no_y_transform.glb",
|
||||||
|
"walk": "resources/models/gooner/walk_no_y_transform.glb",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return []types.ModelAsset{
|
// Apply transformations
|
||||||
{Model: goonerModel, Texture: goonerTexture},
|
transform := rl.MatrixIdentity()
|
||||||
{Model: coomerModel, Texture: coomerTexture},
|
transform = rl.MatrixMultiply(transform, rl.MatrixRotateY(180*rl.Deg2rad))
|
||||||
{Model: shrekeModel, Texture: shrekeTexture},
|
transform = rl.MatrixMultiply(transform, rl.MatrixRotateX(-90*rl.Deg2rad))
|
||||||
}, nil
|
transform = rl.MatrixMultiply(transform, rl.MatrixScale(1.0, 1.0, 1.0))
|
||||||
|
goonerModel.Transform = transform
|
||||||
|
|
||||||
|
// Coomer model (ready for animations)
|
||||||
|
coomerModel := rl.LoadModel("resources/models/coomer/idle_notransy.glb")
|
||||||
|
coomerAnims, err := loadModelAnimations(map[string]string{
|
||||||
|
"idle": "resources/models/coomer/idle_notransy.glb",
|
||||||
|
"walk": "resources/models/coomer/unsteadywalk_notransy.glb",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
coomerModel.Transform = transform
|
||||||
|
|
||||||
|
// Shreke model (ready for animations)
|
||||||
|
shrekeModel := rl.LoadModel("resources/models/shreke/Animation_Slow_Orc_Walk_withSkin.glb")
|
||||||
|
shrekeAnims, err := loadModelAnimations(map[string]string{
|
||||||
|
"idle": "resources/models/shreke/Animation_Slow_Orc_Walk_withSkin.glb",
|
||||||
|
"walk": "resources/models/shreke/Animation_Excited_Walk_M_withSkin.glb",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
shrekeModel.Transform = transform
|
||||||
|
|
||||||
|
// Store loaded models
|
||||||
|
models := []types.ModelAsset{
|
||||||
|
{
|
||||||
|
Name: "gooner",
|
||||||
|
Model: goonerModel,
|
||||||
|
Animation: append(goonerAnims.Idle, goonerAnims.Walk...),
|
||||||
|
AnimFrames: int32(len(goonerAnims.Idle) + len(goonerAnims.Walk)),
|
||||||
|
Animations: goonerAnims,
|
||||||
|
YOffset: 0.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Model: coomerModel,
|
||||||
|
Animation: append(coomerAnims.Idle, coomerAnims.Walk...),
|
||||||
|
AnimFrames: int32(len(coomerAnims.Idle) + len(coomerAnims.Walk)),
|
||||||
|
Animations: coomerAnims,
|
||||||
|
YOffset: -4.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Model: shrekeModel,
|
||||||
|
Animation: append(shrekeAnims.Idle, shrekeAnims.Walk...),
|
||||||
|
AnimFrames: int32(len(shrekeAnims.Idle) + len(shrekeAnims.Walk)),
|
||||||
|
Animations: shrekeAnims,
|
||||||
|
YOffset: 0.0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, model := range models {
|
||||||
|
loadedModels[model.Name] = model
|
||||||
|
}
|
||||||
|
|
||||||
|
return models, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadMusic(filename string) (rl.Music, error) {
|
func LoadMusic(filename string) (rl.Music, error) {
|
||||||
return rl.LoadMusicStream(filename), nil
|
logging.Info.Printf("Loading music from %s", filename)
|
||||||
|
audioMutex.Lock()
|
||||||
|
defer audioMutex.Unlock()
|
||||||
|
|
||||||
|
if !rl.IsAudioDeviceReady() {
|
||||||
|
err := fmt.Errorf("audio device not initialized")
|
||||||
|
logging.Error.Println(err)
|
||||||
|
return rl.Music{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
music := rl.LoadMusicStream(filename)
|
||||||
|
if music.CtxType == 0 {
|
||||||
|
err := fmt.Errorf("failed to load music stream")
|
||||||
|
logging.Error.Println(err)
|
||||||
|
return rl.Music{}, err
|
||||||
|
}
|
||||||
|
logging.Info.Println("Music loaded successfully")
|
||||||
|
return music, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func UnloadModels(models []types.ModelAsset) {
|
func UnloadModels(models []types.ModelAsset) {
|
||||||
|
assetMutex.Lock()
|
||||||
|
defer assetMutex.Unlock()
|
||||||
|
|
||||||
for _, model := range models {
|
for _, model := range models {
|
||||||
|
if model.Animation != nil {
|
||||||
|
for i := int32(0); i < model.AnimFrames; i++ {
|
||||||
|
rl.UnloadModelAnimation(model.Animation[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
rl.UnloadModel(model.Model)
|
rl.UnloadModel(model.Model)
|
||||||
rl.UnloadTexture(model.Texture)
|
rl.UnloadTexture(model.Texture)
|
||||||
|
delete(loadedModels, model.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
7
config/config.go
Normal file
7
config/config.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
PlayMusic bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var Current = Config{PlayMusic: true}
|
@ -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
|
||||||
}
|
}
|
||||||
|
15
game/chat.go
15
game/chat.go
@ -2,6 +2,7 @@ package game
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/types"
|
"gitea.boner.be/bdnugget/goonscape/types"
|
||||||
@ -19,6 +20,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Chat struct {
|
type Chat struct {
|
||||||
|
sync.RWMutex
|
||||||
messages []types.ChatMessage
|
messages []types.ChatMessage
|
||||||
inputBuffer []rune
|
inputBuffer []rune
|
||||||
isTyping bool
|
isTyping bool
|
||||||
@ -49,6 +51,8 @@ func (c *Chat) AddMessage(playerID int32, content string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
|
func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
// 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{
|
||||||
@ -80,13 +84,14 @@ func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
|
|||||||
ExpireTime: time.Now().Add(6 * time.Second),
|
ExpireTime: time.Now().Add(6 * time.Second),
|
||||||
}
|
}
|
||||||
game.Player.Unlock()
|
game.Player.Unlock()
|
||||||
} else if otherPlayer, exists := game.OtherPlayers[msg.PlayerId]; exists {
|
} else if otherPlayer, exists := game.OtherPlayers.Load(msg.PlayerId); exists {
|
||||||
otherPlayer.Lock()
|
other := otherPlayer.(*types.Player)
|
||||||
otherPlayer.FloatingMessage = &types.FloatingMessage{
|
other.Lock()
|
||||||
|
other.FloatingMessage = &types.FloatingMessage{
|
||||||
Content: msg.Content,
|
Content: msg.Content,
|
||||||
ExpireTime: time.Now().Add(6 * time.Second),
|
ExpireTime: time.Now().Add(6 * time.Second),
|
||||||
}
|
}
|
||||||
otherPlayer.Unlock()
|
other.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -94,6 +99,8 @@ func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Chat) Draw(screenWidth, screenHeight int32) {
|
func (c *Chat) Draw(screenWidth, screenHeight int32) {
|
||||||
|
c.RLock()
|
||||||
|
defer c.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)
|
||||||
|
|
||||||
|
38
game/context.go
Normal file
38
game/context.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GameContext struct {
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
wg sync.WaitGroup
|
||||||
|
assetsLock sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGameContext() *GameContext {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
return &GameContext{
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gc *GameContext) Shutdown() {
|
||||||
|
gc.cancel()
|
||||||
|
gc.wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gc *GameContext) LoadAssets(fn func() error) error {
|
||||||
|
gc.assetsLock.Lock()
|
||||||
|
defer gc.assetsLock.Unlock()
|
||||||
|
return fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gc *GameContext) UnloadAssets(fn func()) {
|
||||||
|
gc.assetsLock.Lock()
|
||||||
|
defer gc.assetsLock.Unlock()
|
||||||
|
fn()
|
||||||
|
}
|
195
game/game.go
195
game/game.go
@ -1,33 +1,44 @@
|
|||||||
package game
|
package game
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/assets"
|
"gitea.boner.be/bdnugget/goonscape/assets"
|
||||||
|
"gitea.boner.be/bdnugget/goonscape/config"
|
||||||
|
"gitea.boner.be/bdnugget/goonscape/logging"
|
||||||
"gitea.boner.be/bdnugget/goonscape/network"
|
"gitea.boner.be/bdnugget/goonscape/network"
|
||||||
"gitea.boner.be/bdnugget/goonscape/types"
|
"gitea.boner.be/bdnugget/goonscape/types"
|
||||||
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var audioMutex sync.Mutex
|
||||||
|
var audioInitOnce sync.Once
|
||||||
|
|
||||||
type Game struct {
|
type Game struct {
|
||||||
|
ctx *GameContext
|
||||||
Player *types.Player
|
Player *types.Player
|
||||||
OtherPlayers map[int32]*types.Player
|
OtherPlayers sync.Map // Using sync.Map for concurrent access
|
||||||
Camera rl.Camera3D
|
Camera rl.Camera3D
|
||||||
Models []types.ModelAsset
|
Models []types.ModelAsset
|
||||||
Music rl.Music
|
Music rl.Music
|
||||||
Chat *Chat
|
Chat *Chat
|
||||||
MenuOpen bool
|
MenuOpen atomic.Bool
|
||||||
QuitChan chan struct{} // Channel to signal shutdown
|
QuitChan chan struct{} // Channel to signal shutdown
|
||||||
loginScreen *LoginScreen
|
loginScreen *LoginScreen
|
||||||
isLoggedIn bool
|
isLoggedIn atomic.Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() *Game {
|
func New() *Game {
|
||||||
InitWorld()
|
InitWorld()
|
||||||
game := &Game{
|
game := &Game{
|
||||||
OtherPlayers: make(map[int32]*types.Player),
|
ctx: NewGameContext(),
|
||||||
|
OtherPlayers: sync.Map{},
|
||||||
Camera: rl.Camera3D{
|
Camera: rl.Camera3D{
|
||||||
Position: rl.NewVector3(0, 10, 10),
|
Position: rl.NewVector3(0, 10, 10),
|
||||||
Target: rl.NewVector3(0, 0, 0),
|
Target: rl.NewVector3(0, 0, 0),
|
||||||
@ -44,22 +55,38 @@ func New() *Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) LoadAssets() error {
|
func (g *Game) LoadAssets() error {
|
||||||
|
audioMutex.Lock()
|
||||||
|
defer audioMutex.Unlock()
|
||||||
|
|
||||||
|
logging.Info.Println("Loading game assets")
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
// Load models first
|
||||||
g.Models, err = assets.LoadModels()
|
g.Models, err = assets.LoadModels()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logging.Error.Printf("Failed to load models: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
g.Music, err = assets.LoadMusic("resources/audio/GoonScape2.mp3")
|
// Load music only if enabled
|
||||||
if err != nil {
|
if config.Current.PlayMusic {
|
||||||
return err
|
logging.Info.Println("Loading music stream")
|
||||||
|
g.Music = rl.LoadMusicStream("resources/audio/GoonScape2.mp3")
|
||||||
|
if g.Music.CtxType == 0 {
|
||||||
|
logging.Error.Println("Failed to load music stream")
|
||||||
|
return fmt.Errorf("failed to load music stream")
|
||||||
|
}
|
||||||
|
logging.Info.Println("Music stream loaded successfully")
|
||||||
|
} else {
|
||||||
|
logging.Info.Println("Music disabled by config")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logging.Info.Println("Assets loaded successfully")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) Update(deltaTime float32) {
|
func (g *Game) Update(deltaTime float32) {
|
||||||
if !g.isLoggedIn {
|
if !g.isLoggedIn.Load() {
|
||||||
username, password, isRegistering, submitted := g.loginScreen.Update()
|
username, password, isRegistering, submitted := g.loginScreen.Update()
|
||||||
if submitted {
|
if submitted {
|
||||||
conn, playerID, err := network.ConnectToServer(username, password, isRegistering)
|
conn, playerID, err := network.ConnectToServer(username, password, isRegistering)
|
||||||
@ -68,20 +95,17 @@ 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.Store(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
g.loginScreen.Draw()
|
g.loginScreen.Draw()
|
||||||
@ -90,12 +114,12 @@ func (g *Game) Update(deltaTime float32) {
|
|||||||
|
|
||||||
// Handle ESC for menu
|
// Handle ESC for menu
|
||||||
if rl.IsKeyPressed(rl.KeyEscape) {
|
if rl.IsKeyPressed(rl.KeyEscape) {
|
||||||
g.MenuOpen = !g.MenuOpen
|
g.MenuOpen.Store(!g.MenuOpen.Load())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't process other inputs if menu is open
|
// Don't process other inputs if menu is open
|
||||||
if g.MenuOpen {
|
if g.MenuOpen.Load() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,15 +135,21 @@ func (g *Game) Update(deltaTime float32) {
|
|||||||
|
|
||||||
g.HandleInput()
|
g.HandleInput()
|
||||||
|
|
||||||
|
if len(g.Player.TargetPath) > 0 {
|
||||||
|
g.Player.Lock()
|
||||||
if len(g.Player.TargetPath) > 0 {
|
if len(g.Player.TargetPath) > 0 {
|
||||||
g.Player.MoveTowards(g.Player.TargetPath[0], deltaTime, GetMapGrid())
|
g.Player.MoveTowards(g.Player.TargetPath[0], deltaTime, GetMapGrid())
|
||||||
}
|
}
|
||||||
|
g.Player.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
for _, other := range g.OtherPlayers {
|
g.OtherPlayers.Range(func(key, value any) bool {
|
||||||
|
other := value.(*types.Player)
|
||||||
if len(other.TargetPath) > 0 {
|
if len(other.TargetPath) > 0 {
|
||||||
other.MoveTowards(other.TargetPath[0], deltaTime, GetMapGrid())
|
other.MoveTowards(other.TargetPath[0], deltaTime, GetMapGrid())
|
||||||
}
|
}
|
||||||
}
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
UpdateCamera(&g.Camera, g.Player.PosActual, deltaTime)
|
UpdateCamera(&g.Camera, g.Player.PosActual, deltaTime)
|
||||||
}
|
}
|
||||||
@ -152,19 +182,42 @@ func (g *Game) DrawMap() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) DrawPlayer(player *types.Player, model rl.Model) {
|
func (g *Game) DrawPlayer(player *types.Player) {
|
||||||
player.Lock()
|
player.Lock()
|
||||||
defer player.Unlock()
|
defer player.Unlock()
|
||||||
|
|
||||||
|
if player.Model.Meshes == nil {
|
||||||
|
logging.Error.Println("Player model not initialized")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
grid := GetMapGrid()
|
grid := GetMapGrid()
|
||||||
|
modelIndex := int(player.ID) % len(g.Models)
|
||||||
|
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
|
||||||
|
player.AnimationFrame = player.AnimationFrame % anim.FrameCount
|
||||||
|
rl.UpdateModelAnimation(player.Model, anim, player.AnimationFrame)
|
||||||
|
} else if len(modelAsset.Animations.Idle) > 0 {
|
||||||
|
anim := modelAsset.Animations.Idle[0] // Use first idle animation
|
||||||
|
player.AnimationFrame = player.AnimationFrame % anim.FrameCount
|
||||||
|
rl.UpdateModelAnimation(player.Model, anim, player.AnimationFrame)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.DrawModel(player.Model, playerPos, 16, rl.White)
|
||||||
|
|
||||||
|
// 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,
|
||||||
@ -195,27 +248,36 @@ func (g *Game) DrawPlayer(player *types.Player, model rl.Model) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) Render() {
|
func (g *Game) Render() {
|
||||||
rl.BeginDrawing()
|
if !rl.IsWindowReady() {
|
||||||
rl.ClearBackground(rl.RayWhite)
|
logging.Error.Println("Window not ready for rendering")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if !g.isLoggedIn {
|
rl.BeginDrawing()
|
||||||
g.loginScreen.Draw()
|
defer func() {
|
||||||
|
if rl.IsWindowReady() {
|
||||||
rl.EndDrawing()
|
rl.EndDrawing()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if !g.isLoggedIn.Load() {
|
||||||
|
g.loginScreen.Draw()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rl.BeginMode3D(g.Camera)
|
rl.BeginMode3D(g.Camera)
|
||||||
g.DrawMap()
|
g.DrawMap()
|
||||||
g.DrawPlayer(g.Player, g.Player.Model)
|
g.DrawPlayer(g.Player)
|
||||||
for id, other := range g.OtherPlayers {
|
|
||||||
|
g.OtherPlayers.Range(func(key, value any) bool {
|
||||||
|
other := value.(*types.Player)
|
||||||
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)
|
|
||||||
other.Model = g.Models[modelIndex].Model
|
|
||||||
other.Texture = g.Models[modelIndex].Texture
|
|
||||||
}
|
|
||||||
g.DrawPlayer(other, other.Model)
|
|
||||||
}
|
}
|
||||||
|
g.DrawPlayer(other)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
rl.EndMode3D()
|
rl.EndMode3D()
|
||||||
|
|
||||||
// Draw floating messages
|
// Draw floating messages
|
||||||
@ -243,27 +305,41 @@ func (g *Game) Render() {
|
|||||||
drawFloatingMessage(g.Player.FloatingMessage)
|
drawFloatingMessage(g.Player.FloatingMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, other := range g.OtherPlayers {
|
g.OtherPlayers.Range(func(key, value any) bool {
|
||||||
|
other := value.(*types.Player)
|
||||||
drawFloatingMessage(other.FloatingMessage)
|
drawFloatingMessage(other.FloatingMessage)
|
||||||
}
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
// Draw menu if open
|
// Draw menu if open
|
||||||
if g.MenuOpen {
|
if g.MenuOpen.Load() {
|
||||||
g.DrawMenu()
|
g.DrawMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only draw chat if menu is not open
|
// Only draw chat if menu is not open
|
||||||
if !g.MenuOpen {
|
if !g.MenuOpen.Load() {
|
||||||
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() {
|
||||||
|
// Unload models
|
||||||
|
if g.Models != nil {
|
||||||
assets.UnloadModels(g.Models)
|
assets.UnloadModels(g.Models)
|
||||||
assets.UnloadMusic(g.Music)
|
}
|
||||||
|
|
||||||
|
// Stop and unload music if enabled
|
||||||
|
if config.Current.PlayMusic && g.Music.CtxType != 0 {
|
||||||
|
rl.StopMusicStream(g.Music)
|
||||||
|
rl.UnloadMusicStream(g.Music)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close audio device if it's ready
|
||||||
|
if rl.IsAudioDeviceReady() {
|
||||||
|
rl.CloseAudioDevice()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) HandleInput() {
|
func (g *Game) HandleInput() {
|
||||||
@ -322,7 +398,7 @@ func (g *Game) DrawMenu() {
|
|||||||
if rl.IsMouseButtonPressed(rl.MouseLeftButton) {
|
if rl.IsMouseButtonPressed(rl.MouseLeftButton) {
|
||||||
switch item {
|
switch item {
|
||||||
case "Resume":
|
case "Resume":
|
||||||
g.MenuOpen = false
|
g.MenuOpen.Store(false)
|
||||||
case "Settings":
|
case "Settings":
|
||||||
// TODO: Implement settings
|
// TODO: Implement settings
|
||||||
case "Exit Game":
|
case "Exit Game":
|
||||||
@ -352,3 +428,42 @@ func (g *Game) Shutdown() {
|
|||||||
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) {
|
||||||
|
modelIndex := int(player.ID) % len(g.Models)
|
||||||
|
modelAsset := g.Models[modelIndex]
|
||||||
|
|
||||||
|
player.Model = modelAsset.Model
|
||||||
|
player.Texture = modelAsset.Texture
|
||||||
|
player.AnimationFrame = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) Run() {
|
||||||
|
if config.Current.PlayMusic {
|
||||||
|
audioInitOnce.Do(func() {
|
||||||
|
logging.Info.Println("Initializing audio device")
|
||||||
|
rl.InitAudioDevice()
|
||||||
|
if !rl.IsAudioDeviceReady() {
|
||||||
|
logging.Error.Println("Failed to initialize audio device")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
defer func() {
|
||||||
|
logging.Info.Println("Closing audio device")
|
||||||
|
rl.CloseAudioDevice()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Info.Println("Starting game loop")
|
||||||
|
for !rl.WindowShouldClose() {
|
||||||
|
deltaTime := rl.GetFrameTime()
|
||||||
|
if config.Current.PlayMusic {
|
||||||
|
rl.UpdateMusicStream(g.Music)
|
||||||
|
}
|
||||||
|
g.Update(deltaTime)
|
||||||
|
g.Render()
|
||||||
|
}
|
||||||
|
logging.Info.Println("Game loop ended")
|
||||||
|
|
||||||
|
logging.Info.Println("Closing quit channel")
|
||||||
|
close(g.QuitChan)
|
||||||
|
}
|
||||||
|
@ -83,6 +83,7 @@ func heuristic(a, b types.Tile) float32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func distance(a, b types.Tile) float32 {
|
func distance(a, b types.Tile) float32 {
|
||||||
|
_, _ = a, b
|
||||||
return 1.0 // uniform cost for now
|
return 1.0 // uniform cost for now
|
||||||
}
|
}
|
||||||
|
|
||||||
|
4
go.mod
4
go.mod
@ -10,8 +10,8 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/ebitengine/purego v0.8.2 // indirect
|
github.com/ebitengine/purego v0.8.2 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
|
golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3 // indirect
|
||||||
golang.org/x/sys v0.29.0 // indirect
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace gitea.boner.be/bdnugget/goonserver => ./goonserver
|
replace gitea.boner.be/bdnugget/goonserver => ./goonserver
|
||||||
|
4
go.sum
4
go.sum
@ -6,7 +6,11 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
|||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
|
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
|
||||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||||
|
golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3 h1:qNgPs5exUA+G0C96DrPwNrvLSj7GT/9D+3WMWUcUg34=
|
||||||
|
golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
||||||
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit d25ee0915566a8299201ac27a2041c908824de2f
|
Subproject commit f9ec811b10bbab54e843199eb68156e9e7c143cc
|
16
logging/logging.go
Normal file
16
logging/logging.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Info *log.Logger
|
||||||
|
Error *log.Logger
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Info = log.New(os.Stdout, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)
|
||||||
|
Error = log.New(os.Stderr, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)
|
||||||
|
}
|
45
main.go
45
main.go
@ -5,17 +5,27 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"gitea.boner.be/bdnugget/goonscape/config"
|
||||||
"gitea.boner.be/bdnugget/goonscape/game"
|
"gitea.boner.be/bdnugget/goonscape/game"
|
||||||
|
"gitea.boner.be/bdnugget/goonscape/logging"
|
||||||
"gitea.boner.be/bdnugget/goonscape/network"
|
"gitea.boner.be/bdnugget/goonscape/network"
|
||||||
rl "github.com/gen2brain/raylib-go/raylib"
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
logging.Info.Println("Starting GoonScape client")
|
||||||
|
|
||||||
|
// Raylib log level warn
|
||||||
|
rl.SetTraceLogLevel(rl.LogWarning)
|
||||||
// Parse command line flags
|
// Parse command line flags
|
||||||
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)")
|
||||||
|
noMusic := flag.Bool("no-music", false, "Disable music playback")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
// Set config before any game initialization
|
||||||
|
config.Current.PlayMusic = !*noMusic
|
||||||
|
|
||||||
// Set server address based on flags
|
// Set server address based on flags
|
||||||
if *local {
|
if *local {
|
||||||
if *addr != "" {
|
if *addr != "" {
|
||||||
@ -30,31 +40,40 @@ func main() {
|
|||||||
network.SetServerAddr(*addr)
|
network.SetServerAddr(*addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logging.Info.Println("Initializing window")
|
||||||
rl.InitWindow(1024, 768, "GoonScape")
|
rl.InitWindow(1024, 768, "GoonScape")
|
||||||
rl.SetExitKey(0)
|
defer func() {
|
||||||
defer rl.CloseWindow()
|
logging.Info.Println("Closing window")
|
||||||
|
rl.CloseWindow()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Initialize audio device first
|
||||||
|
if !rl.IsAudioDeviceReady() {
|
||||||
rl.InitAudioDevice()
|
rl.InitAudioDevice()
|
||||||
|
if !rl.IsAudioDeviceReady() {
|
||||||
|
log.Fatal("Failed to initialize audio device")
|
||||||
|
}
|
||||||
|
}
|
||||||
defer rl.CloseAudioDevice()
|
defer rl.CloseAudioDevice()
|
||||||
|
|
||||||
rl.SetTargetFPS(60)
|
|
||||||
|
|
||||||
game := game.New()
|
game := game.New()
|
||||||
|
logging.Info.Println("Loading game assets")
|
||||||
if err := game.LoadAssets(); err != nil {
|
if err := game.LoadAssets(); err != nil {
|
||||||
log.Fatalf("Failed to load assets: %v", err)
|
log.Fatalf("Failed to load assets: %v", err)
|
||||||
}
|
}
|
||||||
defer game.Cleanup()
|
defer func() {
|
||||||
|
logging.Info.Println("Cleaning up game resources")
|
||||||
|
game.Cleanup()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if config.Current.PlayMusic {
|
||||||
|
logging.Info.Println("Starting music playback")
|
||||||
rl.PlayMusicStream(game.Music)
|
rl.PlayMusicStream(game.Music)
|
||||||
rl.SetMusicVolume(game.Music, 0.5)
|
rl.SetMusicVolume(game.Music, 0.5)
|
||||||
|
|
||||||
for !rl.WindowShouldClose() {
|
|
||||||
deltaTime := rl.GetFrameTime()
|
|
||||||
rl.UpdateMusicStream(game.Music)
|
|
||||||
game.Update(deltaTime)
|
|
||||||
game.Render()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for clean shutdown
|
rl.SetTargetFPS(60)
|
||||||
<-game.QuitChan
|
logging.Info.Println("Starting game loop")
|
||||||
|
game.Run()
|
||||||
|
logging.Info.Println("Game exited cleanly")
|
||||||
}
|
}
|
||||||
|
@ -2,16 +2,18 @@ package network
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gitea.boner.be/bdnugget/goonscape/logging"
|
||||||
"gitea.boner.be/bdnugget/goonscape/types"
|
"gitea.boner.be/bdnugget/goonscape/types"
|
||||||
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
||||||
rl "github.com/gen2brain/raylib-go/raylib"
|
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -19,18 +21,84 @@ const protoVersion = 1
|
|||||||
|
|
||||||
var serverAddr = "boner.be:6969"
|
var serverAddr = "boner.be:6969"
|
||||||
|
|
||||||
|
type NetworkManager struct {
|
||||||
|
ctx context.Context
|
||||||
|
conn net.Conn
|
||||||
|
reader *bufio.Reader
|
||||||
|
writer *bufio.Writer
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNetworkManager(ctx context.Context) *NetworkManager {
|
||||||
|
return &NetworkManager{
|
||||||
|
ctx: ctx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func SetServerAddr(addr string) {
|
func SetServerAddr(addr string) {
|
||||||
serverAddr = addr
|
serverAddr = addr
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConnectToServer(username, password string, isRegistering bool) (net.Conn, int32, error) {
|
func (nm *NetworkManager) Connect(addr string) error {
|
||||||
conn, err := net.Dial("tcp", serverAddr)
|
nm.mu.Lock()
|
||||||
|
defer nm.mu.Unlock()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
nm.conn, err = net.Dial("tcp", addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to dial server: %v", err)
|
return err
|
||||||
return nil, 0, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Connected to server. Authenticating...")
|
nm.reader = bufio.NewReader(nm.conn)
|
||||||
|
nm.writer = bufio.NewWriter(nm.conn)
|
||||||
|
|
||||||
|
go nm.readLoop()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nm *NetworkManager) readLoop() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-nm.ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
// Read and process messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nm *NetworkManager) Send(message proto.Message) error {
|
||||||
|
nm.mu.Lock()
|
||||||
|
defer nm.mu.Unlock()
|
||||||
|
|
||||||
|
data, err := proto.Marshal(message)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write length prefix
|
||||||
|
lengthBuf := make([]byte, 4)
|
||||||
|
binary.BigEndian.PutUint32(lengthBuf, uint32(len(data)))
|
||||||
|
if _, err := nm.writer.Write(lengthBuf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write message body
|
||||||
|
if _, err := nm.writer.Write(data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nm.writer.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConnectToServer(username, password string, isRegistering bool) (net.Conn, int32, error) {
|
||||||
|
logging.Info.Println("Attempting to connect to server at", serverAddr)
|
||||||
|
conn, err := net.Dial("tcp", serverAddr)
|
||||||
|
if err != nil {
|
||||||
|
logging.Error.Printf("Failed to dial server: %v", err)
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
logging.Info.Println("Connected to server. Authenticating...")
|
||||||
|
|
||||||
// Send auth message
|
// Send auth message
|
||||||
authAction := &pb.Action{
|
authAction := &pb.Action{
|
||||||
@ -81,7 +149,7 @@ func ConnectToServer(username, password string, isRegistering bool) (net.Conn, i
|
|||||||
|
|
||||||
if !response.AuthSuccess {
|
if !response.AuthSuccess {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
return nil, 0, fmt.Errorf(response.ErrorMessage)
|
return nil, 0, fmt.Errorf("authentication failed: %s", response.ErrorMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
playerID := response.GetPlayerId()
|
playerID := response.GetPlayerId()
|
||||||
@ -89,145 +157,63 @@ func ConnectToServer(username, password string, isRegistering bool) (net.Conn, i
|
|||||||
return conn, playerID, nil
|
return conn, playerID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
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 *sync.Map, quitChan <-chan struct{}) {
|
||||||
reader := bufio.NewReader(conn)
|
defer func() {
|
||||||
|
logging.Info.Println("Closing connection and cleaning up for player", playerID)
|
||||||
actionTicker := time.NewTicker(types.ClientTickRate)
|
conn.Close()
|
||||||
defer actionTicker.Stop()
|
|
||||||
defer conn.Close()
|
|
||||||
defer close(player.QuitDone)
|
|
||||||
|
|
||||||
// Create a channel to signal when goroutines are done
|
|
||||||
done := make(chan struct{})
|
|
||||||
|
|
||||||
// Create a set of current players to track disconnects
|
|
||||||
currentPlayers := make(map[int32]bool)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-quitChan:
|
|
||||||
// Send disconnect message to server
|
|
||||||
disconnectMsg := &pb.ActionBatch{
|
|
||||||
PlayerId: playerID,
|
|
||||||
Actions: []*pb.Action{{
|
|
||||||
Type: pb.Action_DISCONNECT,
|
|
||||||
PlayerId: playerID,
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
writeMessage(conn, disconnectMsg)
|
|
||||||
done <- struct{}{}
|
|
||||||
return
|
|
||||||
case <-actionTicker.C:
|
|
||||||
player.Lock()
|
|
||||||
if len(player.ActionQueue) > 0 {
|
|
||||||
actions := make([]*pb.Action, len(player.ActionQueue))
|
|
||||||
copy(actions, player.ActionQueue)
|
|
||||||
|
|
||||||
batch := &pb.ActionBatch{
|
|
||||||
PlayerId: playerID,
|
|
||||||
Actions: actions,
|
|
||||||
Tick: player.CurrentTick,
|
|
||||||
}
|
|
||||||
|
|
||||||
player.ActionQueue = player.ActionQueue[:0]
|
|
||||||
player.Unlock()
|
|
||||||
|
|
||||||
if err := writeMessage(conn, batch); err != nil {
|
|
||||||
log.Printf("Failed to send actions to server: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
player.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-quitChan:
|
|
||||||
done := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
<-done
|
|
||||||
close(player.QuitDone)
|
close(player.QuitDone)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
reader := bufio.NewReader(conn)
|
||||||
|
ticker := time.NewTicker(100 * time.Millisecond)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
select {
|
select {
|
||||||
case <-done:
|
case <-quitChan:
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
case <-time.After(1 * time.Second):
|
|
||||||
log.Println("Shutdown timed out")
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
default:
|
case <-ticker.C:
|
||||||
// Read message length (4 bytes)
|
// Read message length
|
||||||
lengthBuf := make([]byte, 4)
|
lengthBuf := make([]byte, 4)
|
||||||
if _, err := io.ReadFull(reader, lengthBuf); err != nil {
|
if _, err := io.ReadFull(reader, lengthBuf); err != nil {
|
||||||
log.Printf("Failed to read message length: %v", err)
|
log.Printf("Failed to read message length: %v", err)
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
messageLength := binary.BigEndian.Uint32(lengthBuf)
|
messageLength := binary.BigEndian.Uint32(lengthBuf)
|
||||||
|
|
||||||
// Read the full message
|
// Read message body
|
||||||
messageBuf := make([]byte, messageLength)
|
messageBuf := make([]byte, messageLength)
|
||||||
if _, err := io.ReadFull(reader, messageBuf); err != nil {
|
if _, err := io.ReadFull(reader, messageBuf); err != nil {
|
||||||
log.Printf("Failed to read message body: %v", err)
|
log.Printf("Failed to read message body: %v", err)
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process server message
|
||||||
var serverMessage pb.ServerMessage
|
var serverMessage pb.ServerMessage
|
||||||
if err := proto.Unmarshal(messageBuf, &serverMessage); err != nil {
|
if err := proto.Unmarshal(messageBuf, &serverMessage); err != nil {
|
||||||
log.Printf("Failed to unmarshal server message: %v", err)
|
log.Printf("Failed to unmarshal server message: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
player.Lock()
|
// Update player states
|
||||||
player.CurrentTick = serverMessage.CurrentTick
|
|
||||||
|
|
||||||
tickDiff := serverMessage.CurrentTick - player.CurrentTick
|
|
||||||
if tickDiff > types.MaxTickDesync {
|
|
||||||
for _, state := range serverMessage.Players {
|
for _, state := range serverMessage.Players {
|
||||||
if state.PlayerId == playerID {
|
if state == nil {
|
||||||
player.ForceResync(state)
|
logging.Error.Println("Received nil player state")
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
player.Unlock()
|
|
||||||
|
|
||||||
for _, state := range serverMessage.Players {
|
|
||||||
currentPlayers[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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if otherPlayer, exists := otherPlayers[state.PlayerId]; exists {
|
if state.PlayerId == playerID {
|
||||||
otherPlayer.UpdatePosition(state, types.ServerTickRate)
|
player.UpdatePosition(state, types.ServerTickRate)
|
||||||
|
} else if existing, ok := otherPlayers.Load(state.PlayerId); ok {
|
||||||
|
existing.(*types.Player).UpdatePosition(state, types.ServerTickRate)
|
||||||
} else {
|
} else {
|
||||||
otherPlayers[state.PlayerId] = types.NewPlayer(state)
|
newPlayer := types.NewPlayer(state)
|
||||||
|
otherPlayers.Store(state.PlayerId, newPlayer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
if !currentPlayers[id] {
|
|
||||||
delete(otherPlayers, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if handler, ok := player.UserData.(types.ChatMessageHandler); ok && len(serverMessage.ChatMessages) > 0 {
|
|
||||||
handler.HandleServerMessages(serverMessage.ChatMessages)
|
handler.HandleServerMessages(serverMessage.ChatMessages)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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.
BIN
resources/models/shreke/Animation_Excited_Walk_M_withSkin.glb
Normal file
BIN
resources/models/shreke/Animation_Excited_Walk_M_withSkin.glb
Normal file
Binary file not shown.
BIN
resources/models/shreke/Animation_Running_withSkin.glb
Normal file
BIN
resources/models/shreke/Animation_Running_withSkin.glb
Normal file
Binary file not shown.
BIN
resources/models/shreke/Animation_Slow_Orc_Walk_withSkin.glb
Normal file
BIN
resources/models/shreke/Animation_Slow_Orc_Walk_withSkin.glb
Normal file
Binary file not shown.
BIN
resources/models/shreke/Animation_Unsteady_Walk_withSkin.glb
Normal file
BIN
resources/models/shreke/Animation_Unsteady_Walk_withSkin.glb
Normal file
Binary file not shown.
BIN
resources/models/shreke/Animation_Walking_withSkin.glb
Normal file
BIN
resources/models/shreke/Animation_Walking_withSkin.glb
Normal file
Binary file not shown.
126
segfault.txt
Normal file
126
segfault.txt
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
INFO: 2025/02/10 15:53:21 main.go:16: Starting GoonScape client
|
||||||
|
INFO: 2025/02/10 15:53:21 main.go:43: Initializing window
|
||||||
|
INFO: 2025/02/10 15:53:21 main.go:60: Loading game assets
|
||||||
|
INFO: 2025/02/10 15:53:21 game.go:61: Loading game assets
|
||||||
|
INFO: 2025/02/10 15:53:21 assets.go:51: Loading models
|
||||||
|
INFO: 2025/02/10 15:53:21 game.go:81: Music disabled by config
|
||||||
|
INFO: 2025/02/10 15:53:21 game.go:84: Assets loaded successfully
|
||||||
|
INFO: 2025/02/10 15:53:21 main.go:76: Starting game loop
|
||||||
|
INFO: 2025/02/10 15:53:21 game.go:456: Starting game loop
|
||||||
|
malloc(): unsorted double linked list corrupted
|
||||||
|
SIGABRT: abort
|
||||||
|
PC=0x78a790604c4c m=0 sigcode=18446744073709551610
|
||||||
|
signal arrived during cgo execution
|
||||||
|
|
||||||
|
goroutine 1 gp=0xc0000061c0 m=0 mp=0xaf8e20 [syscall, locked to thread]:
|
||||||
|
runtime.cgocall(0x6da9a0, 0xc00002da50)
|
||||||
|
/usr/lib/go/src/runtime/cgocall.go:167 +0x5b fp=0xc00002da28 sp=0xc00002d9f0 pc=0x46d0fb
|
||||||
|
github.com/gen2brain/raylib-go/raylib._Cfunc_EndDrawing()
|
||||||
|
_cgo_gotypes.go:2539 +0x3f fp=0xc00002da50 sp=0xc00002da28 pc=0x51cdbf
|
||||||
|
github.com/gen2brain/raylib-go/raylib.EndDrawing(...)
|
||||||
|
/home/bd/.cache/go/mod/github.com/gen2brain/raylib-go/raylib@v0.0.0-20250109172833-6dbba4f81a9b/rcore.go:464
|
||||||
|
gitea.boner.be/bdnugget/goonscape/game.(*Game).Render.func1()
|
||||||
|
/home/bd/Projects/go/goonscape/game/game.go:259 +0x36 fp=0xc00002da68 sp=0xc00002da50 pc=0x6b6fb6
|
||||||
|
runtime.deferreturn()
|
||||||
|
/usr/lib/go/src/runtime/panic.go:605 +0x5e fp=0xc00002daf8 sp=0xc00002da68 pc=0x4396fe
|
||||||
|
gitea.boner.be/bdnugget/goonscape/game.(*Game).Render(0xc0001ae000)
|
||||||
|
/home/bd/Projects/go/goonscape/game/game.go:265 +0x405 fp=0xc00002dbf8 sp=0xc00002daf8 pc=0x6b27a5
|
||||||
|
gitea.boner.be/bdnugget/goonscape/game.(*Game).Run(0xc0001ae000)
|
||||||
|
/home/bd/Projects/go/goonscape/game/game.go:463 +0x105 fp=0xc00002dcc0 sp=0xc00002dbf8 pc=0x6b3e85
|
||||||
|
main.main()
|
||||||
|
/home/bd/Projects/go/goonscape/main.go:77 +0xb85 fp=0xc00002df50 sp=0xc00002dcc0 pc=0x6b8345
|
||||||
|
runtime.main()
|
||||||
|
/usr/lib/go/src/runtime/proc.go:272 +0x28b fp=0xc00002dfe0 sp=0xc00002df50 pc=0x43d84b
|
||||||
|
runtime.goexit({})
|
||||||
|
/usr/lib/go/src/runtime/asm_amd64.s:1700 +0x1 fp=0xc00002dfe8 sp=0xc00002dfe0 pc=0x47ac81
|
||||||
|
|
||||||
|
goroutine 2 gp=0xc000006c40 m=nil [force gc (idle)]:
|
||||||
|
runtime.gopark(0xaedc20?, 0xaf8e20?, 0x0?, 0x0?, 0x0?)
|
||||||
|
/usr/lib/go/src/runtime/proc.go:424 +0xce fp=0xc0000567a8 sp=0xc000056788 pc=0x47356e
|
||||||
|
runtime.goparkunlock(...)
|
||||||
|
/usr/lib/go/src/runtime/proc.go:430
|
||||||
|
runtime.forcegchelper()
|
||||||
|
/usr/lib/go/src/runtime/proc.go:337 +0xb3 fp=0xc0000567e0 sp=0xc0000567a8 pc=0x43db93
|
||||||
|
runtime.goexit({})
|
||||||
|
/usr/lib/go/src/runtime/asm_amd64.s:1700 +0x1 fp=0xc0000567e8 sp=0xc0000567e0 pc=0x47ac81
|
||||||
|
created by runtime.init.7 in goroutine 1
|
||||||
|
/usr/lib/go/src/runtime/proc.go:325 +0x1a
|
||||||
|
|
||||||
|
goroutine 3 gp=0xc000007180 m=nil [GC sweep wait]:
|
||||||
|
runtime.gopark(0x0?, 0x0?, 0x0?, 0x0?, 0x0?)
|
||||||
|
/usr/lib/go/src/runtime/proc.go:424 +0xce fp=0xc00006af80 sp=0xc00006af60 pc=0x47356e
|
||||||
|
runtime.goparkunlock(...)
|
||||||
|
/usr/lib/go/src/runtime/proc.go:430
|
||||||
|
runtime.bgsweep(0xc000080000)
|
||||||
|
/usr/lib/go/src/runtime/mgcsweep.go:277 +0x94 fp=0xc00006afc8 sp=0xc00006af80 pc=0x428714
|
||||||
|
runtime.gcenable.gowrap1()
|
||||||
|
/usr/lib/go/src/runtime/mgc.go:204 +0x25 fp=0xc00006afe0 sp=0xc00006afc8 pc=0x41ce25
|
||||||
|
runtime.goexit({})
|
||||||
|
/usr/lib/go/src/runtime/asm_amd64.s:1700 +0x1 fp=0xc00006afe8 sp=0xc00006afe0 pc=0x47ac81
|
||||||
|
created by runtime.gcenable in goroutine 1
|
||||||
|
/usr/lib/go/src/runtime/mgc.go:204 +0x66
|
||||||
|
|
||||||
|
goroutine 4 gp=0xc000007340 m=nil [GC scavenge wait]:
|
||||||
|
runtime.gopark(0xc000080000?, 0x8e5b60?, 0x1?, 0x0?, 0xc000007340?)
|
||||||
|
/usr/lib/go/src/runtime/proc.go:424 +0xce fp=0xc000064f78 sp=0xc000064f58 pc=0x47356e
|
||||||
|
runtime.goparkunlock(...)
|
||||||
|
/usr/lib/go/src/runtime/proc.go:430
|
||||||
|
runtime.(*scavengerState).park(0xaf8060)
|
||||||
|
/usr/lib/go/src/runtime/mgcscavenge.go:425 +0x49 fp=0xc000064fa8 sp=0xc000064f78 pc=0x426149
|
||||||
|
runtime.bgscavenge(0xc000080000)
|
||||||
|
/usr/lib/go/src/runtime/mgcscavenge.go:653 +0x3c fp=0xc000064fc8 sp=0xc000064fa8 pc=0x4266bc
|
||||||
|
runtime.gcenable.gowrap2()
|
||||||
|
/usr/lib/go/src/runtime/mgc.go:205 +0x25 fp=0xc000064fe0 sp=0xc000064fc8 pc=0x41cdc5
|
||||||
|
runtime.goexit({})
|
||||||
|
/usr/lib/go/src/runtime/asm_amd64.s:1700 +0x1 fp=0xc000064fe8 sp=0xc000064fe0 pc=0x47ac81
|
||||||
|
created by runtime.gcenable in goroutine 1
|
||||||
|
/usr/lib/go/src/runtime/mgc.go:205 +0xa5
|
||||||
|
|
||||||
|
goroutine 18 gp=0xc000104700 m=nil [finalizer wait]:
|
||||||
|
runtime.gopark(0x0?, 0x0?, 0x0?, 0x0?, 0x0?)
|
||||||
|
/usr/lib/go/src/runtime/proc.go:424 +0xce fp=0xc000184e20 sp=0xc000184e00 pc=0x47356e
|
||||||
|
runtime.runfinq()
|
||||||
|
/usr/lib/go/src/runtime/mfinal.go:193 +0x145 fp=0xc000184fe0 sp=0xc000184e20 pc=0x41bea5
|
||||||
|
runtime.goexit({})
|
||||||
|
/usr/lib/go/src/runtime/asm_amd64.s:1700 +0x1 fp=0xc000184fe8 sp=0xc000184fe0 pc=0x47ac81
|
||||||
|
created by runtime.createfing in goroutine 1
|
||||||
|
/usr/lib/go/src/runtime/mfinal.go:163 +0x3d
|
||||||
|
|
||||||
|
goroutine 19 gp=0xc0001048c0 m=nil [chan receive]:
|
||||||
|
runtime.gopark(0x0?, 0x0?, 0x0?, 0x0?, 0x0?)
|
||||||
|
/usr/lib/go/src/runtime/proc.go:424 +0xce fp=0xc00006bf18 sp=0xc00006bef8 pc=0x47356e
|
||||||
|
runtime.chanrecv(0xc0001140e0, 0x0, 0x1)
|
||||||
|
/usr/lib/go/src/runtime/chan.go:639 +0x3bc fp=0xc00006bf90 sp=0xc00006bf18 pc=0x40c89c
|
||||||
|
runtime.chanrecv1(0x0?, 0x0?)
|
||||||
|
/usr/lib/go/src/runtime/chan.go:489 +0x12 fp=0xc00006bfb8 sp=0xc00006bf90 pc=0x40c4d2
|
||||||
|
runtime.unique_runtime_registerUniqueMapCleanup.func1(...)
|
||||||
|
/usr/lib/go/src/runtime/mgc.go:1781
|
||||||
|
runtime.unique_runtime_registerUniqueMapCleanup.gowrap1()
|
||||||
|
/usr/lib/go/src/runtime/mgc.go:1784 +0x2f fp=0xc00006bfe0 sp=0xc00006bfb8 pc=0x41fe4f
|
||||||
|
runtime.goexit({})
|
||||||
|
/usr/lib/go/src/runtime/asm_amd64.s:1700 +0x1 fp=0xc00006bfe8 sp=0xc00006bfe0 pc=0x47ac81
|
||||||
|
created by unique.runtime_registerUniqueMapCleanup in goroutine 1
|
||||||
|
/usr/lib/go/src/runtime/mgc.go:1779 +0x96
|
||||||
|
|
||||||
|
rax 0x0
|
||||||
|
rbx 0x4829
|
||||||
|
rcx 0x78a790604c4c
|
||||||
|
rdx 0x6
|
||||||
|
rdi 0x4829
|
||||||
|
rsi 0x4829
|
||||||
|
rbp 0x78a7902fc740
|
||||||
|
rsp 0x7ffd63425870
|
||||||
|
r8 0xffffffff
|
||||||
|
r9 0x0
|
||||||
|
r10 0x8
|
||||||
|
r11 0x246
|
||||||
|
r12 0x7ffd634259d0
|
||||||
|
r13 0x6
|
||||||
|
r14 0x7ffd634259d0
|
||||||
|
r15 0x7ffd634259d0
|
||||||
|
rip 0x78a790604c4c
|
||||||
|
rflags 0x246
|
||||||
|
cs 0x33
|
||||||
|
fs 0x0
|
||||||
|
gs 0x0
|
||||||
|
exit status 2
|
@ -1,12 +1,34 @@
|
|||||||
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Player struct {
|
||||||
|
sync.RWMutex
|
||||||
|
PosActual rl.Vector3
|
||||||
|
PosTile Tile
|
||||||
|
TargetPath []Tile
|
||||||
|
ActionQueue []*pb.Action
|
||||||
|
Speed float32
|
||||||
|
ID int32
|
||||||
|
IsMoving bool
|
||||||
|
AnimationFrame int32
|
||||||
|
LastAnimUpdate time.Time
|
||||||
|
LastUpdateTime time.Time
|
||||||
|
InterpolationProgress float32
|
||||||
|
FloatingMessage *FloatingMessage
|
||||||
|
QuitDone chan struct{}
|
||||||
|
UserData interface{}
|
||||||
|
Model rl.Model
|
||||||
|
Texture rl.Texture2D
|
||||||
|
Username string
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Player) MoveTowards(target Tile, deltaTime float32, mapGrid [][]Tile) {
|
func (p *Player) MoveTowards(target Tile, deltaTime float32, mapGrid [][]Tile) {
|
||||||
p.Lock()
|
p.Lock()
|
||||||
defer p.Unlock()
|
defer p.Unlock()
|
||||||
@ -19,6 +41,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.LogInfo, "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.LogInfo, "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)
|
||||||
}
|
}
|
||||||
@ -44,6 +93,9 @@ func NewPlayer(state *pb.PlayerState) *Player {
|
|||||||
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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChatMessage struct {
|
type ChatMessage struct {
|
||||||
@ -54,6 +48,13 @@ type ChatMessageHandler interface {
|
|||||||
HandleServerMessages([]*pb.ChatMessage)
|
HandleServerMessages([]*pb.ChatMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PlayerState struct {
|
||||||
|
PlayerId int32
|
||||||
|
X int32
|
||||||
|
Y int32
|
||||||
|
Username string
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MapWidth = 50
|
MapWidth = 50
|
||||||
MapHeight = 50
|
MapHeight = 50
|
||||||
|
Loading…
x
Reference in New Issue
Block a user