8 Commits

Author SHA1 Message Date
c01b8d1c59 Floating chat messages and remote server 2025-01-13 14:22:24 +01:00
d301d597e8 Add chat 2025-01-13 13:23:52 +01:00
91cdbab54a Reorganize code 2025-01-13 11:10:48 +01:00
0a58e0453a Fix tickrate and action queue stuff 2025-01-13 10:00:23 +01:00
8d70129c73 Update goonserver submodule 2025-01-13 00:40:28 +01:00
4012a2ed92 Protobuf changes 2025-01-13 00:31:15 +01:00
4f36c2ee1f submodule 2025-01-13 00:02:21 +01:00
63e3837441 Fix movement on other player 2025-01-12 23:57:18 +01:00
23 changed files with 899 additions and 507 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "goonserver"]
path = goonserver
url = https://gitea.boner.be/bdnugget/goonserver

41
assets/assets.go Normal file
View File

@ -0,0 +1,41 @@
package assets
import (
"gitea.boner.be/bdnugget/goonscape/types"
rl "github.com/gen2brain/raylib-go/raylib"
)
func LoadModels() ([]types.ModelAsset, error) {
goonerModel := rl.LoadModel("resources/models/goonion.obj")
goonerTexture := rl.LoadTexture("resources/models/goonion.png")
rl.SetMaterialTexture(goonerModel.Materials, rl.MapDiffuse, goonerTexture)
coomerModel := rl.LoadModel("resources/models/coomer.obj")
coomerTexture := rl.LoadTexture("resources/models/coomer.png")
rl.SetMaterialTexture(coomerModel.Materials, rl.MapDiffuse, coomerTexture)
shrekeModel := rl.LoadModel("resources/models/shreke.obj")
shrekeTexture := rl.LoadTexture("resources/models/shreke.png")
rl.SetMaterialTexture(shrekeModel.Materials, rl.MapDiffuse, shrekeTexture)
return []types.ModelAsset{
{Model: goonerModel, Texture: goonerTexture},
{Model: coomerModel, Texture: coomerTexture},
{Model: shrekeModel, Texture: shrekeTexture},
}, nil
}
func LoadMusic(filename string) (rl.Music, error) {
return rl.LoadMusicStream(filename), nil
}
func UnloadModels(models []types.ModelAsset) {
for _, model := range models {
rl.UnloadModel(model.Model)
rl.UnloadTexture(model.Texture)
}
}
func UnloadMusic(music rl.Music) {
rl.UnloadMusicStream(music)
}

View File

@ -7,6 +7,15 @@ const (
MapHeight = 50 MapHeight = 50
TileSize = 32 TileSize = 32
TileHeight = 2.0 TileHeight = 2.0
TickRate = 2600 * time.Millisecond // Server tick rate (600ms) TickRate = 600 * time.Millisecond // Server tick rate (600ms)
serverAddr = "localhost:6969" serverAddr = "localhost:6969"
// RuneScape-style tick rate (600ms)
ServerTickRate = 600 * time.Millisecond
// Client might run at a higher tick rate for smooth rendering
ClientTickRate = 50 * time.Millisecond
// Maximum number of ticks we can get behind before forcing a resync
MaxTickDesync = 5
) )

View File

@ -1,4 +1,4 @@
package main package game
import ( import (
"math" "math"
@ -6,8 +6,13 @@ import (
rl "github.com/gen2brain/raylib-go/raylib" rl "github.com/gen2brain/raylib-go/raylib"
) )
var (
cameraDistance = float32(20.0)
cameraYaw = float32(145.0)
cameraPitch = float32(45.0)
)
func UpdateCamera(camera *rl.Camera3D, player rl.Vector3, deltaTime float32) { func UpdateCamera(camera *rl.Camera3D, player rl.Vector3, deltaTime float32) {
// Update camera based on mouse wheel
wheelMove := rl.GetMouseWheelMove() wheelMove := rl.GetMouseWheelMove()
if wheelMove != 0 { if wheelMove != 0 {
cameraDistance += -wheelMove * 5 cameraDistance += -wheelMove * 5
@ -19,7 +24,6 @@ func UpdateCamera(camera *rl.Camera3D, player rl.Vector3, deltaTime float32) {
} }
} }
// Orbit camera around the player using arrow keys
if rl.IsKeyDown(rl.KeyRight) { if rl.IsKeyDown(rl.KeyRight) {
cameraYaw += 100 * deltaTime cameraYaw += 100 * deltaTime
} }
@ -39,16 +43,13 @@ func UpdateCamera(camera *rl.Camera3D, player rl.Vector3, deltaTime float32) {
} }
} }
// Calculate the new camera position using spherical coordinates
cameraYawRad := float64(cameraYaw) * rl.Deg2rad cameraYawRad := float64(cameraYaw) * rl.Deg2rad
cameraPitchRad := float64(cameraPitch) * rl.Deg2rad cameraPitchRad := float64(cameraPitch) * rl.Deg2rad
cameraPos := rl.Vector3{
camera.Position = rl.Vector3{
X: player.X + cameraDistance*float32(math.Cos(cameraYawRad))*float32(math.Cos(cameraPitchRad)), X: player.X + cameraDistance*float32(math.Cos(cameraYawRad))*float32(math.Cos(cameraPitchRad)),
Y: player.Y + cameraDistance*float32(math.Sin(cameraPitchRad)), Y: player.Y + cameraDistance*float32(math.Sin(cameraPitchRad)),
Z: player.Z + cameraDistance*float32(math.Sin(cameraYawRad))*float32(math.Cos(cameraPitchRad)), Z: player.Z + cameraDistance*float32(math.Sin(cameraYawRad))*float32(math.Cos(cameraPitchRad)),
} }
camera.Target = player
// Update the camera's position and target
camera.Position = cameraPos
camera.Target = rl.NewVector3(player.X, player.Y, player.Z)
} }

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
}

