goonscape/game/chat.go
2025-02-14 13:35:09 +01:00

235 lines
5.8 KiB
Go

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
}