Compare commits
9 Commits
feature/me
...
feature/db
Author | SHA1 | Date | |
---|---|---|---|
33e355200d | |||
e45066b2a8 | |||
bb01dccf2b | |||
0f56916295 | |||
a1ddbadea0 | |||
e4d0b98945 | |||
509bc8b20b | |||
c40e4ae7ac | |||
cd68581429 |
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"]
|
30
Makefile
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
.PHONY: all clean windows linux darwin
|
||||||
|
|
||||||
|
include scripts/platforms.mk
|
||||||
|
|
||||||
|
BINARY_NAME=goonscape
|
||||||
|
VERSION=1.0.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 ./...
|
20
README.md
@ -63,6 +63,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:
|
||||||
|
BIN
build/goonscape-linux-amd64-v1.0.0.zip
Normal file
BIN
build/goonscape-windows-amd64-v1.0.0.zip
Normal file
BIN
build/linux/amd64/goonscape
Executable file
BIN
build/linux/amd64/resources/audio/GoonScape1.mp3
Normal file
BIN
build/linux/amd64/resources/audio/GoonScape2.mp3
Normal file
12
build/linux/amd64/resources/models/coomer.mtl
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Blender 3.6.0 MTL File: 'None'
|
||||||
|
# www.blender.org
|
||||||
|
|
||||||
|
newmtl Material.001
|
||||||
|
Ns 250.000000
|
||||||
|
Ka 1.000000 1.000000 1.000000
|
||||||
|
Ks 0.500000 0.500000 0.500000
|
||||||
|
Ke 0.000000 0.000000 0.000000
|
||||||
|
Ni 1.450000
|
||||||
|
d 1.000000
|
||||||
|
illum 2
|
||||||
|
map_Kd coomer.png
|
117248
build/linux/amd64/resources/models/coomer.obj
Normal file
BIN
build/linux/amd64/resources/models/coomer.png
Normal file
After Width: | Height: | Size: 2.2 MiB |
12
build/linux/amd64/resources/models/goonion.mtl
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Blender 3.6.0 MTL File: 'None'
|
||||||
|
# www.blender.org
|
||||||
|
|
||||||
|
newmtl Material.001
|
||||||
|
Ns 250.000000
|
||||||
|
Ka 1.000000 1.000000 1.000000
|
||||||
|
Ks 0.500000 0.500000 0.500000
|
||||||
|
Ke 0.000000 0.000000 0.000000
|
||||||
|
Ni 1.450000
|
||||||
|
d 1.000000
|
||||||
|
illum 2
|
||||||
|
map_Kd goonion.png
|
112531
build/linux/amd64/resources/models/goonion.obj
Normal file
BIN
build/linux/amd64/resources/models/goonion.png
Normal file
After Width: | Height: | Size: 2.5 MiB |
12
build/linux/amd64/resources/models/shreke.mtl
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Blender 3.6.0 MTL File: 'None'
|
||||||
|
# www.blender.org
|
||||||
|
|
||||||
|
newmtl Material.001
|
||||||
|
Ns 250.000000
|
||||||
|
Ka 1.000000 1.000000 1.000000
|
||||||
|
Ks 0.500000 0.500000 0.500000
|
||||||
|
Ke 0.000000 0.000000 0.000000
|
||||||
|
Ni 1.450000
|
||||||
|
d 1.000000
|
||||||
|
illum 2
|
||||||
|
map_Kd shreke.png
|
210035
build/linux/amd64/resources/models/shreke.obj
Normal file
BIN
build/linux/amd64/resources/models/shreke.png
Normal file
After Width: | Height: | Size: 4.8 MiB |
BIN
build/linux/amd64/resources/screenshot.png
Normal file
After Width: | Height: | Size: 104 KiB |
BIN
build/windows/amd64/goonscape.exe
Executable file
BIN
build/windows/amd64/resources/audio/GoonScape1.mp3
Normal file
BIN
build/windows/amd64/resources/audio/GoonScape2.mp3
Normal file
12
build/windows/amd64/resources/models/coomer.mtl
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Blender 3.6.0 MTL File: 'None'
|
||||||
|
# www.blender.org
|
||||||
|
|
||||||
|
newmtl Material.001
|
||||||
|
Ns 250.000000
|
||||||
|
Ka 1.000000 1.000000 1.000000
|
||||||
|
Ks 0.500000 0.500000 0.500000
|
||||||
|
Ke 0.000000 0.000000 0.000000
|
||||||
|
Ni 1.450000
|
||||||
|
d 1.000000
|
||||||
|
illum 2
|
||||||
|
map_Kd coomer.png
|
117248
build/windows/amd64/resources/models/coomer.obj
Normal file
BIN
build/windows/amd64/resources/models/coomer.png
Normal file
After Width: | Height: | Size: 2.2 MiB |
12
build/windows/amd64/resources/models/goonion.mtl
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Blender 3.6.0 MTL File: 'None'
|
||||||
|
# www.blender.org
|
||||||
|
|
||||||
|
newmtl Material.001
|
||||||
|
Ns 250.000000
|
||||||
|
Ka 1.000000 1.000000 1.000000
|
||||||
|
Ks 0.500000 0.500000 0.500000
|
||||||
|
Ke 0.000000 0.000000 0.000000
|
||||||
|
Ni 1.450000
|
||||||
|
d 1.000000
|
||||||
|
illum 2
|
||||||
|
map_Kd goonion.png
|
112531
build/windows/amd64/resources/models/goonion.obj
Normal file
BIN
build/windows/amd64/resources/models/goonion.png
Normal file
After Width: | Height: | Size: 2.5 MiB |
12
build/windows/amd64/resources/models/shreke.mtl
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Blender 3.6.0 MTL File: 'None'
|
||||||
|
# www.blender.org
|
||||||
|
|
||||||
|
newmtl Material.001
|
||||||
|
Ns 250.000000
|
||||||
|
Ka 1.000000 1.000000 1.000000
|
||||||
|
Ks 0.500000 0.500000 0.500000
|
||||||
|
Ke 0.000000 0.000000 0.000000
|
||||||
|
Ni 1.450000
|
||||||
|
d 1.000000
|
||||||
|
illum 2
|
||||||
|
map_Kd shreke.png
|
210035
build/windows/amd64/resources/models/shreke.obj
Normal file
BIN
build/windows/amd64/resources/models/shreke.png
Normal file
After Width: | Height: | Size: 4.8 MiB |
BIN
build/windows/amd64/resources/screenshot.png
Normal file
After Width: | Height: | Size: 104 KiB |
@ -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,7 +118,7 @@ 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)
|
text := fmt.Sprintf("%s: %s", msg.Username, msg.Content)
|
||||||
rl.DrawText(text, int32(chatX)+5, int32(messageY), 20, rl.White)
|
rl.DrawText(text, int32(chatX)+5, int32(messageY), 20, rl.White)
|
||||||
messageY += messageHeight
|
messageY += messageHeight
|
||||||
}
|
}
|
||||||
|
78
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),
|
||||||
@ -42,9 +37,8 @@ func New() *Game {
|
|||||||
},
|
},
|
||||||
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
@ -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
|
||||||
|
}
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,9 +144,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)
|
||||||
@ -153,6 +195,17 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
|
|||||||
|
|
||||||
for _, state := range serverMessage.Players {
|
for _, state := range serverMessage.Players {
|
||||||
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 +216,8 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if g, ok := player.UserData.(*game.Game); ok && len(serverMessage.ChatMessages) > 0 {
|
if handler, ok := player.UserData.(types.ChatMessageHandler); ok && len(serverMessage.ChatMessages) > 0 {
|
||||||
g.Chat.HandleServerMessages(serverMessage.ChatMessages)
|
handler.HandleServerMessages(serverMessage.ChatMessages)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
24
scripts/build.sh
Executable file
@ -0,0 +1,24 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Main build process
|
||||||
|
build() {
|
||||||
|
local os=$1
|
||||||
|
local arch=$2
|
||||||
|
local output=$3
|
||||||
|
|
||||||
|
# Set CGO flags for static linking
|
||||||
|
export CGO_ENABLED=1
|
||||||
|
export GOOS=$os
|
||||||
|
export GOARCH=$arch
|
||||||
|
|
||||||
|
# Platform specific flags
|
||||||
|
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
@ -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
|
||||||
|