225
game/game.go Normal file
View File

@ -0,0 +1,225 @@
package game
import (
"time"
"gitea.boner.be/bdnugget/goonscape/assets"
"gitea.boner.be/bdnugget/goonscape/types"
pb "gitea.boner.be/bdnugget/goonserver/actions"
rl "github.com/gen2brain/raylib-go/raylib"
)
type Game struct {
Player *types.Player
OtherPlayers map[int32]*types.Player
Camera rl.Camera3D
Models []types.ModelAsset
Music rl.Music
Chat *Chat
}
func New() *Game {
InitWorld()
game := &Game{
Player: &types.Player{
PosActual: rl.NewVector3(5*types.TileSize, 0, 5*types.TileSize),
PosTile: GetTile(5, 5),
Speed: 50.0,
TargetPath: []types.Tile{},
UserData: nil,
},
OtherPlayers: make(map[int32]*types.Player),
Camera: rl.Camera3D{
Position: rl.NewVector3(0, 10, 10),
Target: rl.NewVector3(0, 0, 0),
Up: rl.NewVector3(0, 1, 0),
Fovy: 45.0,
Projection: rl.CameraPerspective,
},
Chat: NewChat(),
}
game.Player.UserData = game
game.Chat.userData = game
return game
}
func (g *Game) LoadAssets() error {
var err error
g.Models, err = assets.LoadModels()
if err != nil {
return err
}
g.Music, err = assets.LoadMusic("resources/audio/GoonScape2.mp3")
if err != nil {
return err
}
return nil
}
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 {
g.Player.MoveTowards(g.Player.TargetPath[0], deltaTime, GetMapGrid())
}
for _, other := range g.OtherPlayers {
if len(other.TargetPath) > 0 {
other.MoveTowards(other.TargetPath[0], deltaTime, GetMapGrid())
}
}
UpdateCamera(&g.Camera, g.Player.PosActual, deltaTime)
}
func (g *Game) DrawMap() {
for x := 0; x < types.MapWidth; x++ {
for y := 0; y < types.MapHeight; y++ {
height := GetTileHeight(x, y)
// Interpolate height for smoother landscape
if x > 0 {
height += GetTileHeight(x-1, y)
}
if y > 0 {
height += GetTileHeight(x, y-1)
}
if x > 0 && y > 0 {
height += GetTileHeight(x-1, y-1)
}
height /= 4.0
tilePos := rl.Vector3{
X: float32(x * types.TileSize),
Y: height * types.TileHeight,
Z: float32(y * types.TileSize),
}
color := rl.Color{R: uint8(height * 25), G: 100, B: 100, A: 64}
rl.DrawCube(tilePos, types.TileSize, types.TileHeight, types.TileSize, color)
}
}
}
func (g *Game) DrawPlayer(player *types.Player, model rl.Model) {
player.Lock()
defer player.Unlock()
grid := GetMapGrid()
playerPos := rl.Vector3{
X: player.PosActual.X,
Y: grid[player.PosTile.X][player.PosTile.Y].Height*types.TileHeight + 16.0,
Z: player.PosActual.Z,
}
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{
X: float32(targetTile.X * types.TileSize),
Y: grid[targetTile.X][targetTile.Y].Height * types.TileHeight,
Z: float32(targetTile.Y * types.TileSize),
}
rl.DrawCubeWires(targetPos, types.TileSize, types.TileHeight, types.TileSize, rl.Green)
nextTile := player.TargetPath[0]
nextPos := rl.Vector3{
X: float32(nextTile.X * types.TileSize),
Y: grid[nextTile.X][nextTile.Y].Height * types.TileHeight,
Z: float32(nextTile.Y * types.TileSize),
}
rl.DrawCubeWires(nextPos, types.TileSize, types.TileHeight, types.TileSize, rl.Yellow)
}
}
func (g *Game) Render() {
rl.BeginDrawing()
rl.ClearBackground(rl.RayWhite)
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()
}
func (g *Game) Cleanup() {
assets.UnloadModels(g.Models)
assets.UnloadMusic(g.Music)
}
func (g *Game) HandleInput() {
clickedTile, clicked := g.GetTileAtMouse()
if clicked {
path := FindPath(GetTile(g.Player.PosTile.X, g.Player.PosTile.Y), clickedTile)
if len(path) > 1 {
g.Player.Lock()
g.Player.TargetPath = path[1:]
g.Player.ActionQueue = append(g.Player.ActionQueue, &pb.Action{
Type: pb.Action_MOVE,
X: int32(clickedTile.X),
Y: int32(clickedTile.Y),
PlayerId: g.Player.ID,
})
g.Player.Unlock()
}
}
}

