Compare commits
60 Commits
feature/mu
...
master
Author | SHA1 | Date | |
---|---|---|---|
53cc9bca6b | |||
a1aeb71512 | |||
ef3732c53c | |||
7be859d58f | |||
fde07e3e48 | |||
f966d538d3 | |||
220a451475 | |||
417bf4ea63 | |||
84d63ba4c1 | |||
49b84c8540 | |||
0e509ad752 | |||
bcd63efd7b | |||
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 | |||
c01b8d1c59 | |||
d301d597e8 | |||
91cdbab54a | |||
0a58e0453a | |||
8d70129c73 | |||
4012a2ed92 | |||
4f36c2ee1f | |||
63e3837441 | |||
5c5040cd42 | |||
2b9ece3c10 | |||
4bfb5af362 | |||
c7f7c083b1 | |||
1c42ec2802 | |||
7ab75e8128 |
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# Build artifacts
|
||||||
|
build/
|
||||||
|
goonscape
|
||||||
|
goonscape.exe
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
resources/models/old_and_test/
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[submodule "goonserver"]
|
||||||
|
path = goonserver
|
||||||
|
url = https://gitea.boner.be/bdnugget/goonserver
|
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*
|
5
2
Normal file
5
2
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
INFO: 2025/02/10 14:51:58 main.go:15: Starting GoonScape client
|
||||||
|
INFO: 2025/02/10 14:51:58 main.go:38: Initializing window
|
||||||
|
INFO: 2025/02/10 14:51:58 main.go:55: Loading game assets
|
||||||
|
INFO: 2025/02/10 14:51:58 game.go:54: Loading game assets
|
||||||
|
INFO: 2025/02/10 14:51:58 assets.go:51: Loading models
|
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"]
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 bdnugget
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
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 ./...
|
136
README.md
Normal file
136
README.md
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fixing the models
|
||||||
|
This is a note/reminder to myself. The models and animations are generated with [meshy.ai (referral link, give me credits pls)](https://www.meshy.ai/?utm_source=referral-program&utm_medium=link&utm_content=TUZLRK). Somehow the bones armature things are not in the same root as the model and it becomes like a weird pudding when using the animation. I had to manually fix this in Blender by doing:
|
||||||
|
- Selected the char1 mesh thing
|
||||||
|
- Alt + P (the right Alt kek) => Clear Parent and Keep Transform
|
||||||
|
- Tried to do the same for Armature, they should both be in the Scene collection as siblings, so straight from root
|
||||||
|
- Select char1 and press Ctrl + A => All Transforms
|
||||||
|
- Repeat for Armature
|
||||||
|
- Select char1 and make sure it still has the Armature Modifier attached (via purple wrench icon), I remove it and reapply it just to be sure
|
||||||
|
|
||||||
|
When exporting it, I make sure Transform +Y is up direction is turned off, otherwise my gooner walks on the walls instead of the floor lmao
|
||||||
|
|
175
assets/assets.go
Normal file
175
assets/assets.go
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
package assets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"gitea.boner.be/bdnugget/goonscape/logging"
|
||||||
|
"gitea.boner.be/bdnugget/goonscape/types"
|
||||||
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
assetMutex sync.RWMutex
|
||||||
|
loadedModels map[string]types.ModelAsset
|
||||||
|
audioMutex sync.Mutex
|
||||||
|
audioInitialized bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
loadedModels = make(map[string]types.ModelAsset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to load animations for a model
|
||||||
|
func loadModelAnimations(animPaths map[string]string) (types.AnimationSet, error) {
|
||||||
|
var animSet types.AnimationSet
|
||||||
|
|
||||||
|
// Load idle animations if specified
|
||||||
|
if idlePath, ok := animPaths["idle"]; ok {
|
||||||
|
idleAnims := rl.LoadModelAnimations(idlePath)
|
||||||
|
if len(idleAnims) > 0 {
|
||||||
|
animSet.Idle = idleAnims
|
||||||
|
rl.TraceLog(rl.LogInfo, "Loaded idle animation: %s (%d frames, %f seconds)",
|
||||||
|
idlePath, idleAnims[0].FrameCount, float32(idleAnims[0].FrameCount)/60.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load walk animations if specified
|
||||||
|
if walkPath, ok := animPaths["walk"]; ok {
|
||||||
|
walkAnims := rl.LoadModelAnimations(walkPath)
|
||||||
|
if len(walkAnims) > 0 {
|
||||||
|
animSet.Walk = walkAnims
|
||||||
|
rl.TraceLog(rl.LogInfo, "Loaded walk animation: %s (%d frames, %f seconds)",
|
||||||
|
walkPath, walkAnims[0].FrameCount, float32(walkAnims[0].FrameCount)/60.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return animSet, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadModels() ([]types.ModelAsset, error) {
|
||||||
|
logging.Info.Println("Loading models")
|
||||||
|
assetMutex.Lock()
|
||||||
|
defer assetMutex.Unlock()
|
||||||
|
|
||||||
|
if len(loadedModels) > 0 {
|
||||||
|
logging.Info.Println("Returning cached models")
|
||||||
|
models := make([]types.ModelAsset, 0, len(loadedModels))
|
||||||
|
for _, model := range loadedModels {
|
||||||
|
models = append(models, model)
|
||||||
|
}
|
||||||
|
return models, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Goonion model and animations
|
||||||
|
goonerModel := rl.LoadModel("resources/models/gooner/walk_no_y_transform.glb")
|
||||||
|
goonerAnims, err := loadModelAnimations(map[string]string{
|
||||||
|
"idle": "resources/models/gooner/idle_no_y_transform.glb",
|
||||||
|
"walk": "resources/models/gooner/walk_no_y_transform.glb",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply transformations
|
||||||
|
transform := rl.MatrixIdentity()
|
||||||
|
transform = rl.MatrixMultiply(transform, rl.MatrixRotateY(180*rl.Deg2rad))
|
||||||
|
transform = rl.MatrixMultiply(transform, rl.MatrixRotateX(-90*rl.Deg2rad))
|
||||||
|
transform = rl.MatrixMultiply(transform, rl.MatrixScale(1.0, 1.0, 1.0))
|
||||||
|
goonerModel.Transform = transform
|
||||||
|
|
||||||
|
// Coomer model (ready for animations)
|
||||||
|
coomerModel := rl.LoadModel("resources/models/coomer/idle_notransy.glb")
|
||||||
|
coomerAnims, err := loadModelAnimations(map[string]string{
|
||||||
|
"idle": "resources/models/coomer/idle_notransy.glb",
|
||||||
|
"walk": "resources/models/coomer/unsteadywalk_notransy.glb",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
coomerModel.Transform = transform
|
||||||
|
|
||||||
|
// Shreke model (ready for animations)
|
||||||
|
shrekeModel := rl.LoadModel("resources/models/shreke/Animation_Slow_Orc_Walk_withSkin.glb")
|
||||||
|
shrekeAnims, err := loadModelAnimations(map[string]string{
|
||||||
|
"idle": "resources/models/shreke/Animation_Slow_Orc_Walk_withSkin.glb",
|
||||||
|
"walk": "resources/models/shreke/Animation_Excited_Walk_M_withSkin.glb",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
shrekeModel.Transform = transform
|
||||||
|
|
||||||
|
// Store loaded models
|
||||||
|
models := []types.ModelAsset{
|
||||||
|
{
|
||||||
|
Name: "gooner",
|
||||||
|
Model: goonerModel,
|
||||||
|
Animation: append(goonerAnims.Idle, goonerAnims.Walk...),
|
||||||
|
AnimFrames: int32(len(goonerAnims.Idle) + len(goonerAnims.Walk)),
|
||||||
|
Animations: goonerAnims,
|
||||||
|
YOffset: 0.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Model: coomerModel,
|
||||||
|
Animation: append(coomerAnims.Idle, coomerAnims.Walk...),
|
||||||
|
AnimFrames: int32(len(coomerAnims.Idle) + len(coomerAnims.Walk)),
|
||||||
|
Animations: coomerAnims,
|
||||||
|
YOffset: -4.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Model: shrekeModel,
|
||||||
|
Animation: append(shrekeAnims.Idle, shrekeAnims.Walk...),
|
||||||
|
AnimFrames: int32(len(shrekeAnims.Idle) + len(shrekeAnims.Walk)),
|
||||||
|
Animations: shrekeAnims,
|
||||||
|
YOffset: 0.0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, model := range models {
|
||||||
|
loadedModels[model.Name] = model
|
||||||
|
}
|
||||||
|
|
||||||
|
return models, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadMusic(filename string) (rl.Music, error) {
|
||||||
|
logging.Info.Printf("Loading music from %s", filename)
|
||||||
|
audioMutex.Lock()
|
||||||
|
defer audioMutex.Unlock()
|
||||||
|
|
||||||
|
if !rl.IsAudioDeviceReady() {
|
||||||
|
err := fmt.Errorf("audio device not initialized")
|
||||||
|
logging.Error.Println(err)
|
||||||
|
return rl.Music{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
music := rl.LoadMusicStream(filename)
|
||||||
|
if music.CtxType == 0 {
|
||||||
|
err := fmt.Errorf("failed to load music stream")
|
||||||
|
logging.Error.Println(err)
|
||||||
|
return rl.Music{}, err
|
||||||
|
}
|
||||||
|
logging.Info.Println("Music loaded successfully")
|
||||||
|
return music, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UnloadModels(models []types.ModelAsset) {
|
||||||
|
assetMutex.Lock()
|
||||||
|
defer assetMutex.Unlock()
|
||||||
|
|
||||||
|
for _, model := range models {
|
||||||
|
if model.Animation != nil {
|
||||||
|
for i := int32(0); i < model.AnimFrames; i++ {
|
||||||
|
rl.UnloadModelAnimation(model.Animation[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rl.UnloadModel(model.Model)
|
||||||
|
rl.UnloadTexture(model.Texture)
|
||||||
|
delete(loadedModels, model.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UnloadMusic(music rl.Music) {
|
||||||
|
rl.UnloadMusicStream(music)
|
||||||
|
}
|
7
config/config.go
Normal file
7
config/config.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
PlayMusic bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var Current = Config{PlayMusic: true}
|
21
constants.go
Normal file
21
constants.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
const (
|
||||||
|
MapWidth = 50
|
||||||
|
MapHeight = 50
|
||||||
|
TileSize = 32
|
||||||
|
TileHeight = 2.0
|
||||||
|
TickRate = 600 * time.Millisecond // Server tick rate (600ms)
|
||||||
|
serverAddr = "localhost:6969"
|
||||||
|
|
||||||
|
// RuneScape-style tick rate (600ms)
|
||||||
|
ServerTickRate = 600 * time.Millisecond
|
||||||
|
|
||||||
|
// Client might run at a higher tick rate for smooth rendering
|
||||||
|
ClientTickRate = 50 * time.Millisecond
|
||||||
|
|
||||||
|
// Maximum number of ticks we can get behind before forcing a resync
|
||||||
|
MaxTickDesync = 5
|
||||||
|
)
|
@ -1 +0,0 @@
|
|||||||
package utils
|
|
@ -1 +0,0 @@
|
|||||||
package utils
|
|
92
game/camera.go
Normal file
92
game/camera.go
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
cameraDistance = float32(20.0)
|
||||||
|
cameraYaw = float32(145.0)
|
||||||
|
cameraPitch = float32(45.0)
|
||||||
|
lastMousePos rl.Vector2 // Add this to track mouse movement
|
||||||
|
)
|
||||||
|
|
||||||
|
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()
|
||||||
|
if wheelMove != 0 {
|
||||||
|
cameraDistance += -wheelMove * 5
|
||||||
|
if cameraDistance < 10 {
|
||||||
|
cameraDistance = 10
|
||||||
|
}
|
||||||
|
if cameraDistance > 250 {
|
||||||
|
cameraDistance = 250
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle middle mouse camera rotation
|
||||||
|
if rl.IsMouseButtonDown(rl.MouseMiddleButton) {
|
||||||
|
currentMousePos := rl.GetMousePosition()
|
||||||
|
|
||||||
|
// If we just started holding the button, initialize last position
|
||||||
|
if !rl.IsMouseButtonPressed(rl.MouseMiddleButton) {
|
||||||
|
mouseDelta := rl.Vector2{
|
||||||
|
X: currentMousePos.X - lastMousePos.X,
|
||||||
|
Y: currentMousePos.Y - lastMousePos.Y,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust rotation speed as needed
|
||||||
|
cameraYaw += mouseDelta.X * 0.5 * deltaTime * 60
|
||||||
|
cameraPitch += mouseDelta.Y * 0.5 * deltaTime * 60
|
||||||
|
|
||||||
|
// Clamp pitch to prevent camera flipping
|
||||||
|
if cameraPitch < 20 {
|
||||||
|
cameraPitch = 20
|
||||||
|
}
|
||||||
|
if cameraPitch > 85 {
|
||||||
|
cameraPitch = 85
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastMousePos = currentMousePos
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the keyboard controls too
|
||||||
|
if rl.IsKeyDown(rl.KeyRight) {
|
||||||
|
cameraYaw += 100 * deltaTime
|
||||||
|
}
|
||||||
|
if rl.IsKeyDown(rl.KeyLeft) {
|
||||||
|
cameraYaw -= 100 * deltaTime
|
||||||
|
}
|
||||||
|
if rl.IsKeyDown(rl.KeyUp) {
|
||||||
|
cameraPitch -= 50 * deltaTime
|
||||||
|
if cameraPitch < 20 {
|
||||||
|
cameraPitch = 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rl.IsKeyDown(rl.KeyDown) {
|
||||||
|
cameraPitch += 50 * deltaTime
|
||||||
|
if cameraPitch > 85 {
|
||||||
|
cameraPitch = 85
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cameraYawRad := float64(cameraYaw) * rl.Deg2rad
|
||||||
|
cameraPitchRad := float64(cameraPitch) * rl.Deg2rad
|
||||||
|
|
||||||
|
camera.Position = rl.Vector3{
|
||||||
|
X: targetPos.X + cameraDistance*float32(math.Cos(cameraYawRad))*float32(math.Cos(cameraPitchRad)),
|
||||||
|
Y: targetPos.Y + cameraDistance*float32(math.Sin(cameraPitchRad)),
|
||||||
|
Z: targetPos.Z + cameraDistance*float32(math.Sin(cameraYawRad))*float32(math.Cos(cameraPitchRad)),
|
||||||
|
}
|
||||||
|
camera.Target = targetPos
|
||||||
|
}
|
234
game/chat.go
Normal file
234
game/chat.go
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.boner.be/bdnugget/goonscape/types"
|
||||||
|
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
||||||
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxMessages = 50
|
||||||
|
chatMargin = 10 // Margin from screen edges
|
||||||
|
chatHeight = 200
|
||||||
|
messageHeight = 20
|
||||||
|
inputHeight = 30
|
||||||
|
runeLimit = 256
|
||||||
|
)
|
||||||
|
|
||||||
|
type Chat struct {
|
||||||
|
sync.RWMutex
|
||||||
|
messages []types.ChatMessage
|
||||||
|
inputBuffer []rune
|
||||||
|
isTyping bool
|
||||||
|
cursorPos int
|
||||||
|
scrollOffset int
|
||||||
|
userData interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChat() *Chat {
|
||||||
|
return &Chat{
|
||||||
|
messages: make([]types.ChatMessage, 0, maxMessages),
|
||||||
|
inputBuffer: make([]rune, 0, runeLimit),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Chat) AddMessage(playerID int32, content string) {
|
||||||
|
msg := types.ChatMessage{
|
||||||
|
PlayerID: playerID,
|
||||||
|
Content: content,
|
||||||
|
Time: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.messages) >= maxMessages {
|
||||||
|
c.messages = c.messages[1:]
|
||||||
|
}
|
||||||
|
c.messages = append(c.messages, msg)
|
||||||
|
c.scrollOffset = 0 // Reset scroll position for new messages
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
// Convert protobuf messages to our local type
|
||||||
|
for _, msg := range messages {
|
||||||
|
localMsg := types.ChatMessage{
|
||||||
|
PlayerID: msg.PlayerId,
|
||||||
|
Username: msg.Username,
|
||||||
|
Content: msg.Content,
|
||||||
|
Time: time.Unix(0, msg.Timestamp),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add if it's not already in our history
|
||||||
|
if len(c.messages) == 0 || c.messages[len(c.messages)-1].Time.UnixNano() < msg.Timestamp {
|
||||||
|
if len(c.messages) >= maxMessages {
|
||||||
|
c.messages = c.messages[1:]
|
||||||
|
}
|
||||||
|
c.messages = append(c.messages, localMsg)
|
||||||
|
|
||||||
|
// Scroll to latest message if it's not already visible
|
||||||
|
visibleMessages := int((chatHeight - inputHeight) / messageHeight)
|
||||||
|
if len(c.messages) > visibleMessages {
|
||||||
|
c.scrollOffset = len(c.messages) - visibleMessages
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add floating message to the player
|
||||||
|
if game, ok := c.userData.(*Game); ok {
|
||||||
|
if msg.PlayerId == game.Player.ID {
|
||||||
|
game.Player.Lock()
|
||||||
|
game.Player.FloatingMessage = &types.FloatingMessage{
|
||||||
|
Content: msg.Content,
|
||||||
|
ExpireTime: time.Now().Add(6 * time.Second),
|
||||||
|
}
|
||||||
|
game.Player.Unlock()
|
||||||
|
} else if otherPlayer, exists := game.OtherPlayers.Load(msg.PlayerId); exists {
|
||||||
|
other := otherPlayer.(*types.Player)
|
||||||
|
other.Lock()
|
||||||
|
other.FloatingMessage = &types.FloatingMessage{
|
||||||
|
Content: msg.Content,
|
||||||
|
ExpireTime: time.Now().Add(6 * time.Second),
|
||||||
|
}
|
||||||
|
other.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Chat) Draw(screenWidth, screenHeight int32) {
|
||||||
|
c.RLock()
|
||||||
|
defer c.RUnlock()
|
||||||
|
// Calculate chat window width based on screen width
|
||||||
|
chatWindowWidth := screenWidth - (chatMargin * 2)
|
||||||
|
|
||||||
|
// Draw chat window background
|
||||||
|
chatX := float32(chatMargin)
|
||||||
|
chatY := float32(screenHeight - chatHeight - chatMargin)
|
||||||
|
rl.DrawRectangle(int32(chatX), int32(chatY), chatWindowWidth, chatHeight, rl.ColorAlpha(rl.Black, 0.5))
|
||||||
|
|
||||||
|
// Draw messages from oldest to newest
|
||||||
|
messageY := chatY + 5
|
||||||
|
visibleMessages := int((chatHeight - inputHeight) / messageHeight)
|
||||||
|
|
||||||
|
// Auto-scroll to bottom if no manual scrolling has occurred
|
||||||
|
if c.scrollOffset == 0 {
|
||||||
|
if len(c.messages) > visibleMessages {
|
||||||
|
c.scrollOffset = len(c.messages) - visibleMessages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startIdx := max(0, c.scrollOffset)
|
||||||
|
endIdx := min(len(c.messages), startIdx+visibleMessages)
|
||||||
|
|
||||||
|
for i := startIdx; i < endIdx; i++ {
|
||||||
|
msg := c.messages[i]
|
||||||
|
var color rl.Color
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw input field
|
||||||
|
inputY := chatY + float32(chatHeight-inputHeight)
|
||||||
|
rl.DrawRectangle(int32(chatX), int32(inputY), chatWindowWidth, inputHeight, rl.ColorAlpha(rl.White, 0.3))
|
||||||
|
if c.isTyping {
|
||||||
|
inputText := string(c.inputBuffer)
|
||||||
|
rl.DrawText(inputText, int32(chatX)+5, int32(inputY)+5, 20, rl.White)
|
||||||
|
|
||||||
|
// Draw cursor
|
||||||
|
cursorX := rl.MeasureText(inputText[:c.cursorPos], 20)
|
||||||
|
rl.DrawRectangle(int32(chatX)+5+cursorX, int32(inputY)+5, 2, 20, rl.White)
|
||||||
|
} else {
|
||||||
|
rl.DrawText("Press T to chat", int32(chatX)+5, int32(inputY)+5, 20, rl.Gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Chat) Update() (string, bool) {
|
||||||
|
// Handle scrolling with mouse wheel when not typing
|
||||||
|
if !c.isTyping {
|
||||||
|
wheelMove := rl.GetMouseWheelMove()
|
||||||
|
if wheelMove != 0 {
|
||||||
|
maxScroll := max(0, len(c.messages)-int((chatHeight-inputHeight)/messageHeight))
|
||||||
|
c.scrollOffset = clamp(c.scrollOffset-int(wheelMove), 0, maxScroll)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rl.IsKeyPressed(rl.KeyT) {
|
||||||
|
c.isTyping = true
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
key := rl.GetCharPressed()
|
||||||
|
for key > 0 {
|
||||||
|
if len(c.inputBuffer) < runeLimit {
|
||||||
|
c.inputBuffer = append(c.inputBuffer[:c.cursorPos], append([]rune{key}, c.inputBuffer[c.cursorPos:]...)...)
|
||||||
|
c.cursorPos++
|
||||||
|
}
|
||||||
|
key = rl.GetCharPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rl.IsKeyPressed(rl.KeyEnter) || rl.IsKeyPressed(rl.KeyKpEnter) {
|
||||||
|
if len(c.inputBuffer) > 0 {
|
||||||
|
message := string(c.inputBuffer)
|
||||||
|
c.inputBuffer = c.inputBuffer[:0]
|
||||||
|
c.cursorPos = 0
|
||||||
|
c.isTyping = false
|
||||||
|
return message, true
|
||||||
|
}
|
||||||
|
c.isTyping = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if rl.IsKeyPressed(rl.KeyEscape) && c.isTyping {
|
||||||
|
c.inputBuffer = c.inputBuffer[:0]
|
||||||
|
c.cursorPos = 0
|
||||||
|
c.isTyping = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if rl.IsKeyPressed(rl.KeyBackspace) && c.cursorPos > 0 {
|
||||||
|
c.inputBuffer = append(c.inputBuffer[:c.cursorPos-1], c.inputBuffer[c.cursorPos:]...)
|
||||||
|
c.cursorPos--
|
||||||
|
}
|
||||||
|
|
||||||
|
if rl.IsKeyPressed(rl.KeyLeft) && c.cursorPos > 0 {
|
||||||
|
c.cursorPos--
|
||||||
|
}
|
||||||
|
if rl.IsKeyPressed(rl.KeyRight) && c.cursorPos < len(c.inputBuffer) {
|
||||||
|
c.cursorPos++
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add helper functions
|
||||||
|
func max(a, b int) int {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func min(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func clamp(value, min, max int) int {
|
||||||
|
if value < min {
|
||||||
|
return min
|
||||||
|
}
|
||||||
|
if value > max {
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
38
game/context.go
Normal file
38
game/context.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GameContext struct {
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
wg sync.WaitGroup
|
||||||
|
assetsLock sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGameContext() *GameContext {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
return &GameContext{
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gc *GameContext) Shutdown() {
|
||||||
|
gc.cancel()
|
||||||
|
gc.wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gc *GameContext) LoadAssets(fn func() error) error {
|
||||||
|
gc.assetsLock.Lock()
|
||||||
|
defer gc.assetsLock.Unlock()
|
||||||
|
return fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gc *GameContext) UnloadAssets(fn func()) {
|
||||||
|
gc.assetsLock.Lock()
|
||||||
|
defer gc.assetsLock.Unlock()
|
||||||
|
fn()
|
||||||
|
}
|
469
game/game.go
Normal file
469
game/game.go
Normal file
@ -0,0 +1,469 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"gitea.boner.be/bdnugget/goonscape/assets"
|
||||||
|
"gitea.boner.be/bdnugget/goonscape/config"
|
||||||
|
"gitea.boner.be/bdnugget/goonscape/logging"
|
||||||
|
"gitea.boner.be/bdnugget/goonscape/network"
|
||||||
|
"gitea.boner.be/bdnugget/goonscape/types"
|
||||||
|
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
||||||
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
|
)
|
||||||
|
|
||||||
|
var audioMutex sync.Mutex
|
||||||
|
var audioInitOnce sync.Once
|
||||||
|
|
||||||
|
type Game struct {
|
||||||
|
ctx *GameContext
|
||||||
|
Player *types.Player
|
||||||
|
OtherPlayers sync.Map // Using sync.Map for concurrent access
|
||||||
|
Camera rl.Camera3D
|
||||||
|
Models []types.ModelAsset
|
||||||
|
Music rl.Music
|
||||||
|
Chat *Chat
|
||||||
|
MenuOpen atomic.Bool
|
||||||
|
QuitChan chan struct{} // Channel to signal shutdown
|
||||||
|
loginScreen *LoginScreen
|
||||||
|
isLoggedIn atomic.Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *Game {
|
||||||
|
InitWorld()
|
||||||
|
game := &Game{
|
||||||
|
ctx: NewGameContext(),
|
||||||
|
OtherPlayers: sync.Map{},
|
||||||
|
Camera: rl.Camera3D{
|
||||||
|
Position: rl.NewVector3(0, 10, 10),
|
||||||
|
Target: rl.NewVector3(0, 0, 0),
|
||||||
|
Up: rl.NewVector3(0, 1, 0),
|
||||||
|
Fovy: 45.0,
|
||||||
|
Projection: rl.CameraPerspective,
|
||||||
|
},
|
||||||
|
Chat: NewChat(),
|
||||||
|
QuitChan: make(chan struct{}),
|
||||||
|
loginScreen: NewLoginScreen(),
|
||||||
|
}
|
||||||
|
game.Chat.userData = game
|
||||||
|
return game
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) LoadAssets() error {
|
||||||
|
audioMutex.Lock()
|
||||||
|
defer audioMutex.Unlock()
|
||||||
|
|
||||||
|
logging.Info.Println("Loading game assets")
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Load models first
|
||||||
|
g.Models, err = assets.LoadModels()
|
||||||
|
if err != nil {
|
||||||
|
logging.Error.Printf("Failed to load models: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load music only if enabled
|
||||||
|
if config.Current.PlayMusic {
|
||||||
|
logging.Info.Println("Loading music stream")
|
||||||
|
g.Music = rl.LoadMusicStream("resources/audio/GoonScape2.mp3")
|
||||||
|
if g.Music.CtxType == 0 {
|
||||||
|
logging.Error.Println("Failed to load music stream")
|
||||||
|
return fmt.Errorf("failed to load music stream")
|
||||||
|
}
|
||||||
|
logging.Info.Println("Music stream loaded successfully")
|
||||||
|
} else {
|
||||||
|
logging.Info.Println("Music disabled by config")
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Info.Println("Assets loaded successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) Update(deltaTime float32) {
|
||||||
|
if !g.isLoggedIn.Load() {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Player = &types.Player{
|
||||||
|
Speed: 50.0,
|
||||||
|
TargetPath: []types.Tile{},
|
||||||
|
UserData: g,
|
||||||
|
QuitDone: make(chan struct{}),
|
||||||
|
ID: playerID,
|
||||||
|
}
|
||||||
|
g.AssignModelToPlayer(g.Player)
|
||||||
|
|
||||||
|
go network.HandleServerCommunication(conn, playerID, g.Player, &g.OtherPlayers, g.QuitChan)
|
||||||
|
g.isLoggedIn.Store(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
g.loginScreen.Draw()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle ESC for menu
|
||||||
|
if rl.IsKeyPressed(rl.KeyEscape) {
|
||||||
|
g.MenuOpen.Store(!g.MenuOpen.Load())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't process other inputs if menu is open
|
||||||
|
if g.MenuOpen.Load() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if message, sent := g.Chat.Update(); sent {
|
||||||
|
g.Player.Lock()
|
||||||
|
g.Player.ActionQueue = append(g.Player.ActionQueue, &pb.Action{
|
||||||
|
Type: pb.Action_CHAT,
|
||||||
|
ChatMessage: message,
|
||||||
|
PlayerId: g.Player.ID,
|
||||||
|
})
|
||||||
|
g.Player.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
g.HandleInput()
|
||||||
|
|
||||||
|
if len(g.Player.TargetPath) > 0 {
|
||||||
|
g.Player.Lock()
|
||||||
|
if len(g.Player.TargetPath) > 0 {
|
||||||
|
g.Player.MoveTowards(g.Player.TargetPath[0], deltaTime, GetMapGrid())
|
||||||
|
}
|
||||||
|
g.Player.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
g.OtherPlayers.Range(func(key, value any) bool {
|
||||||
|
other := value.(*types.Player)
|
||||||
|
if len(other.TargetPath) > 0 {
|
||||||
|
other.MoveTowards(other.TargetPath[0], deltaTime, GetMapGrid())
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
UpdateCamera(&g.Camera, g.Player.PosActual, deltaTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) DrawMap() {
|
||||||
|
for x := 0; x < types.MapWidth; x++ {
|
||||||
|
for y := 0; y < types.MapHeight; y++ {
|
||||||
|
height := GetTileHeight(x, y)
|
||||||
|
|
||||||
|
// Interpolate height for smoother landscape
|
||||||
|
if x > 0 {
|
||||||
|
height += GetTileHeight(x-1, y)
|
||||||
|
}
|
||||||
|
if y > 0 {
|
||||||
|
height += GetTileHeight(x, y-1)
|
||||||
|
}
|
||||||
|
if x > 0 && y > 0 {
|
||||||
|
height += GetTileHeight(x-1, y-1)
|
||||||
|
}
|
||||||
|
height /= 4.0
|
||||||
|
|
||||||
|
tilePos := rl.Vector3{
|
||||||
|
X: float32(x * types.TileSize),
|
||||||
|
Y: height * types.TileHeight,
|
||||||
|
Z: float32(y * types.TileSize),
|
||||||
|
}
|
||||||
|
color := rl.Color{R: uint8(height * 25), G: 100, B: 100, A: 64}
|
||||||
|
rl.DrawCube(tilePos, types.TileSize, types.TileHeight, types.TileSize, color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) DrawPlayer(player *types.Player) {
|
||||||
|
player.Lock()
|
||||||
|
defer player.Unlock()
|
||||||
|
|
||||||
|
if player.Model.Meshes == nil {
|
||||||
|
logging.Error.Println("Player model not initialized")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
grid := GetMapGrid()
|
||||||
|
modelIndex := int(player.ID) % len(g.Models)
|
||||||
|
modelAsset := g.Models[modelIndex]
|
||||||
|
|
||||||
|
const defaultHeight = 8.0 // Default height above tile, fine tune per model in types.ModelAsset
|
||||||
|
playerPos := rl.Vector3{
|
||||||
|
X: player.PosActual.X,
|
||||||
|
Y: grid[player.PosTile.X][player.PosTile.Y].Height*types.TileHeight + defaultHeight + modelAsset.YOffset,
|
||||||
|
Z: player.PosActual.Z,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if model has animations
|
||||||
|
if modelAsset.Animations.Idle != nil || modelAsset.Animations.Walk != nil {
|
||||||
|
if player.IsMoving && len(modelAsset.Animations.Walk) > 0 {
|
||||||
|
anim := modelAsset.Animations.Walk[0] // Use first walk animation
|
||||||
|
player.AnimationFrame = player.AnimationFrame % anim.FrameCount
|
||||||
|
rl.UpdateModelAnimation(player.Model, anim, player.AnimationFrame)
|
||||||
|
} else if len(modelAsset.Animations.Idle) > 0 {
|
||||||
|
anim := modelAsset.Animations.Idle[0] // Use first idle animation
|
||||||
|
player.AnimationFrame = player.AnimationFrame % anim.FrameCount
|
||||||
|
rl.UpdateModelAnimation(player.Model, anim, player.AnimationFrame)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.DrawModel(player.Model, playerPos, 16, rl.White)
|
||||||
|
|
||||||
|
// Draw floating messages and path indicators
|
||||||
|
if player.FloatingMessage != nil {
|
||||||
|
screenPos := rl.GetWorldToScreen(rl.Vector3{
|
||||||
|
X: playerPos.X,
|
||||||
|
Y: playerPos.Y + 24.0,
|
||||||
|
Z: playerPos.Z,
|
||||||
|
}, g.Camera)
|
||||||
|
|
||||||
|
player.FloatingMessage.ScreenPos = screenPos
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(player.TargetPath) > 0 {
|
||||||
|
targetTile := player.TargetPath[len(player.TargetPath)-1]
|
||||||
|
targetPos := rl.Vector3{
|
||||||
|
X: float32(targetTile.X * types.TileSize),
|
||||||
|
Y: grid[targetTile.X][targetTile.Y].Height * types.TileHeight,
|
||||||
|
Z: float32(targetTile.Y * types.TileSize),
|
||||||
|
}
|
||||||
|
rl.DrawCubeWires(targetPos, types.TileSize, types.TileHeight, types.TileSize, rl.Green)
|
||||||
|
|
||||||
|
nextTile := player.TargetPath[0]
|
||||||
|
nextPos := rl.Vector3{
|
||||||
|
X: float32(nextTile.X * types.TileSize),
|
||||||
|
Y: grid[nextTile.X][nextTile.Y].Height * types.TileHeight,
|
||||||
|
Z: float32(nextTile.Y * types.TileSize),
|
||||||
|
}
|
||||||
|
rl.DrawCubeWires(nextPos, types.TileSize, types.TileHeight, types.TileSize, rl.Yellow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) Render() {
|
||||||
|
if !rl.IsWindowReady() {
|
||||||
|
logging.Error.Println("Window not ready for rendering")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.BeginDrawing()
|
||||||
|
defer func() {
|
||||||
|
if rl.IsWindowReady() {
|
||||||
|
rl.EndDrawing()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if !g.isLoggedIn.Load() {
|
||||||
|
g.loginScreen.Draw()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.BeginMode3D(g.Camera)
|
||||||
|
g.DrawMap()
|
||||||
|
g.DrawPlayer(g.Player)
|
||||||
|
|
||||||
|
g.OtherPlayers.Range(func(key, value any) bool {
|
||||||
|
other := value.(*types.Player)
|
||||||
|
if other.Model.Meshes == nil {
|
||||||
|
g.AssignModelToPlayer(other)
|
||||||
|
}
|
||||||
|
g.DrawPlayer(other)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
rl.EndMode3D()
|
||||||
|
|
||||||
|
// Draw floating messages
|
||||||
|
drawFloatingMessage := func(msg *types.FloatingMessage) {
|
||||||
|
if msg == nil || time.Now().After(msg.ExpireTime) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pos := msg.ScreenPos
|
||||||
|
text := msg.Content
|
||||||
|
textWidth := rl.MeasureText(text, 20)
|
||||||
|
|
||||||
|
for offsetX := -2; offsetX <= 2; offsetX++ {
|
||||||
|
for offsetY := -2; offsetY <= 2; offsetY++ {
|
||||||
|
rl.DrawText(text,
|
||||||
|
int32(pos.X)-textWidth/2+int32(offsetX),
|
||||||
|
int32(pos.Y)+int32(offsetY),
|
||||||
|
20,
|
||||||
|
rl.Black)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rl.DrawText(text, int32(pos.X)-textWidth/2, int32(pos.Y), 20, rl.Yellow)
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.Player.FloatingMessage != nil {
|
||||||
|
drawFloatingMessage(g.Player.FloatingMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
g.OtherPlayers.Range(func(key, value any) bool {
|
||||||
|
other := value.(*types.Player)
|
||||||
|
drawFloatingMessage(other.FloatingMessage)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Draw menu if open
|
||||||
|
if g.MenuOpen.Load() {
|
||||||
|
g.DrawMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only draw chat if menu is not open
|
||||||
|
if !g.MenuOpen.Load() {
|
||||||
|
g.Chat.Draw(int32(rl.GetScreenWidth()), int32(rl.GetScreenHeight()))
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.DrawFPS(10, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) Cleanup() {
|
||||||
|
// Unload models
|
||||||
|
if g.Models != nil {
|
||||||
|
assets.UnloadModels(g.Models)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop and unload music if enabled
|
||||||
|
if config.Current.PlayMusic && g.Music.CtxType != 0 {
|
||||||
|
rl.StopMusicStream(g.Music)
|
||||||
|
rl.UnloadMusicStream(g.Music)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close audio device if it's ready
|
||||||
|
if rl.IsAudioDeviceReady() {
|
||||||
|
rl.CloseAudioDevice()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) HandleInput() {
|
||||||
|
clickedTile, clicked := g.GetTileAtMouse()
|
||||||
|
if clicked {
|
||||||
|
path := FindPath(GetTile(g.Player.PosTile.X, g.Player.PosTile.Y), clickedTile)
|
||||||
|
if len(path) > 1 {
|
||||||
|
g.Player.Lock()
|
||||||
|
g.Player.TargetPath = path[1:]
|
||||||
|
g.Player.ActionQueue = append(g.Player.ActionQueue, &pb.Action{
|
||||||
|
Type: pb.Action_MOVE,
|
||||||
|
X: int32(clickedTile.X),
|
||||||
|
Y: int32(clickedTile.Y),
|
||||||
|
PlayerId: g.Player.ID,
|
||||||
|
})
|
||||||
|
g.Player.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.Store(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) AssignModelToPlayer(player *types.Player) {
|
||||||
|
modelIndex := int(player.ID) % len(g.Models)
|
||||||
|
modelAsset := g.Models[modelIndex]
|
||||||
|
|
||||||
|
player.Model = modelAsset.Model
|
||||||
|
player.Texture = modelAsset.Texture
|
||||||
|
player.AnimationFrame = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) Run() {
|
||||||
|
if config.Current.PlayMusic {
|
||||||
|
audioInitOnce.Do(func() {
|
||||||
|
logging.Info.Println("Initializing audio device")
|
||||||
|
rl.InitAudioDevice()
|
||||||
|
if !rl.IsAudioDeviceReady() {
|
||||||
|
logging.Error.Println("Failed to initialize audio device")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
defer func() {
|
||||||
|
logging.Info.Println("Closing audio device")
|
||||||
|
rl.CloseAudioDevice()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Info.Println("Starting game loop")
|
||||||
|
for !rl.WindowShouldClose() {
|
||||||
|
deltaTime := rl.GetFrameTime()
|
||||||
|
if config.Current.PlayMusic {
|
||||||
|
rl.UpdateMusicStream(g.Music)
|
||||||
|
}
|
||||||
|
g.Update(deltaTime)
|
||||||
|
g.Render()
|
||||||
|
}
|
||||||
|
logging.Info.Println("Game loop ended")
|
||||||
|
|
||||||
|
logging.Info.Println("Closing quit channel")
|
||||||
|
close(g.QuitChan)
|
||||||
|
}
|
31
game/input.go
Normal file
31
game/input.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.boner.be/bdnugget/goonscape/types"
|
||||||
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (g *Game) GetTileAtMouse() (types.Tile, bool) {
|
||||||
|
if !rl.IsMouseButtonPressed(rl.MouseLeftButton) {
|
||||||
|
return types.Tile{}, false
|
||||||
|
}
|
||||||
|
mouse := rl.GetMousePosition()
|
||||||
|
ray := rl.GetMouseRay(mouse, g.Camera)
|
||||||
|
|
||||||
|
for x := 0; x < types.MapWidth; x++ {
|
||||||
|
for y := 0; y < types.MapHeight; y++ {
|
||||||
|
tile := GetTile(x, y)
|
||||||
|
tilePos := rl.NewVector3(float32(x*types.TileSize), tile.Height*types.TileHeight, float32(y*types.TileSize))
|
||||||
|
boxMin := rl.Vector3Subtract(tilePos, rl.NewVector3(types.TileSize/2, types.TileHeight/2, types.TileSize/2))
|
||||||
|
boxMax := rl.Vector3Add(tilePos, rl.NewVector3(types.TileSize/2, types.TileHeight/2, types.TileSize/2))
|
||||||
|
|
||||||
|
if RayIntersectsBox(ray, boxMin, boxMax) {
|
||||||
|
fmt.Printf("Clicked: %d, %d\n", tile.X, tile.Y)
|
||||||
|
return tile, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return types.Tile{}, false
|
||||||
|
}
|
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
|
||||||
|
}
|
113
game/pathfinding.go
Normal file
113
game/pathfinding.go
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.boner.be/bdnugget/goonscape/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Node struct {
|
||||||
|
Tile types.Tile
|
||||||
|
Parent *Node
|
||||||
|
G, H, F float32
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindPath(start, end types.Tile) []types.Tile {
|
||||||
|
openList := []*Node{}
|
||||||
|
closedList := make(map[[2]int]bool)
|
||||||
|
|
||||||
|
startNode := &Node{Tile: start, G: 0, H: heuristic(start, end)}
|
||||||
|
startNode.F = startNode.G + startNode.H
|
||||||
|
openList = append(openList, startNode)
|
||||||
|
|
||||||
|
for len(openList) > 0 {
|
||||||
|
current := openList[0]
|
||||||
|
currentIndex := 0
|
||||||
|
for i, node := range openList {
|
||||||
|
if node.F < current.F {
|
||||||
|
current = node
|
||||||
|
currentIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openList = append(openList[:currentIndex], openList[currentIndex+1:]...)
|
||||||
|
closedList[[2]int{current.Tile.X, current.Tile.Y}] = true
|
||||||
|
|
||||||
|
if current.Tile.X == end.X && current.Tile.Y == end.Y {
|
||||||
|
path := []types.Tile{}
|
||||||
|
node := current
|
||||||
|
for node != nil {
|
||||||
|
path = append([]types.Tile{node.Tile}, path...)
|
||||||
|
node = node.Parent
|
||||||
|
}
|
||||||
|
fmt.Printf("Path found: %v\n", path)
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
neighbors := GetNeighbors(current.Tile)
|
||||||
|
for _, neighbor := range neighbors {
|
||||||
|
if !neighbor.Walkable || closedList[[2]int{neighbor.X, neighbor.Y}] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tentativeG := current.G + distance(current.Tile, neighbor)
|
||||||
|
inOpen := false
|
||||||
|
var existingNode *Node
|
||||||
|
for _, node := range openList {
|
||||||
|
if node.Tile.X == neighbor.X && node.Tile.Y == neighbor.Y {
|
||||||
|
existingNode = node
|
||||||
|
inOpen = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !inOpen || tentativeG < existingNode.G {
|
||||||
|
newNode := &Node{
|
||||||
|
Tile: neighbor,
|
||||||
|
Parent: current,
|
||||||
|
G: tentativeG,
|
||||||
|
H: heuristic(neighbor, end),
|
||||||
|
}
|
||||||
|
newNode.F = newNode.G + newNode.H
|
||||||
|
if !inOpen {
|
||||||
|
openList = append(openList, newNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func heuristic(a, b types.Tile) float32 {
|
||||||
|
return float32(abs(a.X-b.X) + abs(a.Y-b.Y))
|
||||||
|
}
|
||||||
|
|
||||||
|
func distance(a, b types.Tile) float32 {
|
||||||
|
_, _ = a, b
|
||||||
|
return 1.0 // uniform cost for now
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetNeighbors(tile types.Tile) []types.Tile {
|
||||||
|
directions := [][2]int{
|
||||||
|
{1, 0}, {-1, 0}, {0, 1}, {0, -1},
|
||||||
|
{1, 1}, {-1, -1}, {1, -1}, {-1, 1},
|
||||||
|
}
|
||||||
|
neighbors := []types.Tile{}
|
||||||
|
grid := GetMapGrid()
|
||||||
|
for _, dir := range directions {
|
||||||
|
nx, ny := tile.X+dir[0], tile.Y+dir[1]
|
||||||
|
if nx >= 0 && nx < types.MapWidth && ny >= 0 && ny < types.MapHeight {
|
||||||
|
if grid[nx][ny].Walkable {
|
||||||
|
neighbors = append(neighbors, grid[nx][ny])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return neighbors
|
||||||
|
}
|
||||||
|
|
||||||
|
func abs(x int) int {
|
||||||
|
if x < 0 {
|
||||||
|
return -x
|
||||||
|
}
|
||||||
|
return x
|
||||||
|
}
|
45
game/utils.go
Normal file
45
game/utils.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RayIntersectsBox(ray rl.Ray, boxMin, boxMax rl.Vector3) bool {
|
||||||
|
tmin := (boxMin.X - ray.Position.X) / ray.Direction.X
|
||||||
|
tmax := (boxMax.X - ray.Position.X) / ray.Direction.X
|
||||||
|
|
||||||
|
if tmin > tmax {
|
||||||
|
tmin, tmax = tmax, tmin
|
||||||
|
}
|
||||||
|
|
||||||
|
tymin := (boxMin.Z - ray.Position.Z) / ray.Direction.Z
|
||||||
|
tymax := (boxMax.Z - ray.Position.Z) / ray.Direction.Z
|
||||||
|
|
||||||
|
if tymin > tymax {
|
||||||
|
tymin, tymax = tymax, tymin
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tmin > tymax) || (tymin > tmax) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if tymin > tmin {
|
||||||
|
tmin = tymin
|
||||||
|
}
|
||||||
|
if tymax < tmax {
|
||||||
|
tmax = tymax
|
||||||
|
}
|
||||||
|
|
||||||
|
tzmin := (boxMin.Y - ray.Position.Y) / ray.Direction.Y
|
||||||
|
tzmax := (boxMax.Y - ray.Position.Y) / ray.Direction.Y
|
||||||
|
|
||||||
|
if tzmin > tzmax {
|
||||||
|
tzmin, tzmax = tzmax, tzmin
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tmin > tzmax) || (tzmin > tmax) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
39
game/world.go
Normal file
39
game/world.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.boner.be/bdnugget/goonscape/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
mapGrid [][]types.Tile
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetMapGrid() [][]types.Tile {
|
||||||
|
return mapGrid
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitWorld() {
|
||||||
|
mapGrid = make([][]types.Tile, types.MapWidth)
|
||||||
|
for x := 0; x < types.MapWidth; x++ {
|
||||||
|
mapGrid[x] = make([]types.Tile, types.MapHeight)
|
||||||
|
for y := 0; y < types.MapHeight; y++ {
|
||||||
|
mapGrid[x][y] = types.Tile{
|
||||||
|
X: x,
|
||||||
|
Y: y,
|
||||||
|
Height: 1.0 + float32(x%5),
|
||||||
|
Walkable: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTile(x, y int) types.Tile {
|
||||||
|
if x >= 0 && x < types.MapWidth && y >= 0 && y < types.MapHeight {
|
||||||
|
return mapGrid[x][y]
|
||||||
|
}
|
||||||
|
return types.Tile{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTileHeight(x, y int) float32 {
|
||||||
|
return mapGrid[x][y].Height
|
||||||
|
}
|
16
go.mod
16
go.mod
@ -1,15 +1,17 @@
|
|||||||
module goonscape
|
module gitea.boner.be/bdnugget/goonscape
|
||||||
|
|
||||||
go 1.23.0
|
go 1.23.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
gitea.boner.be/bdnugget/goonserver v0.0.0-20241011122434-4bd5303cfd46
|
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-20250207012021-f9890c6ad9f3 // indirect
|
||||||
golang.org/x/sys v0.26.0 // indirect
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
replace gitea.boner.be/bdnugget/goonserver => ./goonserver
|
||||||
|
26
go.sum
26
go.sum
@ -1,14 +1,16 @@
|
|||||||
gitea.boner.be/bdnugget/goonserver v0.0.0-20241011122434-4bd5303cfd46 h1:T2D4QcmvBqzGoHO0VJGNUd1k2lLmUcyg6Rc/vN4/Im8=
|
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||||
gitea.boner.be/bdnugget/goonserver v0.0.0-20241011122434-4bd5303cfd46/go.mod h1:inR1bKrr/vcTba+G1KzmmY6vssMq9oGNOk836VwPa4c=
|
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE=
|
github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b h1:JJfspevP3YOXcSKVABizYOv++yMpTJIdPUtoDzF/RWw=
|
||||||
github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b/go.mod h1:BaY76bZk7nw1/kVOSQObPY1v1iwVE1KHAGMfvI6oK1Q=
|
||||||
github.com/gen2brain/raylib-go/raylib v0.0.0-20240930075631-c66f9e2942fe h1:mInjrbJkUglTM7tBmXG+epnPCE744aj15J7vjJwM4gs=
|
|
||||||
github.com/gen2brain/raylib-go/raylib v0.0.0-20240930075631-c66f9e2942fe/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/exp v0.0.0-20250207012021-f9890c6ad9f3 h1:qNgPs5exUA+G0C96DrPwNrvLSj7GT/9D+3WMWUcUg34=
|
||||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
||||||
|
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
|
1
goonserver
Submodule
1
goonserver
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit f9ec811b10bbab54e843199eb68156e9e7c143cc
|
16
logging/logging.go
Normal file
16
logging/logging.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Info *log.Logger
|
||||||
|
Error *log.Logger
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Info = log.New(os.Stdout, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)
|
||||||
|
Error = log.New(os.Stderr, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)
|
||||||
|
}
|
590
main.go
590
main.go
@ -1,547 +1,79 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"flag"
|
||||||
"log"
|
"log"
|
||||||
"math"
|
"strings"
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
"gitea.boner.be/bdnugget/goonscape/config"
|
||||||
|
"gitea.boner.be/bdnugget/goonscape/game"
|
||||||
|
"gitea.boner.be/bdnugget/goonscape/logging"
|
||||||
|
"gitea.boner.be/bdnugget/goonscape/network"
|
||||||
rl "github.com/gen2brain/raylib-go/raylib"
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
MapWidth = 50
|
|
||||||
MapHeight = 50
|
|
||||||
TileSize = 32
|
|
||||||
TileHeight = 2.0
|
|
||||||
TickRate = 2600 * time.Millisecond // Server tick rate (600ms)
|
|
||||||
serverAddr = "localhost:6969"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
cameraDistance = float32(20.0)
|
|
||||||
cameraYaw = float32(145.0)
|
|
||||||
cameraPitch = float32(45.0) // Adjusted for a more overhead view
|
|
||||||
)
|
|
||||||
|
|
||||||
type Tile struct {
|
|
||||||
X, Y int
|
|
||||||
Height float32
|
|
||||||
Walkable bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type ActionType int
|
|
||||||
|
|
||||||
const (
|
|
||||||
MoveAction ActionType = iota
|
|
||||||
)
|
|
||||||
|
|
||||||
type Action struct {
|
|
||||||
Type ActionType
|
|
||||||
X, Y int // Target position for movement
|
|
||||||
}
|
|
||||||
|
|
||||||
type Player struct {
|
|
||||||
PosActual rl.Vector3
|
|
||||||
PosTile Tile
|
|
||||||
TargetPath []Tile
|
|
||||||
Speed float32
|
|
||||||
ActionQueue []Action // Queue for player actions
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the map with some height data
|
|
||||||
func InitMap() [][]Tile {
|
|
||||||
mapGrid := make([][]Tile, MapWidth)
|
|
||||||
for x := 0; x < MapWidth; x++ {
|
|
||||||
mapGrid[x] = make([]Tile, MapHeight)
|
|
||||||
for y := 0; y < MapHeight; y++ {
|
|
||||||
mapGrid[x][y] = Tile{
|
|
||||||
X: x,
|
|
||||||
Y: y,
|
|
||||||
Height: 1.0 + float32(x%5), // Example height
|
|
||||||
Walkable: true, // Set to false for obstacles
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mapGrid
|
|
||||||
}
|
|
||||||
|
|
||||||
func DrawMap(mapGrid [][]Tile) {
|
|
||||||
for x := 0; x < MapWidth; x++ {
|
|
||||||
for y := 0; y < MapHeight; y++ {
|
|
||||||
tile := mapGrid[x][y]
|
|
||||||
// Interpolate height between adjacent tiles for a smoother landscape
|
|
||||||
height := tile.Height
|
|
||||||
if x > 0 {
|
|
||||||
height += mapGrid[x-1][y].Height
|
|
||||||
}
|
|
||||||
if y > 0 {
|
|
||||||
height += mapGrid[x][y-1].Height
|
|
||||||
}
|
|
||||||
if x > 0 && y > 0 {
|
|
||||||
height += mapGrid[x-1][y-1].Height
|
|
||||||
}
|
|
||||||
height /= 4.0
|
|
||||||
// Draw each tile as a 3D cube based on its height
|
|
||||||
tilePos := rl.Vector3{
|
|
||||||
X: float32(x * TileSize), // X-axis for horizontal position
|
|
||||||
Y: height * TileHeight, // Y-axis for height (Z in 3D is Y here)
|
|
||||||
Z: float32(y * TileSize), // Z-axis for depth (Y in 3D is Z here)
|
|
||||||
}
|
|
||||||
color := rl.Color{R: uint8(height * 25), G: 100, B: 100, A: 64}
|
|
||||||
rl.DrawCube(tilePos, TileSize, TileHeight, TileSize, color) // Draw a cube representing the tile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func DrawPlayer(player Player, model *rl.Model, mapGrid [][]Tile) {
|
|
||||||
// Draw the player based on its actual position (PosActual) and current tile height
|
|
||||||
playerPos := rl.Vector3{
|
|
||||||
X: player.PosActual.X,
|
|
||||||
Y: mapGrid[player.PosTile.X][player.PosTile.Y].Height*TileHeight + 16.0,
|
|
||||||
Z: player.PosActual.Z,
|
|
||||||
}
|
|
||||||
// rl.DrawCube(playerPos, 16, 16, 16, rl.Green) // Draw player cube
|
|
||||||
rl.DrawModel(*model, playerPos, 16, rl.White)
|
|
||||||
|
|
||||||
// Draw highlight around target tile
|
|
||||||
if len(player.TargetPath) > 0 {
|
|
||||||
targetTile := player.TargetPath[len(player.TargetPath)-1] // last tile in the slice
|
|
||||||
targetPos := rl.Vector3{
|
|
||||||
X: float32(targetTile.X * TileSize),
|
|
||||||
Y: mapGrid[targetTile.X][targetTile.Y].Height * TileHeight,
|
|
||||||
Z: float32(targetTile.Y * TileSize),
|
|
||||||
}
|
|
||||||
rl.DrawCubeWires(targetPos, TileSize, TileHeight, TileSize, rl.Green)
|
|
||||||
|
|
||||||
nextTile := player.TargetPath[0] // first tile in the slice
|
|
||||||
nextPos := rl.Vector3{
|
|
||||||
X: float32(nextTile.X * TileSize),
|
|
||||||
Y: mapGrid[nextTile.X][nextTile.Y].Height * TileHeight,
|
|
||||||
Z: float32(nextTile.Y * TileSize),
|
|
||||||
}
|
|
||||||
rl.DrawCubeWires(nextPos, TileSize, TileHeight, TileSize, rl.Yellow)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to test ray-box intersection (slab method)
|
|
||||||
func RayIntersectsBox(ray rl.Ray, boxMin, boxMax rl.Vector3) bool {
|
|
||||||
tmin := (boxMin.X - ray.Position.X) / ray.Direction.X
|
|
||||||
tmax := (boxMax.X - ray.Position.X) / ray.Direction.X
|
|
||||||
|
|
||||||
if tmin > tmax {
|
|
||||||
tmin, tmax = tmax, tmin
|
|
||||||
}
|
|
||||||
|
|
||||||
tymin := (boxMin.Z - ray.Position.Z) / ray.Direction.Z
|
|
||||||
tymax := (boxMax.Z - ray.Position.Z) / ray.Direction.Z
|
|
||||||
|
|
||||||
if tymin > tymax {
|
|
||||||
tymin, tymax = tymax, tymin
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tmin > tymax) || (tymin > tmax) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if tymin > tmin {
|
|
||||||
tmin = tymin
|
|
||||||
}
|
|
||||||
if tymax < tmax {
|
|
||||||
tmax = tymax
|
|
||||||
}
|
|
||||||
|
|
||||||
tzmin := (boxMin.Y - ray.Position.Y) / ray.Direction.Y
|
|
||||||
tzmax := (boxMax.Y - ray.Position.Y) / ray.Direction.Y
|
|
||||||
|
|
||||||
if tzmin > tzmax {
|
|
||||||
tzmin, tzmax = tzmax, tzmin
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tmin > tzmax) || (tzmin > tmax) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetTileAtMouse(mapGrid [][]Tile, camera *rl.Camera3D) (Tile, bool) {
|
|
||||||
if !rl.IsMouseButtonPressed(rl.MouseLeftButton) {
|
|
||||||
return Tile{}, false
|
|
||||||
}
|
|
||||||
mouse := rl.GetMousePosition()
|
|
||||||
ray := rl.GetMouseRay(mouse, *camera)
|
|
||||||
|
|
||||||
for x := 0; x < MapWidth; x++ {
|
|
||||||
for y := 0; y < MapHeight; y++ {
|
|
||||||
tile := mapGrid[x][y]
|
|
||||||
|
|
||||||
// Define the bounding box for each tile based on its position and height
|
|
||||||
tilePos := rl.NewVector3(float32(x*TileSize), tile.Height*TileHeight, float32(y*TileSize))
|
|
||||||
boxMin := rl.Vector3Subtract(tilePos, rl.NewVector3(TileSize/2, TileHeight/2, TileSize/2))
|
|
||||||
boxMax := rl.Vector3Add(tilePos, rl.NewVector3(TileSize/2, TileHeight/2, TileSize/2))
|
|
||||||
|
|
||||||
// Check if the ray intersects the bounding box
|
|
||||||
if RayIntersectsBox(ray, boxMin, boxMax) {
|
|
||||||
fmt.Println("Clicked:", tile.X, tile.Y)
|
|
||||||
return tile, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Tile{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (player *Player) MoveTowards(target Tile, deltaTime float32, mapGrid [][]Tile) {
|
|
||||||
// Calculate the direction vector to the target tile
|
|
||||||
targetPos := rl.Vector3{
|
|
||||||
X: float32(target.X * TileSize),
|
|
||||||
Y: mapGrid[target.X][target.Y].Height * TileHeight,
|
|
||||||
Z: float32(target.Y * TileSize),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate direction and normalize it for smooth movement
|
|
||||||
direction := rl.Vector3Subtract(targetPos, player.PosActual)
|
|
||||||
distance := rl.Vector3Length(direction)
|
|
||||||
if distance > 0 {
|
|
||||||
direction = rl.Vector3Scale(direction, player.Speed*deltaTime/distance)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move the player towards the target tile
|
|
||||||
if distance > 1.0 {
|
|
||||||
player.PosActual = rl.Vector3Add(player.PosActual, direction)
|
|
||||||
} else {
|
|
||||||
// Snap to the target tile when close enough
|
|
||||||
player.PosActual = targetPos
|
|
||||||
player.PosTile = target // Update player's tile
|
|
||||||
player.TargetPath = player.TargetPath[1:] // Move to next tile in path if any
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func HandleInput(player *Player, mapGrid [][]Tile, camera *rl.Camera) {
|
|
||||||
clickedTile, clicked := GetTileAtMouse(mapGrid, camera)
|
|
||||||
if clicked {
|
|
||||||
path := FindPath(mapGrid, mapGrid[player.PosTile.X][player.PosTile.Y], clickedTile)
|
|
||||||
if path != nil {
|
|
||||||
// Exclude the first tile (current position)
|
|
||||||
if len(path) > 1 {
|
|
||||||
player.TargetPath = path[1:]
|
|
||||||
player.ActionQueue = append(player.ActionQueue, Action{Type: MoveAction, X: clickedTile.X, Y: clickedTile.Y})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func UpdateCamera(camera *rl.Camera3D, player rl.Vector3, deltaTime float32) {
|
|
||||||
// Update camera based on mouse wheel
|
|
||||||
wheelMove := rl.GetMouseWheelMove()
|
|
||||||
if wheelMove != 0 {
|
|
||||||
cameraDistance += -wheelMove * 5
|
|
||||||
if cameraDistance < 10 {
|
|
||||||
cameraDistance = 10
|
|
||||||
}
|
|
||||||
if cameraDistance > 250 {
|
|
||||||
cameraDistance = 250
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Orbit camera around the player using arrow keys
|
|
||||||
if rl.IsKeyDown(rl.KeyRight) {
|
|
||||||
cameraYaw += 100 * deltaTime
|
|
||||||
}
|
|
||||||
if rl.IsKeyDown(rl.KeyLeft) {
|
|
||||||
cameraYaw -= 100 * deltaTime
|
|
||||||
}
|
|
||||||
if rl.IsKeyDown(rl.KeyUp) {
|
|
||||||
cameraPitch -= 50 * deltaTime
|
|
||||||
if cameraPitch < 20 {
|
|
||||||
cameraPitch = 20
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if rl.IsKeyDown(rl.KeyDown) {
|
|
||||||
cameraPitch += 50 * deltaTime
|
|
||||||
if cameraPitch > 85 {
|
|
||||||
cameraPitch = 85
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the new camera position using spherical coordinates
|
|
||||||
cameraYawRad := float64(cameraYaw) * rl.Deg2rad
|
|
||||||
cameraPitchRad := float64(cameraPitch) * rl.Deg2rad
|
|
||||||
cameraPos := rl.Vector3{
|
|
||||||
X: player.X + cameraDistance*float32(math.Cos(cameraYawRad))*float32(math.Cos(cameraPitchRad)),
|
|
||||||
Y: player.Y + cameraDistance*float32(math.Sin(cameraPitchRad)),
|
|
||||||
Z: player.Z + cameraDistance*float32(math.Sin(cameraYawRad))*float32(math.Cos(cameraPitchRad)),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the camera's position and target
|
|
||||||
camera.Position = cameraPos
|
|
||||||
camera.Target = rl.NewVector3(player.X, player.Y, player.Z)
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
logging.Info.Println("Starting GoonScape client")
|
||||||
|
|
||||||
|
// Raylib log level warn
|
||||||
|
rl.SetTraceLogLevel(rl.LogWarning)
|
||||||
|
// Parse command line flags
|
||||||
|
local := flag.Bool("local", false, "Connect to local server")
|
||||||
|
addr := flag.String("addr", "", "Server address (host or host:port)")
|
||||||
|
noMusic := flag.Bool("no-music", false, "Disable music playback")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// Set config before any game initialization
|
||||||
|
config.Current.PlayMusic = !*noMusic
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Info.Println("Initializing window")
|
||||||
rl.InitWindow(1024, 768, "GoonScape")
|
rl.InitWindow(1024, 768, "GoonScape")
|
||||||
defer rl.CloseWindow()
|
defer func() {
|
||||||
rl.InitAudioDevice()
|
logging.Info.Println("Closing window")
|
||||||
|
rl.CloseWindow()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Initialize audio device first
|
||||||
|
if !rl.IsAudioDeviceReady() {
|
||||||
|
rl.InitAudioDevice()
|
||||||
|
if !rl.IsAudioDeviceReady() {
|
||||||
|
log.Fatal("Failed to initialize audio device")
|
||||||
|
}
|
||||||
|
}
|
||||||
defer rl.CloseAudioDevice()
|
defer rl.CloseAudioDevice()
|
||||||
|
|
||||||
mapGrid := InitMap()
|
game := game.New()
|
||||||
|
logging.Info.Println("Loading game assets")
|
||||||
player := Player{
|
if err := game.LoadAssets(); err != nil {
|
||||||
PosActual: rl.NewVector3(5*TileSize, 0, 5*TileSize),
|
log.Fatalf("Failed to load assets: %v", err)
|
||||||
PosTile: mapGrid[5][5],
|
|
||||||
Speed: 50.0,
|
|
||||||
TargetPath: []Tile{},
|
|
||||||
}
|
}
|
||||||
|
defer func() {
|
||||||
|
logging.Info.Println("Cleaning up game resources")
|
||||||
|
game.Cleanup()
|
||||||
|
}()
|
||||||
|
|
||||||
camera := rl.Camera3D{
|
if config.Current.PlayMusic {
|
||||||
Position: rl.NewVector3(0, 10, 10), // Will be updated every frame
|
logging.Info.Println("Starting music playback")
|
||||||
Target: player.PosActual,
|
rl.PlayMusicStream(game.Music)
|
||||||
Up: rl.NewVector3(0, 1, 0), // Y is up in 3D
|
rl.SetMusicVolume(game.Music, 0.5)
|
||||||
Fovy: 45.0,
|
|
||||||
Projection: rl.CameraPerspective,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, playerID, err := ConnectToServer()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to connect to server: %v", err)
|
|
||||||
}
|
|
||||||
log.Printf("Player ID: %d", playerID)
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
go HandleServerCommunication(conn, playerID, &player)
|
|
||||||
|
|
||||||
playerModel := rl.LoadModel("resources/models/goonion.obj")
|
|
||||||
defer rl.UnloadModel(playerModel)
|
|
||||||
playerTexture := rl.LoadTexture("resources/models/goonion.png")
|
|
||||||
defer rl.UnloadTexture(playerTexture)
|
|
||||||
rl.SetMaterialTexture(playerModel.Materials, rl.MapDiffuse, playerTexture)
|
|
||||||
|
|
||||||
coomerModel := rl.LoadModel("resources/models/coomer.obj")
|
|
||||||
defer rl.UnloadModel(coomerModel)
|
|
||||||
coomerTexture := rl.LoadTexture("resources/models/coomer.png")
|
|
||||||
defer rl.UnloadTexture(coomerTexture)
|
|
||||||
rl.SetMaterialTexture(coomerModel.Materials, rl.MapDiffuse, coomerTexture)
|
|
||||||
|
|
||||||
rl.SetTargetFPS(60)
|
rl.SetTargetFPS(60)
|
||||||
|
logging.Info.Println("Starting game loop")
|
||||||
// Music
|
game.Run()
|
||||||
music := rl.LoadMusicStream("resources/audio/GoonScape2.mp3")
|
logging.Info.Println("Game exited cleanly")
|
||||||
rl.PlayMusicStream(music)
|
|
||||||
rl.SetMusicVolume(music, 0.5)
|
|
||||||
defer rl.UnloadMusicStream(music)
|
|
||||||
|
|
||||||
for !rl.WindowShouldClose() {
|
|
||||||
|
|
||||||
rl.UpdateMusicStream(music)
|
|
||||||
|
|
||||||
// Time management
|
|
||||||
deltaTime := rl.GetFrameTime()
|
|
||||||
|
|
||||||
// Handle input
|
|
||||||
HandleInput(&player, mapGrid, &camera)
|
|
||||||
|
|
||||||
// Update player
|
|
||||||
if len(player.TargetPath) > 0 {
|
|
||||||
player.MoveTowards(player.TargetPath[0], deltaTime, mapGrid)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update camera
|
|
||||||
UpdateCamera(&camera, player.PosActual, deltaTime)
|
|
||||||
|
|
||||||
// Rendering
|
|
||||||
rl.BeginDrawing()
|
|
||||||
rl.ClearBackground(rl.RayWhite)
|
|
||||||
rl.BeginMode3D(camera)
|
|
||||||
DrawMap(mapGrid)
|
|
||||||
DrawPlayer(player, &playerModel, mapGrid)
|
|
||||||
|
|
||||||
rl.DrawModel(coomerModel, rl.NewVector3(5*TileSize+32, 32, 5*TileSize+32), 16, rl.White)
|
|
||||||
|
|
||||||
rl.DrawFPS(10, 10)
|
|
||||||
|
|
||||||
rl.EndMode3D()
|
|
||||||
rl.EndDrawing()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ConnectToServer() (net.Conn, int32, error) {
|
|
||||||
// Attempt to connect to the server
|
|
||||||
conn, err := net.Dial("tcp", serverAddr)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to dial server: %v", err)
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Connected to server. Waiting for player ID...")
|
|
||||||
// Buffer for incoming server message
|
|
||||||
buf := make([]byte, 1024)
|
|
||||||
n, err := conn.Read(buf)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error reading player ID from server: %v", err)
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Received data: %x", buf[:n])
|
|
||||||
|
|
||||||
// Unmarshal server message to extract the player ID
|
|
||||||
var response pb.ServerMessage
|
|
||||||
if err := proto.Unmarshal(buf[:n], &response); err != nil {
|
|
||||||
log.Printf("Failed to unmarshal server response: %v", err)
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
playerID := response.GetPlayerId()
|
|
||||||
log.Printf("Successfully connected with player ID: %d", playerID)
|
|
||||||
return conn, playerID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func HandleServerCommunication(conn net.Conn, playerID int32, player *Player) {
|
|
||||||
for {
|
|
||||||
// Check if there are actions in the player's queue
|
|
||||||
if len(player.ActionQueue) > 0 {
|
|
||||||
// Process the first action in the queue
|
|
||||||
actionData := player.ActionQueue[0]
|
|
||||||
action := &pb.Action{
|
|
||||||
PlayerId: playerID,
|
|
||||||
Type: pb.Action_MOVE,
|
|
||||||
X: int32(actionData.X),
|
|
||||||
Y: int32(actionData.Y),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serialize the action
|
|
||||||
data, err := proto.Marshal(action)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to marshal action: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send action to server
|
|
||||||
_, err = conn.Write(data)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to send action to server: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the action from the queue once it's sent
|
|
||||||
player.ActionQueue = player.ActionQueue[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a delay based on the server's tick rate
|
|
||||||
time.Sleep(TickRate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// pathfinding
|
|
||||||
type Node struct {
|
|
||||||
Tile Tile
|
|
||||||
Parent *Node
|
|
||||||
G, H, F float32
|
|
||||||
}
|
|
||||||
|
|
||||||
func FindPath(mapGrid [][]Tile, start, end Tile) []Tile {
|
|
||||||
openList := []*Node{}
|
|
||||||
closedList := make(map[[2]int]bool)
|
|
||||||
|
|
||||||
startNode := &Node{Tile: start, G: 0, H: heuristic(start, end)}
|
|
||||||
startNode.F = startNode.G + startNode.H
|
|
||||||
openList = append(openList, startNode)
|
|
||||||
|
|
||||||
for len(openList) > 0 {
|
|
||||||
// Find node with lowest F
|
|
||||||
current := openList[0]
|
|
||||||
currentIndex := 0
|
|
||||||
for i, node := range openList {
|
|
||||||
if node.F < current.F {
|
|
||||||
current = node
|
|
||||||
currentIndex = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move current to closed list
|
|
||||||
openList = append(openList[:currentIndex], openList[currentIndex+1:]...)
|
|
||||||
closedList[[2]int{current.Tile.X, current.Tile.Y}] = true
|
|
||||||
|
|
||||||
// Check if reached the end
|
|
||||||
if current.Tile.X == end.X && current.Tile.Y == end.Y {
|
|
||||||
path := []Tile{}
|
|
||||||
node := current
|
|
||||||
for node != nil {
|
|
||||||
path = append([]Tile{node.Tile}, path...)
|
|
||||||
node = node.Parent
|
|
||||||
}
|
|
||||||
fmt.Printf("Path found: %v\n", path)
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate neighbors
|
|
||||||
neighbors := GetNeighbors(mapGrid, current.Tile)
|
|
||||||
for _, neighbor := range neighbors {
|
|
||||||
if !neighbor.Walkable || closedList[[2]int{neighbor.X, neighbor.Y}] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
tentativeG := current.G + distance(current.Tile, neighbor)
|
|
||||||
inOpen := false
|
|
||||||
var existingNode *Node
|
|
||||||
for _, node := range openList {
|
|
||||||
if node.Tile.X == neighbor.X && node.Tile.Y == neighbor.Y {
|
|
||||||
existingNode = node
|
|
||||||
inOpen = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !inOpen || tentativeG < existingNode.G {
|
|
||||||
newNode := &Node{
|
|
||||||
Tile: neighbor,
|
|
||||||
Parent: current,
|
|
||||||
G: tentativeG,
|
|
||||||
H: heuristic(neighbor, end),
|
|
||||||
}
|
|
||||||
newNode.F = newNode.G + newNode.H
|
|
||||||
if !inOpen {
|
|
||||||
openList = append(openList, newNode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No path found
|
|
||||||
fmt.Println("No path found")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func heuristic(a, b Tile) float32 {
|
|
||||||
return float32(abs(a.X-b.X) + abs(a.Y-b.Y))
|
|
||||||
}
|
|
||||||
|
|
||||||
func distance(a, b Tile) float32 {
|
|
||||||
_ = a
|
|
||||||
_ = b
|
|
||||||
return 1.0 //uniform cost for now
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetNeighbors(mapGrid [][]Tile, tile Tile) []Tile {
|
|
||||||
directions := [][2]int{
|
|
||||||
{1, 0}, {-1, 0}, {0, 1}, {0, -1},
|
|
||||||
{1, 1}, {-1, -1}, {1, -1}, {-1, 1},
|
|
||||||
}
|
|
||||||
neighbors := []Tile{}
|
|
||||||
for _, dir := range directions {
|
|
||||||
nx, ny := tile.X+dir[0], tile.Y+dir[1]
|
|
||||||
if nx >= 0 && nx < MapWidth && ny >= 0 && ny < MapHeight {
|
|
||||||
neighbors = append(neighbors, mapGrid[nx][ny])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return neighbors
|
|
||||||
}
|
|
||||||
|
|
||||||
func abs(x int) int {
|
|
||||||
if x < 0 {
|
|
||||||
return -x
|
|
||||||
}
|
|
||||||
return x
|
|
||||||
}
|
}
|
||||||
|
240
network/network.go
Normal file
240
network/network.go
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.boner.be/bdnugget/goonscape/logging"
|
||||||
|
"gitea.boner.be/bdnugget/goonscape/types"
|
||||||
|
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
const protoVersion = 1
|
||||||
|
|
||||||
|
var serverAddr = "boner.be:6969"
|
||||||
|
|
||||||
|
type NetworkManager struct {
|
||||||
|
ctx context.Context
|
||||||
|
conn net.Conn
|
||||||
|
reader *bufio.Reader
|
||||||
|
writer *bufio.Writer
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNetworkManager(ctx context.Context) *NetworkManager {
|
||||||
|
return &NetworkManager{
|
||||||
|
ctx: ctx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetServerAddr(addr string) {
|
||||||
|
serverAddr = addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nm *NetworkManager) Connect(addr string) error {
|
||||||
|
nm.mu.Lock()
|
||||||
|
defer nm.mu.Unlock()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
nm.conn, err = net.Dial("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
nm.reader = bufio.NewReader(nm.conn)
|
||||||
|
nm.writer = bufio.NewWriter(nm.conn)
|
||||||
|
|
||||||
|
go nm.readLoop()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nm *NetworkManager) readLoop() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-nm.ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
// Read and process messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nm *NetworkManager) Send(message proto.Message) error {
|
||||||
|
nm.mu.Lock()
|
||||||
|
defer nm.mu.Unlock()
|
||||||
|
|
||||||
|
data, err := proto.Marshal(message)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write length prefix
|
||||||
|
lengthBuf := make([]byte, 4)
|
||||||
|
binary.BigEndian.PutUint32(lengthBuf, uint32(len(data)))
|
||||||
|
if _, err := nm.writer.Write(lengthBuf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write message body
|
||||||
|
if _, err := nm.writer.Write(data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nm.writer.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConnectToServer(username, password string, isRegistering bool) (net.Conn, int32, error) {
|
||||||
|
logging.Info.Println("Attempting to connect to server at", serverAddr)
|
||||||
|
conn, err := net.Dial("tcp", serverAddr)
|
||||||
|
if err != nil {
|
||||||
|
logging.Error.Printf("Failed to dial server: %v", err)
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
logging.Info.Println("Connected to server. Authenticating...")
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
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
|
||||||
|
if err := proto.Unmarshal(messageBuf, &response); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
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("authentication failed: %s", response.ErrorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
playerID := response.GetPlayerId()
|
||||||
|
log.Printf("Successfully authenticated with player ID: %d", playerID)
|
||||||
|
return conn, playerID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Player, otherPlayers *sync.Map, quitChan <-chan struct{}) {
|
||||||
|
defer func() {
|
||||||
|
logging.Info.Println("Closing connection and cleaning up for player", playerID)
|
||||||
|
conn.Close()
|
||||||
|
close(player.QuitDone)
|
||||||
|
}()
|
||||||
|
|
||||||
|
reader := bufio.NewReader(conn)
|
||||||
|
ticker := time.NewTicker(100 * time.Millisecond)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-quitChan:
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
// Read message length
|
||||||
|
lengthBuf := make([]byte, 4)
|
||||||
|
if _, err := io.ReadFull(reader, lengthBuf); err != nil {
|
||||||
|
log.Printf("Failed to read message length: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
messageLength := binary.BigEndian.Uint32(lengthBuf)
|
||||||
|
|
||||||
|
// Read message body
|
||||||
|
messageBuf := make([]byte, messageLength)
|
||||||
|
if _, err := io.ReadFull(reader, messageBuf); err != nil {
|
||||||
|
log.Printf("Failed to read message body: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process server message
|
||||||
|
var serverMessage pb.ServerMessage
|
||||||
|
if err := proto.Unmarshal(messageBuf, &serverMessage); err != nil {
|
||||||
|
log.Printf("Failed to unmarshal server message: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update player states
|
||||||
|
for _, state := range serverMessage.Players {
|
||||||
|
if state == nil {
|
||||||
|
logging.Error.Println("Received nil player state")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.PlayerId == playerID {
|
||||||
|
player.UpdatePosition(state, types.ServerTickRate)
|
||||||
|
} else if existing, ok := otherPlayers.Load(state.PlayerId); ok {
|
||||||
|
existing.(*types.Player).UpdatePosition(state, types.ServerTickRate)
|
||||||
|
} else {
|
||||||
|
newPlayer := types.NewPlayer(state)
|
||||||
|
otherPlayers.Store(state.PlayerId, newPlayer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle chat messages
|
||||||
|
if handler, ok := player.UserData.(types.ChatMessageHandler); ok {
|
||||||
|
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/models/coomer/Animation_Confused_Scratch_withSkin.glb
Normal file
BIN
resources/models/coomer/Animation_Confused_Scratch_withSkin.glb
Normal file
Binary file not shown.
BIN
resources/models/coomer/Animation_Idle_withSkin.glb
Normal file
BIN
resources/models/coomer/Animation_Idle_withSkin.glb
Normal file
Binary file not shown.
BIN
resources/models/coomer/Animation_Running_withSkin.glb
Normal file
BIN
resources/models/coomer/Animation_Running_withSkin.glb
Normal file
Binary file not shown.
BIN
resources/models/coomer/Animation_Unsteady_Walk_withSkin.glb
Normal file
BIN
resources/models/coomer/Animation_Unsteady_Walk_withSkin.glb
Normal file
Binary file not shown.
BIN
resources/models/coomer/Animation_Walking_withSkin.glb
Normal file
BIN
resources/models/coomer/Animation_Walking_withSkin.glb
Normal file
Binary file not shown.
BIN
resources/models/coomer/idle_notransy.glb
Normal file
BIN
resources/models/coomer/idle_notransy.glb
Normal file
Binary file not shown.
BIN
resources/models/coomer/unsteadywalk_notransy.glb
Normal file
BIN
resources/models/coomer/unsteadywalk_notransy.glb
Normal file
Binary file not shown.
BIN
resources/models/coomerAnim.zip
Normal file
BIN
resources/models/coomerAnim.zip
Normal file
Binary file not shown.
BIN
resources/models/gooner/idle_no_y_transform.glb
Normal file
BIN
resources/models/gooner/idle_no_y_transform.glb
Normal file
Binary file not shown.
BIN
resources/models/gooner/walk_no_y_transform.glb
Normal file
BIN
resources/models/gooner/walk_no_y_transform.glb
Normal file
Binary file not shown.
12
resources/models/shreke.mtl
Normal file
12
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
resources/models/shreke.obj
Normal file
210035
resources/models/shreke.obj
Normal file
File diff suppressed because it is too large
Load Diff
BIN
resources/models/shreke.png
Normal file
BIN
resources/models/shreke.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.8 MiB |
BIN
resources/models/shreke/Animation_Excited_Walk_M_withSkin.glb
Normal file
BIN
resources/models/shreke/Animation_Excited_Walk_M_withSkin.glb
Normal file
Binary file not shown.
BIN
resources/models/shreke/Animation_Running_withSkin.glb
Normal file
BIN
resources/models/shreke/Animation_Running_withSkin.glb
Normal file
Binary file not shown.
BIN
resources/models/shreke/Animation_Slow_Orc_Walk_withSkin.glb
Normal file
BIN
resources/models/shreke/Animation_Slow_Orc_Walk_withSkin.glb
Normal file
Binary file not shown.
BIN
resources/models/shreke/Animation_Unsteady_Walk_withSkin.glb
Normal file
BIN
resources/models/shreke/Animation_Unsteady_Walk_withSkin.glb
Normal file
Binary file not shown.
BIN
resources/models/shreke/Animation_Walking_withSkin.glb
Normal file
BIN
resources/models/shreke/Animation_Walking_withSkin.glb
Normal file
Binary file not shown.
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
|
126
segfault.txt
Normal file
126
segfault.txt
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
INFO: 2025/02/10 15:53:21 main.go:16: Starting GoonScape client
|
||||||
|
INFO: 2025/02/10 15:53:21 main.go:43: Initializing window
|
||||||
|
INFO: 2025/02/10 15:53:21 main.go:60: Loading game assets
|
||||||
|
INFO: 2025/02/10 15:53:21 game.go:61: Loading game assets
|
||||||
|
INFO: 2025/02/10 15:53:21 assets.go:51: Loading models
|
||||||
|
INFO: 2025/02/10 15:53:21 game.go:81: Music disabled by config
|
||||||
|
INFO: 2025/02/10 15:53:21 game.go:84: Assets loaded successfully
|
||||||
|
INFO: 2025/02/10 15:53:21 main.go:76: Starting game loop
|
||||||
|
INFO: 2025/02/10 15:53:21 game.go:456: Starting game loop
|
||||||
|
malloc(): unsorted double linked list corrupted
|
||||||
|
SIGABRT: abort
|
||||||
|
PC=0x78a790604c4c m=0 sigcode=18446744073709551610
|
||||||
|
signal arrived during cgo execution
|
||||||
|
|
||||||
|
goroutine 1 gp=0xc0000061c0 m=0 mp=0xaf8e20 [syscall, locked to thread]:
|
||||||
|
runtime.cgocall(0x6da9a0, 0xc00002da50)
|
||||||
|
/usr/lib/go/src/runtime/cgocall.go:167 +0x5b fp=0xc00002da28 sp=0xc00002d9f0 pc=0x46d0fb
|
||||||
|
github.com/gen2brain/raylib-go/raylib._Cfunc_EndDrawing()
|
||||||
|
_cgo_gotypes.go:2539 +0x3f fp=0xc00002da50 sp=0xc00002da28 pc=0x51cdbf
|
||||||
|
github.com/gen2brain/raylib-go/raylib.EndDrawing(...)
|
||||||
|
/home/bd/.cache/go/mod/github.com/gen2brain/raylib-go/raylib@v0.0.0-20250109172833-6dbba4f81a9b/rcore.go:464
|
||||||
|
gitea.boner.be/bdnugget/goonscape/game.(*Game).Render.func1()
|
||||||
|
/home/bd/Projects/go/goonscape/game/game.go:259 +0x36 fp=0xc00002da68 sp=0xc00002da50 pc=0x6b6fb6
|
||||||
|
runtime.deferreturn()
|
||||||
|
/usr/lib/go/src/runtime/panic.go:605 +0x5e fp=0xc00002daf8 sp=0xc00002da68 pc=0x4396fe
|
||||||
|
gitea.boner.be/bdnugget/goonscape/game.(*Game).Render(0xc0001ae000)
|
||||||
|
/home/bd/Projects/go/goonscape/game/game.go:265 +0x405 fp=0xc00002dbf8 sp=0xc00002daf8 pc=0x6b27a5
|
||||||
|
gitea.boner.be/bdnugget/goonscape/game.(*Game).Run(0xc0001ae000)
|
||||||
|
/home/bd/Projects/go/goonscape/game/game.go:463 +0x105 fp=0xc00002dcc0 sp=0xc00002dbf8 pc=0x6b3e85
|
||||||
|
main.main()
|
||||||
|
/home/bd/Projects/go/goonscape/main.go:77 +0xb85 fp=0xc00002df50 sp=0xc00002dcc0 pc=0x6b8345
|
||||||
|
runtime.main()
|
||||||
|
/usr/lib/go/src/runtime/proc.go:272 +0x28b fp=0xc00002dfe0 sp=0xc00002df50 pc=0x43d84b
|
||||||
|
runtime.goexit({})
|
||||||
|
/usr/lib/go/src/runtime/asm_amd64.s:1700 +0x1 fp=0xc00002dfe8 sp=0xc00002dfe0 pc=0x47ac81
|
||||||
|
|
||||||
|
goroutine 2 gp=0xc000006c40 m=nil [force gc (idle)]:
|
||||||
|
runtime.gopark(0xaedc20?, 0xaf8e20?, 0x0?, 0x0?, 0x0?)
|
||||||
|
/usr/lib/go/src/runtime/proc.go:424 +0xce fp=0xc0000567a8 sp=0xc000056788 pc=0x47356e
|
||||||
|
runtime.goparkunlock(...)
|
||||||
|
/usr/lib/go/src/runtime/proc.go:430
|
||||||
|
runtime.forcegchelper()
|
||||||
|
/usr/lib/go/src/runtime/proc.go:337 +0xb3 fp=0xc0000567e0 sp=0xc0000567a8 pc=0x43db93
|
||||||
|
runtime.goexit({})
|
||||||
|
/usr/lib/go/src/runtime/asm_amd64.s:1700 +0x1 fp=0xc0000567e8 sp=0xc0000567e0 pc=0x47ac81
|
||||||
|
created by runtime.init.7 in goroutine 1
|
||||||
|
/usr/lib/go/src/runtime/proc.go:325 +0x1a
|
||||||
|
|
||||||
|
goroutine 3 gp=0xc000007180 m=nil [GC sweep wait]:
|
||||||
|
runtime.gopark(0x0?, 0x0?, 0x0?, 0x0?, 0x0?)
|
||||||
|
/usr/lib/go/src/runtime/proc.go:424 +0xce fp=0xc00006af80 sp=0xc00006af60 pc=0x47356e
|
||||||
|
runtime.goparkunlock(...)
|
||||||
|
/usr/lib/go/src/runtime/proc.go:430
|
||||||
|
runtime.bgsweep(0xc000080000)
|
||||||
|
/usr/lib/go/src/runtime/mgcsweep.go:277 +0x94 fp=0xc00006afc8 sp=0xc00006af80 pc=0x428714
|
||||||
|
runtime.gcenable.gowrap1()
|
||||||
|
/usr/lib/go/src/runtime/mgc.go:204 +0x25 fp=0xc00006afe0 sp=0xc00006afc8 pc=0x41ce25
|
||||||
|
runtime.goexit({})
|
||||||
|
/usr/lib/go/src/runtime/asm_amd64.s:1700 +0x1 fp=0xc00006afe8 sp=0xc00006afe0 pc=0x47ac81
|
||||||
|
created by runtime.gcenable in goroutine 1
|
||||||
|
/usr/lib/go/src/runtime/mgc.go:204 +0x66
|
||||||
|
|
||||||
|
goroutine 4 gp=0xc000007340 m=nil [GC scavenge wait]:
|
||||||
|
runtime.gopark(0xc000080000?, 0x8e5b60?, 0x1?, 0x0?, 0xc000007340?)
|
||||||
|
/usr/lib/go/src/runtime/proc.go:424 +0xce fp=0xc000064f78 sp=0xc000064f58 pc=0x47356e
|
||||||
|
runtime.goparkunlock(...)
|
||||||
|
/usr/lib/go/src/runtime/proc.go:430
|
||||||
|
runtime.(*scavengerState).park(0xaf8060)
|
||||||
|
/usr/lib/go/src/runtime/mgcscavenge.go:425 +0x49 fp=0xc000064fa8 sp=0xc000064f78 pc=0x426149
|
||||||
|
runtime.bgscavenge(0xc000080000)
|
||||||
|
/usr/lib/go/src/runtime/mgcscavenge.go:653 +0x3c fp=0xc000064fc8 sp=0xc000064fa8 pc=0x4266bc
|
||||||
|
runtime.gcenable.gowrap2()
|
||||||
|
/usr/lib/go/src/runtime/mgc.go:205 +0x25 fp=0xc000064fe0 sp=0xc000064fc8 pc=0x41cdc5
|
||||||
|
runtime.goexit({})
|
||||||
|
/usr/lib/go/src/runtime/asm_amd64.s:1700 +0x1 fp=0xc000064fe8 sp=0xc000064fe0 pc=0x47ac81
|
||||||
|
created by runtime.gcenable in goroutine 1
|
||||||
|
/usr/lib/go/src/runtime/mgc.go:205 +0xa5
|
||||||
|
|
||||||
|
goroutine 18 gp=0xc000104700 m=nil [finalizer wait]:
|
||||||
|
runtime.gopark(0x0?, 0x0?, 0x0?, 0x0?, 0x0?)
|
||||||
|
/usr/lib/go/src/runtime/proc.go:424 +0xce fp=0xc000184e20 sp=0xc000184e00 pc=0x47356e
|
||||||
|
runtime.runfinq()
|
||||||
|
/usr/lib/go/src/runtime/mfinal.go:193 +0x145 fp=0xc000184fe0 sp=0xc000184e20 pc=0x41bea5
|
||||||
|
runtime.goexit({})
|
||||||
|
/usr/lib/go/src/runtime/asm_amd64.s:1700 +0x1 fp=0xc000184fe8 sp=0xc000184fe0 pc=0x47ac81
|
||||||
|
created by runtime.createfing in goroutine 1
|
||||||
|
/usr/lib/go/src/runtime/mfinal.go:163 +0x3d
|
||||||
|
|
||||||
|
goroutine 19 gp=0xc0001048c0 m=nil [chan receive]:
|
||||||
|
runtime.gopark(0x0?, 0x0?, 0x0?, 0x0?, 0x0?)
|
||||||
|
/usr/lib/go/src/runtime/proc.go:424 +0xce fp=0xc00006bf18 sp=0xc00006bef8 pc=0x47356e
|
||||||
|
runtime.chanrecv(0xc0001140e0, 0x0, 0x1)
|
||||||
|
/usr/lib/go/src/runtime/chan.go:639 +0x3bc fp=0xc00006bf90 sp=0xc00006bf18 pc=0x40c89c
|
||||||
|
runtime.chanrecv1(0x0?, 0x0?)
|
||||||
|
/usr/lib/go/src/runtime/chan.go:489 +0x12 fp=0xc00006bfb8 sp=0xc00006bf90 pc=0x40c4d2
|
||||||
|
runtime.unique_runtime_registerUniqueMapCleanup.func1(...)
|
||||||
|
/usr/lib/go/src/runtime/mgc.go:1781
|
||||||
|
runtime.unique_runtime_registerUniqueMapCleanup.gowrap1()
|
||||||
|
/usr/lib/go/src/runtime/mgc.go:1784 +0x2f fp=0xc00006bfe0 sp=0xc00006bfb8 pc=0x41fe4f
|
||||||
|
runtime.goexit({})
|
||||||
|
/usr/lib/go/src/runtime/asm_amd64.s:1700 +0x1 fp=0xc00006bfe8 sp=0xc00006bfe0 pc=0x47ac81
|
||||||
|
created by unique.runtime_registerUniqueMapCleanup in goroutine 1
|
||||||
|
/usr/lib/go/src/runtime/mgc.go:1779 +0x96
|
||||||
|
|
||||||
|
rax 0x0
|
||||||
|
rbx 0x4829
|
||||||
|
rcx 0x78a790604c4c
|
||||||
|
rdx 0x6
|
||||||
|
rdi 0x4829
|
||||||
|
rsi 0x4829
|
||||||
|
rbp 0x78a7902fc740
|
||||||
|
rsp 0x7ffd63425870
|
||||||
|
r8 0xffffffff
|
||||||
|
r9 0x0
|
||||||
|
r10 0x8
|
||||||
|
r11 0x246
|
||||||
|
r12 0x7ffd634259d0
|
||||||
|
r13 0x6
|
||||||
|
r14 0x7ffd634259d0
|
||||||
|
r15 0x7ffd634259d0
|
||||||
|
rip 0x78a790604c4c
|
||||||
|
rflags 0x246
|
||||||
|
cs 0x33
|
||||||
|
fs 0x0
|
||||||
|
gs 0x0
|
||||||
|
exit status 2
|
128
types/player.go
Normal file
128
types/player.go
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
||||||
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Player struct {
|
||||||
|
sync.RWMutex
|
||||||
|
PosActual rl.Vector3
|
||||||
|
PosTile Tile
|
||||||
|
TargetPath []Tile
|
||||||
|
ActionQueue []*pb.Action
|
||||||
|
Speed float32
|
||||||
|
ID int32
|
||||||
|
IsMoving bool
|
||||||
|
AnimationFrame int32
|
||||||
|
LastAnimUpdate time.Time
|
||||||
|
LastUpdateTime time.Time
|
||||||
|
InterpolationProgress float32
|
||||||
|
FloatingMessage *FloatingMessage
|
||||||
|
QuitDone chan struct{}
|
||||||
|
UserData interface{}
|
||||||
|
Model rl.Model
|
||||||
|
Texture rl.Texture2D
|
||||||
|
Username string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Player) MoveTowards(target Tile, deltaTime float32, mapGrid [][]Tile) {
|
||||||
|
p.Lock()
|
||||||
|
defer p.Unlock()
|
||||||
|
|
||||||
|
targetPos := rl.Vector3{
|
||||||
|
X: float32(target.X * TileSize),
|
||||||
|
Y: mapGrid[target.X][target.Y].Height * TileHeight,
|
||||||
|
Z: float32(target.Y * TileSize),
|
||||||
|
}
|
||||||
|
|
||||||
|
direction := rl.Vector3Subtract(targetPos, p.PosActual)
|
||||||
|
distance := rl.Vector3Length(direction)
|
||||||
|
|
||||||
|
if distance > 1.0 {
|
||||||
|
wasMoving := p.IsMoving
|
||||||
|
p.IsMoving = true
|
||||||
|
|
||||||
|
if !wasMoving {
|
||||||
|
p.AnimationFrame = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
oldFrame := p.AnimationFrame
|
||||||
|
p.AnimationFrame += int32(deltaTime * 60)
|
||||||
|
rl.TraceLog(rl.LogInfo, "Walk frame update: %d -> %d (delta: %f)",
|
||||||
|
oldFrame, p.AnimationFrame, deltaTime)
|
||||||
|
} else {
|
||||||
|
wasMoving := p.IsMoving
|
||||||
|
p.IsMoving = false
|
||||||
|
|
||||||
|
if wasMoving {
|
||||||
|
p.AnimationFrame = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
oldFrame := p.AnimationFrame
|
||||||
|
p.AnimationFrame += int32(deltaTime * 60)
|
||||||
|
rl.TraceLog(rl.LogInfo, "Idle frame update: %d -> %d (delta: %f)",
|
||||||
|
oldFrame, p.AnimationFrame, deltaTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
if distance > 0 {
|
||||||
|
direction = rl.Vector3Scale(direction, p.Speed*deltaTime/distance)
|
||||||
|
}
|
||||||
|
|
||||||
|
if distance > 1.0 {
|
||||||
|
p.PosActual = rl.Vector3Add(p.PosActual, direction)
|
||||||
|
} else {
|
||||||
|
p.PosActual = targetPos
|
||||||
|
p.PosTile = target
|
||||||
|
if len(p.TargetPath) > 1 {
|
||||||
|
p.TargetPath = p.TargetPath[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPlayer(state *pb.PlayerState) *Player {
|
||||||
|
return &Player{
|
||||||
|
PosActual: rl.Vector3{
|
||||||
|
X: float32(state.X * TileSize),
|
||||||
|
Y: float32(state.Y * TileHeight),
|
||||||
|
Z: float32(state.Y * TileSize),
|
||||||
|
},
|
||||||
|
PosTile: Tile{X: int(state.X), Y: int(state.Y)},
|
||||||
|
Speed: 50.0,
|
||||||
|
ID: state.PlayerId,
|
||||||
|
IsMoving: false,
|
||||||
|
AnimationFrame: 0,
|
||||||
|
LastAnimUpdate: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Player) UpdatePosition(state *pb.PlayerState, tickRate time.Duration) {
|
||||||
|
p.Lock()
|
||||||
|
defer p.Unlock()
|
||||||
|
|
||||||
|
targetTile := Tile{X: int(state.X), Y: int(state.Y)}
|
||||||
|
if p.PosTile != targetTile {
|
||||||
|
p.PosTile = targetTile
|
||||||
|
p.LastUpdateTime = time.Now()
|
||||||
|
p.InterpolationProgress = 0
|
||||||
|
p.TargetPath = []Tile{targetTile}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Player) ForceResync(state *pb.PlayerState) {
|
||||||
|
p.Lock()
|
||||||
|
defer p.Unlock()
|
||||||
|
|
||||||
|
p.PosTile = Tile{X: int(state.X), Y: int(state.Y)}
|
||||||
|
p.PosActual = rl.Vector3{
|
||||||
|
X: float32(state.X * TileSize),
|
||||||
|
Y: float32(state.Y * TileHeight),
|
||||||
|
Z: float32(state.Y * TileSize),
|
||||||
|
}
|
||||||
|
p.TargetPath = nil
|
||||||
|
p.ActionQueue = nil
|
||||||
|
p.InterpolationProgress = 1.0
|
||||||
|
}
|
68
types/types.go
Normal file
68
types/types.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
||||||
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Tile struct {
|
||||||
|
X, Y int
|
||||||
|
Height float32
|
||||||
|
Walkable bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnimationSet struct {
|
||||||
|
Idle []rl.ModelAnimation
|
||||||
|
Walk []rl.ModelAnimation
|
||||||
|
// Can add more animation types later like:
|
||||||
|
// Attack []ModelAnimation
|
||||||
|
// Jump []ModelAnimation
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModelAsset struct {
|
||||||
|
Model rl.Model
|
||||||
|
Texture rl.Texture2D
|
||||||
|
Animation []rl.ModelAnimation // Keep this for compatibility
|
||||||
|
AnimFrames int32 // Keep this for compatibility
|
||||||
|
Animations AnimationSet // New field for organized animations
|
||||||
|
YOffset float32 // Additional height offset (added to default 8.0)
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChatMessage struct {
|
||||||
|
PlayerID int32
|
||||||
|
Username string
|
||||||
|
Content string
|
||||||
|
Time time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type FloatingMessage struct {
|
||||||
|
Content string
|
||||||
|
ExpireTime time.Time
|
||||||
|
ScreenPos rl.Vector2 // Store the screen position for 2D rendering
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChatMessageHandler interface {
|
||||||
|
HandleServerMessages([]*pb.ChatMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlayerState struct {
|
||||||
|
PlayerId int32
|
||||||
|
X int32
|
||||||
|
Y int32
|
||||||
|
Username string
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
MapWidth = 50
|
||||||
|
MapHeight = 50
|
||||||
|
TileSize = 32
|
||||||
|
TileHeight = 2.0
|
||||||
|
|
||||||
|
// RuneScape-style tick rate (600ms)
|
||||||
|
ServerTickRate = 600 * time.Millisecond
|
||||||
|
ClientTickRate = 50 * time.Millisecond
|
||||||
|
MaxTickDesync = 5
|
||||||
|
)
|
@ -1 +0,0 @@
|
|||||||
package utils
|
|
Loading…
x
Reference in New Issue
Block a user