Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
53cc9bca6b | |||
a1aeb71512 | |||
ef3732c53c | |||
7be859d58f | |||
fde07e3e48 | |||
f966d538d3 | |||
220a451475 | |||
417bf4ea63 |
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
|
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
|
||||||
|
|
||||||
|
108
assets/assets.go
108
assets/assets.go
@ -1,10 +1,25 @@
|
|||||||
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
|
// Helper function to load animations for a model
|
||||||
func loadModelAnimations(animPaths map[string]string) (types.AnimationSet, error) {
|
func loadModelAnimations(animPaths map[string]string) (types.AnimationSet, error) {
|
||||||
var animSet types.AnimationSet
|
var animSet types.AnimationSet
|
||||||
@ -33,9 +48,28 @@ func loadModelAnimations(animPaths map[string]string) (types.AnimationSet, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
func LoadModels() ([]types.ModelAsset, error) {
|
func LoadModels() ([]types.ModelAsset, error) {
|
||||||
|
logging.Info.Println("Loading models")
|
||||||
|
assetMutex.Lock()
|
||||||
|
defer assetMutex.Unlock()
|
||||||
|
|
||||||
|
if len(loadedModels) > 0 {
|
||||||
|
logging.Info.Println("Returning cached models")
|
||||||
|
models := make([]types.ModelAsset, 0, len(loadedModels))
|
||||||
|
for _, model := range loadedModels {
|
||||||
|
models = append(models, model)
|
||||||
|
}
|
||||||
|
return models, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Goonion model and animations
|
// Goonion model and animations
|
||||||
goonerModel := rl.LoadModel("resources/models/gooner/walk_no_y_transform.glb")
|
goonerModel := rl.LoadModel("resources/models/gooner/walk_no_y_transform.glb")
|
||||||
goonerAnims, _ := loadModelAnimations(map[string]string{"idle": "resources/models/gooner/idle_no_y_transform.glb", "walk": "resources/models/gooner/walk_no_y_transform.glb"})
|
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
|
||||||
|
}
|
||||||
|
|
||||||
// Apply transformations
|
// Apply transformations
|
||||||
transform := rl.MatrixIdentity()
|
transform := rl.MatrixIdentity()
|
||||||
@ -46,25 +80,30 @@ func LoadModels() ([]types.ModelAsset, error) {
|
|||||||
|
|
||||||
// Coomer model (ready for animations)
|
// Coomer model (ready for animations)
|
||||||
coomerModel := rl.LoadModel("resources/models/coomer/idle_notransy.glb")
|
coomerModel := rl.LoadModel("resources/models/coomer/idle_notransy.glb")
|
||||||
// coomerTexture := rl.LoadTexture("resources/models/coomer.png")
|
coomerAnims, err := loadModelAnimations(map[string]string{
|
||||||
// rl.SetMaterialTexture(coomerModel.Materials, rl.MapDiffuse, coomerTexture)
|
"idle": "resources/models/coomer/idle_notransy.glb",
|
||||||
// When you have animations, add them like:
|
"walk": "resources/models/coomer/unsteadywalk_notransy.glb",
|
||||||
coomerAnims, _ := 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
|
coomerModel.Transform = transform
|
||||||
|
|
||||||
// Shreke model (ready for animations)
|
// Shreke model (ready for animations)
|
||||||
shrekeModel := rl.LoadModel("resources/models/shreke.obj")
|
shrekeModel := rl.LoadModel("resources/models/shreke/Animation_Slow_Orc_Walk_withSkin.glb")
|
||||||
shrekeTexture := rl.LoadTexture("resources/models/shreke.png")
|
shrekeAnims, err := loadModelAnimations(map[string]string{
|
||||||
rl.SetMaterialTexture(shrekeModel.Materials, rl.MapDiffuse, shrekeTexture)
|
"idle": "resources/models/shreke/Animation_Slow_Orc_Walk_withSkin.glb",
|
||||||
// When you have animations, add them like:
|
"walk": "resources/models/shreke/Animation_Excited_Walk_M_withSkin.glb",
|
||||||
// shrekeAnims, _ := loadModelAnimations("resources/models/shreke.glb",
|
})
|
||||||
// map[string]string{
|
if err != nil {
|
||||||
// "idle": "resources/models/shreke_idle.glb",
|
return nil, err
|
||||||
// "walk": "resources/models/shreke_walk.glb",
|
}
|
||||||
// })
|
shrekeModel.Transform = transform
|
||||||
|
|
||||||
return []types.ModelAsset{
|
// Store loaded models
|
||||||
|
models := []types.ModelAsset{
|
||||||
{
|
{
|
||||||
|
Name: "gooner",
|
||||||
Model: goonerModel,
|
Model: goonerModel,
|
||||||
Animation: append(goonerAnims.Idle, goonerAnims.Walk...),
|
Animation: append(goonerAnims.Idle, goonerAnims.Walk...),
|
||||||
AnimFrames: int32(len(goonerAnims.Idle) + len(goonerAnims.Walk)),
|
AnimFrames: int32(len(goonerAnims.Idle) + len(goonerAnims.Walk)),
|
||||||
@ -78,15 +117,47 @@ func LoadModels() ([]types.ModelAsset, error) {
|
|||||||
Animations: coomerAnims,
|
Animations: coomerAnims,
|
||||||
YOffset: -4.0,
|
YOffset: -4.0,
|
||||||
},
|
},
|
||||||
{Model: shrekeModel, Texture: shrekeTexture},
|
{
|
||||||
}, nil
|
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 {
|
if model.Animation != nil {
|
||||||
for i := int32(0); i < model.AnimFrames; i++ {
|
for i := int32(0); i < model.AnimFrames; i++ {
|
||||||
@ -95,6 +166,7 @@ func UnloadModels(models []types.ModelAsset) {
|
|||||||
}
|
}
|
||||||
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
|
||||||
}
|
}
|
||||||
|
33
game/chat.go
33
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,20 +20,19 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Chat struct {
|
type Chat struct {
|
||||||
|
sync.RWMutex
|
||||||
messages []types.ChatMessage
|
messages []types.ChatMessage
|
||||||
inputBuffer []rune
|
inputBuffer []rune
|
||||||
isTyping bool
|
isTyping bool
|
||||||
cursorPos int
|
cursorPos int
|
||||||
scrollOffset int
|
scrollOffset int
|
||||||
userData interface{}
|
userData interface{}
|
||||||
input InputHandler
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewChat() *Chat {
|
func NewChat() *Chat {
|
||||||
return &Chat{
|
return &Chat{
|
||||||
messages: make([]types.ChatMessage, 0, maxMessages),
|
messages: make([]types.ChatMessage, 0, maxMessages),
|
||||||
inputBuffer: make([]rune, 0, runeLimit),
|
inputBuffer: make([]rune, 0, runeLimit),
|
||||||
input: &RaylibInput{},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,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{
|
||||||
@ -82,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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -96,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)
|
||||||
|
|
||||||
@ -155,23 +160,23 @@ func (c *Chat) Update() (string, bool) {
|
|||||||
c.scrollOffset = clamp(c.scrollOffset-int(wheelMove), 0, maxScroll)
|
c.scrollOffset = clamp(c.scrollOffset-int(wheelMove), 0, maxScroll)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.input.IsKeyPressed(rl.KeyT) {
|
if rl.IsKeyPressed(rl.KeyT) {
|
||||||
c.isTyping = true
|
c.isTyping = true
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
key := c.input.GetCharPressed()
|
key := rl.GetCharPressed()
|
||||||
for key > 0 {
|
for key > 0 {
|
||||||
if len(c.inputBuffer) < runeLimit {
|
if len(c.inputBuffer) < runeLimit {
|
||||||
c.inputBuffer = append(c.inputBuffer[:c.cursorPos], append([]rune{key}, c.inputBuffer[c.cursorPos:]...)...)
|
c.inputBuffer = append(c.inputBuffer[:c.cursorPos], append([]rune{key}, c.inputBuffer[c.cursorPos:]...)...)
|
||||||
c.cursorPos++
|
c.cursorPos++
|
||||||
}
|
}
|
||||||
key = c.input.GetCharPressed()
|
key = rl.GetCharPressed()
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.input.IsKeyPressed(rl.KeyEnter) || c.input.IsKeyPressed(rl.KeyKpEnter) {
|
if rl.IsKeyPressed(rl.KeyEnter) || rl.IsKeyPressed(rl.KeyKpEnter) {
|
||||||
if len(c.inputBuffer) > 0 {
|
if len(c.inputBuffer) > 0 {
|
||||||
message := string(c.inputBuffer)
|
message := string(c.inputBuffer)
|
||||||
c.inputBuffer = c.inputBuffer[:0]
|
c.inputBuffer = c.inputBuffer[:0]
|
||||||
@ -182,21 +187,21 @@ func (c *Chat) Update() (string, bool) {
|
|||||||
c.isTyping = false
|
c.isTyping = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.input.IsKeyPressed(rl.KeyEscape) && c.isTyping {
|
if rl.IsKeyPressed(rl.KeyEscape) && c.isTyping {
|
||||||
c.inputBuffer = c.inputBuffer[:0]
|
c.inputBuffer = c.inputBuffer[:0]
|
||||||
c.cursorPos = 0
|
c.cursorPos = 0
|
||||||
c.isTyping = false
|
c.isTyping = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.input.IsKeyPressed(rl.KeyBackspace) && c.cursorPos > 0 {
|
if rl.IsKeyPressed(rl.KeyBackspace) && c.cursorPos > 0 {
|
||||||
c.inputBuffer = append(c.inputBuffer[:c.cursorPos-1], c.inputBuffer[c.cursorPos:]...)
|
c.inputBuffer = append(c.inputBuffer[:c.cursorPos-1], c.inputBuffer[c.cursorPos:]...)
|
||||||
c.cursorPos--
|
c.cursorPos--
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.input.IsKeyPressed(rl.KeyLeft) && c.cursorPos > 0 {
|
if rl.IsKeyPressed(rl.KeyLeft) && c.cursorPos > 0 {
|
||||||
c.cursorPos--
|
c.cursorPos--
|
||||||
}
|
}
|
||||||
if c.input.IsKeyPressed(rl.KeyRight) && c.cursorPos < len(c.inputBuffer) {
|
if rl.IsKeyPressed(rl.KeyRight) && c.cursorPos < len(c.inputBuffer) {
|
||||||
c.cursorPos++
|
c.cursorPos++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,106 +0,0 @@
|
|||||||
package game
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/game/testutils"
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/types"
|
|
||||||
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
|
||||||
rl "github.com/gen2brain/raylib-go/raylib"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestChat_AddMessage(t *testing.T) {
|
|
||||||
chat := NewChat()
|
|
||||||
|
|
||||||
// Test adding single message
|
|
||||||
chat.AddMessage(1, "Hello")
|
|
||||||
assert.Equal(t, 1, len(chat.messages))
|
|
||||||
assert.Equal(t, int32(1), chat.messages[0].PlayerID)
|
|
||||||
assert.Equal(t, "Hello", chat.messages[0].Content)
|
|
||||||
|
|
||||||
// Test message limit
|
|
||||||
for i := 0; i < maxMessages+10; i++ {
|
|
||||||
chat.AddMessage(1, "spam")
|
|
||||||
}
|
|
||||||
assert.Equal(t, maxMessages, len(chat.messages))
|
|
||||||
assert.Equal(t, "spam", chat.messages[len(chat.messages)-1].Content)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestChat_HandleServerMessages(t *testing.T) {
|
|
||||||
chat := NewChat()
|
|
||||||
mockGame := &Game{
|
|
||||||
Player: &types.Player{ID: 1},
|
|
||||||
OtherPlayers: map[int32]*types.Player{
|
|
||||||
2: {ID: 2},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
chat.userData = mockGame
|
|
||||||
|
|
||||||
messages := []*pb.ChatMessage{
|
|
||||||
{
|
|
||||||
PlayerId: 1,
|
|
||||||
Username: "player1",
|
|
||||||
Content: "test1",
|
|
||||||
Timestamp: time.Now().UnixNano(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
PlayerId: 2,
|
|
||||||
Username: "player2",
|
|
||||||
Content: "test2",
|
|
||||||
Timestamp: time.Now().UnixNano(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
chat.HandleServerMessages(messages)
|
|
||||||
assert.Equal(t, 2, len(chat.messages))
|
|
||||||
assert.Equal(t, "test1", chat.messages[0].Content)
|
|
||||||
assert.Equal(t, "test2", chat.messages[1].Content)
|
|
||||||
|
|
||||||
// Test duplicate message prevention
|
|
||||||
chat.HandleServerMessages(messages)
|
|
||||||
assert.Equal(t, 2, len(chat.messages))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestChat_Update(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
done := make(chan bool)
|
|
||||||
go func() {
|
|
||||||
game, cleanup := setupTestEnvironment(t)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
chat := game.Chat
|
|
||||||
|
|
||||||
// Test starting chat
|
|
||||||
testutils.SimulateKeyPress(rl.KeyT)
|
|
||||||
msg, sent := chat.Update()
|
|
||||||
assert.True(t, chat.isTyping)
|
|
||||||
assert.False(t, sent)
|
|
||||||
assert.Empty(t, msg)
|
|
||||||
|
|
||||||
// Test typing message
|
|
||||||
testutils.SimulateCharInput('h')
|
|
||||||
msg, sent = chat.Update()
|
|
||||||
testutils.SimulateCharInput('i')
|
|
||||||
msg, sent = chat.Update()
|
|
||||||
assert.Equal(t, 2, len(chat.inputBuffer))
|
|
||||||
assert.False(t, sent)
|
|
||||||
assert.Empty(t, msg)
|
|
||||||
|
|
||||||
// Test sending message
|
|
||||||
testutils.SimulateKeyPress(rl.KeyEnter)
|
|
||||||
msg, sent = chat.Update()
|
|
||||||
assert.True(t, sent)
|
|
||||||
assert.Equal(t, "hi", msg)
|
|
||||||
assert.False(t, chat.isTyping)
|
|
||||||
done <- true
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
// Test completed successfully
|
|
||||||
case <-time.After(5 * time.Second):
|
|
||||||
t.Fatal("Test timed out")
|
|
||||||
}
|
|
||||||
}
|
|
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()
|
||||||
|
}
|
170
game/game.go
170
game/game.go
@ -1,34 +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
|
||||||
input InputHandler
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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),
|
||||||
@ -39,29 +49,44 @@ func New() *Game {
|
|||||||
Chat: NewChat(),
|
Chat: NewChat(),
|
||||||
QuitChan: make(chan struct{}),
|
QuitChan: make(chan struct{}),
|
||||||
loginScreen: NewLoginScreen(),
|
loginScreen: NewLoginScreen(),
|
||||||
input: &RaylibInput{},
|
|
||||||
}
|
}
|
||||||
game.Chat.userData = game
|
game.Chat.userData = game
|
||||||
return game
|
return 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)
|
||||||
@ -79,8 +104,8 @@ func (g *Game) Update(deltaTime float32) {
|
|||||||
}
|
}
|
||||||
g.AssignModelToPlayer(g.Player)
|
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()
|
||||||
@ -88,13 +113,13 @@ func (g *Game) Update(deltaTime float32) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle ESC for menu
|
// Handle ESC for menu
|
||||||
if g.input.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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,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)
|
||||||
}
|
}
|
||||||
@ -151,10 +182,15 @@ 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)
|
modelIndex := int(player.ID) % len(g.Models)
|
||||||
modelAsset := g.Models[modelIndex]
|
modelAsset := g.Models[modelIndex]
|
||||||
@ -171,15 +207,15 @@ func (g *Game) DrawPlayer(player *types.Player, model rl.Model) {
|
|||||||
if player.IsMoving && len(modelAsset.Animations.Walk) > 0 {
|
if player.IsMoving && len(modelAsset.Animations.Walk) > 0 {
|
||||||
anim := modelAsset.Animations.Walk[0] // Use first walk animation
|
anim := modelAsset.Animations.Walk[0] // Use first walk animation
|
||||||
player.AnimationFrame = player.AnimationFrame % anim.FrameCount
|
player.AnimationFrame = player.AnimationFrame % anim.FrameCount
|
||||||
rl.UpdateModelAnimation(model, anim, player.AnimationFrame)
|
rl.UpdateModelAnimation(player.Model, anim, player.AnimationFrame)
|
||||||
} else if len(modelAsset.Animations.Idle) > 0 {
|
} else if len(modelAsset.Animations.Idle) > 0 {
|
||||||
anim := modelAsset.Animations.Idle[0] // Use first idle animation
|
anim := modelAsset.Animations.Idle[0] // Use first idle animation
|
||||||
player.AnimationFrame = player.AnimationFrame % anim.FrameCount
|
player.AnimationFrame = player.AnimationFrame % anim.FrameCount
|
||||||
rl.UpdateModelAnimation(model, anim, player.AnimationFrame)
|
rl.UpdateModelAnimation(player.Model, anim, player.AnimationFrame)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rl.DrawModel(model, playerPos, 16, rl.White)
|
rl.DrawModel(player.Model, playerPos, 16, rl.White)
|
||||||
|
|
||||||
// Draw floating messages and path indicators
|
// Draw floating messages and path indicators
|
||||||
if player.FloatingMessage != nil {
|
if player.FloatingMessage != nil {
|
||||||
@ -212,24 +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 _, 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 {
|
||||||
g.AssignModelToPlayer(other)
|
g.AssignModelToPlayer(other)
|
||||||
}
|
}
|
||||||
g.DrawPlayer(other, other.Model)
|
g.DrawPlayer(other)
|
||||||
}
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
rl.EndMode3D()
|
rl.EndMode3D()
|
||||||
|
|
||||||
// Draw floating messages
|
// Draw floating messages
|
||||||
@ -257,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() {
|
||||||
@ -327,16 +389,16 @@ func (g *Game) DrawMenu() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check mouse hover
|
// Check mouse hover
|
||||||
mousePoint := g.input.GetMousePosition()
|
mousePoint := rl.GetMousePosition()
|
||||||
mouseHover := rl.CheckCollisionPointRec(mousePoint, buttonRect)
|
mouseHover := rl.CheckCollisionPointRec(mousePoint, buttonRect)
|
||||||
|
|
||||||
// Draw button
|
// Draw button
|
||||||
if mouseHover {
|
if mouseHover {
|
||||||
rl.DrawRectangleRec(buttonRect, rl.ColorAlpha(rl.White, 0.3))
|
rl.DrawRectangleRec(buttonRect, rl.ColorAlpha(rl.White, 0.3))
|
||||||
if g.input.IsMouseButtonPressed(toInt32(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":
|
||||||
@ -371,7 +433,37 @@ func (g *Game) AssignModelToPlayer(player *types.Player) {
|
|||||||
modelIndex := int(player.ID) % len(g.Models)
|
modelIndex := int(player.ID) % len(g.Models)
|
||||||
modelAsset := g.Models[modelIndex]
|
modelAsset := g.Models[modelIndex]
|
||||||
|
|
||||||
// Just use the original model - don't try to copy it
|
|
||||||
player.Model = modelAsset.Model
|
player.Model = modelAsset.Model
|
||||||
player.Texture = modelAsset.Texture
|
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)
|
||||||
}
|
}
|
||||||
|
@ -1,106 +0,0 @@
|
|||||||
package game
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/game/testutils"
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/types"
|
|
||||||
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
|
||||||
rl "github.com/gen2brain/raylib-go/raylib"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGame_HandleInput(t *testing.T) {
|
|
||||||
game := New()
|
|
||||||
game.Player = &types.Player{
|
|
||||||
ID: 1,
|
|
||||||
Speed: 50.0,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test valid click
|
|
||||||
simulateMouseRay(rl.Ray{
|
|
||||||
Position: rl.Vector3{X: 0, Y: 10, Z: 0},
|
|
||||||
Direction: rl.Vector3{X: 0, Y: -1, Z: 0},
|
|
||||||
})
|
|
||||||
simulateMouseButton(toInt32(rl.MouseLeftButton), true)
|
|
||||||
game.HandleInput()
|
|
||||||
assert.NotEmpty(t, game.Player.TargetPath)
|
|
||||||
|
|
||||||
// Test invalid click (outside map)
|
|
||||||
simulateMouseRay(rl.Ray{
|
|
||||||
Position: rl.Vector3{X: 1000, Y: 10, Z: 1000},
|
|
||||||
Direction: rl.Vector3{X: 0, Y: -1, Z: 0},
|
|
||||||
})
|
|
||||||
simulateMouseButton(toInt32(rl.MouseLeftButton), true)
|
|
||||||
game.HandleInput()
|
|
||||||
assert.Empty(t, game.Player.TargetPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGame_UpdateCamera(t *testing.T) {
|
|
||||||
game := New()
|
|
||||||
|
|
||||||
// Test zoom limits
|
|
||||||
testutils.SimulateMouseWheel(1.0) // Zoom in
|
|
||||||
testutils.SimulateMouseWheel(1.0)
|
|
||||||
assert.GreaterOrEqual(t, cameraDistance, float32(10.0))
|
|
||||||
|
|
||||||
testutils.SimulateMouseWheel(-1.0) // Zoom out
|
|
||||||
testutils.SimulateMouseWheel(-1.0)
|
|
||||||
assert.LessOrEqual(t, cameraDistance, float32(250.0))
|
|
||||||
|
|
||||||
// Test camera rotation
|
|
||||||
originalYaw := cameraYaw
|
|
||||||
testutils.SimulateKeyDown(rl.KeyRight, true)
|
|
||||||
game.Update(0.1)
|
|
||||||
assert.Greater(t, cameraYaw, originalYaw)
|
|
||||||
|
|
||||||
// Test pitch limits
|
|
||||||
simulateKeyDown(rl.KeyUp, true)
|
|
||||||
for i := 0; i < 100; i++ {
|
|
||||||
game.Update(0.1)
|
|
||||||
}
|
|
||||||
assert.GreaterOrEqual(t, cameraPitch, float32(20.0))
|
|
||||||
assert.LessOrEqual(t, cameraPitch, float32(85.0))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGame_ChatIntegration(t *testing.T) {
|
|
||||||
game := New()
|
|
||||||
game.Player = &types.Player{ID: 1}
|
|
||||||
|
|
||||||
// Test chat message to action queue
|
|
||||||
testutils.SimulateKeyPress(rl.KeyT)
|
|
||||||
game.Update(0.1)
|
|
||||||
assert.True(t, game.Chat.isTyping)
|
|
||||||
|
|
||||||
testutils.SimulateCharInput('h')
|
|
||||||
testutils.SimulateCharInput('i')
|
|
||||||
testutils.SimulateKeyPress(rl.KeyEnter)
|
|
||||||
game.Update(0.1)
|
|
||||||
|
|
||||||
assert.Equal(t, 1, len(game.Player.ActionQueue))
|
|
||||||
assert.Equal(t, pb.Action_CHAT, game.Player.ActionQueue[0].Type)
|
|
||||||
assert.Equal(t, "hi", game.Player.ActionQueue[0].ChatMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGame_MenuHandling(t *testing.T) {
|
|
||||||
game := New()
|
|
||||||
|
|
||||||
// Test menu toggle
|
|
||||||
assert.False(t, game.MenuOpen)
|
|
||||||
testutils.SimulateKeyPress(rl.KeyEscape)
|
|
||||||
game.Update(0.1)
|
|
||||||
assert.True(t, game.MenuOpen)
|
|
||||||
|
|
||||||
// Test input blocking when menu is open
|
|
||||||
game.Player = &types.Player{ID: 1}
|
|
||||||
testutils.SimulateMouseButton(testutils.ToInt32(rl.MouseLeftButton), true)
|
|
||||||
game.Update(0.1)
|
|
||||||
assert.Empty(t, game.Player.TargetPath)
|
|
||||||
|
|
||||||
// Test menu close
|
|
||||||
testutils.SimulateKeyPress(rl.KeyEscape)
|
|
||||||
game.Update(0.1)
|
|
||||||
assert.False(t, game.MenuOpen)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add more test helpers as needed...
|
|
@ -3,95 +3,16 @@ package game
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/game/mock"
|
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
// InputHandler abstracts raylib input functions for testing
|
|
||||||
type InputHandler interface {
|
|
||||||
IsKeyPressed(key int32) bool
|
|
||||||
IsKeyDown(key int32) bool
|
|
||||||
IsMouseButtonPressed(button int32) bool
|
|
||||||
GetMousePosition() rl.Vector2
|
|
||||||
GetMouseRay(mousePos rl.Vector2, camera rl.Camera3D) rl.Ray
|
|
||||||
GetMouseWheelMove() float32
|
|
||||||
GetCharPressed() rune
|
|
||||||
}
|
|
||||||
|
|
||||||
// RaylibInput implements InputHandler using actual raylib functions
|
|
||||||
type RaylibInput struct{}
|
|
||||||
|
|
||||||
func (r *RaylibInput) IsKeyPressed(key int32) bool { return rl.IsKeyPressed(key) }
|
|
||||||
func (r *RaylibInput) IsKeyDown(key int32) bool { return rl.IsKeyDown(key) }
|
|
||||||
func (r *RaylibInput) IsMouseButtonPressed(button int32) bool {
|
|
||||||
return rl.IsMouseButtonPressed(rl.MouseButton(button))
|
|
||||||
}
|
|
||||||
func (r *RaylibInput) GetMousePosition() rl.Vector2 { return rl.GetMousePosition() }
|
|
||||||
func (r *RaylibInput) GetMouseRay(mousePos rl.Vector2, camera rl.Camera3D) rl.Ray {
|
|
||||||
return rl.GetMouseRay(mousePos, camera)
|
|
||||||
}
|
|
||||||
func (r *RaylibInput) GetMouseWheelMove() float32 { return rl.GetMouseWheelMove() }
|
|
||||||
func (r *RaylibInput) GetCharPressed() rune { return rl.GetCharPressed() }
|
|
||||||
|
|
||||||
// MockInput implements InputHandler using our mock functions
|
|
||||||
type MockInput struct{}
|
|
||||||
|
|
||||||
func (m *MockInput) IsKeyPressed(key int32) bool {
|
|
||||||
if mock.IsKeyPressed == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return mock.IsKeyPressed(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockInput) IsKeyDown(key int32) bool {
|
|
||||||
if mock.IsKeyDown == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return mock.IsKeyDown(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockInput) IsMouseButtonPressed(button int32) bool {
|
|
||||||
if mock.IsMouseButtonPressed == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return mock.IsMouseButtonPressed(button)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockInput) GetMousePosition() rl.Vector2 {
|
|
||||||
if mock.GetMousePosition == nil {
|
|
||||||
return rl.Vector2{}
|
|
||||||
}
|
|
||||||
return mock.GetMousePosition()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockInput) GetMouseRay(mousePos rl.Vector2, camera rl.Camera3D) rl.Ray {
|
|
||||||
if mock.GetMouseRay == nil {
|
|
||||||
return rl.Ray{}
|
|
||||||
}
|
|
||||||
return mock.GetMouseRay(mousePos, camera)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockInput) GetMouseWheelMove() float32 {
|
|
||||||
if mock.GetMouseWheelMove == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return mock.GetMouseWheelMove()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockInput) GetCharPressed() rune {
|
|
||||||
if mock.GetCharPressed == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return mock.GetCharPressed()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *Game) GetTileAtMouse() (types.Tile, bool) {
|
func (g *Game) GetTileAtMouse() (types.Tile, bool) {
|
||||||
if !g.input.IsMouseButtonPressed(toInt32(rl.MouseLeftButton)) {
|
if !rl.IsMouseButtonPressed(rl.MouseLeftButton) {
|
||||||
return types.Tile{}, false
|
return types.Tile{}, false
|
||||||
}
|
}
|
||||||
mouse := g.input.GetMousePosition()
|
mouse := rl.GetMousePosition()
|
||||||
ray := g.input.GetMouseRay(mouse, g.Camera)
|
ray := rl.GetMouseRay(mouse, g.Camera)
|
||||||
|
|
||||||
for x := 0; x < types.MapWidth; x++ {
|
for x := 0; x < types.MapWidth; x++ {
|
||||||
for y := 0; y < types.MapHeight; y++ {
|
for y := 0; y < types.MapHeight; y++ {
|
||||||
|
@ -1,187 +0,0 @@
|
|||||||
package game
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/game/testutils"
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/types"
|
|
||||||
rl "github.com/gen2brain/raylib-go/raylib"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMouseInput_EdgeCases(t *testing.T) {
|
|
||||||
game, cleanup := setupTestEnvironment(t)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
game.Player = &types.Player{ID: 1}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
ray rl.Ray
|
|
||||||
expected bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Click outside map bounds",
|
|
||||||
ray: rl.Ray{
|
|
||||||
Position: rl.Vector3{X: 1000, Y: 10, Z: 1000},
|
|
||||||
Direction: rl.Vector3{X: 0, Y: -1, Z: 0},
|
|
||||||
},
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Click at map edge",
|
|
||||||
ray: rl.Ray{
|
|
||||||
Position: rl.Vector3{X: float32(types.MapWidth * types.TileSize), Y: 10, Z: 0},
|
|
||||||
Direction: rl.Vector3{X: 0, Y: -1, Z: 0},
|
|
||||||
},
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Click on valid tile",
|
|
||||||
ray: rl.Ray{
|
|
||||||
Position: rl.Vector3{X: 32, Y: 10, Z: 32},
|
|
||||||
Direction: rl.Vector3{X: 0, Y: -1, Z: 0},
|
|
||||||
},
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
testutils.ResetMockInput()
|
|
||||||
testutils.SimulateMouseRay(tt.ray)
|
|
||||||
testutils.SimulateMouseButton(testutils.ToInt32(rl.MouseLeftButton), true)
|
|
||||||
tile, clicked := game.GetTileAtMouse()
|
|
||||||
assert.Equal(t, tt.expected, clicked)
|
|
||||||
if tt.expected {
|
|
||||||
assert.NotEmpty(t, tile)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestChat_InputValidation(t *testing.T) {
|
|
||||||
game, cleanup := setupTestEnvironment(t)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
game.Player = &types.Player{ID: 1}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input []rune
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Empty message",
|
|
||||||
input: []rune{},
|
|
||||||
expected: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Message with only spaces",
|
|
||||||
input: []rune(" "),
|
|
||||||
expected: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Very long message",
|
|
||||||
input: []rune(string(make([]rune, runeLimit))),
|
|
||||||
expected: string(make([]rune, runeLimit)),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Unicode characters",
|
|
||||||
input: []rune("Hello 世界"),
|
|
||||||
expected: "Hello 世界",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
testutils.ResetMockInput()
|
|
||||||
testutils.SimulateKeyPress(rl.KeyT)
|
|
||||||
game.Update(0.1)
|
|
||||||
|
|
||||||
for _, r := range tt.input {
|
|
||||||
testutils.SimulateCharInput(r)
|
|
||||||
game.Update(0.1)
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.SimulateKeyPress(rl.KeyEnter)
|
|
||||||
game.Update(0.1)
|
|
||||||
|
|
||||||
if tt.expected != "" {
|
|
||||||
assert.Equal(t, 1, len(game.Player.ActionQueue))
|
|
||||||
assert.Equal(t, tt.expected, game.Player.ActionQueue[0].ChatMessage)
|
|
||||||
} else {
|
|
||||||
assert.Empty(t, game.Player.ActionQueue)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLogin_InputValidation(t *testing.T) {
|
|
||||||
_, cleanup := setupTestEnvironment(t)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
username string
|
|
||||||
password string
|
|
||||||
expectSuccess bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Valid credentials",
|
|
||||||
username: "validuser",
|
|
||||||
password: "validpass",
|
|
||||||
expectSuccess: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Empty username",
|
|
||||||
username: "",
|
|
||||||
password: "password",
|
|
||||||
expectSuccess: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Empty password",
|
|
||||||
username: "username",
|
|
||||||
password: "",
|
|
||||||
expectSuccess: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Username too long",
|
|
||||||
username: "verylongusername123",
|
|
||||||
password: "password",
|
|
||||||
expectSuccess: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Special characters in username",
|
|
||||||
username: "user@name",
|
|
||||||
password: "password",
|
|
||||||
expectSuccess: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
login := NewLoginScreen()
|
|
||||||
testutils.ResetMockInput()
|
|
||||||
|
|
||||||
// Simulate typing username
|
|
||||||
login.focusedField = 0
|
|
||||||
for _, r := range tt.username {
|
|
||||||
testutils.SimulateCharInput(r)
|
|
||||||
login.Update()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simulate typing password
|
|
||||||
login.focusedField = 1
|
|
||||||
for _, r := range tt.password {
|
|
||||||
testutils.SimulateCharInput(r)
|
|
||||||
login.Update()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simulate clicking login button
|
|
||||||
testutils.SimulateMouseClick(400, 365)
|
|
||||||
_, _, _, submitted := login.Update()
|
|
||||||
assert.Equal(t, tt.expectSuccess, submitted)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
package game
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/game/testutils"
|
|
||||||
rl "github.com/gen2brain/raylib-go/raylib"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLoginScreen_Update(t *testing.T) {
|
|
||||||
login := NewLoginScreen()
|
|
||||||
|
|
||||||
// Test field focus switching
|
|
||||||
simulateMouseClick(400, 215) // Click username field
|
|
||||||
assert.Equal(t, 0, login.focusedField)
|
|
||||||
|
|
||||||
simulateMouseClick(400, 265) // Click password field
|
|
||||||
assert.Equal(t, 1, login.focusedField)
|
|
||||||
|
|
||||||
// Test input length limits
|
|
||||||
login.focusedField = 0
|
|
||||||
for i := 0; i < 20; i++ {
|
|
||||||
simulateCharInput('x')
|
|
||||||
}
|
|
||||||
assert.LessOrEqual(t, len(login.username), 12)
|
|
||||||
|
|
||||||
login.focusedField = 1
|
|
||||||
for i := 0; i < 30; i++ {
|
|
||||||
simulateCharInput('x')
|
|
||||||
}
|
|
||||||
assert.LessOrEqual(t, len(login.password), 20)
|
|
||||||
|
|
||||||
// Test mode switching
|
|
||||||
simulateMouseClick(600, 365) // Click switch mode button
|
|
||||||
assert.True(t, login.isRegistering)
|
|
||||||
simulateMouseClick(600, 365) // Click again
|
|
||||||
assert.False(t, login.isRegistering)
|
|
||||||
|
|
||||||
// Test submission
|
|
||||||
login.username = "test"
|
|
||||||
login.password = "password"
|
|
||||||
testutils.SimulateMousePosition(400, 365)
|
|
||||||
testutils.SimulateMouseButton(testutils.ToInt32(rl.MouseLeftButton), true)
|
|
||||||
username, password, isRegistering, submitted := login.Update()
|
|
||||||
assert.True(t, submitted)
|
|
||||||
assert.Equal(t, "test", username)
|
|
||||||
assert.Equal(t, "password", password)
|
|
||||||
assert.False(t, isRegistering)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoginScreen_ErrorHandling(t *testing.T) {
|
|
||||||
login := NewLoginScreen()
|
|
||||||
|
|
||||||
// Test empty fields
|
|
||||||
login.username = ""
|
|
||||||
login.password = "test"
|
|
||||||
testutils.SimulateMousePosition(400, 365)
|
|
||||||
testutils.SimulateMouseButton(testutils.ToInt32(rl.MouseLeftButton), true)
|
|
||||||
_, _, _, submitted := login.Update()
|
|
||||||
assert.False(t, submitted)
|
|
||||||
assert.Contains(t, login.errorMessage, "username")
|
|
||||||
|
|
||||||
// Test special characters
|
|
||||||
login.username = "test!@#"
|
|
||||||
login.password = "password"
|
|
||||||
testutils.SimulateMousePosition(400, 365)
|
|
||||||
testutils.SimulateMouseButton(testutils.ToInt32(rl.MouseLeftButton), true)
|
|
||||||
_, _, _, submitted = login.Update()
|
|
||||||
assert.False(t, submitted)
|
|
||||||
assert.Contains(t, login.errorMessage, "invalid characters")
|
|
||||||
|
|
||||||
// Test error message display
|
|
||||||
login.SetError("Test error")
|
|
||||||
assert.Equal(t, "Test error", login.errorMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
func simulateMouseClick(x, y float32) {
|
|
||||||
// Implementation would depend on how raylib is mocked
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
package mock
|
|
||||||
|
|
||||||
import (
|
|
||||||
rl "github.com/gen2brain/raylib-go/raylib"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
IsKeyPressed func(key int32) bool
|
|
||||||
IsKeyDown func(key int32) bool
|
|
||||||
IsMouseButtonPressed func(button int32) bool
|
|
||||||
GetMousePosition func() rl.Vector2
|
|
||||||
GetMouseRay func(mousePos rl.Vector2, camera rl.Camera3D) rl.Ray
|
|
||||||
GetMouseWheelMove func() float32
|
|
||||||
GetCharPressed func() rune
|
|
||||||
)
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,141 +0,0 @@
|
|||||||
package game
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/game/mock"
|
|
||||||
rl "github.com/gen2brain/raylib-go/raylib"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
mockInput struct {
|
|
||||||
sync.Mutex
|
|
||||||
keyPressed map[int32]bool
|
|
||||||
keyDown map[int32]bool
|
|
||||||
mousePressed map[int32]bool
|
|
||||||
mousePosition rl.Vector2
|
|
||||||
mouseRay rl.Ray
|
|
||||||
mouseWheel float32
|
|
||||||
charPressed rune
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
resetMockInput()
|
|
||||||
setupMockFunctions()
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupMockFunctions() {
|
|
||||||
mock.IsKeyPressed = mockIsKeyPressed
|
|
||||||
mock.IsKeyDown = mockIsKeyDown
|
|
||||||
mock.IsMouseButtonPressed = mockIsMouseButtonPressed
|
|
||||||
mock.GetMousePosition = mockGetMousePosition
|
|
||||||
mock.GetMouseRay = mockGetMouseRay
|
|
||||||
mock.GetMouseWheelMove = mockGetMouseWheelMove
|
|
||||||
mock.GetCharPressed = mockGetCharPressed
|
|
||||||
}
|
|
||||||
|
|
||||||
func resetMockInput() {
|
|
||||||
mockInput.Lock()
|
|
||||||
defer mockInput.Unlock()
|
|
||||||
mockInput.keyPressed = make(map[int32]bool)
|
|
||||||
mockInput.keyDown = make(map[int32]bool)
|
|
||||||
mockInput.mousePressed = make(map[int32]bool)
|
|
||||||
mockInput.mousePosition = rl.Vector2{}
|
|
||||||
mockInput.mouseRay = rl.Ray{}
|
|
||||||
mockInput.mouseWheel = 0
|
|
||||||
mockInput.charPressed = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock input simulation functions
|
|
||||||
func simulateKeyPress(key int32) {
|
|
||||||
mockInput.Lock()
|
|
||||||
mockInput.keyPressed[key] = true
|
|
||||||
mockInput.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func simulateKeyDown(key int32, isDown bool) {
|
|
||||||
mockInput.Lock()
|
|
||||||
mockInput.keyDown[key] = isDown
|
|
||||||
mockInput.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func simulateMouseButton(button int32, isPressed bool) {
|
|
||||||
mockInput.Lock()
|
|
||||||
mockInput.mousePressed[button] = isPressed
|
|
||||||
mockInput.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func simulateMousePosition(x, y float32) {
|
|
||||||
mockInput.Lock()
|
|
||||||
mockInput.mousePosition = rl.Vector2{X: x, Y: y}
|
|
||||||
mockInput.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func simulateMouseRay(ray rl.Ray) {
|
|
||||||
mockInput.Lock()
|
|
||||||
mockInput.mouseRay = ray
|
|
||||||
mockInput.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func simulateMouseWheel(move float32) {
|
|
||||||
mockInput.Lock()
|
|
||||||
mockInput.mouseWheel = move
|
|
||||||
mockInput.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func simulateCharInput(char rune) {
|
|
||||||
mockInput.Lock()
|
|
||||||
mockInput.charPressed = char
|
|
||||||
mockInput.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock raylib functions
|
|
||||||
func mockIsKeyPressed(key int32) bool {
|
|
||||||
mockInput.Lock()
|
|
||||||
defer mockInput.Unlock()
|
|
||||||
return mockInput.keyPressed[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
func mockIsKeyDown(key int32) bool {
|
|
||||||
mockInput.Lock()
|
|
||||||
defer mockInput.Unlock()
|
|
||||||
return mockInput.keyDown[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
func mockIsMouseButtonPressed(button int32) bool {
|
|
||||||
mockInput.Lock()
|
|
||||||
defer mockInput.Unlock()
|
|
||||||
return mockInput.mousePressed[button]
|
|
||||||
}
|
|
||||||
|
|
||||||
func mockGetMousePosition() rl.Vector2 {
|
|
||||||
mockInput.Lock()
|
|
||||||
defer mockInput.Unlock()
|
|
||||||
return mockInput.mousePosition
|
|
||||||
}
|
|
||||||
|
|
||||||
func mockGetMouseRay(mousePos rl.Vector2, camera rl.Camera3D) rl.Ray {
|
|
||||||
mockInput.Lock()
|
|
||||||
defer mockInput.Unlock()
|
|
||||||
return mockInput.mouseRay
|
|
||||||
}
|
|
||||||
|
|
||||||
func mockGetMouseWheelMove() float32 {
|
|
||||||
mockInput.Lock()
|
|
||||||
defer mockInput.Unlock()
|
|
||||||
return mockInput.mouseWheel
|
|
||||||
}
|
|
||||||
|
|
||||||
func mockGetCharPressed() rune {
|
|
||||||
mockInput.Lock()
|
|
||||||
defer mockInput.Unlock()
|
|
||||||
return mockInput.charPressed
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add more mock implementations...
|
|
||||||
|
|
||||||
// Add this helper function
|
|
||||||
func toInt32(button rl.MouseButton) int32 {
|
|
||||||
return int32(button)
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
package game
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/game/mock"
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/game/testutils"
|
|
||||||
)
|
|
||||||
|
|
||||||
func setupTestEnvironment(t *testing.T) (*Game, func()) {
|
|
||||||
testutils.ResetMockInput()
|
|
||||||
testutils.SetupMockFunctions()
|
|
||||||
|
|
||||||
game := New()
|
|
||||||
game.input = &MockInput{}
|
|
||||||
game.Chat.input = &MockInput{} // Also inject mock input into chat
|
|
||||||
|
|
||||||
// Verify mock setup
|
|
||||||
if mock.IsKeyPressed == nil || mock.GetCharPressed == nil {
|
|
||||||
t.Fatal("Mock functions not properly initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
return game, func() {
|
|
||||||
testutils.ResetMockInput()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,153 +0,0 @@
|
|||||||
package testutils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/game/mock"
|
|
||||||
rl "github.com/gen2brain/raylib-go/raylib"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
mockInput struct {
|
|
||||||
sync.Mutex
|
|
||||||
keyPressed map[int32]bool
|
|
||||||
keyDown map[int32]bool
|
|
||||||
mousePressed map[int32]bool
|
|
||||||
mousePosition rl.Vector2
|
|
||||||
mouseRay rl.Ray
|
|
||||||
mouseWheel float32
|
|
||||||
charPressed rune
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
ResetMockInput()
|
|
||||||
SetupMockFunctions()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetupMockFunctions initializes mock functions
|
|
||||||
func SetupMockFunctions() {
|
|
||||||
mock.IsKeyPressed = MockIsKeyPressed
|
|
||||||
mock.IsKeyDown = MockIsKeyDown
|
|
||||||
mock.IsMouseButtonPressed = MockIsMouseButtonPressed
|
|
||||||
mock.GetMousePosition = MockGetMousePosition
|
|
||||||
mock.GetMouseRay = MockGetMouseRay
|
|
||||||
mock.GetMouseWheelMove = MockGetMouseWheelMove
|
|
||||||
mock.GetCharPressed = MockGetCharPressed
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResetMockInput resets all mock input states
|
|
||||||
func ResetMockInput() {
|
|
||||||
mockInput.Lock()
|
|
||||||
defer mockInput.Unlock()
|
|
||||||
mockInput.keyPressed = make(map[int32]bool)
|
|
||||||
mockInput.keyDown = make(map[int32]bool)
|
|
||||||
mockInput.mousePressed = make(map[int32]bool)
|
|
||||||
mockInput.mousePosition = rl.Vector2{}
|
|
||||||
mockInput.mouseRay = rl.Ray{}
|
|
||||||
mockInput.mouseWheel = 0
|
|
||||||
mockInput.charPressed = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// SimulateKeyPress simulates a key press
|
|
||||||
func SimulateKeyPress(key int32) {
|
|
||||||
mockInput.Lock()
|
|
||||||
mockInput.keyPressed[key] = true
|
|
||||||
mockInput.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SimulateKeyDown simulates holding a key down
|
|
||||||
func SimulateKeyDown(key int32, isDown bool) {
|
|
||||||
mockInput.Lock()
|
|
||||||
mockInput.keyDown[key] = isDown
|
|
||||||
mockInput.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SimulateMouseButton simulates a mouse button press
|
|
||||||
func SimulateMouseButton(button int32, isPressed bool) {
|
|
||||||
mockInput.Lock()
|
|
||||||
mockInput.mousePressed[button] = isPressed
|
|
||||||
mockInput.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SimulateMousePosition simulates mouse movement
|
|
||||||
func SimulateMousePosition(x, y float32) {
|
|
||||||
mockInput.Lock()
|
|
||||||
mockInput.mousePosition = rl.Vector2{X: x, Y: y}
|
|
||||||
mockInput.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SimulateMouseClick simulates a mouse click at the given position
|
|
||||||
func SimulateMouseClick(x, y float32) {
|
|
||||||
SimulateMousePosition(x, y)
|
|
||||||
SimulateMouseButton(ToInt32(rl.MouseLeftButton), true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SimulateMouseRay simulates a mouse ray
|
|
||||||
func SimulateMouseRay(ray rl.Ray) {
|
|
||||||
mockInput.Lock()
|
|
||||||
mockInput.mouseRay = ray
|
|
||||||
mockInput.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SimulateMouseWheel simulates mouse wheel movement
|
|
||||||
func SimulateMouseWheel(move float32) {
|
|
||||||
mockInput.Lock()
|
|
||||||
mockInput.mouseWheel = move
|
|
||||||
mockInput.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SimulateCharInput simulates character input
|
|
||||||
func SimulateCharInput(char rune) {
|
|
||||||
mockInput.Lock()
|
|
||||||
mockInput.charPressed = char
|
|
||||||
mockInput.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock raylib functions
|
|
||||||
func MockIsKeyPressed(key int32) bool {
|
|
||||||
mockInput.Lock()
|
|
||||||
defer mockInput.Unlock()
|
|
||||||
return mockInput.keyPressed[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
func MockIsKeyDown(key int32) bool {
|
|
||||||
mockInput.Lock()
|
|
||||||
defer mockInput.Unlock()
|
|
||||||
return mockInput.keyDown[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
func MockIsMouseButtonPressed(button int32) bool {
|
|
||||||
mockInput.Lock()
|
|
||||||
defer mockInput.Unlock()
|
|
||||||
return mockInput.mousePressed[button]
|
|
||||||
}
|
|
||||||
|
|
||||||
func MockGetMousePosition() rl.Vector2 {
|
|
||||||
mockInput.Lock()
|
|
||||||
defer mockInput.Unlock()
|
|
||||||
return mockInput.mousePosition
|
|
||||||
}
|
|
||||||
|
|
||||||
func MockGetMouseRay(mousePos rl.Vector2, camera rl.Camera3D) rl.Ray {
|
|
||||||
mockInput.Lock()
|
|
||||||
defer mockInput.Unlock()
|
|
||||||
return mockInput.mouseRay
|
|
||||||
}
|
|
||||||
|
|
||||||
func MockGetMouseWheelMove() float32 {
|
|
||||||
mockInput.Lock()
|
|
||||||
defer mockInput.Unlock()
|
|
||||||
return mockInput.mouseWheel
|
|
||||||
}
|
|
||||||
|
|
||||||
func MockGetCharPressed() rune {
|
|
||||||
mockInput.Lock()
|
|
||||||
defer mockInput.Unlock()
|
|
||||||
return mockInput.charPressed
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToInt32 converts MouseButton to int32
|
|
||||||
func ToInt32(button rl.MouseButton) int32 {
|
|
||||||
return int32(button)
|
|
||||||
}
|
|
11
go.mod
11
go.mod
@ -8,17 +8,10 @@ require (
|
|||||||
google.golang.org/protobuf v1.36.3
|
google.golang.org/protobuf v1.36.3
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/ebitengine/purego v0.8.2 // indirect
|
github.com/ebitengine/purego v0.8.2 // indirect
|
||||||
github.com/stretchr/testify v1.10.0
|
golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
golang.org/x/sys v0.29.0 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
||||||
replace gitea.boner.be/bdnugget/goonserver => ./goonserver
|
replace gitea.boner.be/bdnugget/goonserver => ./goonserver
|
||||||
|
13
go.sum
13
go.sum
@ -1,21 +1,16 @@
|
|||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||||
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b h1:JJfspevP3YOXcSKVABizYOv++yMpTJIdPUtoDzF/RWw=
|
github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b h1:JJfspevP3YOXcSKVABizYOv++yMpTJIdPUtoDzF/RWw=
|
||||||
github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b/go.mod h1:BaY76bZk7nw1/kVOSQObPY1v1iwVE1KHAGMfvI6oK1Q=
|
github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b/go.mod h1:BaY76bZk7nw1/kVOSQObPY1v1iwVE1KHAGMfvI6oK1Q=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
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=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
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=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
|
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/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()
|
||||||
|
@ -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,6 @@ type Tile struct {
|
|||||||
Walkable bool
|
Walkable bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Player struct {
|
|
||||||
sync.Mutex
|
|
||||||
PosActual rl.Vector3
|
|
||||||
PosTile Tile
|
|
||||||
TargetPath []Tile
|
|
||||||
ActionQueue []*pb.Action
|
|
||||||
Speed float32
|
|
||||||
Model rl.Model
|
|
||||||
Texture rl.Texture2D
|
|
||||||
ID int32
|
|
||||||
CurrentTick int64
|
|
||||||
LastUpdateTime time.Time
|
|
||||||
LastAnimUpdate time.Time
|
|
||||||
InterpolationProgress float32
|
|
||||||
UserData interface{}
|
|
||||||
FloatingMessage *FloatingMessage
|
|
||||||
QuitDone chan struct{}
|
|
||||||
AnimationFrame int32
|
|
||||||
IsMoving bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type AnimationSet struct {
|
type AnimationSet struct {
|
||||||
Idle []rl.ModelAnimation
|
Idle []rl.ModelAnimation
|
||||||
Walk []rl.ModelAnimation
|
Walk []rl.ModelAnimation
|
||||||
@ -50,6 +28,7 @@ type ModelAsset struct {
|
|||||||
AnimFrames int32 // Keep this for compatibility
|
AnimFrames int32 // Keep this for compatibility
|
||||||
Animations AnimationSet // New field for organized animations
|
Animations AnimationSet // New field for organized animations
|
||||||
YOffset float32 // Additional height offset (added to default 8.0)
|
YOffset float32 // Additional height offset (added to default 8.0)
|
||||||
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChatMessage struct {
|
type ChatMessage struct {
|
||||||
@ -69,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