31
game/input.go Normal file
View File

@ -0,0 +1,31 @@
package game
import (
"fmt"
"gitea.boner.be/bdnugget/goonscape/types"
rl "github.com/gen2brain/raylib-go/raylib"
)
func (g *Game) GetTileAtMouse() (types.Tile, bool) {
if !rl.IsMouseButtonPressed(rl.MouseLeftButton) {
return types.Tile{}, false
}
mouse := rl.GetMousePosition()
ray := rl.GetMouseRay(mouse, g.Camera)
for x := 0; x < types.MapWidth; x++ {
for y := 0; y < types.MapHeight; y++ {
tile := GetTile(x, y)
tilePos := rl.NewVector3(float32(x*types.TileSize), tile.Height*types.TileHeight, float32(y*types.TileSize))
boxMin := rl.Vector3Subtract(tilePos, rl.NewVector3(types.TileSize/2, types.TileHeight/2, types.TileSize/2))
boxMax := rl.Vector3Add(tilePos, rl.NewVector3(types.TileSize/2, types.TileHeight/2, types.TileSize/2))
if RayIntersectsBox(ray, boxMin, boxMax) {
fmt.Printf("Clicked: %d, %d\n", tile.X, tile.Y)
return tile, true
}
}
}
return types.Tile{}, false
}

View File

