Merge pull request 'feature/chat' (#2) from feature/chat into master

Reviewed-on: #2
This commit is contained in:
bdnugget 2025-01-13 13:24:33 +00:00
commit 567ec40c3d
5 changed files with 277 additions and 6 deletions

194
game/chat.go Normal file
View File

@ -0,0 +1,194 @@
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
userData interface{}
}
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)
// 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[msg.PlayerId]; exists {
otherPlayer.Lock()
otherPlayer.FloatingMessage = &types.FloatingMessage{
Content: msg.Content,
ExpireTime: time.Now().Add(6 * time.Second),
}
otherPlayer.Unlock()
}
}
}
}
}
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
}

View File

@ -1,6 +1,8 @@
package game
import (
"time"
"gitea.boner.be/bdnugget/goonscape/assets"
"gitea.boner.be/bdnugget/goonscape/types"
pb "gitea.boner.be/bdnugget/goonserver/actions"
@ -13,6 +15,7 @@ type Game struct {
Camera rl.Camera3D
Models []types.ModelAsset
Music rl.Music
Chat *Chat
}
func New() *Game {
@ -23,6 +26,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 +36,10 @@ func New() *Game {
Fovy: 45.0,
Projection: rl.CameraPerspective,
},
Chat: NewChat(),
}
game.Player.UserData = game
game.Chat.userData = game
return game
}
@ -52,6 +59,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 {
@ -108,6 +125,18 @@ func (g *Game) DrawPlayer(player *types.Player, model rl.Model) {
rl.DrawModel(model, playerPos, 16, rl.White)
if player.FloatingMessage != nil && time.Now().Before(player.FloatingMessage.ExpireTime) {
screenPos := rl.GetWorldToScreen(rl.Vector3{
X: playerPos.X,
Y: playerPos.Y + 24.0,
Z: playerPos.Z,
}, g.Camera)
player.FloatingMessage.ScreenPos = screenPos
} else if player.FloatingMessage != nil {
player.FloatingMessage = nil
}
if len(player.TargetPath) > 0 {
targetTile := player.TargetPath[len(player.TargetPath)-1]
targetPos := rl.Vector3{
@ -130,17 +159,45 @@ func (g *Game) DrawPlayer(player *types.Player, model rl.Model) {
func (g *Game) Render() {
rl.BeginDrawing()
rl.ClearBackground(rl.RayWhite)
rl.BeginMode3D(g.Camera)
rl.BeginMode3D(g.Camera)
g.DrawMap()
g.DrawPlayer(g.Player, g.Player.Model)
for id, other := range g.OtherPlayers {
g.DrawPlayer(other, g.Models[int(id)%len(g.Models)].Model)
}
rl.EndMode3D()
drawFloatingMessage := func(msg *types.FloatingMessage) {
if msg == nil || time.Now().After(msg.ExpireTime) {
return
}
pos := msg.ScreenPos
text := msg.Content
textWidth := rl.MeasureText(text, 20)
for offsetX := -2; offsetX <= 2; offsetX++ {
for offsetY := -2; offsetY <= 2; offsetY++ {
rl.DrawText(text,
int32(pos.X)-textWidth/2+int32(offsetX),
int32(pos.Y)+int32(offsetY),
20,
rl.Black)
}
}
rl.DrawText(text, int32(pos.X)-textWidth/2, int32(pos.Y), 20, rl.Yellow)
}
if g.Player.FloatingMessage != nil {
drawFloatingMessage(g.Player.FloatingMessage)
}
for _, other := range g.OtherPlayers {
drawFloatingMessage(other.FloatingMessage)
}
rl.DrawFPS(10, 10)
g.Chat.Draw(int32(rl.GetScreenWidth()), int32(rl.GetScreenHeight()))
rl.EndDrawing()
}
@ -153,7 +210,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{

@ -1 +1 @@
Subproject commit 4b73492ffc0824faccddd81053c08d3d7607919a
Subproject commit 368fbdbc4743ad5d571789e5ebb7f76cfb964743

View File

@ -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)
}
}
}

View File

@ -27,6 +27,8 @@ type Player struct {
CurrentTick int64
LastUpdateTime time.Time
InterpolationProgress float32
UserData interface{} // Used to store reference to game
FloatingMessage *FloatingMessage
}
type ModelAsset struct {
@ -34,6 +36,18 @@ type ModelAsset struct {
Texture rl.Texture2D
}
type ChatMessage struct {
PlayerID int32
Content string
Time time.Time
}
type FloatingMessage struct {
Content string
ExpireTime time.Time
ScreenPos rl.Vector2 // Store the screen position for 2D rendering
}
const (
MapWidth = 50
MapHeight = 50
@ -44,5 +58,6 @@ const (
ServerTickRate = 600 * time.Millisecond
ClientTickRate = 50 * time.Millisecond
MaxTickDesync = 5
ServerAddr = "localhost:6969"
// ServerAddr = "localhost:6969"
ServerAddr = "boner.be:6969"
)