1 Commits

Author SHA1 Message Date
75eff6c5ad Stash bunch of broken generated test with shitty broken mocking 2025-01-22 00:40:31 +01:00
19 changed files with 1089 additions and 353 deletions

View File

@ -85,3 +85,19 @@ func LoadModels() ([]types.ModelAsset, error) {
func LoadMusic(filename string) (rl.Music, error) {
return rl.LoadMusicStream(filename), nil
}
func UnloadModels(models []types.ModelAsset) {
for _, model := range models {
if model.Animation != nil {
for i := int32(0); i < model.AnimFrames; i++ {
rl.UnloadModelAnimation(model.Animation[i])
}
}
rl.UnloadModel(model.Model)
rl.UnloadTexture(model.Texture)
}
}
func UnloadMusic(music rl.Music) {
rl.UnloadMusicStream(music)
}

View File

@ -10,7 +10,6 @@ var (
cameraDistance = float32(20.0)
cameraYaw = float32(145.0)
cameraPitch = float32(45.0)
lastMousePos rl.Vector2 // Add this to track mouse movement
)
func UpdateCamera(camera *rl.Camera3D, player rl.Vector3, deltaTime float32) {
@ -33,34 +32,6 @@ 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) {
cameraYaw += 100 * deltaTime
}

View File

@ -2,7 +2,6 @@ package game
import (
"fmt"
"sync"
"time"
"gitea.boner.be/bdnugget/goonscape/types"
@ -26,13 +25,14 @@ type Chat struct {
cursorPos int
scrollOffset int
userData interface{}
mutex sync.RWMutex
input InputHandler
}
func NewChat() *Chat {
return &Chat{
messages: make([]types.ChatMessage, 0, maxMessages),
inputBuffer: make([]rune, 0, runeLimit),
input: &RaylibInput{},
}
}
@ -51,9 +51,6 @@ func (c *Chat) AddMessage(playerID int32, content string) {
}
func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
c.mutex.Lock()
defer c.mutex.Unlock()
// Convert protobuf messages to our local type
for _, msg := range messages {
localMsg := types.ChatMessage{
@ -99,9 +96,6 @@ func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
}
func (c *Chat) Draw(screenWidth, screenHeight int32) {
c.mutex.RLock()
defer c.mutex.RUnlock()
// Calculate chat window width based on screen width
chatWindowWidth := screenWidth - (chatMargin * 2)
@ -161,23 +155,23 @@ func (c *Chat) Update() (string, bool) {
c.scrollOffset = clamp(c.scrollOffset-int(wheelMove), 0, maxScroll)
}
if rl.IsKeyPressed(rl.KeyT) {
if c.input.IsKeyPressed(rl.KeyT) {
c.isTyping = true
return "", false
}
return "", false
}
key := rl.GetCharPressed()
key := c.input.GetCharPressed()
for key > 0 {
if len(c.inputBuffer) < runeLimit {
c.inputBuffer = append(c.inputBuffer[:c.cursorPos], append([]rune{key}, c.inputBuffer[c.cursorPos:]...)...)
c.cursorPos++
}
key = rl.GetCharPressed()
key = c.input.GetCharPressed()
}
if rl.IsKeyPressed(rl.KeyEnter) || rl.IsKeyPressed(rl.KeyKpEnter) {
if c.input.IsKeyPressed(rl.KeyEnter) || c.input.IsKeyPressed(rl.KeyKpEnter) {
if len(c.inputBuffer) > 0 {
message := string(c.inputBuffer)
c.inputBuffer = c.inputBuffer[:0]
@ -188,21 +182,21 @@ func (c *Chat) Update() (string, bool) {
c.isTyping = false
}
if rl.IsKeyPressed(rl.KeyEscape) && c.isTyping {
if c.input.IsKeyPressed(rl.KeyEscape) && c.isTyping {
c.inputBuffer = c.inputBuffer[:0]
c.cursorPos = 0
c.isTyping = false
}
if rl.IsKeyPressed(rl.KeyBackspace) && c.cursorPos > 0 {
if c.input.IsKeyPressed(rl.KeyBackspace) && c.cursorPos > 0 {
c.inputBuffer = append(c.inputBuffer[:c.cursorPos-1], c.inputBuffer[c.cursorPos:]...)
c.cursorPos--
}
if rl.IsKeyPressed(rl.KeyLeft) && c.cursorPos > 0 {
if c.input.IsKeyPressed(rl.KeyLeft) && c.cursorPos > 0 {
c.cursorPos--
}
if rl.IsKeyPressed(rl.KeyRight) && c.cursorPos < len(c.inputBuffer) {
if c.input.IsKeyPressed(rl.KeyRight) && c.cursorPos < len(c.inputBuffer) {
c.cursorPos++
}

106
game/chat_test.go Normal file
View File

@ -0,0 +1,106 @@
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")
}
}

View File

@ -1,8 +1,7 @@
package game
import (
"fmt"
"sync"
"os"
"time"
"gitea.boner.be/bdnugget/goonscape/assets"
@ -20,10 +19,10 @@ type Game struct {
Music rl.Music
Chat *Chat
MenuOpen bool
quitChan chan struct{}
QuitChan chan struct{} // Channel to signal shutdown
loginScreen *LoginScreen
isLoggedIn bool
cleanupOnce sync.Once
input InputHandler
}
func New() *Game {
@ -38,40 +37,24 @@ func New() *Game {
Projection: rl.CameraPerspective,
},
Chat: NewChat(),
quitChan: make(chan struct{}),
QuitChan: make(chan struct{}),
loginScreen: NewLoginScreen(),
input: &RaylibInput{},
}
game.Chat.userData = game
return game
}
func (g *Game) LoadAssets() error {
var loadErr error
defer func() {
if r := recover(); r != nil {
loadErr = fmt.Errorf("panic during asset loading: %v", r)
// Cleanup any partially loaded assets
g.Cleanup()
}
}()
// Load models with better error handling
g.Models, loadErr = assets.LoadModels()
if loadErr != nil {
return fmt.Errorf("failed to load models: %v", loadErr)
var err error
g.Models, err = assets.LoadModels()
if err != nil {
return err
}
// Verify model loading
for i, model := range g.Models {
if model.Model.Meshes == nil {
return fmt.Errorf("model %d failed to load properly", i)
}
}
// Load music with better error handling
g.Music, loadErr = assets.LoadMusic("resources/audio/GoonScape2.mp3")
if loadErr != nil {
return fmt.Errorf("failed to load music: %v", loadErr)
g.Music, err = assets.LoadMusic("resources/audio/GoonScape2.mp3")
if err != nil {
return err
}
return nil
@ -96,14 +79,16 @@ func (g *Game) Update(deltaTime float32) {
}
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
return
}
g.loginScreen.Draw()
return
}
// Handle ESC for menu
if rl.IsKeyPressed(rl.KeyEscape) {
if g.input.IsKeyPressed(rl.KeyEscape) {
g.MenuOpen = !g.MenuOpen
return
}
@ -291,20 +276,8 @@ func (g *Game) Render() {
}
func (g *Game) Cleanup() {
g.cleanupOnce.Do(func() {
// Stop music first
if g.Music.Stream.Buffer != nil {
rl.StopMusicStream(g.Music)
rl.UnloadMusicStream(g.Music)
}
// Unload textures
for _, model := range g.Models {
if model.Texture.ID > 0 {
rl.UnloadTexture(model.Texture)
}
}
})
assets.UnloadModels(g.Models)
assets.UnloadMusic(g.Music)
}
func (g *Game) HandleInput() {
@ -354,13 +327,13 @@ func (g *Game) DrawMenu() {
}
// Check mouse hover
mousePoint := rl.GetMousePosition()
mousePoint := g.input.GetMousePosition()
mouseHover := rl.CheckCollisionPointRec(mousePoint, buttonRect)
// Draw button
if mouseHover {
rl.DrawRectangleRec(buttonRect, rl.ColorAlpha(rl.White, 0.3))
if rl.IsMouseButtonPressed(rl.MouseLeftButton) {
if g.input.IsMouseButtonPressed(toInt32(rl.MouseLeftButton)) {
switch item {
case "Resume":
g.MenuOpen = false
@ -384,7 +357,10 @@ func (g *Game) DrawMenu() {
}
func (g *Game) Shutdown() {
close(g.quitChan)
close(g.QuitChan)
<-g.Player.QuitDone
rl.CloseWindow()
os.Exit(0)
}
func (g *Game) HandleServerMessages(messages []*pb.ChatMessage) {
@ -399,7 +375,3 @@ func (g *Game) AssignModelToPlayer(player *types.Player) {
player.Model = modelAsset.Model
player.Texture = modelAsset.Texture
}
func (g *Game) QuitChan() <-chan struct{} {
return g.quitChan
}

106
game/game_test.go Normal file
View File

@ -0,0 +1,106 @@
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...

View File

@ -3,16 +3,95 @@ package game
import (
"fmt"
"gitea.boner.be/bdnugget/goonscape/game/mock"
"gitea.boner.be/bdnugget/goonscape/types"
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) {
if !rl.IsMouseButtonPressed(rl.MouseLeftButton) {
if !g.input.IsMouseButtonPressed(toInt32(rl.MouseLeftButton)) {
return types.Tile{}, false
}
mouse := rl.GetMousePosition()
ray := rl.GetMouseRay(mouse, g.Camera)
mouse := g.input.GetMousePosition()
ray := g.input.GetMouseRay(mouse, g.Camera)
for x := 0; x < types.MapWidth; x++ {
for y := 0; y < types.MapHeight; y++ {

187
game/input_test.go Normal file
View File

@ -0,0 +1,187 @@
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)
})
}
}

80
game/login_test.go Normal file
View File

@ -0,0 +1,80 @@
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
}

15
game/mock/raylib.go Normal file
View File

@ -0,0 +1,15 @@
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
)

141
game/test_helpers.go Normal file
View File

@ -0,0 +1,141 @@
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)
}

26
game/test_setup.go Normal file
View File

@ -0,0 +1,26 @@
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()
}
}

153
game/testutils/helpers.go Normal file
View File

@ -0,0 +1,153 @@
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)
}

7
go.mod
View File

@ -8,8 +8,15 @@ require (
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 (
github.com/ebitengine/purego v0.8.2 // indirect
github.com/stretchr/testify v1.10.0
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
golang.org/x/sys v0.29.0 // indirect
)

9
go.sum
View File

@ -1,12 +1,21 @@
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/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/go.mod h1:BaY76bZk7nw1/kVOSQObPY1v1iwVE1KHAGMfvI6oK1Q=
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/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/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
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=

82
main.go
View File

@ -3,10 +3,7 @@ package main
import (
"flag"
"log"
"os"
"os/signal"
"strings"
"syscall"
"gitea.boner.be/bdnugget/goonscape/game"
"gitea.boner.be/bdnugget/goonscape/network"
@ -14,24 +11,11 @@ import (
)
func main() {
defer func() {
if r := recover(); r != nil {
log.Printf("Recovered from panic in main: %v", r)
}
}()
// Parse command line flags
verbose := flag.Bool("v", false, "Also show info logs (spammy)")
local := flag.Bool("local", false, "Connect to local server")
addr := flag.String("addr", "", "Server address (host or host:port)")
flag.Parse()
if *verbose {
rl.SetTraceLogLevel(rl.LogTrace)
} else {
rl.SetTraceLogLevel(rl.LogWarning)
}
// Set server address based on flags
if *local {
if *addr != "" {
@ -48,63 +32,29 @@ func main() {
rl.InitWindow(1024, 768, "GoonScape")
rl.SetExitKey(0)
defer rl.CloseWindow()
rl.InitAudioDevice()
gameInstance := game.New()
if err := gameInstance.LoadAssets(); err != nil {
log.Printf("Failed to load assets: %v", err)
return
}
defer func() {
gameInstance.Cleanup()
rl.CloseWindow()
rl.CloseAudioDevice()
}()
defer rl.CloseAudioDevice()
rl.SetTargetFPS(60)
rl.PlayMusicStream(gameInstance.Music)
rl.SetMusicVolume(gameInstance.Music, 0.5)
game := game.New()
if err := game.LoadAssets(); err != nil {
log.Fatalf("Failed to load assets: %v", err)
}
defer game.Cleanup()
// Handle OS signals for clean shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
go func() {
<-sigChan
if gameInstance != nil {
gameInstance.Shutdown()
}
}()
rl.PlayMusicStream(game.Music)
rl.SetMusicVolume(game.Music, 0.5)
// Keep game loop in main thread for Raylib
for !rl.WindowShouldClose() {
deltaTime := rl.GetFrameTime()
rl.UpdateMusicStream(gameInstance.Music)
func() {
defer func() {
if r := recover(); r != nil {
log.Printf("Recovered from panic in game update: %v", r)
}
}()
gameInstance.Update(deltaTime)
}()
func() {
defer func() {
if r := recover(); r != nil {
log.Printf("Recovered from panic in game render: %v", r)
}
}()
gameInstance.Render()
}()
// Check if game requested shutdown
select {
case <-gameInstance.QuitChan():
return
default:
}
rl.UpdateMusicStream(game.Music)
game.Update(deltaTime)
game.Render()
}
// Wait for clean shutdown
<-game.QuitChan
}

View File

@ -7,7 +7,6 @@ import (
"io"
"log"
"net"
"sync"
"time"
"gitea.boner.be/bdnugget/goonscape/types"
@ -92,32 +91,19 @@ func ConnectToServer(username, password string, isRegistering bool) (net.Conn, i
func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Player, otherPlayers map[int32]*types.Player, quitChan <-chan struct{}) {
reader := bufio.NewReader(conn)
defer func() {
if r := recover(); r != nil {
log.Printf("Recovered from panic in HandleServerCommunication: %v", r)
}
conn.Close()
if player.QuitDone != nil {
close(player.QuitDone)
}
}()
actionTicker := time.NewTicker(types.ClientTickRate)
defer actionTicker.Stop()
defer conn.Close()
defer close(player.QuitDone)
// Create error channel for goroutine communication
errChan := make(chan error, 1)
// Create a channel to signal when goroutines are done
done := make(chan struct{})
// Start message sending goroutine
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("Recovered from panic in message sender: %v", r)
errChan <- fmt.Errorf("message sender panic: %v", r)
}
}()
// Create a set of current players to track disconnects
currentPlayers := make(map[int32]bool)
go func() {
for {
select {
case <-quitChan:
@ -132,23 +118,23 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
writeMessage(conn, disconnectMsg)
done <- struct{}{}
return
case <-done:
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 {
errChan <- err
log.Printf("Failed to send actions to server: %v", err)
return
}
} else {
@ -158,106 +144,93 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
}
}()
// Main message receiving loop
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("Recovered from panic in message receiver: %v", r)
errChan <- fmt.Errorf("message receiver panic: %v", r)
}
}()
for {
select {
case <-quitChan:
done := make(chan struct{})
go func() {
<-done
close(player.QuitDone)
}()
for {
select {
case <-quitChan:
case <-done:
time.Sleep(100 * time.Millisecond)
case <-time.After(1 * time.Second):
log.Println("Shutdown timed out")
}
return
default:
// Read message length (4 bytes)
lengthBuf := make([]byte, 4)
if _, err := io.ReadFull(reader, lengthBuf); err != nil {
log.Printf("Failed to read message length: %v", err)
return
default:
lengthBuf := make([]byte, 4)
if _, err := io.ReadFull(reader, lengthBuf); err != nil {
if err != io.EOF {
errChan <- fmt.Errorf("failed to read message length: %v", err)
}
messageLength := binary.BigEndian.Uint32(lengthBuf)
// Read the full message
messageBuf := make([]byte, messageLength)
if _, err := io.ReadFull(reader, messageBuf); err != nil {
log.Printf("Failed to read message body: %v", err)
return
}
var serverMessage pb.ServerMessage
if err := proto.Unmarshal(messageBuf, &serverMessage); err != nil {
log.Printf("Failed to unmarshal server message: %v", err)
continue
}
player.Lock()
player.CurrentTick = serverMessage.CurrentTick
tickDiff := serverMessage.CurrentTick - player.CurrentTick
if tickDiff > types.MaxTickDesync {
for _, state := range serverMessage.Players {
if state.PlayerId == playerID {
player.ForceResync(state)
break
}
return
}
messageLength := binary.BigEndian.Uint32(lengthBuf)
}
player.Unlock()
messageBuf := make([]byte, messageLength)
if _, err := io.ReadFull(reader, messageBuf); err != nil {
log.Printf("Failed to read message body: %v", err)
return
}
var serverMessage pb.ServerMessage
if err := proto.Unmarshal(messageBuf, &serverMessage); err != nil {
log.Printf("Failed to unmarshal server message: %v", err)
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
}
player.Lock()
player.CurrentTick = serverMessage.CurrentTick
tickDiff := serverMessage.CurrentTick - player.CurrentTick
if tickDiff > types.MaxTickDesync {
for _, state := range serverMessage.Players {
if state.PlayerId == playerID {
player.ForceResync(state)
break
}
}
}
player.Unlock()
for _, state := range serverMessage.Players {
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
}
if otherPlayer, exists := otherPlayers[state.PlayerId]; exists {
otherPlayer.UpdatePosition(state, types.ServerTickRate)
} else {
otherPlayers[state.PlayerId] = types.NewPlayer(state)
}
}
// Remove players that are no longer in the server state
for id := range otherPlayers {
if id != playerID {
delete(otherPlayers, id)
}
}
if handler, ok := player.UserData.(types.ChatMessageHandler); ok && len(serverMessage.ChatMessages) > 0 {
handler.HandleServerMessages(serverMessage.ChatMessages)
if otherPlayer, exists := otherPlayers[state.PlayerId]; exists {
otherPlayer.UpdatePosition(state, types.ServerTickRate)
} else {
otherPlayers[state.PlayerId] = types.NewPlayer(state)
}
}
}
}()
// Wait for error or quit signal
select {
case <-quitChan:
// Send disconnect message
disconnectMsg := &pb.ActionBatch{
PlayerId: playerID,
Actions: []*pb.Action{{
Type: pb.Action_DISCONNECT,
PlayerId: playerID,
}},
// Remove players that are no longer in the server state
for id := range otherPlayers {
if !currentPlayers[id] {
delete(otherPlayers, id)
}
}
if handler, ok := player.UserData.(types.ChatMessageHandler); ok && len(serverMessage.ChatMessages) > 0 {
handler.HandleServerMessages(serverMessage.ChatMessages)
}
}
writeMessage(conn, disconnectMsg)
case err := <-errChan:
log.Printf("Network error: %v", err)
}
}
@ -279,50 +252,3 @@ func writeMessage(conn net.Conn, msg proto.Message) error {
_, err = conn.Write(data)
return err
}
type Connection struct {
conn net.Conn
playerID int32
quitChan chan struct{}
quitDone chan struct{}
closeOnce sync.Once
}
func NewConnection(username, password string, isRegistering bool) (*Connection, error) {
conn, playerID, err := ConnectToServer(username, password, isRegistering)
if err != nil {
return nil, err
}
return &Connection{
conn: conn,
playerID: playerID,
quitChan: make(chan struct{}),
quitDone: make(chan struct{}),
}, nil
}
func (c *Connection) Close() {
c.closeOnce.Do(func() {
close(c.quitChan)
// Wait with timeout for network cleanup
select {
case <-c.quitDone:
// Clean shutdown completed
case <-time.After(500 * time.Millisecond):
log.Println("Network cleanup timed out")
}
c.conn.Close()
})
}
func (c *Connection) PlayerID() int32 {
return c.playerID
}
func (c *Connection) Start(player *types.Player, otherPlayers map[int32]*types.Player) {
go HandleServerCommunication(c.conn, c.playerID, player, otherPlayers, c.quitChan)
}
func (c *Connection) QuitChan() <-chan struct{} {
return c.quitChan
}

View File

@ -1,34 +1,12 @@
package types
import (
"sync"
"time"
pb "gitea.boner.be/bdnugget/goonserver/actions"
rl "github.com/gen2brain/raylib-go/raylib"
)
type Player struct {
sync.RWMutex
Model rl.Model
Texture rl.Texture2D
PosActual rl.Vector3
PosTile Tile
TargetPath []Tile
Speed float32
ActionQueue []*pb.Action
ID int32
QuitDone chan struct{}
CurrentTick int64
UserData interface{}
FloatingMessage *FloatingMessage
IsMoving bool
AnimationFrame int32
LastAnimUpdate time.Time
LastUpdateTime time.Time
InterpolationProgress float32
}
func (p *Player) MoveTowards(target Tile, deltaTime float32, mapGrid [][]Tile) {
p.Lock()
defer p.Unlock()
@ -90,14 +68,12 @@ func NewPlayer(state *pb.PlayerState) *Player {
Y: float32(state.Y * TileHeight),
Z: float32(state.Y * TileSize),
},
PosTile: Tile{X: int(state.X), Y: int(state.Y)},
Speed: 50.0,
ID: state.PlayerId,
IsMoving: false,
AnimationFrame: 0,
LastAnimUpdate: time.Now(),
LastUpdateTime: time.Now(),
InterpolationProgress: 1.0,
PosTile: Tile{X: int(state.X), Y: int(state.Y)},
Speed: 50.0,
ID: state.PlayerId,
IsMoving: false,
AnimationFrame: 0,
LastAnimUpdate: time.Now(),
}
}

View File

@ -1,6 +1,7 @@
package types
import (
"sync"
"time"
pb "gitea.boner.be/bdnugget/goonserver/actions"
@ -13,6 +14,27 @@ type Tile struct {
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 {
Idle []rl.ModelAnimation
Walk []rl.ModelAnimation