@ -1,14 +1,18 @@
package main package game
import "fmt" import (
"fmt"
"gitea.boner.be/bdnugget/goonscape/types"
)
type Node struct { type Node struct {
Tile Tile Tile types.Tile
Parent *Node Parent *Node
G, H, F float32 G, H, F float32
} }
func FindPath(start, end Tile) []Tile { func FindPath(start, end types.Tile) []types.Tile {
openList := []*Node{} openList := []*Node{}
closedList := make(map[[2]int]bool) closedList := make(map[[2]int]bool)
@ -17,7 +21,6 @@ func FindPath(start, end Tile) []Tile {
openList = append(openList, startNode) openList = append(openList, startNode)
for len(openList) > 0 { for len(openList) > 0 {
// Find node with lowest F
current := openList[0] current := openList[0]
currentIndex := 0 currentIndex := 0
for i, node := range openList { for i, node := range openList {
@ -27,23 +30,20 @@ func FindPath(start, end Tile) []Tile {
} }
} }
// Move current to closed list
openList = append(openList[:currentIndex], openList[currentIndex+1:]...) openList = append(openList[:currentIndex], openList[currentIndex+1:]...)
closedList[[2]int{current.Tile.X, current.Tile.Y}] = true closedList[[2]int{current.Tile.X, current.Tile.Y}] = true
// Check if reached the end
if current.Tile.X == end.X && current.Tile.Y == end.Y { if current.Tile.X == end.X && current.Tile.Y == end.Y {
path := []Tile{} path := []types.Tile{}
node := current node := current
for node != nil { for node != nil {
path = append([]Tile{node.Tile}, path...) path = append([]types.Tile{node.Tile}, path...)
node = node.Parent node = node.Parent
} }
fmt.Printf("Path found: %v\n", path) fmt.Printf("Path found: %v\n", path)
return path return path
} }
// Generate neighbors
neighbors := GetNeighbors(current.Tile) neighbors := GetNeighbors(current.Tile)
for _, neighbor := range neighbors { for _, neighbor := range neighbors {
if !neighbor.Walkable || closedList[[2]int{neighbor.X, neighbor.Y}] { if !neighbor.Walkable || closedList[[2]int{neighbor.X, neighbor.Y}] {
@ -75,32 +75,30 @@ func FindPath(start, end Tile) []Tile {
} }
} }
} }
// No path found
fmt.Println("No path found")
return nil return nil
} }
func heuristic(a, b Tile) float32 { func heuristic(a, b types.Tile) float32 {
return float32(abs(a.X-b.X) + abs(a.Y-b.Y)) return float32(abs(a.X-b.X) + abs(a.Y-b.Y))
} }
func distance(a, b Tile) float32 { func distance(a, b types.Tile) float32 {
_ = a return 1.0 // uniform cost for now
_ = b
return 1.0 //uniform cost for now
} }
func GetNeighbors(tile Tile) []Tile { func GetNeighbors(tile types.Tile) []types.Tile {
directions := [][2]int{ directions := [][2]int{
{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 0}, {-1, 0}, {0, 1}, {0, -1},
{1, 1}, {-1, -1}, {1, -1}, {-1, 1}, {1, 1}, {-1, -1}, {1, -1}, {-1, 1},
} }
neighbors := []Tile{} neighbors := []types.Tile{}
grid := GetMapGrid()
for _, dir := range directions { for _, dir := range directions {
nx, ny := tile.X+dir[0], tile.Y+dir[1] nx, ny := tile.X+dir[0], tile.Y+dir[1]
if nx >= 0 && nx < MapWidth && ny >= 0 && ny < MapHeight { if nx >= 0 && nx < types.MapWidth && ny >= 0 && ny < types.MapHeight {
neighbors = append(neighbors, mapGrid[nx][ny]) if grid[nx][ny].Walkable {
neighbors = append(neighbors, grid[nx][ny])
}
} }
} }
return neighbors return neighbors

45
game/utils.go Normal file
View File

@ -0,0 +1,45 @@
package game
import (
rl "github.com/gen2brain/raylib-go/raylib"
)
func RayIntersectsBox(ray rl.Ray, boxMin, boxMax rl.Vector3) bool {
tmin := (boxMin.X - ray.Position.X) / ray.Direction.X
tmax := (boxMax.X - ray.Position.X) / ray.Direction.X
if tmin > tmax {
tmin, tmax = tmax, tmin
}
tymin := (boxMin.Z - ray.Position.Z) / ray.Direction.Z
tymax := (boxMax.Z - ray.Position.Z) / ray.Direction.Z
if tymin > tymax {
tymin, tymax = tymax, tymin
}
if (tmin > tymax) || (tymin > tmax) {
return false
}
if tymin > tmin {
tmin = tymin
}
if tymax < tmax {
tmax = tymax
}
tzmin := (boxMin.Y - ray.Position.Y) / ray.Direction.Y
tzmax := (boxMax.Y - ray.Position.Y) / ray.Direction.Y
if tzmin > tzmax {
tzmin, tzmax = tzmax, tzmin
}
if (tmin > tzmax) || (tzmin > tmax) {
return false
}
return true
}

39
game/world.go Normal file
View File

@ -0,0 +1,39 @@
package game
import (
"gitea.boner.be/bdnugget/goonscape/types"
)
var (
mapGrid [][]types.Tile
)
func GetMapGrid() [][]types.Tile {
return mapGrid
}
func InitWorld() {
mapGrid = make([][]types.Tile, types.MapWidth)
for x := 0; x < types.MapWidth; x++ {
mapGrid[x] = make([]types.Tile, types.MapHeight)
for y := 0; y < types.MapHeight; y++ {
mapGrid[x][y] = types.Tile{
X: x,
Y: y,
Height: 1.0 + float32(x%5),
Walkable: true,
}
}
}
}
func GetTile(x, y int) types.Tile {
if x >= 0 && x < types.MapWidth && y >= 0 && y < types.MapHeight {
return mapGrid[x][y]
}
return types.Tile{}
}
func GetTileHeight(x, y int) float32 {
return mapGrid[x][y].Height
}

4
go.mod
View File

@ -1,4 +1,4 @@
module goonscape module gitea.boner.be/bdnugget/goonscape
go 1.23.0 go 1.23.0
@ -13,3 +13,5 @@ require (
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
golang.org/x/sys v0.26.0 // indirect golang.org/x/sys v0.26.0 // indirect
) )
replace gitea.boner.be/bdnugget/goonserver => ./goonserver

2
go.sum
View File

@ -1,5 +1,3 @@
gitea.boner.be/bdnugget/goonserver v0.0.0-20241011195320-f16e8647dc6b h1:hdhCZH0YGqCsnSl6ru+8I7rxvCyOj5pCtf92urwyruA=
gitea.boner.be/bdnugget/goonserver v0.0.0-20241011195320-f16e8647dc6b/go.mod h1:inR1bKrr/vcTba+G1KzmmY6vssMq9oGNOk836VwPa4c=
github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE= github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE=
github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.8.0/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-20240930075631-c66f9e2942fe h1:mInjrbJkUglTM7tBmXG+epnPCE744aj15J7vjJwM4gs=

1
goonserver Submodule

Submodule goonserver added at 368fbdbc47

View File

@ -1,88 +0,0 @@
package main
import (
"fmt"
rl "github.com/gen2brain/raylib-go/raylib"
)
func GetTileAtMouse(camera *rl.Camera3D) (Tile, bool) {
if !rl.IsMouseButtonPressed(rl.MouseLeftButton) {
return Tile{}, false
}
mouse := rl.GetMousePosition()
ray := rl.GetMouseRay(mouse, *camera)
for x := 0; x < MapWidth; x++ {
for y := 0; y < MapHeight; y++ {
tile := mapGrid[x][y]
// Define the bounding box for each tile based on its position and height
tilePos := rl.NewVector3(float32(x*TileSize), tile.Height*TileHeight, float32(y*TileSize))
boxMin := rl.Vector3Subtract(tilePos, rl.NewVector3(TileSize/2, TileHeight/2, TileSize/2))
boxMax := rl.Vector3Add(tilePos, rl.NewVector3(TileSize/2, TileHeight/2, TileSize/2))
// Check if the ray intersects the bounding box
if RayIntersectsBox(ray, boxMin, boxMax) {
fmt.Println("Clicked:", tile.X, tile.Y)
return tile, true
}
}
}
return Tile{}, false
}
func HandleInput(player *Player, camera *rl.Camera) {
clickedTile, clicked := GetTileAtMouse(camera)
if clicked {
path := FindPath(mapGrid[player.PosTile.X][player.PosTile.Y], clickedTile)
if path != nil {
// Exclude the first tile (current position)
if len(path) > 1 {
player.TargetPath = path[1:]
player.ActionQueue = append(player.ActionQueue, Action{Type: MoveAction, X: clickedTile.X, Y: clickedTile.Y})
}
}
}
}
// Helper function to test ray-box intersection (slab method)
func RayIntersectsBox(ray rl.Ray, boxMin, boxMax rl.Vector3) bool {
tmin := (boxMin.X - ray.Position.X) / ray.Direction.X
tmax := (boxMax.X - ray.Position.X) / ray.Direction.X
if tmin > tmax {
tmin, tmax = tmax, tmin
}
tymin := (boxMin.Z - ray.Position.Z) / ray.Direction.Z
tymax := (boxMax.Z - ray.Position.Z) / ray.Direction.Z
if tymin > tymax {
tymin, tymax = tymax, tymin
}
if (tmin > tymax) || (tymin > tmax) {
return false
}
if tymin > tmin {
tmin = tymin
}
if tymax < tmax {
tmax = tymax
}
tzmin := (boxMin.Y - ray.Position.Y) / ray.Direction.Y
tzmax := (boxMax.Y - ray.Position.Y) / ray.Direction.Y
if tzmin > tzmax {
tzmin, tzmax = tzmax, tzmin
}
if (tmin > tzmax) || (tzmin > tmax) {
return false
}
return true
}

137
main.go
View File

@ -3,148 +3,45 @@ package main
import ( import (
"log" "log"
"gitea.boner.be/bdnugget/goonscape/game"
"gitea.boner.be/bdnugget/goonscape/network"
rl "github.com/gen2brain/raylib-go/raylib" rl "github.com/gen2brain/raylib-go/raylib"
) )
var (
cameraDistance = float32(20.0)
cameraYaw = float32(145.0)
cameraPitch = float32(45.0) // Adjusted for a more overhead view
mapGrid = InitMap()
)
func main() { func main() {
rl.InitWindow(1024, 768, "GoonScape") rl.InitWindow(1024, 768, "GoonScape")
defer rl.CloseWindow() defer rl.CloseWindow()
rl.InitAudioDevice() rl.InitAudioDevice()
defer rl.CloseAudioDevice() defer rl.CloseAudioDevice()
player := Player{ game := game.New()
PosActual: rl.NewVector3(5*TileSize, 0, 5*TileSize), if err := game.LoadAssets(); err != nil {
PosTile: mapGrid[5][5], log.Fatalf("Failed to load assets: %v", err)
Speed: 50.0,
TargetPath: []Tile{},
} }
defer game.Cleanup()
camera := rl.Camera3D{ conn, playerID, err := network.ConnectToServer()
Position: rl.NewVector3(0, 10, 10), // Will be updated every frame
Target: player.PosActual,
Up: rl.NewVector3(0, 1, 0), // Y is up in 3D
Fovy: 45.0,
Projection: rl.CameraPerspective,
}
conn, playerID, err := ConnectToServer()
if err != nil { if err != nil {
log.Fatalf("Failed to connect to server: %v", err) log.Fatalf("Failed to connect to server: %v", err)
} }
log.Printf("Player ID: %d", playerID)
player.ID = playerID
defer conn.Close() defer conn.Close()
otherPlayers := make(map[int32]*Player) game.Player.ID = playerID
modelIndex := int(playerID) % len(game.Models)
game.Player.Model = game.Models[modelIndex].Model
game.Player.Texture = game.Models[modelIndex].Texture
go HandleServerCommunication(conn, playerID, &player, otherPlayers) go network.HandleServerCommunication(conn, playerID, game.Player, game.OtherPlayers)
models, err := LoadModels()
if err != nil {
log.Fatalf("Failed to load models: %v", err)
}
defer UnloadModels(models)
modelIndex := int(playerID) % len(models)
player.Model = models[modelIndex].Model
player.Texture = models[modelIndex].Texture
music, err := LoadMusic("resources/audio/GoonScape2.mp3")
if err != nil {
log.Fatalf("Failed to load music: %v", err)
}
defer UnloadMusic(music)
rl.PlayMusicStream(music)
rl.SetMusicVolume(music, 0.5)
rl.PlayMusicStream(game.Music)
rl.SetMusicVolume(game.Music, 0.5)
rl.SetTargetFPS(60) rl.SetTargetFPS(60)
for !rl.WindowShouldClose() { for !rl.WindowShouldClose() {
rl.UpdateMusicStream(game.Music)
rl.UpdateMusicStream(music)
// Time management
deltaTime := rl.GetFrameTime() deltaTime := rl.GetFrameTime()
// Handle input game.Update(deltaTime)
HandleInput(&player, &camera) game.Render()
// Update player
if len(player.TargetPath) > 0 {
player.MoveTowards(player.TargetPath[0], deltaTime)
}
// Update camera
UpdateCamera(&camera, player.PosActual, deltaTime)
// Rendering
rl.BeginDrawing()
rl.ClearBackground(rl.RayWhite)
rl.BeginMode3D(camera)
DrawMap()
DrawPlayer(player, player.Model)
for id, other := range otherPlayers {
if len(other.TargetPath) > 0 {
other.MoveTowards(other.TargetPath[0], deltaTime)
}
DrawPlayer(*other, models[int(id)%len(models)].Model)
}
rl.EndMode3D()
rl.DrawFPS(10, 10)
rl.EndDrawing()
} }
} }
func LoadModels() ([]struct {
Model rl.Model
Texture rl.Texture2D
}, error) {
goonerModel := rl.LoadModel("resources/models/goonion.obj")
goonerTexture := rl.LoadTexture("resources/models/goonion.png")
rl.SetMaterialTexture(goonerModel.Materials, rl.MapDiffuse, goonerTexture)
coomerModel := rl.LoadModel("resources/models/coomer.obj")
coomerTexture := rl.LoadTexture("resources/models/coomer.png")
rl.SetMaterialTexture(coomerModel.Materials, rl.MapDiffuse, coomerTexture)
shrekeModel := rl.LoadModel("resources/models/shreke.obj")
shrekeTexture := rl.LoadTexture("resources/models/shreke.png")
rl.SetMaterialTexture(shrekeModel.Materials, rl.MapDiffuse, shrekeTexture)
return []struct {
Model rl.Model
Texture rl.Texture2D
}{
{Model: goonerModel, Texture: goonerTexture},
{Model: coomerModel, Texture: coomerTexture},
{Model: shrekeModel, Texture: shrekeTexture},
}, nil
}
func UnloadModels(models []struct {
Model rl.Model
Texture rl.Texture2D
}) {
for _, model := range models {
rl.UnloadModel(model.Model)
rl.UnloadTexture(model.Texture)
}
}
func LoadMusic(filename string) (rl.Music, error) {
music := rl.LoadMusicStream(filename)
return music, nil
}
func UnloadMusic(music rl.Music) {
rl.UnloadMusicStream(music)
}

48
map.go
View File

@ -1,48 +0,0 @@
package main
import rl "github.com/gen2brain/raylib-go/raylib"
// Initialize the map with some height data
func InitMap() [][]Tile {
mapGrid := make([][]Tile, MapWidth)
for x := 0; x < MapWidth; x++ {
mapGrid[x] = make([]Tile, MapHeight)
for y := 0; y < MapHeight; y++ {
mapGrid[x][y] = Tile{
X: x,
Y: y,
Height: 1.0 + float32(x%5), // Example height
Walkable: true, // Set to false for obstacles
}
}
}
return mapGrid
}
func DrawMap() {
for x := 0; x < MapWidth; x++ {
for y := 0; y < MapHeight; y++ {
tile := mapGrid[x][y]
// Interpolate height between adjacent tiles for a smoother landscape
height := tile.Height
if x > 0 {
height += mapGrid[x-1][y].Height
}
if y > 0 {
height += mapGrid[x][y-1].Height
}
if x > 0 && y > 0 {
height += mapGrid[x-1][y-1].Height
}
height /= 4.0
// Draw each tile as a 3D cube based on its height
tilePos := rl.Vector3{
X: float32(x * TileSize), // X-axis for horizontal position
Y: height * TileHeight, // Y-axis for height (Z in 3D is Y here)
Z: float32(y * TileSize), // Z-axis for depth (Y in 3D is Z here)
}
color := rl.Color{R: uint8(height * 25), G: 100, B: 100, A: 64}
rl.DrawCube(tilePos, TileSize, TileHeight, TileSize, color) // Draw a cube representing the tile
}
}
}

View File

@ -1,121 +0,0 @@
package main
import (
"log"
"net"
"time"
pb "gitea.boner.be/bdnugget/goonserver/actions"
rl "github.com/gen2brain/raylib-go/raylib"
"google.golang.org/protobuf/proto"
)
func ConnectToServer() (net.Conn, int32, error) {
// Attempt to connect to the server
conn, err := net.Dial("tcp", serverAddr)
if err != nil {
log.Printf("Failed to dial server: %v", err)
return nil, 0, err
}
log.Println("Connected to server. Waiting for player ID...")
// Buffer for incoming server message
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
log.Printf("Error reading player ID from server: %v", err)
return nil, 0, err
}
log.Printf("Received data: %x", buf[:n])
// Unmarshal server message to extract the player ID
var response pb.ServerMessage
if err := proto.Unmarshal(buf[:n], &response); err != nil {
log.Printf("Failed to unmarshal server response: %v", err)
return nil, 0, err
}
playerID := response.GetPlayerId()
log.Printf("Successfully connected with player ID: %d", playerID)
return conn, playerID, nil
}
func HandleServerCommunication(conn net.Conn, playerID int32, player *Player, otherPlayers map[int32]*Player) {
// Goroutine to handle sending player's actions to the server
go func() {
for {
if len(player.ActionQueue) > 0 {
// Process the first action in the queue
actionData := player.ActionQueue[0]
action := &pb.Action{
PlayerId: playerID,
Type: pb.Action_MOVE,
X: int32(actionData.X),
Y: int32(actionData.Y),
}
// Serialize the action
data, err := proto.Marshal(action)
if err != nil {
log.Printf("Failed to marshal action: %v", err)
continue
}
// Send action to server
_, err = conn.Write(data)
if err != nil {
log.Printf("Failed to send action to server: %v", err)
return
}
// Remove the action from the queue once it's sent
player.ActionQueue = player.ActionQueue[1:]
}
// Add a delay to match the server's tick rate
time.Sleep(TickRate)
}
}()
// Main loop to handle receiving updates from the server
for {
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if err != nil {
log.Printf("Failed to read from server: %v", err)
return
}
var serverMessage pb.ServerMessage
if err := proto.Unmarshal(buf[:n], &serverMessage); err != nil {
log.Printf("Failed to unmarshal server message: %v", err)
continue
}
// Update other players' states
for _, state := range serverMessage.Players {
if state.PlayerId != playerID {
if otherPlayer, exists := otherPlayers[state.PlayerId]; exists {
otherPlayer.PosTile = Tile{X: int(state.X), Y: int(state.Y)}
otherPlayer.PosActual = rl.Vector3{
X: float32(state.X * TileSize),
Y: float32(state.Y * TileHeight),
Z: float32(state.Y * TileSize),
}
otherPlayer.MoveTowards(Tile{X: int(state.X), Y: int(state.Y)}, 0)
} else {
otherPlayers[state.PlayerId] = &Player{
PosTile: Tile{X: int(state.X), Y: int(state.Y)},
PosActual: rl.Vector3{
X: float32(state.X * TileSize),
Y: float32(state.Y * TileHeight),
Z: float32(state.Y * TileSize),
},
ID: state.PlayerId,
}
}
}
}
}
}

121
network/network.go Normal file
View File

@ -0,0 +1,121 @@
package network
import (
"log"
"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"
)
func ConnectToServer() (net.Conn, int32, error) {
conn, err := net.Dial("tcp", types.ServerAddr)
if err != nil {
log.Printf("Failed to dial server: %v", err)
return nil, 0, err
}
log.Println("Connected to server. Waiting for player ID...")
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
log.Printf("Error reading player ID from server: %v", err)
return nil, 0, err
}
var response pb.ServerMessage
if err := proto.Unmarshal(buf[:n], &response); err != nil {
log.Printf("Failed to unmarshal server response: %v", err)
return nil, 0, err
}
playerID := response.GetPlayerId()
log.Printf("Successfully connected with player ID: %d", playerID)
return conn, playerID, nil
}
func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Player, otherPlayers map[int32]*types.Player) {
buf := make([]byte, 4096)
actionTicker := time.NewTicker(types.ClientTickRate)
defer actionTicker.Stop()
go func() {
for range actionTicker.C {
player.Lock()
if len(player.ActionQueue) > 0 {
actions := make([]*pb.Action, len(player.ActionQueue))
copy(actions, player.ActionQueue)
batch := &pb.ActionBatch{
PlayerId: playerID,
Actions: actions,
Tick: player.CurrentTick,
}
player.ActionQueue = player.ActionQueue[:0]
player.Unlock()
data, err := proto.Marshal(batch)
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)
return
}
} else {
player.Unlock()
}
}
}()
for {
n, err := conn.Read(buf)
if err != nil {
log.Printf("Failed to read from server: %v", err)
return
}
var serverMessage pb.ServerMessage
if err := proto.Unmarshal(buf[:n], &serverMessage); err != nil {
log.Printf("Failed to unmarshal server message: %v", err)
continue
}
player.Lock()
player.CurrentTick = serverMessage.CurrentTick
tickDiff := serverMessage.CurrentTick - player.CurrentTick
if tickDiff > types.MaxTickDesync {
for _, state := range serverMessage.Players {
if state.PlayerId == playerID {
player.ForceResync(state)
break
}
}
}
player.Unlock()
for _, state := range serverMessage.Players {
if state.PlayerId == playerID {
continue
}
if otherPlayer, exists := otherPlayers[state.PlayerId]; exists {
otherPlayer.UpdatePosition(state, types.ServerTickRate)
} else {
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

@ -1,63 +0,0 @@
package main
import (
rl "github.com/gen2brain/raylib-go/raylib"
)
func DrawPlayer(player Player, model rl.Model) {
// Draw the player based on its actual position (PosActual) and current tile height
playerPos := rl.Vector3{
X: player.PosActual.X,
Y: mapGrid[player.PosTile.X][player.PosTile.Y].Height*TileHeight + 16.0,
Z: player.PosActual.Z,
}
rl.DrawModel(model, playerPos, 16, rl.White)
// Draw highlight around target tile
if len(player.TargetPath) > 0 {
targetTile := player.TargetPath[len(player.TargetPath)-1] // last tile in the slice
targetPos := rl.Vector3{
X: float32(targetTile.X * TileSize),
Y: mapGrid[targetTile.X][targetTile.Y].Height * TileHeight,
Z: float32(targetTile.Y * TileSize),
}
rl.DrawCubeWires(targetPos, TileSize, TileHeight, TileSize, rl.Green)
nextTile := player.TargetPath[0] // first tile in the slice
nextPos := rl.Vector3{
X: float32(nextTile.X * TileSize),
Y: mapGrid[nextTile.X][nextTile.Y].Height * TileHeight,
Z: float32(nextTile.Y * TileSize),
}
rl.DrawCubeWires(nextPos, TileSize, TileHeight, TileSize, rl.Yellow)
}
}
func (player *Player) MoveTowards(target Tile, deltaTime float32) {
// Calculate the direction vector to the target tile
targetPos := rl.Vector3{
X: float32(target.X * TileSize),
Y: mapGrid[target.X][target.Y].Height * TileHeight,
Z: float32(target.Y * TileSize),
}
// Calculate direction and normalize it for smooth movement
direction := rl.Vector3Subtract(targetPos, player.PosActual)
distance := rl.Vector3Length(direction)
if distance > 0 {
direction = rl.Vector3Scale(direction, player.Speed*deltaTime/distance)
}
// Move the player towards the target tile
if distance > 1.0 {
player.PosActual = rl.Vector3Add(player.PosActual, direction)
} else {
// Snap to the target tile when close enough
player.PosActual = targetPos
player.PosTile = target // Update player's tile
if len(player.TargetPath) > 1 {
player.TargetPath = player.TargetPath[1:] // Move to next tile in path if any
}
}
}

View File

@ -1 +0,0 @@
package main

View File

@ -1,31 +0,0 @@
package main
import rl "github.com/gen2brain/raylib-go/raylib"
type Tile struct {
X, Y int
Height float32
Walkable bool
}
type ActionType int
const (
MoveAction ActionType = iota
)
type Action struct {
Type ActionType
X, Y int // Target position for movement
}
type Player struct {
PosActual rl.Vector3
PosTile Tile
TargetPath []Tile
Speed float32
ActionQueue []Action // Queue for player actions
Model rl.Model
Texture rl.Texture2D
ID int32
}

76
types/player.go Normal file
View File

@ -0,0 +1,76 @@
package types
import (
"time"
pb "gitea.boner.be/bdnugget/goonserver/actions"
rl "github.com/gen2brain/raylib-go/raylib"
)
func (p *Player) MoveTowards(target Tile, deltaTime float32, mapGrid [][]Tile) {
p.Lock()
defer p.Unlock()
targetPos := rl.Vector3{
X: float32(target.X * TileSize),
Y: mapGrid[target.X][target.Y].Height * TileHeight,
Z: float32(target.Y * TileSize),
}
direction := rl.Vector3Subtract(targetPos, p.PosActual)
distance := rl.Vector3Length(direction)
if distance > 0 {
direction = rl.Vector3Scale(direction, p.Speed*deltaTime/distance)
}
if distance > 1.0 {
p.PosActual = rl.Vector3Add(p.PosActual, direction)
} else {
p.PosActual = targetPos
p.PosTile = target
if len(p.TargetPath) > 1 {
p.TargetPath = p.TargetPath[1:]
}
}
}
func NewPlayer(state *pb.PlayerState) *Player {
return &Player{
PosActual: rl.Vector3{
X: float32(state.X * TileSize),
Y: float32(state.Y * TileHeight),
Z: float32(state.Y * TileSize),
},
PosTile: Tile{X: int(state.X), Y: int(state.Y)},
Speed: 50.0,
ID: state.PlayerId,
}
}
func (p *Player) UpdatePosition(state *pb.PlayerState, tickRate time.Duration) {
p.Lock()
defer p.Unlock()
targetTile := Tile{X: int(state.X), Y: int(state.Y)}
if p.PosTile != targetTile {
p.PosTile = targetTile
p.LastUpdateTime = time.Now()
p.InterpolationProgress = 0
p.TargetPath = []Tile{targetTile}
}
}
func (p *Player) ForceResync(state *pb.PlayerState) {
p.Lock()
defer p.Unlock()
p.PosTile = Tile{X: int(state.X), Y: int(state.Y)}
p.PosActual = rl.Vector3{
X: float32(state.X * TileSize),
Y: float32(state.Y * TileHeight),
Z: float32(state.Y * TileSize),
}
p.TargetPath = nil
p.ActionQueue = nil
p.InterpolationProgress = 1.0
}

63
types/types.go Normal file
View File

@ -0,0 +1,63 @@
package types
import (
"sync"
"time"
pb "gitea.boner.be/bdnugget/goonserver/actions"
rl "github.com/gen2brain/raylib-go/raylib"
)
type Tile struct {
X, Y int
Height float32
Walkable bool
}
type Player struct {
sync.Mutex
PosActual rl.Vector3
PosTile Tile
TargetPath []Tile
ActionQueue []*pb.Action
Speed float32
Model rl.Model
Texture rl.Texture2D
ID int32
CurrentTick int64
LastUpdateTime time.Time
InterpolationProgress float32
UserData interface{} // Used to store reference to game
FloatingMessage *FloatingMessage
}
type ModelAsset struct {
Model rl.Model
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
TileSize = 32
TileHeight = 2.0
// RuneScape-style tick rate (600ms)
ServerTickRate = 600 * time.Millisecond
ClientTickRate = 50 * time.Millisecond
MaxTickDesync = 5
// ServerAddr = "localhost:6969"
ServerAddr = "boner.be:6969"
)