From d301d597e8edcbfdc8850a26a4acce3e3c43bfdd Mon Sep 17 00:00:00 2001 From: bdnugget Date: Mon, 13 Jan 2025 13:23:52 +0100 Subject: [PATCH] Add chat --- game/chat.go | 174 +++++++++++++++++++++++++++++++++++++++++++++ game/game.go | 17 ++++- goonserver | 2 +- network/network.go | 5 ++ types/types.go | 7 ++ 5 files changed, 203 insertions(+), 2 deletions(-) create mode 100644 game/chat.go diff --git a/game/chat.go b/game/chat.go new file mode 100644 index 0000000..b91213f --- /dev/null +++ b/game/chat.go @@ -0,0 +1,174 @@ +package game + +import ( + "fmt" + "time" + + "gitea.boner.be/bdnugget/goonscape/types" + pb "gitea.boner.be/bdnugget/goonserver/actions" + rl "github.com/gen2brain/raylib-go/raylib" +) + +const ( + maxMessages = 50 + chatWindowWidth = 400 + chatHeight = 200 + messageHeight = 20 + inputHeight = 30 +) + +type Chat struct { + messages []types.ChatMessage + inputBuffer []rune + isTyping bool + cursorPos int + scrollOffset int +} + +func NewChat() *Chat { + return &Chat{ + messages: make([]types.ChatMessage, 0, maxMessages), + inputBuffer: make([]rune, 0, 256), + } +} + +func (c *Chat) AddMessage(playerID int32, content string) { + msg := types.ChatMessage{ + PlayerID: playerID, + Content: content, + Time: time.Now(), + } + + if len(c.messages) >= maxMessages { + c.messages = c.messages[1:] + } + c.messages = append(c.messages, msg) + c.scrollOffset = 0 // Reset scroll position for new messages +} + +func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) { + // Convert protobuf messages to our local type + for _, msg := range messages { + localMsg := types.ChatMessage{ + PlayerID: msg.PlayerId, + Content: msg.Content, + Time: time.Unix(0, msg.Timestamp), + } + + // Only add if it's not already in our history + if len(c.messages) == 0 || c.messages[len(c.messages)-1].Time.UnixNano() < msg.Timestamp { + if len(c.messages) >= maxMessages { + c.messages = c.messages[1:] + } + c.messages = append(c.messages, localMsg) + } + } +} + +func (c *Chat) Draw(screenWidth, screenHeight int32) { + // Draw chat window background + chatX := float32(10) + chatY := float32(screenHeight - chatHeight - 10) + rl.DrawRectangle(int32(chatX), int32(chatY), chatWindowWidth, chatHeight, rl.ColorAlpha(rl.Black, 0.5)) + + // Draw messages + messageY := chatY + 5 + startIdx := len(c.messages) - 1 - c.scrollOffset + endIdx := max(0, startIdx-int((chatHeight-inputHeight)/messageHeight)) + + for i := startIdx; i >= endIdx && i >= 0; i-- { + msg := c.messages[i] + text := fmt.Sprintf("[%d]: %s", msg.PlayerID, msg.Content) + rl.DrawText(text, int32(chatX)+5, int32(messageY), 20, rl.White) + messageY += messageHeight + } + + // Draw input field + inputY := chatY + float32(chatHeight-inputHeight) + rl.DrawRectangle(int32(chatX), int32(inputY), chatWindowWidth, inputHeight, rl.ColorAlpha(rl.White, 0.3)) + if c.isTyping { + inputText := string(c.inputBuffer) + rl.DrawText(inputText, int32(chatX)+5, int32(inputY)+5, 20, rl.White) + + // Draw cursor + cursorX := rl.MeasureText(inputText[:c.cursorPos], 20) + rl.DrawRectangle(int32(chatX)+5+cursorX, int32(inputY)+5, 2, 20, rl.White) + } else { + rl.DrawText("Press T to chat", int32(chatX)+5, int32(inputY)+5, 20, rl.Gray) + } +} + +func (c *Chat) Update() (string, bool) { + // Handle scrolling with mouse wheel when not typing + if !c.isTyping { + wheelMove := rl.GetMouseWheelMove() + if wheelMove != 0 { + maxScroll := max(0, len(c.messages)-int((chatHeight-inputHeight)/messageHeight)) + c.scrollOffset = clamp(c.scrollOffset-int(wheelMove), 0, maxScroll) + } + + if rl.IsKeyPressed(rl.KeyT) { + c.isTyping = true + return "", false + } + return "", false + } + + key := rl.GetCharPressed() + for key > 0 { + if len(c.inputBuffer) < 256 { + c.inputBuffer = append(c.inputBuffer[:c.cursorPos], append([]rune{key}, c.inputBuffer[c.cursorPos:]...)...) + c.cursorPos++ + } + key = rl.GetCharPressed() + } + + if rl.IsKeyPressed(rl.KeyEnter) { + if len(c.inputBuffer) > 0 { + message := string(c.inputBuffer) + c.inputBuffer = c.inputBuffer[:0] + c.cursorPos = 0 + c.isTyping = false + return message, true + } + c.isTyping = false + } + + if rl.IsKeyPressed(rl.KeyEscape) { + c.inputBuffer = c.inputBuffer[:0] + c.cursorPos = 0 + c.isTyping = false + } + + if rl.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 { + c.cursorPos-- + } + if rl.IsKeyPressed(rl.KeyRight) && c.cursorPos < len(c.inputBuffer) { + c.cursorPos++ + } + + return "", false +} + +// Add helper functions +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func clamp(value, min, max int) int { + if value < min { + return min + } + if value > max { + return max + } + return value +} diff --git a/game/game.go b/game/game.go index eb10d3b..9690cb4 100644 --- a/game/game.go +++ b/game/game.go @@ -13,6 +13,7 @@ type Game struct { Camera rl.Camera3D Models []types.ModelAsset Music rl.Music + Chat *Chat } func New() *Game { @@ -23,6 +24,7 @@ func New() *Game { PosTile: GetTile(5, 5), Speed: 50.0, TargetPath: []types.Tile{}, + UserData: nil, }, OtherPlayers: make(map[int32]*types.Player), Camera: rl.Camera3D{ @@ -32,7 +34,9 @@ func New() *Game { Fovy: 45.0, Projection: rl.CameraPerspective, }, + Chat: NewChat(), } + game.Player.UserData = game return game } @@ -52,6 +56,16 @@ func (g *Game) LoadAssets() error { } func (g *Game) Update(deltaTime float32) { + if message, sent := g.Chat.Update(); sent { + g.Player.Lock() + g.Player.ActionQueue = append(g.Player.ActionQueue, &pb.Action{ + Type: pb.Action_CHAT, + ChatMessage: message, + PlayerId: g.Player.ID, + }) + g.Player.Unlock() + } + g.HandleInput() if len(g.Player.TargetPath) > 0 { @@ -141,6 +155,7 @@ func (g *Game) Render() { rl.EndMode3D() rl.DrawFPS(10, 10) + g.Chat.Draw(int32(rl.GetScreenWidth()), int32(rl.GetScreenHeight())) rl.EndDrawing() } @@ -153,7 +168,7 @@ func (g *Game) HandleInput() { clickedTile, clicked := g.GetTileAtMouse() if clicked { path := FindPath(GetTile(g.Player.PosTile.X, g.Player.PosTile.Y), clickedTile) - if path != nil && len(path) > 1 { + if len(path) > 1 { g.Player.Lock() g.Player.TargetPath = path[1:] g.Player.ActionQueue = append(g.Player.ActionQueue, &pb.Action{ diff --git a/goonserver b/goonserver index 4b73492..368fbdb 160000 --- a/goonserver +++ b/goonserver @@ -1 +1 @@ -Subproject commit 4b73492ffc0824faccddd81053c08d3d7607919a +Subproject commit 368fbdbc4743ad5d571789e5ebb7f76cfb964743 diff --git a/network/network.go b/network/network.go index 1dd29b8..4a0dcf1 100644 --- a/network/network.go +++ b/network/network.go @@ -5,6 +5,7 @@ import ( "net" "time" + "gitea.boner.be/bdnugget/goonscape/game" "gitea.boner.be/bdnugget/goonscape/types" pb "gitea.boner.be/bdnugget/goonserver/actions" "google.golang.org/protobuf/proto" @@ -112,5 +113,9 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play otherPlayers[state.PlayerId] = types.NewPlayer(state) } } + + if g, ok := player.UserData.(*game.Game); ok && len(serverMessage.ChatMessages) > 0 { + g.Chat.HandleServerMessages(serverMessage.ChatMessages) + } } } diff --git a/types/types.go b/types/types.go index 68744fa..6d649c4 100644 --- a/types/types.go +++ b/types/types.go @@ -27,6 +27,7 @@ type Player struct { CurrentTick int64 LastUpdateTime time.Time InterpolationProgress float32 + UserData interface{} // Used to store reference to game } type ModelAsset struct { @@ -34,6 +35,12 @@ type ModelAsset struct { Texture rl.Texture2D } +type ChatMessage struct { + PlayerID int32 + Content string + Time time.Time +} + const ( MapWidth = 50 MapHeight = 50