package game import ( "fmt" "sync" "time" "gitea.boner.be/bdnugget/goonscape/types" pb "gitea.boner.be/bdnugget/goonserver/actions" rl "github.com/gen2brain/raylib-go/raylib" ) const ( maxMessages = 50 chatMargin = 10 // Margin from screen edges chatHeight = 200 messageHeight = 20 inputHeight = 30 runeLimit = 256 ) type Chat struct { sync.RWMutex messages []types.ChatMessage inputBuffer []rune isTyping bool cursorPos int scrollOffset int userData interface{} } func NewChat() *Chat { return &Chat{ messages: make([]types.ChatMessage, 0, maxMessages), inputBuffer: make([]rune, 0, runeLimit), } } 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) { c.Lock() defer c.Unlock() // Convert protobuf messages to our local type for _, msg := range messages { localMsg := types.ChatMessage{ PlayerID: msg.PlayerId, Username: msg.Username, 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) // Scroll to latest message if it's not already visible visibleMessages := int((chatHeight - inputHeight) / messageHeight) if len(c.messages) > visibleMessages { c.scrollOffset = len(c.messages) - visibleMessages } // Add floating message to the player if game, ok := c.userData.(*Game); ok { if msg.PlayerId == game.Player.ID { game.Player.Lock() game.Player.FloatingMessage = &types.FloatingMessage{ Content: msg.Content, ExpireTime: time.Now().Add(6 * time.Second), } game.Player.Unlock() } else if otherPlayer, exists := game.OtherPlayers.Load(msg.PlayerId); exists { other := otherPlayer.(*types.Player) other.Lock() other.FloatingMessage = &types.FloatingMessage{ Content: msg.Content, ExpireTime: time.Now().Add(6 * time.Second), } other.Unlock() } } } } } func (c *Chat) Draw(screenWidth, screenHeight int32) { c.RLock() defer c.RUnlock() // Calculate chat window width based on screen width chatWindowWidth := screenWidth - (chatMargin * 2) // Draw chat window background chatX := float32(chatMargin) chatY := float32(screenHeight - chatHeight - chatMargin) rl.DrawRectangle(int32(chatX), int32(chatY), chatWindowWidth, chatHeight, rl.ColorAlpha(rl.Black, 0.5)) // Draw messages from oldest to newest messageY := chatY + 5 visibleMessages := int((chatHeight - inputHeight) / messageHeight) // Auto-scroll to bottom if no manual scrolling has occurred if c.scrollOffset == 0 { if len(c.messages) > visibleMessages { c.scrollOffset = len(c.messages) - visibleMessages } } startIdx := max(0, c.scrollOffset) endIdx := min(len(c.messages), startIdx+visibleMessages) for i := startIdx; i < endIdx; i++ { msg := c.messages[i] var color rl.Color if msg.PlayerID == 0 { // System message color = rl.Gold } else { color = rl.White } text := fmt.Sprintf("%s: %s", msg.Username, msg.Content) rl.DrawText(text, int32(chatX)+5, int32(messageY), 20, color) 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) < runeLimit { c.inputBuffer = append(c.inputBuffer[:c.cursorPos], append([]rune{key}, c.inputBuffer[c.cursorPos:]...)...) c.cursorPos++ } key = rl.GetCharPressed() } if rl.IsKeyPressed(rl.KeyEnter) || rl.IsKeyPressed(rl.KeyKpEnter) { 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.isTyping { 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 min(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 }