Compare commits
34 Commits
feature/ch
...
v1.1.1
Author | SHA1 | Date | |
---|---|---|---|
944c33ce3b | |||
d5bb464d9f | |||
4549ee7517 | |||
31ae9c525f | |||
06913a5217 | |||
49663c9094 | |||
a843680b09 | |||
7183df4a8b | |||
33e355200d | |||
e45066b2a8 | |||
bb01dccf2b | |||
0f56916295 | |||
a1ddbadea0 | |||
e4d0b98945 | |||
509bc8b20b | |||
c40e4ae7ac | |||
863f5a939c | |||
cd68581429 | |||
b9d0d46bd6 | |||
b96c7ada7a | |||
d86cbe15a3 | |||
fb018e2a7d | |||
5ca973fdf1 | |||
2a0f9348e9 | |||
d6d0f36cee | |||
e8d062c4b7 | |||
0cd3145d28 | |||
0b6ab17ad5 | |||
50952309f4 | |||
afc44710f2 | |||
1a7b0eff42 | |||
bf7bf12a53 | |||
e661320508 | |||
567ec40c3d |
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Build artifacts
|
||||||
|
build/
|
||||||
|
goonscape
|
||||||
|
goonscape.exe
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
25
.woodpecker.yml
Normal file
25
.woodpecker.yml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
pipeline:
|
||||||
|
build:
|
||||||
|
image: golang:1.23
|
||||||
|
commands:
|
||||||
|
# Install build dependencies
|
||||||
|
- apt-get update && apt-get install -y gcc-mingw-w64 cmake zip libasound2-dev mesa-common-dev libx11-dev libxrandr-dev libxi-dev xorg-dev libgl1-mesa-dev libglu1-mesa-dev libwayland-dev wayland-protocols libxkbcommon-dev
|
||||||
|
|
||||||
|
# Build for all platforms
|
||||||
|
- make all
|
||||||
|
|
||||||
|
when:
|
||||||
|
event: tag
|
||||||
|
tag: v*
|
||||||
|
|
||||||
|
# Optional: Create Gitea release with built artifacts
|
||||||
|
release:
|
||||||
|
image: plugins/gitea-release
|
||||||
|
settings:
|
||||||
|
api_key:
|
||||||
|
from_secret: gitea_token
|
||||||
|
base_url: https://gitea.boner.be
|
||||||
|
files: build/*.zip
|
||||||
|
when:
|
||||||
|
event: tag
|
||||||
|
tag: v*
|
30
Dockerfile.build
Normal file
30
Dockerfile.build
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
FROM golang:1.23
|
||||||
|
|
||||||
|
# Install build dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
gcc-mingw-w64 \
|
||||||
|
cmake \
|
||||||
|
zip \
|
||||||
|
libasound2-dev \
|
||||||
|
mesa-common-dev \
|
||||||
|
libx11-dev \
|
||||||
|
libxrandr-dev \
|
||||||
|
libxi-dev \
|
||||||
|
xorg-dev \
|
||||||
|
libgl1-mesa-dev \
|
||||||
|
libglu1-mesa-dev \
|
||||||
|
libwayland-dev \
|
||||||
|
wayland-protocols \
|
||||||
|
libxkbcommon-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Copy build scripts
|
||||||
|
COPY . /build/
|
||||||
|
|
||||||
|
# Set execute permissions
|
||||||
|
RUN chmod +x /build/scripts/build.sh
|
||||||
|
|
||||||
|
# Build command
|
||||||
|
CMD ["make", "all"]
|
24
LICENSE
24
LICENSE
@ -1,11 +1,21 @@
|
|||||||
“Commons Clause” License Condition v1.0
|
MIT License
|
||||||
|
|
||||||
The Software is provided to you by the Licensor under the License, as defined below, subject to the following condition.
|
Copyright (c) 2025 bdnugget
|
||||||
|
|
||||||
Without limiting other conditions in the License, the grant of rights under the License will not include, and the License does not grant to you, right to Sell the Software.
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
For purposes of the foregoing, “Sell” means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration (including without limitation fees for hosting or consulting/ support services related to the Software), a product or service whose value derives, entirely or substantially, from the functionality of the Software. Any license notice or attribution required by the License must also include this Commons Cause License Condition notice.
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
Software: GoonScape
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
License: Commons Clause v1.0
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
Licensor: bdnugget
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
30
Makefile
Normal file
30
Makefile
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
.PHONY: all clean windows linux darwin
|
||||||
|
|
||||||
|
include scripts/platforms.mk
|
||||||
|
|
||||||
|
BINARY_NAME=goonscape
|
||||||
|
VERSION=1.1.0
|
||||||
|
BUILD_DIR=build
|
||||||
|
ASSETS_DIR=resources
|
||||||
|
|
||||||
|
all: clean $(PLATFORMS)
|
||||||
|
|
||||||
|
$(PLATFORMS):
|
||||||
|
@echo "Building for $@..."
|
||||||
|
@mkdir -p $(BUILD_DIR)/$@
|
||||||
|
@scripts/build.sh $(word 1,$(subst /, ,$@)) $(word 2,$(subst /, ,$@)) \
|
||||||
|
$(BUILD_DIR)/$@/$(BINARY_NAME)$(if $(findstring windows,$@),.exe,)
|
||||||
|
@cp -r $(ASSETS_DIR) $(BUILD_DIR)/$@/
|
||||||
|
@cd $(BUILD_DIR) && zip -r $(BINARY_NAME)-$(word 1,$(subst /, ,$@))-$(word 2,$(subst /, ,$@))-v$(VERSION).zip $@
|
||||||
|
@echo "Done building for $@"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf $(BUILD_DIR)
|
||||||
|
|
||||||
|
# Development build for current platform
|
||||||
|
dev:
|
||||||
|
go build -o $(BINARY_NAME)
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
test:
|
||||||
|
go test ./...
|
124
README.md
Normal file
124
README.md
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
# 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
|
||||||
|
- GCC (for CGO/SQLite support)
|
||||||
|
- OpenGL development libraries
|
||||||
|
- Raylib dependencies (see [raylib-go](https://github.com/gen2brain/raylib-go#requirements))
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Pre-built Binaries
|
||||||
|
The easiest way to get started is to download the latest release from:
|
||||||
|
```
|
||||||
|
https://gitea.boner.be/bdnugget/goonscape/releases
|
||||||
|
```
|
||||||
|
Choose the appropriate zip file for your platform:
|
||||||
|
- Windows: `goonscape-windows-amd64-v1.1.0.zip`
|
||||||
|
- Linux: `goonscape-linux-amd64-v1.1.0.zip`
|
||||||
|
|
||||||
|
Extract the zip and run the executable.
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
For development:
|
||||||
|
```bash
|
||||||
|
# Run directly (recommended for development)
|
||||||
|
go run main.go
|
||||||
|
|
||||||
|
# Run with local server
|
||||||
|
go run main.go -local
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server Setup
|
||||||
|
The server requires CGO for SQLite support:
|
||||||
|
```bash
|
||||||
|
# Enable CGO
|
||||||
|
go env -w CGO_ENABLED=1
|
||||||
|
|
||||||
|
# Clone and build server
|
||||||
|
git clone https://gitea.boner.be/bdnugget/goonserver.git
|
||||||
|
cd goonserver
|
||||||
|
go build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Client Installation
|
||||||
|
Then install or build:
|
||||||
|
```bash
|
||||||
|
# Install the client
|
||||||
|
go install gitea.boner.be/bdnugget/goonscape@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Or build from source:
|
||||||
|
```bash
|
||||||
|
git clone https://gitea.boner.be/bdnugget/goonscape.git
|
||||||
|
cd goonscape
|
||||||
|
go build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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`.
|
||||||
|
|
||||||
|
## Building Release Binaries
|
||||||
|
|
||||||
|
The project uses Docker to create consistent builds across platforms. To build release binaries:
|
||||||
|
|
||||||
|
1. Build the Docker image (only needed once):
|
||||||
|
```bash
|
||||||
|
sudo docker build -t goonscape-builder -f Dockerfile.build .
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create release builds:
|
||||||
|
```bash
|
||||||
|
sudo docker run -v $(pwd):/build goonscape-builder
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create zip files in the `build` directory for:
|
||||||
|
- Windows (64-bit): `goonscape-windows-amd64-v1.0.0.zip`
|
||||||
|
- Linux (64-bit): `goonscape-linux-amd64-v1.0.0.zip`
|
||||||
|
|
||||||
|
Each zip contains the binary and all required assets.
|
||||||
|
|
||||||
|
## 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
|
||||||
}
|
}
|
||||||
|
67
game/chat.go
67
game/chat.go
@ -10,11 +10,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
maxMessages = 50
|
maxMessages = 50
|
||||||
chatWindowWidth = 400
|
chatMargin = 10 // Margin from screen edges
|
||||||
chatHeight = 200
|
chatHeight = 200
|
||||||
messageHeight = 20
|
messageHeight = 20
|
||||||
inputHeight = 30
|
inputHeight = 30
|
||||||
|
runeLimit = 256
|
||||||
)
|
)
|
||||||
|
|
||||||
type Chat struct {
|
type Chat struct {
|
||||||
@ -29,7 +30,7 @@ type Chat struct {
|
|||||||
func NewChat() *Chat {
|
func NewChat() *Chat {
|
||||||
return &Chat{
|
return &Chat{
|
||||||
messages: make([]types.ChatMessage, 0, maxMessages),
|
messages: make([]types.ChatMessage, 0, maxMessages),
|
||||||
inputBuffer: make([]rune, 0, 256),
|
inputBuffer: make([]rune, 0, runeLimit),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,6 +53,7 @@ func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
|
|||||||
for _, msg := range messages {
|
for _, msg := range messages {
|
||||||
localMsg := types.ChatMessage{
|
localMsg := types.ChatMessage{
|
||||||
PlayerID: msg.PlayerId,
|
PlayerID: msg.PlayerId,
|
||||||
|
Username: msg.Username,
|
||||||
Content: msg.Content,
|
Content: msg.Content,
|
||||||
Time: time.Unix(0, msg.Timestamp),
|
Time: time.Unix(0, msg.Timestamp),
|
||||||
}
|
}
|
||||||
@ -63,6 +65,12 @@ func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
|
|||||||
}
|
}
|
||||||
c.messages = append(c.messages, localMsg)
|
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
|
// Add floating message to the player
|
||||||
if game, ok := c.userData.(*Game); ok {
|
if game, ok := c.userData.(*Game); ok {
|
||||||
if msg.PlayerId == game.Player.ID {
|
if msg.PlayerId == game.Player.ID {
|
||||||
@ -86,20 +94,38 @@ func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Chat) Draw(screenWidth, screenHeight int32) {
|
func (c *Chat) Draw(screenWidth, screenHeight int32) {
|
||||||
|
// Calculate chat window width based on screen width
|
||||||
|
chatWindowWidth := screenWidth - (chatMargin * 2)
|
||||||
|
|
||||||
// Draw chat window background
|
// Draw chat window background
|
||||||
chatX := float32(10)
|
chatX := float32(chatMargin)
|
||||||
chatY := float32(screenHeight - chatHeight - 10)
|
chatY := float32(screenHeight - chatHeight - chatMargin)
|
||||||
rl.DrawRectangle(int32(chatX), int32(chatY), chatWindowWidth, chatHeight, rl.ColorAlpha(rl.Black, 0.5))
|
rl.DrawRectangle(int32(chatX), int32(chatY), chatWindowWidth, chatHeight, rl.ColorAlpha(rl.Black, 0.5))
|
||||||
|
|
||||||
// Draw messages
|
// Draw messages from oldest to newest
|
||||||
messageY := chatY + 5
|
messageY := chatY + 5
|
||||||
startIdx := len(c.messages) - 1 - c.scrollOffset
|
visibleMessages := int((chatHeight - inputHeight) / messageHeight)
|
||||||
endIdx := max(0, startIdx-int((chatHeight-inputHeight)/messageHeight))
|
|
||||||
|
|
||||||
for i := startIdx; i >= endIdx && i >= 0; i-- {
|
// 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]
|
msg := c.messages[i]
|
||||||
text := fmt.Sprintf("[%d]: %s", msg.PlayerID, msg.Content)
|
var color rl.Color
|
||||||
rl.DrawText(text, int32(chatX)+5, int32(messageY), 20, rl.White)
|
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
|
messageY += messageHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,14 +162,14 @@ func (c *Chat) Update() (string, bool) {
|
|||||||
|
|
||||||
key := rl.GetCharPressed()
|
key := rl.GetCharPressed()
|
||||||
for key > 0 {
|
for key > 0 {
|
||||||
if len(c.inputBuffer) < 256 {
|
if len(c.inputBuffer) < runeLimit {
|
||||||
c.inputBuffer = append(c.inputBuffer[:c.cursorPos], append([]rune{key}, c.inputBuffer[c.cursorPos:]...)...)
|
c.inputBuffer = append(c.inputBuffer[:c.cursorPos], append([]rune{key}, c.inputBuffer[c.cursorPos:]...)...)
|
||||||
c.cursorPos++
|
c.cursorPos++
|
||||||
}
|
}
|
||||||
key = rl.GetCharPressed()
|
key = rl.GetCharPressed()
|
||||||
}
|
}
|
||||||
|
|
||||||
if rl.IsKeyPressed(rl.KeyEnter) {
|
if rl.IsKeyPressed(rl.KeyEnter) || rl.IsKeyPressed(rl.KeyKpEnter) {
|
||||||
if len(c.inputBuffer) > 0 {
|
if len(c.inputBuffer) > 0 {
|
||||||
message := string(c.inputBuffer)
|
message := string(c.inputBuffer)
|
||||||
c.inputBuffer = c.inputBuffer[:0]
|
c.inputBuffer = c.inputBuffer[:0]
|
||||||
@ -154,7 +180,7 @@ func (c *Chat) Update() (string, bool) {
|
|||||||
c.isTyping = false
|
c.isTyping = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if rl.IsKeyPressed(rl.KeyEscape) {
|
if rl.IsKeyPressed(rl.KeyEscape) && c.isTyping {
|
||||||
c.inputBuffer = c.inputBuffer[:0]
|
c.inputBuffer = c.inputBuffer[:0]
|
||||||
c.cursorPos = 0
|
c.cursorPos = 0
|
||||||
c.isTyping = false
|
c.isTyping = false
|
||||||
@ -183,6 +209,13 @@ func max(a, b int) int {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func min(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
func clamp(value, min, max int) int {
|
func clamp(value, min, max int) int {
|
||||||
if value < min {
|
if value < min {
|
||||||
return min
|
return min
|
||||||
|
157
game/game.go
157
game/game.go
@ -1,9 +1,11 @@
|
|||||||
package game
|
package game
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/assets"
|
"gitea.boner.be/bdnugget/goonscape/assets"
|
||||||
|
"gitea.boner.be/bdnugget/goonscape/network"
|
||||||
"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"
|
||||||
rl "github.com/gen2brain/raylib-go/raylib"
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
@ -16,18 +18,15 @@ type Game struct {
|
|||||||
Models []types.ModelAsset
|
Models []types.ModelAsset
|
||||||
Music rl.Music
|
Music rl.Music
|
||||||
Chat *Chat
|
Chat *Chat
|
||||||
|
MenuOpen bool
|
||||||
|
QuitChan chan struct{} // Channel to signal shutdown
|
||||||
|
loginScreen *LoginScreen
|
||||||
|
isLoggedIn bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() *Game {
|
func New() *Game {
|
||||||
InitWorld()
|
InitWorld()
|
||||||
game := &Game{
|
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),
|
OtherPlayers: make(map[int32]*types.Player),
|
||||||
Camera: rl.Camera3D{
|
Camera: rl.Camera3D{
|
||||||
Position: rl.NewVector3(0, 10, 10),
|
Position: rl.NewVector3(0, 10, 10),
|
||||||
@ -36,9 +35,10 @@ func New() *Game {
|
|||||||
Fovy: 45.0,
|
Fovy: 45.0,
|
||||||
Projection: rl.CameraPerspective,
|
Projection: rl.CameraPerspective,
|
||||||
},
|
},
|
||||||
Chat: NewChat(),
|
Chat: NewChat(),
|
||||||
|
QuitChan: make(chan struct{}),
|
||||||
|
loginScreen: NewLoginScreen(),
|
||||||
}
|
}
|
||||||
game.Player.UserData = game
|
|
||||||
game.Chat.userData = game
|
game.Chat.userData = game
|
||||||
return game
|
return game
|
||||||
}
|
}
|
||||||
@ -59,6 +59,46 @@ func (g *Game) LoadAssets() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) Update(deltaTime float32) {
|
func (g *Game) Update(deltaTime float32) {
|
||||||
|
if !g.isLoggedIn {
|
||||||
|
username, password, isRegistering, submitted := g.loginScreen.Update()
|
||||||
|
if submitted {
|
||||||
|
conn, playerID, err := network.ConnectToServer(username, password, isRegistering)
|
||||||
|
if err != nil {
|
||||||
|
g.loginScreen.SetError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign model based on player ID
|
||||||
|
modelIndex := int(playerID) % len(g.Models)
|
||||||
|
g.Player = &types.Player{
|
||||||
|
Speed: 50.0,
|
||||||
|
TargetPath: []types.Tile{},
|
||||||
|
UserData: g,
|
||||||
|
QuitDone: make(chan struct{}),
|
||||||
|
ID: playerID,
|
||||||
|
Model: g.Models[modelIndex].Model,
|
||||||
|
Texture: g.Models[modelIndex].Texture,
|
||||||
|
}
|
||||||
|
|
||||||
|
go network.HandleServerCommunication(conn, playerID, g.Player, g.OtherPlayers, g.QuitChan)
|
||||||
|
g.isLoggedIn = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
g.loginScreen.Draw()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
if message, sent := g.Chat.Update(); sent {
|
||||||
g.Player.Lock()
|
g.Player.Lock()
|
||||||
g.Player.ActionQueue = append(g.Player.ActionQueue, &pb.Action{
|
g.Player.ActionQueue = append(g.Player.ActionQueue, &pb.Action{
|
||||||
@ -125,7 +165,7 @@ 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) {
|
if player.FloatingMessage != nil {
|
||||||
screenPos := rl.GetWorldToScreen(rl.Vector3{
|
screenPos := rl.GetWorldToScreen(rl.Vector3{
|
||||||
X: playerPos.X,
|
X: playerPos.X,
|
||||||
Y: playerPos.Y + 24.0,
|
Y: playerPos.Y + 24.0,
|
||||||
@ -133,8 +173,6 @@ func (g *Game) DrawPlayer(player *types.Player, model rl.Model) {
|
|||||||
}, g.Camera)
|
}, g.Camera)
|
||||||
|
|
||||||
player.FloatingMessage.ScreenPos = screenPos
|
player.FloatingMessage.ScreenPos = screenPos
|
||||||
} else if player.FloatingMessage != nil {
|
|
||||||
player.FloatingMessage = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(player.TargetPath) > 0 {
|
if len(player.TargetPath) > 0 {
|
||||||
@ -160,14 +198,27 @@ func (g *Game) Render() {
|
|||||||
rl.BeginDrawing()
|
rl.BeginDrawing()
|
||||||
rl.ClearBackground(rl.RayWhite)
|
rl.ClearBackground(rl.RayWhite)
|
||||||
|
|
||||||
|
if !g.isLoggedIn {
|
||||||
|
g.loginScreen.Draw()
|
||||||
|
rl.EndDrawing()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
if other.Model.Meshes == nil {
|
||||||
|
// Assign model based on player ID for consistency
|
||||||
|
modelIndex := int(id) % len(g.Models)
|
||||||
|
other.Model = g.Models[modelIndex].Model
|
||||||
|
other.Texture = g.Models[modelIndex].Texture
|
||||||
|
}
|
||||||
|
g.DrawPlayer(other, other.Model)
|
||||||
}
|
}
|
||||||
rl.EndMode3D()
|
rl.EndMode3D()
|
||||||
|
|
||||||
|
// Draw floating messages
|
||||||
drawFloatingMessage := func(msg *types.FloatingMessage) {
|
drawFloatingMessage := func(msg *types.FloatingMessage) {
|
||||||
if msg == nil || time.Now().After(msg.ExpireTime) {
|
if msg == nil || time.Now().After(msg.ExpireTime) {
|
||||||
return
|
return
|
||||||
@ -196,8 +247,17 @@ func (g *Game) Render() {
|
|||||||
drawFloatingMessage(other.FloatingMessage)
|
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)
|
||||||
g.Chat.Draw(int32(rl.GetScreenWidth()), int32(rl.GetScreenHeight()))
|
|
||||||
rl.EndDrawing()
|
rl.EndDrawing()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,3 +283,72 @@ 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":
|
||||||
|
g.Shutdown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) Shutdown() {
|
||||||
|
close(g.QuitChan)
|
||||||
|
<-g.Player.QuitDone
|
||||||
|
rl.CloseWindow()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) HandleServerMessages(messages []*pb.ChatMessage) {
|
||||||
|
g.Chat.HandleServerMessages(messages)
|
||||||
|
}
|
||||||
|
185
game/login.go
Normal file
185
game/login.go
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LoginScreen struct {
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
errorMessage string
|
||||||
|
isRegistering bool
|
||||||
|
focusedField int // 0 = username, 1 = password
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLoginScreen() *LoginScreen {
|
||||||
|
return &LoginScreen{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LoginScreen) Draw() {
|
||||||
|
screenWidth := float32(rl.GetScreenWidth())
|
||||||
|
screenHeight := float32(rl.GetScreenHeight())
|
||||||
|
|
||||||
|
// Draw background
|
||||||
|
rl.DrawRectangle(0, 0, int32(screenWidth), int32(screenHeight), rl.RayWhite)
|
||||||
|
|
||||||
|
// Draw title
|
||||||
|
title := "GoonScape"
|
||||||
|
if l.isRegistering {
|
||||||
|
title += " - Register"
|
||||||
|
} else {
|
||||||
|
title += " - Login"
|
||||||
|
}
|
||||||
|
titleSize := int32(40)
|
||||||
|
titleWidth := rl.MeasureText(title, titleSize)
|
||||||
|
rl.DrawText(title, int32(screenWidth/2)-titleWidth/2, 100, titleSize, rl.Black)
|
||||||
|
|
||||||
|
// Draw input fields
|
||||||
|
inputWidth := float32(200)
|
||||||
|
inputHeight := float32(30)
|
||||||
|
inputX := screenWidth/2 - inputWidth/2
|
||||||
|
|
||||||
|
// Username field
|
||||||
|
rl.DrawRectangleRec(rl.Rectangle{
|
||||||
|
X: inputX, Y: 200,
|
||||||
|
Width: inputWidth, Height: inputHeight,
|
||||||
|
}, rl.LightGray)
|
||||||
|
rl.DrawText(l.username, int32(inputX)+5, 205, 20, rl.Black)
|
||||||
|
if l.focusedField == 0 {
|
||||||
|
rl.DrawRectangleLinesEx(rl.Rectangle{
|
||||||
|
X: inputX - 2, Y: 198,
|
||||||
|
Width: inputWidth + 4, Height: inputHeight + 4,
|
||||||
|
}, 2, rl.Blue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password field
|
||||||
|
rl.DrawRectangleRec(rl.Rectangle{
|
||||||
|
X: inputX, Y: 250,
|
||||||
|
Width: inputWidth, Height: inputHeight,
|
||||||
|
}, rl.LightGray)
|
||||||
|
masked := ""
|
||||||
|
for range l.password {
|
||||||
|
masked += "*"
|
||||||
|
}
|
||||||
|
rl.DrawText(masked, int32(inputX)+5, 255, 20, rl.Black)
|
||||||
|
if l.focusedField == 1 {
|
||||||
|
rl.DrawRectangleLinesEx(rl.Rectangle{
|
||||||
|
X: inputX - 2, Y: 248,
|
||||||
|
Width: inputWidth + 4, Height: inputHeight + 4,
|
||||||
|
}, 2, rl.Blue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw error message
|
||||||
|
if l.errorMessage != "" {
|
||||||
|
msgWidth := rl.MeasureText(l.errorMessage, 20)
|
||||||
|
rl.DrawText(l.errorMessage, int32(screenWidth/2)-msgWidth/2, 300, 20, rl.Red)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw buttons
|
||||||
|
buttonWidth := float32(100)
|
||||||
|
buttonHeight := float32(30)
|
||||||
|
buttonY := float32(350)
|
||||||
|
|
||||||
|
// Login/Register button
|
||||||
|
actionBtn := rl.Rectangle{
|
||||||
|
X: screenWidth/2 - buttonWidth - 10,
|
||||||
|
Y: buttonY,
|
||||||
|
Width: buttonWidth,
|
||||||
|
Height: buttonHeight,
|
||||||
|
}
|
||||||
|
rl.DrawRectangleRec(actionBtn, rl.Blue)
|
||||||
|
actionText := "Login"
|
||||||
|
if l.isRegistering {
|
||||||
|
actionText = "Register"
|
||||||
|
}
|
||||||
|
actionWidth := rl.MeasureText(actionText, 20)
|
||||||
|
rl.DrawText(actionText,
|
||||||
|
int32(actionBtn.X+actionBtn.Width/2)-actionWidth/2,
|
||||||
|
int32(actionBtn.Y+5),
|
||||||
|
20, rl.White)
|
||||||
|
|
||||||
|
// Switch mode button
|
||||||
|
switchBtn := rl.Rectangle{
|
||||||
|
X: screenWidth/2 + 10,
|
||||||
|
Y: buttonY,
|
||||||
|
Width: buttonWidth,
|
||||||
|
Height: buttonHeight,
|
||||||
|
}
|
||||||
|
rl.DrawRectangleRec(switchBtn, rl.DarkGray)
|
||||||
|
switchText := "Register"
|
||||||
|
if l.isRegistering {
|
||||||
|
switchText = "Login"
|
||||||
|
}
|
||||||
|
switchWidth := rl.MeasureText(switchText, 20)
|
||||||
|
rl.DrawText(switchText,
|
||||||
|
int32(switchBtn.X+switchBtn.Width/2)-switchWidth/2,
|
||||||
|
int32(switchBtn.Y+5),
|
||||||
|
20, rl.White)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LoginScreen) Update() (string, string, bool, bool) {
|
||||||
|
// Handle input field focus
|
||||||
|
if rl.IsMouseButtonPressed(rl.MouseLeftButton) {
|
||||||
|
mousePos := rl.GetMousePosition()
|
||||||
|
screenWidth := float32(rl.GetScreenWidth())
|
||||||
|
inputWidth := float32(200)
|
||||||
|
inputX := screenWidth/2 - inputWidth/2
|
||||||
|
|
||||||
|
// Check username field
|
||||||
|
if mousePos.X >= inputX && mousePos.X <= inputX+inputWidth &&
|
||||||
|
mousePos.Y >= 200 && mousePos.Y <= 230 {
|
||||||
|
l.focusedField = 0
|
||||||
|
}
|
||||||
|
// Check password field
|
||||||
|
if mousePos.X >= inputX && mousePos.X <= inputX+inputWidth &&
|
||||||
|
mousePos.Y >= 250 && mousePos.Y <= 280 {
|
||||||
|
l.focusedField = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check buttons
|
||||||
|
buttonWidth := float32(100)
|
||||||
|
if mousePos.Y >= 350 && mousePos.Y <= 380 {
|
||||||
|
// Action button
|
||||||
|
if mousePos.X >= screenWidth/2-buttonWidth-10 &&
|
||||||
|
mousePos.X <= screenWidth/2-10 {
|
||||||
|
return l.username, l.password, l.isRegistering, true
|
||||||
|
}
|
||||||
|
// Switch mode button
|
||||||
|
if mousePos.X >= screenWidth/2+10 &&
|
||||||
|
mousePos.X <= screenWidth/2+buttonWidth+10 {
|
||||||
|
l.isRegistering = !l.isRegistering
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle text input
|
||||||
|
key := rl.GetCharPressed()
|
||||||
|
for key > 0 {
|
||||||
|
if l.focusedField == 0 && len(l.username) < 12 {
|
||||||
|
l.username += string(key)
|
||||||
|
} else if l.focusedField == 1 && len(l.password) < 20 {
|
||||||
|
l.password += string(key)
|
||||||
|
}
|
||||||
|
key = rl.GetCharPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle backspace
|
||||||
|
if rl.IsKeyPressed(rl.KeyBackspace) {
|
||||||
|
if l.focusedField == 0 && len(l.username) > 0 {
|
||||||
|
l.username = l.username[:len(l.username)-1]
|
||||||
|
} else if l.focusedField == 1 && len(l.password) > 0 {
|
||||||
|
l.password = l.password[:len(l.password)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle tab to switch fields
|
||||||
|
if rl.IsKeyPressed(rl.KeyTab) {
|
||||||
|
l.focusedField = (l.focusedField + 1) % 2
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", "", false, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LoginScreen) SetError(msg string) {
|
||||||
|
l.errorMessage = msg
|
||||||
|
}
|
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 v1.1.0
|
||||||
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: 368fbdbc47...f9ec811b10
45
main.go
45
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,39 +11,50 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// Parse command line flags
|
||||||
|
local := flag.Bool("local", false, "Connect to local server")
|
||||||
|
addr := flag.String("addr", "", "Server address (host or host:port)")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// Set server address based on flags
|
||||||
|
if *local {
|
||||||
|
if *addr != "" {
|
||||||
|
log.Fatal("Cannot use -local and -addr together")
|
||||||
|
}
|
||||||
|
network.SetServerAddr("localhost:6969")
|
||||||
|
} else if *addr != "" {
|
||||||
|
// If port is not specified, append default port
|
||||||
|
if !strings.Contains(*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()
|
||||||
|
|
||||||
|
rl.SetTargetFPS(60)
|
||||||
|
|
||||||
game := game.New()
|
game := game.New()
|
||||||
if err := game.LoadAssets(); err != nil {
|
if err := game.LoadAssets(); err != nil {
|
||||||
log.Fatalf("Failed to load assets: %v", err)
|
log.Fatalf("Failed to load assets: %v", err)
|
||||||
}
|
}
|
||||||
defer game.Cleanup()
|
defer game.Cleanup()
|
||||||
|
|
||||||
conn, playerID, err := network.ConnectToServer()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to connect to server: %v", err)
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
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 network.HandleServerCommunication(conn, playerID, game.Player, game.OtherPlayers)
|
|
||||||
|
|
||||||
rl.PlayMusicStream(game.Music)
|
rl.PlayMusicStream(game.Music)
|
||||||
rl.SetMusicVolume(game.Music, 0.5)
|
rl.SetMusicVolume(game.Music, 0.5)
|
||||||
rl.SetTargetFPS(60)
|
|
||||||
|
|
||||||
for !rl.WindowShouldClose() {
|
for !rl.WindowShouldClose() {
|
||||||
rl.UpdateMusicStream(game.Music)
|
|
||||||
deltaTime := rl.GetFrameTime()
|
deltaTime := rl.GetFrameTime()
|
||||||
|
rl.UpdateMusicStream(game.Music)
|
||||||
game.Update(deltaTime)
|
game.Update(deltaTime)
|
||||||
game.Render()
|
game.Render()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait for clean shutdown
|
||||||
|
<-game.QuitChan
|
||||||
}
|
}
|
||||||
|
@ -1,121 +1,254 @@
|
|||||||
package network
|
package network
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"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"
|
||||||
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ConnectToServer() (net.Conn, int32, error) {
|
const protoVersion = 1
|
||||||
conn, err := net.Dial("tcp", types.ServerAddr)
|
|
||||||
|
var serverAddr = "boner.be:6969"
|
||||||
|
|
||||||
|
func SetServerAddr(addr string) {
|
||||||
|
serverAddr = addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConnectToServer(username, password string, isRegistering bool) (net.Conn, int32, error) {
|
||||||
|
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. Authenticating...")
|
||||||
buf := make([]byte, 1024)
|
|
||||||
n, err := conn.Read(buf)
|
// Send auth message
|
||||||
if err != nil {
|
authAction := &pb.Action{
|
||||||
log.Printf("Error reading player ID from server: %v", err)
|
Type: pb.Action_LOGIN,
|
||||||
return nil, 0, err
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
}
|
||||||
|
if isRegistering {
|
||||||
|
authAction.Type = pb.Action_REGISTER
|
||||||
|
}
|
||||||
|
|
||||||
|
authBatch := &pb.ActionBatch{
|
||||||
|
Actions: []*pb.Action{authAction},
|
||||||
|
ProtocolVersion: protoVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeMessage(conn, authBatch); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, 0, fmt.Errorf("failed to send auth: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read server response
|
||||||
|
reader := bufio.NewReader(conn)
|
||||||
|
lengthBuf := make([]byte, 4)
|
||||||
|
if _, err := io.ReadFull(reader, lengthBuf); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, 0, fmt.Errorf("failed to read auth response: %v", err)
|
||||||
|
}
|
||||||
|
messageLength := binary.BigEndian.Uint32(lengthBuf)
|
||||||
|
|
||||||
|
messageBuf := make([]byte, messageLength)
|
||||||
|
if _, err := io.ReadFull(reader, messageBuf); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, 0, fmt.Errorf("failed to read auth response body: %v", 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)
|
conn.Close()
|
||||||
return nil, 0, err
|
return nil, 0, fmt.Errorf("failed to unmarshal auth response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.ProtocolVersion > protoVersion {
|
||||||
|
conn.Close()
|
||||||
|
return nil, 0, fmt.Errorf("server requires newer protocol version (server: %d, client: %d)",
|
||||||
|
response.ProtocolVersion, protoVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !response.AuthSuccess {
|
||||||
|
conn.Close()
|
||||||
|
return nil, 0, fmt.Errorf(response.ErrorMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
playerID := response.GetPlayerId()
|
playerID := response.GetPlayerId()
|
||||||
log.Printf("Successfully connected with player ID: %d", playerID)
|
log.Printf("Successfully authenticated with player ID: %d", playerID)
|
||||||
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{})
|
||||||
|
|
||||||
|
// Create a set of current players to track disconnects
|
||||||
|
currentPlayers := make(map[int32]bool)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for range actionTicker.C {
|
for {
|
||||||
player.Lock()
|
select {
|
||||||
if len(player.ActionQueue) > 0 {
|
case <-quitChan:
|
||||||
actions := make([]*pb.Action, len(player.ActionQueue))
|
// Send disconnect message to server
|
||||||
copy(actions, player.ActionQueue)
|
disconnectMsg := &pb.ActionBatch{
|
||||||
|
|
||||||
batch := &pb.ActionBatch{
|
|
||||||
PlayerId: playerID,
|
PlayerId: playerID,
|
||||||
Actions: actions,
|
Actions: []*pb.Action{{
|
||||||
Tick: player.CurrentTick,
|
Type: pb.Action_DISCONNECT,
|
||||||
|
PlayerId: playerID,
|
||||||
|
}},
|
||||||
}
|
}
|
||||||
|
writeMessage(conn, disconnectMsg)
|
||||||
|
done <- struct{}{}
|
||||||
|
return
|
||||||
|
case <-actionTicker.C:
|
||||||
|
player.Lock()
|
||||||
|
if len(player.ActionQueue) > 0 {
|
||||||
|
actions := make([]*pb.Action, len(player.ActionQueue))
|
||||||
|
copy(actions, player.ActionQueue)
|
||||||
|
|
||||||
player.ActionQueue = player.ActionQueue[:0]
|
batch := &pb.ActionBatch{
|
||||||
player.Unlock()
|
PlayerId: playerID,
|
||||||
|
Actions: actions,
|
||||||
|
Tick: player.CurrentTick,
|
||||||
|
}
|
||||||
|
|
||||||
data, err := proto.Marshal(batch)
|
player.ActionQueue = player.ActionQueue[:0]
|
||||||
if err != nil {
|
player.Unlock()
|
||||||
log.Printf("Failed to marshal action batch: %v", err)
|
|
||||||
continue
|
if err := writeMessage(conn, batch); err != nil {
|
||||||
|
log.Printf("Failed to send actions to server: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
player.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = conn.Write(data); err != nil {
|
|
||||||
log.Printf("Failed to send actions to server: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
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 := make(chan struct{})
|
||||||
return
|
go func() {
|
||||||
}
|
<-done
|
||||||
|
close(player.QuitDone)
|
||||||
|
}()
|
||||||
|
|
||||||
var serverMessage pb.ServerMessage
|
select {
|
||||||
if err := proto.Unmarshal(buf[:n], &serverMessage); err != nil {
|
case <-done:
|
||||||
log.Printf("Failed to unmarshal server message: %v", err)
|
time.Sleep(100 * time.Millisecond)
|
||||||
continue
|
case <-time.After(1 * time.Second):
|
||||||
}
|
log.Println("Shutdown timed out")
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
return
|
||||||
player.Unlock()
|
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)
|
||||||
|
|
||||||
for _, state := range serverMessage.Players {
|
// Read the full message
|
||||||
if state.PlayerId == playerID {
|
messageBuf := make([]byte, messageLength)
|
||||||
|
if _, err := io.ReadFull(reader, messageBuf); err != nil {
|
||||||
|
log.Printf("Failed to read message body: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverMessage pb.ServerMessage
|
||||||
|
if err := proto.Unmarshal(messageBuf, &serverMessage); err != nil {
|
||||||
|
log.Printf("Failed to unmarshal server message: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if otherPlayer, exists := otherPlayers[state.PlayerId]; exists {
|
player.Lock()
|
||||||
otherPlayer.UpdatePosition(state, types.ServerTickRate)
|
player.CurrentTick = serverMessage.CurrentTick
|
||||||
} else {
|
|
||||||
otherPlayers[state.PlayerId] = types.NewPlayer(state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if g, ok := player.UserData.(*game.Game); ok && len(serverMessage.ChatMessages) > 0 {
|
tickDiff := serverMessage.CurrentTick - player.CurrentTick
|
||||||
g.Chat.HandleServerMessages(serverMessage.ChatMessages)
|
if tickDiff > types.MaxTickDesync {
|
||||||
|
for _, state := range serverMessage.Players {
|
||||||
|
if state.PlayerId == playerID {
|
||||||
|
player.ForceResync(state)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
player.Unlock()
|
||||||
|
|
||||||
|
for _, state := range serverMessage.Players {
|
||||||
|
currentPlayers[state.PlayerId] = true
|
||||||
|
if state.PlayerId == playerID {
|
||||||
|
player.Lock()
|
||||||
|
// Update initial position if not set
|
||||||
|
if player.PosActual.X == 0 && player.PosActual.Z == 0 {
|
||||||
|
player.PosActual = rl.Vector3{
|
||||||
|
X: float32(state.X * types.TileSize),
|
||||||
|
Y: 0,
|
||||||
|
Z: float32(state.Y * types.TileSize),
|
||||||
|
}
|
||||||
|
player.PosTile = types.Tile{X: int(state.X), Y: int(state.Y)}
|
||||||
|
}
|
||||||
|
player.Unlock()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if otherPlayer, exists := otherPlayers[state.PlayerId]; exists {
|
||||||
|
otherPlayer.UpdatePosition(state, types.ServerTickRate)
|
||||||
|
} else {
|
||||||
|
otherPlayers[state.PlayerId] = types.NewPlayer(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove players that are no longer in the server state
|
||||||
|
for id := range otherPlayers {
|
||||||
|
if !currentPlayers[id] {
|
||||||
|
delete(otherPlayers, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if handler, ok := player.UserData.(types.ChatMessageHandler); ok && len(serverMessage.ChatMessages) > 0 {
|
||||||
|
handler.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
scripts/build.sh
Executable file
27
scripts/build.sh
Executable file
@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Main build process
|
||||||
|
build() {
|
||||||
|
local os=$1
|
||||||
|
local arch=$2
|
||||||
|
local output=$3
|
||||||
|
|
||||||
|
# Set GOOS and GOARCH for cross-compilation
|
||||||
|
export GOOS=$os
|
||||||
|
export GOARCH=$arch
|
||||||
|
|
||||||
|
# Disable CGO only for cross-compilation
|
||||||
|
if [ "$os" != "$(go env GOOS)" ] || [ "$arch" != "$(go env GOARCH)" ]; then
|
||||||
|
export CGO_ENABLED=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$os" = "windows" ]; then
|
||||||
|
export CC=x86_64-w64-mingw32-gcc
|
||||||
|
export CXX=x86_64-w64-mingw32-g++
|
||||||
|
fi
|
||||||
|
|
||||||
|
go build -buildvcs=false -ldflags="-s -w" -o $output
|
||||||
|
}
|
||||||
|
|
||||||
|
# Call build with provided arguments
|
||||||
|
build "$1" "$2" "$3"
|
1
scripts/platforms.mk
Normal file
1
scripts/platforms.mk
Normal file
@ -0,0 +1 @@
|
|||||||
|
PLATFORMS=windows/amd64 linux/amd64
|
@ -27,8 +27,9 @@ type Player struct {
|
|||||||
CurrentTick int64
|
CurrentTick int64
|
||||||
LastUpdateTime time.Time
|
LastUpdateTime time.Time
|
||||||
InterpolationProgress float32
|
InterpolationProgress float32
|
||||||
UserData interface{} // Used to store reference to game
|
UserData interface{}
|
||||||
FloatingMessage *FloatingMessage
|
FloatingMessage *FloatingMessage
|
||||||
|
QuitDone chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModelAsset struct {
|
type ModelAsset struct {
|
||||||
@ -38,6 +39,7 @@ type ModelAsset struct {
|
|||||||
|
|
||||||
type ChatMessage struct {
|
type ChatMessage struct {
|
||||||
PlayerID int32
|
PlayerID int32
|
||||||
|
Username string
|
||||||
Content string
|
Content string
|
||||||
Time time.Time
|
Time time.Time
|
||||||
}
|
}
|
||||||
@ -48,6 +50,10 @@ type FloatingMessage struct {
|
|||||||
ScreenPos rl.Vector2 // Store the screen position for 2D rendering
|
ScreenPos rl.Vector2 // Store the screen position for 2D rendering
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ChatMessageHandler interface {
|
||||||
|
HandleServerMessages([]*pb.ChatMessage)
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MapWidth = 50
|
MapWidth = 50
|
||||||
MapHeight = 50
|
MapHeight = 50
|
||||||
@ -58,6 +64,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"
|
|
||||||
ServerAddr = "boner.be:6969"
|
|
||||||
)
|
)
|
||||||
|
Reference in New Issue
Block a user