Compare commits
No commits in common. "master" and "develop" have entirely different histories.
15
.gitignore
vendored
15
.gitignore
vendored
@ -1,15 +0,0 @@
|
|||||||
# 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
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
|||||||
[submodule "goonserver"]
|
|
||||||
path = goonserver
|
|
||||||
url = https://gitea.boner.be/bdnugget/goonserver
|
|
@ -1,25 +0,0 @@
|
|||||||
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
5
2
@ -1,5 +0,0 @@
|
|||||||
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
|
|
@ -1,30 +0,0 @@
|
|||||||
FROM golang:1.23
|
|
||||||
|
|
||||||
# Install build dependencies
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
gcc-mingw-w64 \
|
|
||||||
cmake \
|
|
||||||
zip \
|
|
||||||
libasound2-dev \
|
|
||||||
mesa-common-dev \
|
|
||||||
libx11-dev \
|
|
||||||
libxrandr-dev \
|
|
||||||
libxi-dev \
|
|
||||||
xorg-dev \
|
|
||||||
libgl1-mesa-dev \
|
|
||||||
libglu1-mesa-dev \
|
|
||||||
libwayland-dev \
|
|
||||||
wayland-protocols \
|
|
||||||
libxkbcommon-dev \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /build
|
|
||||||
|
|
||||||
# Copy build scripts
|
|
||||||
COPY . /build/
|
|
||||||
|
|
||||||
# Set execute permissions
|
|
||||||
RUN chmod +x /build/scripts/build.sh
|
|
||||||
|
|
||||||
# Build command
|
|
||||||
CMD ["make", "all"]
|
|
24
LICENSE
24
LICENSE
@ -1,21 +1,11 @@
|
|||||||
MIT License
|
“Commons Clause” License Condition v1.0
|
||||||
|
|
||||||
Copyright (c) 2025 bdnugget
|
The Software is provided to you by the Licensor under the License, as defined below, subject to the following condition.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Without limiting other conditions in the License, the grant of rights under the License will not include, and the License does not grant to you, right to Sell the Software.
|
||||||
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
|
For purposes of the foregoing, “Sell” means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration (including without limitation fees for hosting or consulting/ support services related to the Software), a product or service whose value derives, entirely or substantially, from the functionality of the Software. Any license notice or attribution required by the License must also include this Commons Cause License Condition notice.
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
Software: GoonScape
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
License: Commons Clause v1.0
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
Licensor: bdnugget
|
||||||
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
30
Makefile
@ -1,30 +0,0 @@
|
|||||||
.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
136
README.md
@ -1,136 +0,0 @@
|
|||||||
# 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
175
assets/assets.go
@ -1,175 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
54
camera.go
Normal file
54
camera.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
@ -1,7 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
PlayMusic bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var Current = Config{PlayMusic: true}
|
|
11
constants.go
11
constants.go
@ -7,15 +7,6 @@ const (
|
|||||||
MapHeight = 50
|
MapHeight = 50
|
||||||
TileSize = 32
|
TileSize = 32
|
||||||
TileHeight = 2.0
|
TileHeight = 2.0
|
||||||
TickRate = 600 * time.Millisecond // Server tick rate (600ms)
|
TickRate = 2600 * time.Millisecond // Server tick rate (600ms)
|
||||||
serverAddr = "localhost:6969"
|
serverAddr = "localhost:6969"
|
||||||
|
|
||||||
// RuneScape-style tick rate (600ms)
|
|
||||||
ServerTickRate = 600 * time.Millisecond
|
|
||||||
|
|
||||||
// Client might run at a higher tick rate for smooth rendering
|
|
||||||
ClientTickRate = 50 * time.Millisecond
|
|
||||||
|
|
||||||
// Maximum number of ticks we can get behind before forcing a resync
|
|
||||||
MaxTickDesync = 5
|
|
||||||
)
|
)
|
||||||
|
@ -1,92 +0,0 @@
|
|||||||
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
234
game/chat.go
@ -1,234 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
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
469
game/game.go
@ -1,469 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
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
185
game/login.go
@ -1,185 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
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,17 +1,15 @@
|
|||||||
module gitea.boner.be/bdnugget/goonscape
|
module goonscape
|
||||||
|
|
||||||
go 1.23.0
|
go 1.23.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
gitea.boner.be/bdnugget/goonserver v1.1.0
|
gitea.boner.be/bdnugget/goonserver v0.0.0-20241011195320-f16e8647dc6b
|
||||||
github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b
|
github.com/gen2brain/raylib-go/raylib v0.0.0-20240930075631-c66f9e2942fe
|
||||||
google.golang.org/protobuf v1.36.3
|
google.golang.org/protobuf v1.35.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/ebitengine/purego v0.8.2 // indirect
|
github.com/ebitengine/purego v0.8.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3 // indirect
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
|
||||||
golang.org/x/sys v0.30.0 // indirect
|
golang.org/x/sys v0.26.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace gitea.boner.be/bdnugget/goonserver => ./goonserver
|
|
||||||
|
26
go.sum
26
go.sum
@ -1,16 +1,14 @@
|
|||||||
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
gitea.boner.be/bdnugget/goonserver v0.0.0-20241011195320-f16e8647dc6b h1:hdhCZH0YGqCsnSl6ru+8I7rxvCyOj5pCtf92urwyruA=
|
||||||
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
gitea.boner.be/bdnugget/goonserver v0.0.0-20241011195320-f16e8647dc6b/go.mod h1:inR1bKrr/vcTba+G1KzmmY6vssMq9oGNOk836VwPa4c=
|
||||||
github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b h1:JJfspevP3YOXcSKVABizYOv++yMpTJIdPUtoDzF/RWw=
|
github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE=
|
||||||
github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b/go.mod h1:BaY76bZk7nw1/kVOSQObPY1v1iwVE1KHAGMfvI6oK1Q=
|
github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
|
github.com/gen2brain/raylib-go/raylib v0.0.0-20240930075631-c66f9e2942fe h1:mInjrbJkUglTM7tBmXG+epnPCE744aj15J7vjJwM4gs=
|
||||||
|
github.com/gen2brain/raylib-go/raylib v0.0.0-20240930075631-c66f9e2942fe/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-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
||||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
||||||
golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3 h1:qNgPs5exUA+G0C96DrPwNrvLSj7GT/9D+3WMWUcUg34=
|
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||||
golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
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 +0,0 @@
|
|||||||
Subproject commit f9ec811b10bbab54e843199eb68156e9e7c143cc
|
|
88
input.go
Normal file
88
input.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetTileAtMouse(camera *rl.Camera3D) (Tile, bool) {
|
||||||
|
if !rl.IsMouseButtonPressed(rl.MouseLeftButton) {
|
||||||
|
return Tile{}, false
|
||||||
|
}
|
||||||
|
mouse := rl.GetMousePosition()
|
||||||
|
ray := rl.GetMouseRay(mouse, *camera)
|
||||||
|
|
||||||
|
for x := 0; x < MapWidth; x++ {
|
||||||
|
for y := 0; y < MapHeight; y++ {
|
||||||
|
tile := mapGrid[x][y]
|
||||||
|
|
||||||
|
// Define the bounding box for each tile based on its position and height
|
||||||
|
tilePos := rl.NewVector3(float32(x*TileSize), tile.Height*TileHeight, float32(y*TileSize))
|
||||||
|
boxMin := rl.Vector3Subtract(tilePos, rl.NewVector3(TileSize/2, TileHeight/2, TileSize/2))
|
||||||
|
boxMax := rl.Vector3Add(tilePos, rl.NewVector3(TileSize/2, TileHeight/2, TileSize/2))
|
||||||
|
|
||||||
|
// Check if the ray intersects the bounding box
|
||||||
|
if RayIntersectsBox(ray, boxMin, boxMax) {
|
||||||
|
fmt.Println("Clicked:", tile.X, tile.Y)
|
||||||
|
return tile, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Tile{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleInput(player *Player, camera *rl.Camera) {
|
||||||
|
clickedTile, clicked := GetTileAtMouse(camera)
|
||||||
|
if clicked {
|
||||||
|
path := FindPath(mapGrid[player.PosTile.X][player.PosTile.Y], clickedTile)
|
||||||
|
if path != nil {
|
||||||
|
// Exclude the first tile (current position)
|
||||||
|
if len(path) > 1 {
|
||||||
|
player.TargetPath = path[1:]
|
||||||
|
player.ActionQueue = append(player.ActionQueue, Action{Type: MoveAction, X: clickedTile.X, Y: clickedTile.Y})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to test ray-box intersection (slab method)
|
||||||
|
func RayIntersectsBox(ray rl.Ray, boxMin, boxMax rl.Vector3) bool {
|
||||||
|
tmin := (boxMin.X - ray.Position.X) / ray.Direction.X
|
||||||
|
tmax := (boxMax.X - ray.Position.X) / ray.Direction.X
|
||||||
|
|
||||||
|
if tmin > tmax {
|
||||||
|
tmin, tmax = tmax, tmin
|
||||||
|
}
|
||||||
|
|
||||||
|
tymin := (boxMin.Z - ray.Position.Z) / ray.Direction.Z
|
||||||
|
tymax := (boxMax.Z - ray.Position.Z) / ray.Direction.Z
|
||||||
|
|
||||||
|
if tymin > tymax {
|
||||||
|
tymin, tymax = tymax, tymin
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tmin > tymax) || (tymin > tmax) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if tymin > tmin {
|
||||||
|
tmin = tymin
|
||||||
|
}
|
||||||
|
if tymax < tmax {
|
||||||
|
tmax = tymax
|
||||||
|
}
|
||||||
|
|
||||||
|
tzmin := (boxMin.Y - ray.Position.Y) / ray.Direction.Y
|
||||||
|
tzmax := (boxMax.Y - ray.Position.Y) / ray.Direction.Y
|
||||||
|
|
||||||
|
if tzmin > tzmax {
|
||||||
|
tzmin, tzmax = tzmax, tzmin
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tmin > tzmax) || (tzmin > tmax) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
@ -1,16 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
193
main.go
193
main.go
@ -1,79 +1,150 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
cameraDistance = float32(20.0)
|
||||||
|
cameraYaw = float32(145.0)
|
||||||
|
cameraPitch = float32(45.0) // Adjusted for a more overhead view
|
||||||
|
mapGrid = InitMap()
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
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 func() {
|
defer rl.CloseWindow()
|
||||||
logging.Info.Println("Closing window")
|
rl.InitAudioDevice()
|
||||||
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()
|
||||||
|
|
||||||
game := game.New()
|
player := Player{
|
||||||
logging.Info.Println("Loading game assets")
|
PosActual: rl.NewVector3(5*TileSize, 0, 5*TileSize),
|
||||||
if err := game.LoadAssets(); err != nil {
|
PosTile: mapGrid[5][5],
|
||||||
log.Fatalf("Failed to load assets: %v", err)
|
Speed: 50.0,
|
||||||
|
TargetPath: []Tile{},
|
||||||
}
|
}
|
||||||
defer func() {
|
|
||||||
logging.Info.Println("Cleaning up game resources")
|
|
||||||
game.Cleanup()
|
|
||||||
}()
|
|
||||||
|
|
||||||
if config.Current.PlayMusic {
|
camera := rl.Camera3D{
|
||||||
logging.Info.Println("Starting music playback")
|
Position: rl.NewVector3(0, 10, 10), // Will be updated every frame
|
||||||
rl.PlayMusicStream(game.Music)
|
Target: player.PosActual,
|
||||||
rl.SetMusicVolume(game.Music, 0.5)
|
Up: rl.NewVector3(0, 1, 0), // Y is up in 3D
|
||||||
|
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)
|
||||||
|
player.ID = playerID
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
otherPlayers := make(map[int32]*Player)
|
||||||
|
|
||||||
|
go HandleServerCommunication(conn, playerID, &player, otherPlayers)
|
||||||
|
|
||||||
|
models, err := LoadModels()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load models: %v", err)
|
||||||
|
}
|
||||||
|
defer UnloadModels(models)
|
||||||
|
|
||||||
|
modelIndex := int(playerID) % len(models)
|
||||||
|
player.Model = models[modelIndex].Model
|
||||||
|
player.Texture = models[modelIndex].Texture
|
||||||
|
|
||||||
|
music, err := LoadMusic("resources/audio/GoonScape2.mp3")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load music: %v", err)
|
||||||
|
}
|
||||||
|
defer UnloadMusic(music)
|
||||||
|
rl.PlayMusicStream(music)
|
||||||
|
rl.SetMusicVolume(music, 0.5)
|
||||||
|
|
||||||
rl.SetTargetFPS(60)
|
rl.SetTargetFPS(60)
|
||||||
logging.Info.Println("Starting game loop")
|
|
||||||
game.Run()
|
for !rl.WindowShouldClose() {
|
||||||
logging.Info.Println("Game exited cleanly")
|
|
||||||
|
rl.UpdateMusicStream(music)
|
||||||
|
|
||||||
|
// Time management
|
||||||
|
deltaTime := rl.GetFrameTime()
|
||||||
|
|
||||||
|
// Handle input
|
||||||
|
HandleInput(&player, &camera)
|
||||||
|
|
||||||
|
// Update player
|
||||||
|
if len(player.TargetPath) > 0 {
|
||||||
|
player.MoveTowards(player.TargetPath[0], deltaTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update camera
|
||||||
|
UpdateCamera(&camera, player.PosActual, deltaTime)
|
||||||
|
|
||||||
|
// Rendering
|
||||||
|
rl.BeginDrawing()
|
||||||
|
rl.ClearBackground(rl.RayWhite)
|
||||||
|
rl.BeginMode3D(camera)
|
||||||
|
DrawMap()
|
||||||
|
DrawPlayer(player, player.Model)
|
||||||
|
|
||||||
|
for id, other := range otherPlayers {
|
||||||
|
if len(other.TargetPath) > 0 {
|
||||||
|
other.MoveTowards(other.TargetPath[0], deltaTime)
|
||||||
|
}
|
||||||
|
DrawPlayer(*other, models[int(id)%len(models)].Model)
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.EndMode3D()
|
||||||
|
rl.DrawFPS(10, 10)
|
||||||
|
rl.EndDrawing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadModels() ([]struct {
|
||||||
|
Model rl.Model
|
||||||
|
Texture rl.Texture2D
|
||||||
|
}, error) {
|
||||||
|
goonerModel := rl.LoadModel("resources/models/goonion.obj")
|
||||||
|
goonerTexture := rl.LoadTexture("resources/models/goonion.png")
|
||||||
|
rl.SetMaterialTexture(goonerModel.Materials, rl.MapDiffuse, goonerTexture)
|
||||||
|
|
||||||
|
coomerModel := rl.LoadModel("resources/models/coomer.obj")
|
||||||
|
coomerTexture := rl.LoadTexture("resources/models/coomer.png")
|
||||||
|
rl.SetMaterialTexture(coomerModel.Materials, rl.MapDiffuse, coomerTexture)
|
||||||
|
|
||||||
|
shrekeModel := rl.LoadModel("resources/models/shreke.obj")
|
||||||
|
shrekeTexture := rl.LoadTexture("resources/models/shreke.png")
|
||||||
|
rl.SetMaterialTexture(shrekeModel.Materials, rl.MapDiffuse, shrekeTexture)
|
||||||
|
|
||||||
|
return []struct {
|
||||||
|
Model rl.Model
|
||||||
|
Texture rl.Texture2D
|
||||||
|
}{
|
||||||
|
{Model: goonerModel, Texture: goonerTexture},
|
||||||
|
{Model: coomerModel, Texture: coomerTexture},
|
||||||
|
{Model: shrekeModel, Texture: shrekeTexture},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UnloadModels(models []struct {
|
||||||
|
Model rl.Model
|
||||||
|
Texture rl.Texture2D
|
||||||
|
}) {
|
||||||
|
for _, model := range models {
|
||||||
|
rl.UnloadModel(model.Model)
|
||||||
|
rl.UnloadTexture(model.Texture)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadMusic(filename string) (rl.Music, error) {
|
||||||
|
music := rl.LoadMusicStream(filename)
|
||||||
|
return music, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UnloadMusic(music rl.Music) {
|
||||||
|
rl.UnloadMusicStream(music)
|
||||||
}
|
}
|
||||||
|
48
map.go
Normal file
48
map.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
|
|
||||||
|
// Initialize the map with some height data
|
||||||
|
func InitMap() [][]Tile {
|
||||||
|
mapGrid := make([][]Tile, MapWidth)
|
||||||
|
for x := 0; x < MapWidth; x++ {
|
||||||
|
mapGrid[x] = make([]Tile, MapHeight)
|
||||||
|
for y := 0; y < MapHeight; y++ {
|
||||||
|
mapGrid[x][y] = Tile{
|
||||||
|
X: x,
|
||||||
|
Y: y,
|
||||||
|
Height: 1.0 + float32(x%5), // Example height
|
||||||
|
Walkable: true, // Set to false for obstacles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mapGrid
|
||||||
|
}
|
||||||
|
|
||||||
|
func DrawMap() {
|
||||||
|
for x := 0; x < MapWidth; x++ {
|
||||||
|
for y := 0; y < MapHeight; y++ {
|
||||||
|
tile := mapGrid[x][y]
|
||||||
|
// Interpolate height between adjacent tiles for a smoother landscape
|
||||||
|
height := tile.Height
|
||||||
|
if x > 0 {
|
||||||
|
height += mapGrid[x-1][y].Height
|
||||||
|
}
|
||||||
|
if y > 0 {
|
||||||
|
height += mapGrid[x][y-1].Height
|
||||||
|
}
|
||||||
|
if x > 0 && y > 0 {
|
||||||
|
height += mapGrid[x-1][y-1].Height
|
||||||
|
}
|
||||||
|
height /= 4.0
|
||||||
|
// Draw each tile as a 3D cube based on its height
|
||||||
|
tilePos := rl.Vector3{
|
||||||
|
X: float32(x * TileSize), // X-axis for horizontal position
|
||||||
|
Y: height * TileHeight, // Y-axis for height (Z in 3D is Y here)
|
||||||
|
Z: float32(y * TileSize), // Z-axis for depth (Y in 3D is Z here)
|
||||||
|
}
|
||||||
|
color := rl.Color{R: uint8(height * 25), G: 100, B: 100, A: 64}
|
||||||
|
rl.DrawCube(tilePos, TileSize, TileHeight, TileSize, color) // Draw a cube representing the tile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
121
network.go
Normal file
121
network.go
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
||||||
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ConnectToServer() (net.Conn, int32, error) {
|
||||||
|
// Attempt to connect to the server
|
||||||
|
conn, err := net.Dial("tcp", serverAddr)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to dial server: %v", err)
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Connected to server. Waiting for player ID...")
|
||||||
|
// Buffer for incoming server message
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
n, err := conn.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error reading player ID from server: %v", err)
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Received data: %x", buf[:n])
|
||||||
|
|
||||||
|
// Unmarshal server message to extract the player ID
|
||||||
|
var response pb.ServerMessage
|
||||||
|
if err := proto.Unmarshal(buf[:n], &response); err != nil {
|
||||||
|
log.Printf("Failed to unmarshal server response: %v", err)
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
playerID := response.GetPlayerId()
|
||||||
|
log.Printf("Successfully connected with player ID: %d", playerID)
|
||||||
|
return conn, playerID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleServerCommunication(conn net.Conn, playerID int32, player *Player, otherPlayers map[int32]*Player) {
|
||||||
|
// Goroutine to handle sending player's actions to the server
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
if len(player.ActionQueue) > 0 {
|
||||||
|
// Process the first action in the queue
|
||||||
|
actionData := player.ActionQueue[0]
|
||||||
|
action := &pb.Action{
|
||||||
|
PlayerId: playerID,
|
||||||
|
Type: pb.Action_MOVE,
|
||||||
|
X: int32(actionData.X),
|
||||||
|
Y: int32(actionData.Y),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize the action
|
||||||
|
data, err := proto.Marshal(action)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to marshal action: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send action to server
|
||||||
|
_, err = conn.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to send action to server: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the action from the queue once it's sent
|
||||||
|
player.ActionQueue = player.ActionQueue[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a delay to match the server's tick rate
|
||||||
|
time.Sleep(TickRate)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Main loop to handle receiving updates from the server
|
||||||
|
for {
|
||||||
|
buf := make([]byte, 4096)
|
||||||
|
n, err := conn.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to read from server: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverMessage pb.ServerMessage
|
||||||
|
if err := proto.Unmarshal(buf[:n], &serverMessage); err != nil {
|
||||||
|
log.Printf("Failed to unmarshal server message: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update other players' states
|
||||||
|
for _, state := range serverMessage.Players {
|
||||||
|
if state.PlayerId != playerID {
|
||||||
|
if otherPlayer, exists := otherPlayers[state.PlayerId]; exists {
|
||||||
|
otherPlayer.PosTile = Tile{X: int(state.X), Y: int(state.Y)}
|
||||||
|
otherPlayer.PosActual = rl.Vector3{
|
||||||
|
X: float32(state.X * TileSize),
|
||||||
|
Y: float32(state.Y * TileHeight),
|
||||||
|
Z: float32(state.Y * TileSize),
|
||||||
|
}
|
||||||
|
otherPlayer.MoveTowards(Tile{X: int(state.X), Y: int(state.Y)}, 0)
|
||||||
|
} else {
|
||||||
|
otherPlayers[state.PlayerId] = &Player{
|
||||||
|
PosTile: Tile{X: int(state.X), Y: int(state.Y)},
|
||||||
|
PosActual: rl.Vector3{
|
||||||
|
X: float32(state.X * TileSize),
|
||||||
|
Y: float32(state.Y * TileHeight),
|
||||||
|
Z: float32(state.Y * TileSize),
|
||||||
|
},
|
||||||
|
ID: state.PlayerId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,240 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,18 +1,14 @@
|
|||||||
package game
|
package main
|
||||||
|
|
||||||
import (
|
import "fmt"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Node struct {
|
type Node struct {
|
||||||
Tile types.Tile
|
Tile Tile
|
||||||
Parent *Node
|
Parent *Node
|
||||||
G, H, F float32
|
G, H, F float32
|
||||||
}
|
}
|
||||||
|
|
||||||
func FindPath(start, end types.Tile) []types.Tile {
|
func FindPath(start, end Tile) []Tile {
|
||||||
openList := []*Node{}
|
openList := []*Node{}
|
||||||
closedList := make(map[[2]int]bool)
|
closedList := make(map[[2]int]bool)
|
||||||
|
|
||||||
@ -21,6 +17,7 @@ func FindPath(start, end types.Tile) []types.Tile {
|
|||||||
openList = append(openList, startNode)
|
openList = append(openList, startNode)
|
||||||
|
|
||||||
for len(openList) > 0 {
|
for len(openList) > 0 {
|
||||||
|
// Find node with lowest F
|
||||||
current := openList[0]
|
current := openList[0]
|
||||||
currentIndex := 0
|
currentIndex := 0
|
||||||
for i, node := range openList {
|
for i, node := range openList {
|
||||||
@ -30,20 +27,23 @@ func FindPath(start, end types.Tile) []types.Tile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Move current to closed list
|
||||||
openList = append(openList[:currentIndex], openList[currentIndex+1:]...)
|
openList = append(openList[:currentIndex], openList[currentIndex+1:]...)
|
||||||
closedList[[2]int{current.Tile.X, current.Tile.Y}] = true
|
closedList[[2]int{current.Tile.X, current.Tile.Y}] = true
|
||||||
|
|
||||||
|
// Check if reached the end
|
||||||
if current.Tile.X == end.X && current.Tile.Y == end.Y {
|
if current.Tile.X == end.X && current.Tile.Y == end.Y {
|
||||||
path := []types.Tile{}
|
path := []Tile{}
|
||||||
node := current
|
node := current
|
||||||
for node != nil {
|
for node != nil {
|
||||||
path = append([]types.Tile{node.Tile}, path...)
|
path = append([]Tile{node.Tile}, path...)
|
||||||
node = node.Parent
|
node = node.Parent
|
||||||
}
|
}
|
||||||
fmt.Printf("Path found: %v\n", path)
|
fmt.Printf("Path found: %v\n", path)
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate neighbors
|
||||||
neighbors := GetNeighbors(current.Tile)
|
neighbors := GetNeighbors(current.Tile)
|
||||||
for _, neighbor := range neighbors {
|
for _, neighbor := range neighbors {
|
||||||
if !neighbor.Walkable || closedList[[2]int{neighbor.X, neighbor.Y}] {
|
if !neighbor.Walkable || closedList[[2]int{neighbor.X, neighbor.Y}] {
|
||||||
@ -75,31 +75,32 @@ func FindPath(start, end types.Tile) []types.Tile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No path found
|
||||||
|
fmt.Println("No path found")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func heuristic(a, b types.Tile) float32 {
|
func heuristic(a, b Tile) float32 {
|
||||||
return float32(abs(a.X-b.X) + abs(a.Y-b.Y))
|
return float32(abs(a.X-b.X) + abs(a.Y-b.Y))
|
||||||
}
|
}
|
||||||
|
|
||||||
func distance(a, b types.Tile) float32 {
|
func distance(a, b Tile) float32 {
|
||||||
_, _ = a, b
|
_ = a
|
||||||
return 1.0 // uniform cost for now
|
_ = b
|
||||||
|
return 1.0 //uniform cost for now
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetNeighbors(tile types.Tile) []types.Tile {
|
func GetNeighbors(tile Tile) []Tile {
|
||||||
directions := [][2]int{
|
directions := [][2]int{
|
||||||
{1, 0}, {-1, 0}, {0, 1}, {0, -1},
|
{1, 0}, {-1, 0}, {0, 1}, {0, -1},
|
||||||
{1, 1}, {-1, -1}, {1, -1}, {-1, 1},
|
{1, 1}, {-1, -1}, {1, -1}, {-1, 1},
|
||||||
}
|
}
|
||||||
neighbors := []types.Tile{}
|
neighbors := []Tile{}
|
||||||
grid := GetMapGrid()
|
|
||||||
for _, dir := range directions {
|
for _, dir := range directions {
|
||||||
nx, ny := tile.X+dir[0], tile.Y+dir[1]
|
nx, ny := tile.X+dir[0], tile.Y+dir[1]
|
||||||
if nx >= 0 && nx < types.MapWidth && ny >= 0 && ny < types.MapHeight {
|
if nx >= 0 && nx < MapWidth && ny >= 0 && ny < MapHeight {
|
||||||
if grid[nx][ny].Walkable {
|
neighbors = append(neighbors, mapGrid[nx][ny])
|
||||||
neighbors = append(neighbors, grid[nx][ny])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return neighbors
|
return neighbors
|
63
player.go
Normal file
63
player.go
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DrawPlayer(player Player, model rl.Model) {
|
||||||
|
// Draw the player based on its actual position (PosActual) and current tile height
|
||||||
|
playerPos := rl.Vector3{
|
||||||
|
X: player.PosActual.X,
|
||||||
|
Y: mapGrid[player.PosTile.X][player.PosTile.Y].Height*TileHeight + 16.0,
|
||||||
|
Z: player.PosActual.Z,
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.DrawModel(model, playerPos, 16, rl.White)
|
||||||
|
|
||||||
|
// Draw highlight around target tile
|
||||||
|
if len(player.TargetPath) > 0 {
|
||||||
|
targetTile := player.TargetPath[len(player.TargetPath)-1] // last tile in the slice
|
||||||
|
targetPos := rl.Vector3{
|
||||||
|
X: float32(targetTile.X * TileSize),
|
||||||
|
Y: mapGrid[targetTile.X][targetTile.Y].Height * TileHeight,
|
||||||
|
Z: float32(targetTile.Y * TileSize),
|
||||||
|
}
|
||||||
|
rl.DrawCubeWires(targetPos, TileSize, TileHeight, TileSize, rl.Green)
|
||||||
|
|
||||||
|
nextTile := player.TargetPath[0] // first tile in the slice
|
||||||
|
nextPos := rl.Vector3{
|
||||||
|
X: float32(nextTile.X * TileSize),
|
||||||
|
Y: mapGrid[nextTile.X][nextTile.Y].Height * TileHeight,
|
||||||
|
Z: float32(nextTile.Y * TileSize),
|
||||||
|
}
|
||||||
|
rl.DrawCubeWires(nextPos, TileSize, TileHeight, TileSize, rl.Yellow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (player *Player) MoveTowards(target Tile, deltaTime float32) {
|
||||||
|
// Calculate the direction vector to the target tile
|
||||||
|
targetPos := rl.Vector3{
|
||||||
|
X: float32(target.X * TileSize),
|
||||||
|
Y: mapGrid[target.X][target.Y].Height * TileHeight,
|
||||||
|
Z: float32(target.Y * TileSize),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate direction and normalize it for smooth movement
|
||||||
|
direction := rl.Vector3Subtract(targetPos, player.PosActual)
|
||||||
|
distance := rl.Vector3Length(direction)
|
||||||
|
if distance > 0 {
|
||||||
|
direction = rl.Vector3Scale(direction, player.Speed*deltaTime/distance)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move the player towards the target tile
|
||||||
|
if distance > 1.0 {
|
||||||
|
player.PosActual = rl.Vector3Add(player.PosActual, direction)
|
||||||
|
} else {
|
||||||
|
// Snap to the target tile when close enough
|
||||||
|
player.PosActual = targetPos
|
||||||
|
player.PosTile = target // Update player's tile
|
||||||
|
if len(player.TargetPath) > 1 {
|
||||||
|
player.TargetPath = player.TargetPath[1:] // Move to next tile in path if any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 104 KiB |
@ -1,27 +0,0 @@
|
|||||||
#!/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 +0,0 @@
|
|||||||
PLATFORMS=windows/amd64 linux/amd64
|
|
126
segfault.txt
126
segfault.txt
@ -1,126 +0,0 @@
|
|||||||
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
|
|
31
types.go
Normal file
31
types.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
|
|
||||||
|
type Tile struct {
|
||||||
|
X, Y int
|
||||||
|
Height float32
|
||||||
|
Walkable bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
MoveAction ActionType = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
type Action struct {
|
||||||
|
Type ActionType
|
||||||
|
X, Y int // Target position for movement
|
||||||
|
}
|
||||||
|
|
||||||
|
type Player struct {
|
||||||
|
PosActual rl.Vector3
|
||||||
|
PosTile Tile
|
||||||
|
TargetPath []Tile
|
||||||
|
Speed float32
|
||||||
|
ActionQueue []Action // Queue for player actions
|
||||||
|
Model rl.Model
|
||||||
|
Texture rl.Texture2D
|
||||||
|
ID int32
|
||||||
|
}
|
128
types/player.go
128
types/player.go
@ -1,128 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,68 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
Loading…
x
Reference in New Issue
Block a user