Compare commits
17 Commits
feature/me
...
v1.1.1
Author | SHA1 | Date | |
---|---|---|---|
944c33ce3b | |||
d5bb464d9f | |||
4549ee7517 | |||
31ae9c525f | |||
06913a5217 | |||
49663c9094 | |||
a843680b09 | |||
7183df4a8b | |||
33e355200d | |||
e45066b2a8 | |||
bb01dccf2b | |||
0f56916295 | |||
a1ddbadea0 | |||
e4d0b98945 | |||
509bc8b20b | |||
c40e4ae7ac | |||
cd68581429 |
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 ./...
|
75
README.md
75
README.md
@ -16,24 +16,57 @@ A multiplayer isometric game inspired by Oldschool RuneScape, built with Go and
|
|||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- Go 1.23 or higher
|
- 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))
|
- Raylib dependencies (see [raylib-go](https://github.com/gen2brain/raylib-go#requirements))
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Clone the repository:
|
### 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
|
```bash
|
||||||
git clone https://gitea.boner.be/bdnugget/goonscape.git
|
git clone https://gitea.boner.be/bdnugget/goonscape.git
|
||||||
cd goonscape
|
cd goonscape
|
||||||
```
|
go build
|
||||||
|
|
||||||
2. Install dependencies:
|
|
||||||
```bash
|
|
||||||
go mod tidy
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Build and run:
|
|
||||||
```bash
|
|
||||||
go run main.go
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Controls
|
## Controls
|
||||||
@ -63,6 +96,26 @@ 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`.
|
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
|
## Development
|
||||||
|
|
||||||
The project uses Protocol Buffers for network communication. If you modify the `.proto` files, regenerate the Go code with:
|
The project uses Protocol Buffers for network communication. If you modify the `.proto` files, regenerate the Go code with:
|
||||||
|
11
game/chat.go
11
game/chat.go
@ -53,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),
|
||||||
}
|
}
|
||||||
@ -117,8 +118,14 @@ func (c *Chat) Draw(screenWidth, screenHeight int32) {
|
|||||||
|
|
||||||
for i := startIdx; i < endIdx; i++ {
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
82
game/game.go
82
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"
|
||||||
@ -18,20 +20,13 @@ type Game struct {
|
|||||||
Chat *Chat
|
Chat *Chat
|
||||||
MenuOpen bool
|
MenuOpen bool
|
||||||
QuitChan chan struct{} // Channel to signal shutdown
|
QuitChan chan struct{} // Channel to signal shutdown
|
||||||
QuitDone chan struct{} // New channel to signal when cleanup is complete
|
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,
|
|
||||||
QuitDone: make(chan struct{}),
|
|
||||||
},
|
|
||||||
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),
|
||||||
@ -40,11 +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{}),
|
QuitChan: make(chan struct{}),
|
||||||
QuitDone: make(chan struct{}),
|
loginScreen: NewLoginScreen(),
|
||||||
}
|
}
|
||||||
game.Player.UserData = game
|
|
||||||
game.Chat.userData = game
|
game.Chat.userData = game
|
||||||
return game
|
return game
|
||||||
}
|
}
|
||||||
@ -65,6 +59,35 @@ 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
|
// Handle ESC for menu
|
||||||
if rl.IsKeyPressed(rl.KeyEscape) {
|
if rl.IsKeyPressed(rl.KeyEscape) {
|
||||||
g.MenuOpen = !g.MenuOpen
|
g.MenuOpen = !g.MenuOpen
|
||||||
@ -142,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,
|
||||||
@ -150,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 {
|
||||||
@ -177,11 +198,23 @@ 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()
|
||||||
|
|
||||||
@ -293,9 +326,7 @@ func (g *Game) DrawMenu() {
|
|||||||
case "Settings":
|
case "Settings":
|
||||||
// TODO: Implement settings
|
// TODO: Implement settings
|
||||||
case "Exit Game":
|
case "Exit Game":
|
||||||
close(g.QuitChan)
|
g.Shutdown()
|
||||||
<-g.QuitDone
|
|
||||||
rl.CloseWindow()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -310,3 +341,14 @@ func (g *Game) DrawMenu() {
|
|||||||
buttonY += buttonSpacing
|
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
|
||||||
|
}
|
2
go.mod
2
go.mod
@ -3,7 +3,7 @@ module gitea.boner.be/bdnugget/goonscape
|
|||||||
go 1.23.0
|
go 1.23.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
gitea.boner.be/bdnugget/goonserver v0.0.0-20250113131525-49e23114973c
|
gitea.boner.be/bdnugget/goonserver v1.1.0
|
||||||
github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b
|
github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b
|
||||||
google.golang.org/protobuf v1.36.3
|
google.golang.org/protobuf v1.36.3
|
||||||
)
|
)
|
||||||
|
Submodule goonserver updated: be32dec202...f9ec811b10
37
main.go
37
main.go
@ -11,20 +11,21 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
local := flag.Bool("local", false, "Use local server instead of remote")
|
// Parse command line flags
|
||||||
addr := flag.String("addr", "boner.be:6969", "Server address (hostname:port or hostname)")
|
local := flag.Bool("local", false, "Connect to local server")
|
||||||
|
addr := flag.String("addr", "", "Server address (host or host:port)")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *local && *addr != "boner.be:6969" {
|
// Set server address based on flags
|
||||||
log.Fatal("Cannot use both -local and -addr flags")
|
|
||||||
}
|
|
||||||
|
|
||||||
if *local {
|
if *local {
|
||||||
|
if *addr != "" {
|
||||||
|
log.Fatal("Cannot use -local and -addr together")
|
||||||
|
}
|
||||||
network.SetServerAddr("localhost:6969")
|
network.SetServerAddr("localhost:6969")
|
||||||
} else if *addr != "" {
|
} else if *addr != "" {
|
||||||
// If only hostname is provided, append default port
|
// If port is not specified, append default port
|
||||||
if !strings.Contains(*addr, ":") {
|
if !strings.Contains(*addr, ":") {
|
||||||
*addr = *addr + ":6969"
|
*addr += ":6969"
|
||||||
}
|
}
|
||||||
network.SetServerAddr(*addr)
|
network.SetServerAddr(*addr)
|
||||||
}
|
}
|
||||||
@ -32,36 +33,24 @@ func main() {
|
|||||||
rl.InitWindow(1024, 768, "GoonScape")
|
rl.InitWindow(1024, 768, "GoonScape")
|
||||||
rl.SetExitKey(0)
|
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, game.QuitChan)
|
|
||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
@ -3,56 +3,89 @@ package network
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
"io"
|
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const protoVersion = 1
|
||||||
|
|
||||||
var serverAddr = "boner.be:6969"
|
var serverAddr = "boner.be:6969"
|
||||||
|
|
||||||
func SetServerAddr(addr string) {
|
func SetServerAddr(addr string) {
|
||||||
serverAddr = addr
|
serverAddr = addr
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConnectToServer() (net.Conn, int32, error) {
|
func ConnectToServer(username, password string, isRegistering bool) (net.Conn, int32, error) {
|
||||||
conn, err := net.Dial("tcp", serverAddr)
|
conn, err := net.Dial("tcp", serverAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to dial server: %v", err)
|
log.Printf("Failed to dial server: %v", err)
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Connected to server. Waiting for player ID...")
|
log.Println("Connected to server. Authenticating...")
|
||||||
reader := bufio.NewReader(conn)
|
|
||||||
|
|
||||||
// Read message length (4 bytes)
|
// Send auth message
|
||||||
|
authAction := &pb.Action{
|
||||||
|
Type: pb.Action_LOGIN,
|
||||||
|
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)
|
lengthBuf := make([]byte, 4)
|
||||||
if _, err := io.ReadFull(reader, lengthBuf); err != nil {
|
if _, err := io.ReadFull(reader, lengthBuf); err != nil {
|
||||||
log.Printf("Failed to read message length: %v", err)
|
conn.Close()
|
||||||
return nil, 0, err
|
return nil, 0, fmt.Errorf("failed to read auth response: %v", err)
|
||||||
}
|
}
|
||||||
messageLength := binary.BigEndian.Uint32(lengthBuf)
|
messageLength := binary.BigEndian.Uint32(lengthBuf)
|
||||||
|
|
||||||
// Read the full message
|
|
||||||
messageBuf := make([]byte, messageLength)
|
messageBuf := make([]byte, messageLength)
|
||||||
if _, err := io.ReadFull(reader, messageBuf); err != nil {
|
if _, err := io.ReadFull(reader, messageBuf); err != nil {
|
||||||
log.Printf("Failed to read message body: %v", err)
|
conn.Close()
|
||||||
return nil, 0, err
|
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(messageBuf, &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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,6 +100,9 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
|
|||||||
// Create a channel to signal when goroutines are done
|
// Create a channel to signal when goroutines are done
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
|
|
||||||
|
// Create a set of current players to track disconnects
|
||||||
|
currentPlayers := make(map[int32]bool)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@ -111,9 +147,18 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-quitChan:
|
case <-quitChan:
|
||||||
<-done // Wait for action goroutine to finish
|
done := make(chan struct{})
|
||||||
close(done)
|
go func() {
|
||||||
time.Sleep(100 * time.Millisecond) // Give time for disconnect message to be sent
|
<-done
|
||||||
|
close(player.QuitDone)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
case <-time.After(1 * time.Second):
|
||||||
|
log.Println("Shutdown timed out")
|
||||||
|
}
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
// Read message length (4 bytes)
|
// Read message length (4 bytes)
|
||||||
@ -152,7 +197,19 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
|
|||||||
player.Unlock()
|
player.Unlock()
|
||||||
|
|
||||||
for _, state := range serverMessage.Players {
|
for _, state := range serverMessage.Players {
|
||||||
|
currentPlayers[state.PlayerId] = true
|
||||||
if state.PlayerId == playerID {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,8 +220,15 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if g, ok := player.UserData.(*game.Game); ok && len(serverMessage.ChatMessages) > 0 {
|
// Remove players that are no longer in the server state
|
||||||
g.Chat.HandleServerMessages(serverMessage.ChatMessages)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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
|
@ -39,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
|
||||||
}
|
}
|
||||||
@ -49,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
|
||||||
|
Reference in New Issue
Block a user