Compare commits
19 Commits
91cdbab54a
...
feature/me
Author | SHA1 | Date | |
---|---|---|---|
863f5a939c | |||
b9d0d46bd6 | |||
b96c7ada7a | |||
d86cbe15a3 | |||
fb018e2a7d | |||
5ca973fdf1 | |||
2a0f9348e9 | |||
d6d0f36cee | |||
e8d062c4b7 | |||
0cd3145d28 | |||
0b6ab17ad5 | |||
50952309f4 | |||
afc44710f2 | |||
1a7b0eff42 | |||
bf7bf12a53 | |||
e661320508 | |||
567ec40c3d | |||
c01b8d1c59 | |||
d301d597e8 |
71
README.md
Normal file
71
README.md
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
# GoonScape
|
||||||
|
|
||||||
|
A multiplayer isometric game inspired by Oldschool RuneScape, built with Go and Raylib.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 3D isometric world with height-mapped terrain
|
||||||
|
- Multiplayer support with client-server architecture
|
||||||
|
- Pathfinding and click-to-move navigation
|
||||||
|
- Global chat system with floating messages
|
||||||
|
- Multiple character models
|
||||||
|
- Background music
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Go 1.23 or higher
|
||||||
|
- Raylib dependencies (see [raylib-go](https://github.com/gen2brain/raylib-go#requirements))
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
```bash
|
||||||
|
git clone https://gitea.boner.be/bdnugget/goonscape.git
|
||||||
|
cd goonscape
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install dependencies:
|
||||||
|
```bash
|
||||||
|
go mod tidy
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Build and run:
|
||||||
|
```bash
|
||||||
|
go run main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
- **Mouse Click**: Move to location
|
||||||
|
- **T**: Open chat
|
||||||
|
- **Enter**: Send chat message
|
||||||
|
- **Escape**: Cancel chat/Close game (it does both of these at the same time so gg)
|
||||||
|
- **Arrow Keys**: Rotate camera
|
||||||
|
- **Mouse Wheel**: Zoom in/out
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Server connection can be configured using command-line flags:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Connect to default server (boner.be:6969)
|
||||||
|
go run main.go
|
||||||
|
|
||||||
|
# Connect to local server
|
||||||
|
go run main.go -local
|
||||||
|
|
||||||
|
# Connect to specific server
|
||||||
|
go run main.go -addr somehost # Uses somehost:6969
|
||||||
|
go run main.go -addr somehost:6970 # Uses somehost:6970
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: The `-local` flag is a shorthand for `-addr localhost:6969` and cannot be used together with `-addr`.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
The project uses Protocol Buffers for network communication. If you modify the `.proto` files, regenerate the Go code with:
|
||||||
|
```bash
|
||||||
|
protoc --go_out=. goonserver/actions/actions.proto
|
||||||
|
```
|
@ -13,6 +13,14 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func UpdateCamera(camera *rl.Camera3D, player rl.Vector3, deltaTime float32) {
|
func UpdateCamera(camera *rl.Camera3D, player rl.Vector3, deltaTime float32) {
|
||||||
|
// Adjust target position to be at character's torso height (about half character height)
|
||||||
|
characterHeight := float32(32.0) // Assuming character is roughly 32 units tall
|
||||||
|
targetPos := rl.Vector3{
|
||||||
|
X: player.X,
|
||||||
|
Y: player.Y + characterHeight*0.5, // Focus on middle of character
|
||||||
|
Z: player.Z,
|
||||||
|
}
|
||||||
|
|
||||||
wheelMove := rl.GetMouseWheelMove()
|
wheelMove := rl.GetMouseWheelMove()
|
||||||
if wheelMove != 0 {
|
if wheelMove != 0 {
|
||||||
cameraDistance += -wheelMove * 5
|
cameraDistance += -wheelMove * 5
|
||||||
@ -47,9 +55,9 @@ func UpdateCamera(camera *rl.Camera3D, player rl.Vector3, deltaTime float32) {
|
|||||||
cameraPitchRad := float64(cameraPitch) * rl.Deg2rad
|
cameraPitchRad := float64(cameraPitch) * rl.Deg2rad
|
||||||
|
|
||||||
camera.Position = rl.Vector3{
|
camera.Position = rl.Vector3{
|
||||||
X: player.X + cameraDistance*float32(math.Cos(cameraYawRad))*float32(math.Cos(cameraPitchRad)),
|
X: targetPos.X + cameraDistance*float32(math.Cos(cameraYawRad))*float32(math.Cos(cameraPitchRad)),
|
||||||
Y: player.Y + cameraDistance*float32(math.Sin(cameraPitchRad)),
|
Y: targetPos.Y + cameraDistance*float32(math.Sin(cameraPitchRad)),
|
||||||
Z: player.Z + cameraDistance*float32(math.Sin(cameraYawRad))*float32(math.Cos(cameraPitchRad)),
|
Z: targetPos.Z + cameraDistance*float32(math.Sin(cameraYawRad))*float32(math.Cos(cameraPitchRad)),
|
||||||
}
|
}
|
||||||
camera.Target = player
|
camera.Target = targetPos
|
||||||
}
|
}
|
||||||
|
220
game/chat.go
Normal file
220
game/chat.go
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
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
|
||||||
|
chatMargin = 10 // Margin from screen edges
|
||||||
|
chatHeight = 200
|
||||||
|
messageHeight = 20
|
||||||
|
inputHeight = 30
|
||||||
|
runeLimit = 256
|
||||||
|
)
|
||||||
|
|
||||||
|
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, 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) {
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
// 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[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) {
|
||||||
|
// 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]
|
||||||
|
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) < 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
|
||||||
|
}
|
152
game/game.go
152
game/game.go
@ -1,6 +1,8 @@
|
|||||||
package game
|
package game
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/assets"
|
"gitea.boner.be/bdnugget/goonscape/assets"
|
||||||
"gitea.boner.be/bdnugget/goonscape/types"
|
"gitea.boner.be/bdnugget/goonscape/types"
|
||||||
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
||||||
@ -13,6 +15,10 @@ type Game struct {
|
|||||||
Camera rl.Camera3D
|
Camera rl.Camera3D
|
||||||
Models []types.ModelAsset
|
Models []types.ModelAsset
|
||||||
Music rl.Music
|
Music rl.Music
|
||||||
|
Chat *Chat
|
||||||
|
MenuOpen bool
|
||||||
|
QuitChan chan struct{} // Channel to signal shutdown
|
||||||
|
QuitDone chan struct{} // New channel to signal when cleanup is complete
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() *Game {
|
func New() *Game {
|
||||||
@ -23,6 +29,8 @@ func New() *Game {
|
|||||||
PosTile: GetTile(5, 5),
|
PosTile: GetTile(5, 5),
|
||||||
Speed: 50.0,
|
Speed: 50.0,
|
||||||
TargetPath: []types.Tile{},
|
TargetPath: []types.Tile{},
|
||||||
|
UserData: nil,
|
||||||
|
QuitDone: make(chan struct{}),
|
||||||
},
|
},
|
||||||
OtherPlayers: make(map[int32]*types.Player),
|
OtherPlayers: make(map[int32]*types.Player),
|
||||||
Camera: rl.Camera3D{
|
Camera: rl.Camera3D{
|
||||||
@ -32,7 +40,12 @@ func New() *Game {
|
|||||||
Fovy: 45.0,
|
Fovy: 45.0,
|
||||||
Projection: rl.CameraPerspective,
|
Projection: rl.CameraPerspective,
|
||||||
},
|
},
|
||||||
|
Chat: NewChat(),
|
||||||
|
QuitChan: make(chan struct{}),
|
||||||
|
QuitDone: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
game.Player.UserData = game
|
||||||
|
game.Chat.userData = game
|
||||||
return game
|
return game
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,6 +65,27 @@ func (g *Game) LoadAssets() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) Update(deltaTime float32) {
|
func (g *Game) Update(deltaTime float32) {
|
||||||
|
// Handle ESC for menu
|
||||||
|
if rl.IsKeyPressed(rl.KeyEscape) {
|
||||||
|
g.MenuOpen = !g.MenuOpen
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't process other inputs if menu is open
|
||||||
|
if g.MenuOpen {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
g.HandleInput()
|
||||||
|
|
||||||
if len(g.Player.TargetPath) > 0 {
|
if len(g.Player.TargetPath) > 0 {
|
||||||
@ -108,6 +142,18 @@ func (g *Game) DrawPlayer(player *types.Player, model rl.Model) {
|
|||||||
|
|
||||||
rl.DrawModel(model, playerPos, 16, rl.White)
|
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 {
|
if len(player.TargetPath) > 0 {
|
||||||
targetTile := player.TargetPath[len(player.TargetPath)-1]
|
targetTile := player.TargetPath[len(player.TargetPath)-1]
|
||||||
targetPos := rl.Vector3{
|
targetPos := rl.Vector3{
|
||||||
@ -130,16 +176,54 @@ func (g *Game) DrawPlayer(player *types.Player, model rl.Model) {
|
|||||||
func (g *Game) Render() {
|
func (g *Game) Render() {
|
||||||
rl.BeginDrawing()
|
rl.BeginDrawing()
|
||||||
rl.ClearBackground(rl.RayWhite)
|
rl.ClearBackground(rl.RayWhite)
|
||||||
rl.BeginMode3D(g.Camera)
|
|
||||||
|
|
||||||
|
rl.BeginMode3D(g.Camera)
|
||||||
g.DrawMap()
|
g.DrawMap()
|
||||||
g.DrawPlayer(g.Player, g.Player.Model)
|
g.DrawPlayer(g.Player, g.Player.Model)
|
||||||
|
|
||||||
for id, other := range g.OtherPlayers {
|
for id, other := range g.OtherPlayers {
|
||||||
g.DrawPlayer(other, g.Models[int(id)%len(g.Models)].Model)
|
g.DrawPlayer(other, g.Models[int(id)%len(g.Models)].Model)
|
||||||
}
|
}
|
||||||
|
|
||||||
rl.EndMode3D()
|
rl.EndMode3D()
|
||||||
|
|
||||||
|
// Draw floating messages
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw menu if open
|
||||||
|
if g.MenuOpen {
|
||||||
|
g.DrawMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only draw chat if menu is not open
|
||||||
|
if !g.MenuOpen {
|
||||||
|
g.Chat.Draw(int32(rl.GetScreenWidth()), int32(rl.GetScreenHeight()))
|
||||||
|
}
|
||||||
|
|
||||||
rl.DrawFPS(10, 10)
|
rl.DrawFPS(10, 10)
|
||||||
rl.EndDrawing()
|
rl.EndDrawing()
|
||||||
}
|
}
|
||||||
@ -153,7 +237,7 @@ func (g *Game) HandleInput() {
|
|||||||
clickedTile, clicked := g.GetTileAtMouse()
|
clickedTile, clicked := g.GetTileAtMouse()
|
||||||
if clicked {
|
if clicked {
|
||||||
path := FindPath(GetTile(g.Player.PosTile.X, g.Player.PosTile.Y), clickedTile)
|
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.Lock()
|
||||||
g.Player.TargetPath = path[1:]
|
g.Player.TargetPath = path[1:]
|
||||||
g.Player.ActionQueue = append(g.Player.ActionQueue, &pb.Action{
|
g.Player.ActionQueue = append(g.Player.ActionQueue, &pb.Action{
|
||||||
@ -166,3 +250,63 @@ func (g *Game) HandleInput() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *Game) DrawMenu() {
|
||||||
|
screenWidth := float32(rl.GetScreenWidth())
|
||||||
|
screenHeight := float32(rl.GetScreenHeight())
|
||||||
|
|
||||||
|
// Semi-transparent background
|
||||||
|
rl.DrawRectangle(0, 0, int32(screenWidth), int32(screenHeight), rl.ColorAlpha(rl.Black, 0.7))
|
||||||
|
|
||||||
|
// Menu title
|
||||||
|
title := "Menu"
|
||||||
|
titleSize := int32(40)
|
||||||
|
titleWidth := rl.MeasureText(title, titleSize)
|
||||||
|
rl.DrawText(title, int32(screenWidth/2)-titleWidth/2, 100, titleSize, rl.White)
|
||||||
|
|
||||||
|
// Menu buttons
|
||||||
|
buttonWidth := float32(200)
|
||||||
|
buttonHeight := float32(40)
|
||||||
|
buttonY := float32(200)
|
||||||
|
buttonSpacing := float32(60)
|
||||||
|
|
||||||
|
menuItems := []string{"Resume", "Settings", "Exit Game"}
|
||||||
|
for _, item := range menuItems {
|
||||||
|
buttonRect := rl.Rectangle{
|
||||||
|
X: screenWidth/2 - buttonWidth/2,
|
||||||
|
Y: buttonY,
|
||||||
|
Width: buttonWidth,
|
||||||
|
Height: buttonHeight,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check mouse hover
|
||||||
|
mousePoint := rl.GetMousePosition()
|
||||||
|
mouseHover := rl.CheckCollisionPointRec(mousePoint, buttonRect)
|
||||||
|
|
||||||
|
// Draw button
|
||||||
|
if mouseHover {
|
||||||
|
rl.DrawRectangleRec(buttonRect, rl.ColorAlpha(rl.White, 0.3))
|
||||||
|
if rl.IsMouseButtonPressed(rl.MouseLeftButton) {
|
||||||
|
switch item {
|
||||||
|
case "Resume":
|
||||||
|
g.MenuOpen = false
|
||||||
|
case "Settings":
|
||||||
|
// TODO: Implement settings
|
||||||
|
case "Exit Game":
|
||||||
|
close(g.QuitChan)
|
||||||
|
<-g.QuitDone
|
||||||
|
rl.CloseWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw button text
|
||||||
|
textSize := int32(20)
|
||||||
|
textWidth := rl.MeasureText(item, textSize)
|
||||||
|
textX := int32(buttonRect.X+buttonRect.Width/2) - textWidth/2
|
||||||
|
textY := int32(buttonRect.Y + buttonRect.Height/2 - float32(textSize)/2)
|
||||||
|
rl.DrawText(item, textX, textY, textSize, rl.White)
|
||||||
|
|
||||||
|
buttonY += buttonSpacing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
12
go.mod
12
go.mod
@ -3,15 +3,15 @@ module gitea.boner.be/bdnugget/goonscape
|
|||||||
go 1.23.0
|
go 1.23.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
gitea.boner.be/bdnugget/goonserver v0.0.0-20241011195320-f16e8647dc6b
|
gitea.boner.be/bdnugget/goonserver v0.0.0-20250113131525-49e23114973c
|
||||||
github.com/gen2brain/raylib-go/raylib v0.0.0-20240930075631-c66f9e2942fe
|
github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b
|
||||||
google.golang.org/protobuf v1.35.1
|
google.golang.org/protobuf v1.36.3
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/ebitengine/purego v0.8.0 // indirect
|
github.com/ebitengine/purego v0.8.2 // indirect
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
|
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
|
||||||
golang.org/x/sys v0.26.0 // indirect
|
golang.org/x/sys v0.29.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace gitea.boner.be/bdnugget/goonserver => ./goonserver
|
replace gitea.boner.be/bdnugget/goonserver => ./goonserver
|
||||||
|
20
go.sum
20
go.sum
@ -1,12 +1,12 @@
|
|||||||
github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE=
|
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||||
github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
github.com/gen2brain/raylib-go/raylib v0.0.0-20240930075631-c66f9e2942fe h1:mInjrbJkUglTM7tBmXG+epnPCE744aj15J7vjJwM4gs=
|
github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b h1:JJfspevP3YOXcSKVABizYOv++yMpTJIdPUtoDzF/RWw=
|
||||||
github.com/gen2brain/raylib-go/raylib v0.0.0-20240930075631-c66f9e2942fe/go.mod h1:BaY76bZk7nw1/kVOSQObPY1v1iwVE1KHAGMfvI6oK1Q=
|
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 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
||||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
|
Submodule goonserver updated: 4b73492ffc...be32dec202
26
main.go
26
main.go
@ -1,7 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"flag"
|
||||||
"log"
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/game"
|
"gitea.boner.be/bdnugget/goonscape/game"
|
||||||
"gitea.boner.be/bdnugget/goonscape/network"
|
"gitea.boner.be/bdnugget/goonscape/network"
|
||||||
@ -9,7 +11,26 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
local := flag.Bool("local", false, "Use local server instead of remote")
|
||||||
|
addr := flag.String("addr", "boner.be:6969", "Server address (hostname:port or hostname)")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *local && *addr != "boner.be:6969" {
|
||||||
|
log.Fatal("Cannot use both -local and -addr flags")
|
||||||
|
}
|
||||||
|
|
||||||
|
if *local {
|
||||||
|
network.SetServerAddr("localhost:6969")
|
||||||
|
} else if *addr != "" {
|
||||||
|
// If only hostname is provided, append default port
|
||||||
|
if !strings.Contains(*addr, ":") {
|
||||||
|
*addr = *addr + ":6969"
|
||||||
|
}
|
||||||
|
network.SetServerAddr(*addr)
|
||||||
|
}
|
||||||
|
|
||||||
rl.InitWindow(1024, 768, "GoonScape")
|
rl.InitWindow(1024, 768, "GoonScape")
|
||||||
|
rl.SetExitKey(0)
|
||||||
defer rl.CloseWindow()
|
defer rl.CloseWindow()
|
||||||
rl.InitAudioDevice()
|
rl.InitAudioDevice()
|
||||||
defer rl.CloseAudioDevice()
|
defer rl.CloseAudioDevice()
|
||||||
@ -31,7 +52,7 @@ func main() {
|
|||||||
game.Player.Model = game.Models[modelIndex].Model
|
game.Player.Model = game.Models[modelIndex].Model
|
||||||
game.Player.Texture = game.Models[modelIndex].Texture
|
game.Player.Texture = game.Models[modelIndex].Texture
|
||||||
|
|
||||||
go network.HandleServerCommunication(conn, playerID, game.Player, game.OtherPlayers)
|
go network.HandleServerCommunication(conn, playerID, game.Player, game.OtherPlayers, game.QuitChan)
|
||||||
|
|
||||||
rl.PlayMusicStream(game.Music)
|
rl.PlayMusicStream(game.Music)
|
||||||
rl.SetMusicVolume(game.Music, 0.5)
|
rl.SetMusicVolume(game.Music, 0.5)
|
||||||
@ -44,4 +65,7 @@ func main() {
|
|||||||
game.Update(deltaTime)
|
game.Update(deltaTime)
|
||||||
game.Render()
|
game.Render()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait for clean shutdown
|
||||||
|
<-game.QuitChan
|
||||||
}
|
}
|
||||||
|
@ -1,32 +1,52 @@
|
|||||||
package network
|
package network
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gitea.boner.be/bdnugget/goonscape/game"
|
||||||
"gitea.boner.be/bdnugget/goonscape/types"
|
"gitea.boner.be/bdnugget/goonscape/types"
|
||||||
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var serverAddr = "boner.be:6969"
|
||||||
|
|
||||||
|
func SetServerAddr(addr string) {
|
||||||
|
serverAddr = addr
|
||||||
|
}
|
||||||
|
|
||||||
func ConnectToServer() (net.Conn, int32, error) {
|
func ConnectToServer() (net.Conn, int32, error) {
|
||||||
conn, err := net.Dial("tcp", types.ServerAddr)
|
conn, err := net.Dial("tcp", serverAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to dial server: %v", err)
|
log.Printf("Failed to dial server: %v", err)
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Connected to server. Waiting for player ID...")
|
log.Println("Connected to server. Waiting for player ID...")
|
||||||
buf := make([]byte, 1024)
|
reader := bufio.NewReader(conn)
|
||||||
n, err := conn.Read(buf)
|
|
||||||
if err != nil {
|
// Read message length (4 bytes)
|
||||||
log.Printf("Error reading player ID from server: %v", err)
|
lengthBuf := make([]byte, 4)
|
||||||
|
if _, err := io.ReadFull(reader, lengthBuf); err != nil {
|
||||||
|
log.Printf("Failed to read message length: %v", err)
|
||||||
|
return nil, 0, 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 nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var response pb.ServerMessage
|
var response pb.ServerMessage
|
||||||
if err := proto.Unmarshal(buf[:n], &response); err != nil {
|
if err := proto.Unmarshal(messageBuf, &response); err != nil {
|
||||||
log.Printf("Failed to unmarshal server response: %v", err)
|
log.Printf("Failed to unmarshal server response: %v", err)
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
@ -36,14 +56,33 @@ func ConnectToServer() (net.Conn, int32, error) {
|
|||||||
return conn, playerID, nil
|
return conn, playerID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Player, otherPlayers map[int32]*types.Player) {
|
func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Player, otherPlayers map[int32]*types.Player, quitChan <-chan struct{}) {
|
||||||
buf := make([]byte, 4096)
|
reader := bufio.NewReader(conn)
|
||||||
|
|
||||||
actionTicker := time.NewTicker(types.ClientTickRate)
|
actionTicker := time.NewTicker(types.ClientTickRate)
|
||||||
defer actionTicker.Stop()
|
defer actionTicker.Stop()
|
||||||
|
defer conn.Close()
|
||||||
|
defer close(player.QuitDone)
|
||||||
|
|
||||||
|
// Create a channel to signal when goroutines are done
|
||||||
|
done := make(chan struct{})
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for range actionTicker.C {
|
for {
|
||||||
|
select {
|
||||||
|
case <-quitChan:
|
||||||
|
// Send disconnect message to server
|
||||||
|
disconnectMsg := &pb.ActionBatch{
|
||||||
|
PlayerId: playerID,
|
||||||
|
Actions: []*pb.Action{{
|
||||||
|
Type: pb.Action_DISCONNECT,
|
||||||
|
PlayerId: playerID,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
writeMessage(conn, disconnectMsg)
|
||||||
|
done <- struct{}{}
|
||||||
|
return
|
||||||
|
case <-actionTicker.C:
|
||||||
player.Lock()
|
player.Lock()
|
||||||
if len(player.ActionQueue) > 0 {
|
if len(player.ActionQueue) > 0 {
|
||||||
actions := make([]*pb.Action, len(player.ActionQueue))
|
actions := make([]*pb.Action, len(player.ActionQueue))
|
||||||
@ -58,13 +97,7 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
|
|||||||
player.ActionQueue = player.ActionQueue[:0]
|
player.ActionQueue = player.ActionQueue[:0]
|
||||||
player.Unlock()
|
player.Unlock()
|
||||||
|
|
||||||
data, err := proto.Marshal(batch)
|
if err := writeMessage(conn, batch); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to marshal action batch: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = conn.Write(data); err != nil {
|
|
||||||
log.Printf("Failed to send actions to server: %v", err)
|
log.Printf("Failed to send actions to server: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -72,17 +105,34 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
|
|||||||
player.Unlock()
|
player.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
n, err := conn.Read(buf)
|
select {
|
||||||
if err != nil {
|
case <-quitChan:
|
||||||
log.Printf("Failed to read from server: %v", err)
|
<-done // Wait for action goroutine to finish
|
||||||
|
close(done)
|
||||||
|
time.Sleep(100 * time.Millisecond) // Give time for disconnect message to be sent
|
||||||
|
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
|
||||||
|
}
|
||||||
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var serverMessage pb.ServerMessage
|
var serverMessage pb.ServerMessage
|
||||||
if err := proto.Unmarshal(buf[:n], &serverMessage); err != nil {
|
if err := proto.Unmarshal(messageBuf, &serverMessage); err != nil {
|
||||||
log.Printf("Failed to unmarshal server message: %v", err)
|
log.Printf("Failed to unmarshal server message: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -112,5 +162,29 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
|
|||||||
otherPlayers[state.PlayerId] = types.NewPlayer(state)
|
otherPlayers[state.PlayerId] = types.NewPlayer(state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if g, ok := player.UserData.(*game.Game); ok && len(serverMessage.ChatMessages) > 0 {
|
||||||
|
g.Chat.HandleServerMessages(serverMessage.ChatMessages)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to write length-prefixed messages
|
||||||
|
func writeMessage(conn net.Conn, msg proto.Message) error {
|
||||||
|
data, err := proto.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write length prefix
|
||||||
|
lengthBuf := make([]byte, 4)
|
||||||
|
binary.BigEndian.PutUint32(lengthBuf, uint32(len(data)))
|
||||||
|
if _, err := conn.Write(lengthBuf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write message body
|
||||||
|
_, err = conn.Write(data)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
BIN
resources/screenshot.png
Normal file
BIN
resources/screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 104 KiB |
@ -27,6 +27,9 @@ type Player struct {
|
|||||||
CurrentTick int64
|
CurrentTick int64
|
||||||
LastUpdateTime time.Time
|
LastUpdateTime time.Time
|
||||||
InterpolationProgress float32
|
InterpolationProgress float32
|
||||||
|
UserData interface{}
|
||||||
|
FloatingMessage *FloatingMessage
|
||||||
|
QuitDone chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModelAsset struct {
|
type ModelAsset struct {
|
||||||
@ -34,6 +37,18 @@ type ModelAsset struct {
|
|||||||
Texture rl.Texture2D
|
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 (
|
const (
|
||||||
MapWidth = 50
|
MapWidth = 50
|
||||||
MapHeight = 50
|
MapHeight = 50
|
||||||
@ -44,5 +59,4 @@ const (
|
|||||||
ServerTickRate = 600 * time.Millisecond
|
ServerTickRate = 600 * time.Millisecond
|
||||||
ClientTickRate = 50 * time.Millisecond
|
ClientTickRate = 50 * time.Millisecond
|
||||||
MaxTickDesync = 5
|
MaxTickDesync = 5
|
||||||
ServerAddr = "localhost:6969"
|
|
||||||
)
|
)
|
||||||
|
Reference in New Issue
Block a user