Compare commits
19 Commits
testbuild-
...
555b8118f2
Author | SHA1 | Date | |
---|---|---|---|
555b8118f2 | |||
5bf962a18d | |||
220a451475 | |||
417bf4ea63 | |||
84d63ba4c1 | |||
49b84c8540 | |||
0e509ad752 | |||
bcd63efd7b | |||
944c33ce3b | |||
d5bb464d9f | |||
4549ee7517 | |||
31ae9c525f | |||
06913a5217 | |||
49663c9094 | |||
a843680b09 | |||
7183df4a8b | |||
33e355200d | |||
e45066b2a8 | |||
bb01dccf2b |
15
.gitignore
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# Build artifacts
|
||||||
|
build/
|
||||||
|
goonscape
|
||||||
|
goonscape.exe
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
resources/models/old_and_test/
|
25
.woodpecker.yml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
pipeline:
|
||||||
|
build:
|
||||||
|
image: golang:1.23
|
||||||
|
commands:
|
||||||
|
# Install build dependencies
|
||||||
|
- apt-get update && apt-get install -y gcc-mingw-w64 cmake zip libasound2-dev mesa-common-dev libx11-dev libxrandr-dev libxi-dev xorg-dev libgl1-mesa-dev libglu1-mesa-dev libwayland-dev wayland-protocols libxkbcommon-dev
|
||||||
|
|
||||||
|
# Build for all platforms
|
||||||
|
- make all
|
||||||
|
|
||||||
|
when:
|
||||||
|
event: tag
|
||||||
|
tag: v*
|
||||||
|
|
||||||
|
# Optional: Create Gitea release with built artifacts
|
||||||
|
release:
|
||||||
|
image: plugins/gitea-release
|
||||||
|
settings:
|
||||||
|
api_key:
|
||||||
|
from_secret: gitea_token
|
||||||
|
base_url: https://gitea.boner.be
|
||||||
|
files: build/*.zip
|
||||||
|
when:
|
||||||
|
event: tag
|
||||||
|
tag: v*
|
24
LICENSE
@ -1,11 +1,21 @@
|
|||||||
“Commons Clause” License Condition v1.0
|
MIT License
|
||||||
|
|
||||||
The Software is provided to you by the Licensor under the License, as defined below, subject to the following condition.
|
Copyright (c) 2025 bdnugget
|
||||||
|
|
||||||
Without limiting other conditions in the License, the grant of rights under the License will not include, and the License does not grant to you, right to Sell the Software.
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
For purposes of the foregoing, “Sell” means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration (including without limitation fees for hosting or consulting/ support services related to the Software), a product or service whose value derives, entirely or substantially, from the functionality of the Software. Any license notice or attribution required by the License must also include this Commons Cause License Condition notice.
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
Software: GoonScape
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
License: Commons Clause v1.0
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
Licensor: bdnugget
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
2
Makefile
@ -3,7 +3,7 @@
|
|||||||
include scripts/platforms.mk
|
include scripts/platforms.mk
|
||||||
|
|
||||||
BINARY_NAME=goonscape
|
BINARY_NAME=goonscape
|
||||||
VERSION=1.0.0
|
VERSION=1.1.0
|
||||||
BUILD_DIR=build
|
BUILD_DIR=build
|
||||||
ASSETS_DIR=resources
|
ASSETS_DIR=resources
|
||||||
|
|
||||||
|
55
README.md
@ -16,24 +16,57 @@ A multiplayer isometric game inspired by Oldschool RuneScape, built with Go and
|
|||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- Go 1.23 or higher
|
- Go 1.23 or higher
|
||||||
|
- GCC (for CGO/SQLite support)
|
||||||
|
- OpenGL development libraries
|
||||||
- Raylib dependencies (see [raylib-go](https://github.com/gen2brain/raylib-go#requirements))
|
- Raylib dependencies (see [raylib-go](https://github.com/gen2brain/raylib-go#requirements))
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Clone the repository:
|
### Pre-built Binaries
|
||||||
|
The easiest way to get started is to download the latest release from:
|
||||||
|
```
|
||||||
|
https://gitea.boner.be/bdnugget/goonscape/releases
|
||||||
|
```
|
||||||
|
Choose the appropriate zip file for your platform:
|
||||||
|
- Windows: `goonscape-windows-amd64-v1.1.0.zip`
|
||||||
|
- Linux: `goonscape-linux-amd64-v1.1.0.zip`
|
||||||
|
|
||||||
|
Extract the zip and run the executable.
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
For development:
|
||||||
|
```bash
|
||||||
|
# Run directly (recommended for development)
|
||||||
|
go run main.go
|
||||||
|
|
||||||
|
# Run with local server
|
||||||
|
go run main.go -local
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server Setup
|
||||||
|
The server requires CGO for SQLite support:
|
||||||
|
```bash
|
||||||
|
# Enable CGO
|
||||||
|
go env -w CGO_ENABLED=1
|
||||||
|
|
||||||
|
# Clone and build server
|
||||||
|
git clone https://gitea.boner.be/bdnugget/goonserver.git
|
||||||
|
cd goonserver
|
||||||
|
go build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Client Installation
|
||||||
|
Then install or build:
|
||||||
|
```bash
|
||||||
|
# Install the client
|
||||||
|
go install gitea.boner.be/bdnugget/goonscape@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Or build from source:
|
||||||
```bash
|
```bash
|
||||||
git clone https://gitea.boner.be/bdnugget/goonscape.git
|
git clone https://gitea.boner.be/bdnugget/goonscape.git
|
||||||
cd goonscape
|
cd goonscape
|
||||||
```
|
go build
|
||||||
|
|
||||||
2. Install dependencies:
|
|
||||||
```bash
|
|
||||||
go mod tidy
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Build and run:
|
|
||||||
```bash
|
|
||||||
go run main.go
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Controls
|
## Controls
|
||||||
|
277
assets/assets.go
@ -1,41 +1,274 @@
|
|||||||
package assets
|
package assets
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/types"
|
"gitea.boner.be/bdnugget/goonscape/types"
|
||||||
rl "github.com/gen2brain/raylib-go/raylib"
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Helper function to load animations for a model
|
||||||
|
func loadModelAnimations(animPaths map[string]string) (types.AnimationSet, error) {
|
||||||
|
var animSet types.AnimationSet
|
||||||
|
|
||||||
|
// Only try to load animations if environment variable isn't set
|
||||||
|
if os.Getenv("GOONSCAPE_DISABLE_ANIMATIONS") == "1" {
|
||||||
|
return animSet, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateModel checks if a model is valid and properly loaded
|
||||||
|
func ValidateModel(model rl.Model) error {
|
||||||
|
if model.Meshes == nil {
|
||||||
|
return fmt.Errorf("model has nil meshes")
|
||||||
|
}
|
||||||
|
if model.Meshes.VertexCount <= 0 {
|
||||||
|
return fmt.Errorf("model has invalid vertex count")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompletelyAvoidExternalModels determines if we should avoid loading external models
|
||||||
|
func CompletelyAvoidExternalModels() bool {
|
||||||
|
return os.Getenv("GOONSCAPE_SAFE_MODE") == "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
// SafeLoadModel attempts to load a model, returning a placeholder if it fails
|
||||||
|
func SafeLoadModel(fileName string, fallbackShape int, fallbackColor rl.Color) (rl.Model, bool, rl.Color) {
|
||||||
|
// Don't even try to load external models in safe mode
|
||||||
|
if CompletelyAvoidExternalModels() {
|
||||||
|
rl.TraceLog(rl.LogInfo, "Safe mode enabled, using primitive shape instead of %s", fileName)
|
||||||
|
return createPrimitiveShape(fallbackShape), false, fallbackColor
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
// Recover from any panics during model loading
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
rl.TraceLog(rl.LogError, "Panic in SafeLoadModel: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Try to load the model
|
||||||
|
model := rl.LoadModel(fileName)
|
||||||
|
|
||||||
|
// Check if the model is valid
|
||||||
|
if model.Meshes == nil || model.Meshes.VertexCount <= 0 {
|
||||||
|
rl.TraceLog(rl.LogWarning, "Failed to load model %s, using placeholder", fileName)
|
||||||
|
return createPrimitiveShape(fallbackShape), false, fallbackColor
|
||||||
|
}
|
||||||
|
|
||||||
|
// For real models, return zero color since we don't need it
|
||||||
|
return model, true, rl.Color{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// createPrimitiveShape creates a simple shape without loading external models
|
||||||
|
func createPrimitiveShape(shapeType int) rl.Model {
|
||||||
|
var mesh rl.Mesh
|
||||||
|
|
||||||
|
switch shapeType {
|
||||||
|
case 0: // Cube
|
||||||
|
mesh = rl.GenMeshCube(1.0, 2.0, 1.0)
|
||||||
|
case 1: // Sphere
|
||||||
|
mesh = rl.GenMeshSphere(1.0, 8, 8)
|
||||||
|
case 2: // Cylinder
|
||||||
|
mesh = rl.GenMeshCylinder(0.8, 2.0, 8)
|
||||||
|
case 3: // Cone
|
||||||
|
mesh = rl.GenMeshCone(1.0, 2.0, 8)
|
||||||
|
default: // Default to cube
|
||||||
|
mesh = rl.GenMeshCube(1.0, 2.0, 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
model := rl.LoadModelFromMesh(mesh)
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
|
||||||
func LoadModels() ([]types.ModelAsset, error) {
|
func LoadModels() ([]types.ModelAsset, error) {
|
||||||
goonerModel := rl.LoadModel("resources/models/goonion.obj")
|
// Force safe mode for now until we fix the segfault
|
||||||
goonerTexture := rl.LoadTexture("resources/models/goonion.png")
|
os.Setenv("GOONSCAPE_SAFE_MODE", "1")
|
||||||
rl.SetMaterialTexture(goonerModel.Materials, rl.MapDiffuse, goonerTexture)
|
|
||||||
|
|
||||||
coomerModel := rl.LoadModel("resources/models/coomer.obj")
|
models := make([]types.ModelAsset, 0, 3)
|
||||||
coomerTexture := rl.LoadTexture("resources/models/coomer.png")
|
|
||||||
rl.SetMaterialTexture(coomerModel.Materials, rl.MapDiffuse, coomerTexture)
|
|
||||||
|
|
||||||
shrekeModel := rl.LoadModel("resources/models/shreke.obj")
|
// Use environment variable to completely disable model loading
|
||||||
|
safeMode := CompletelyAvoidExternalModels()
|
||||||
|
|
||||||
|
// Colors for the different models
|
||||||
|
goonerColor := rl.Color{R: 255, G: 200, B: 200, A: 255} // Pinkish
|
||||||
|
coomerColor := rl.Color{R: 200, G: 230, B: 255, A: 255} // Light blue
|
||||||
|
shrekeColor := rl.Color{R: 180, G: 255, B: 180, A: 255} // Light green
|
||||||
|
|
||||||
|
// If in safe mode, create all models directly without loading
|
||||||
|
if safeMode {
|
||||||
|
// Gooner model (cube)
|
||||||
|
cube := createPrimitiveShape(0)
|
||||||
|
models = append(models, types.ModelAsset{
|
||||||
|
Model: cube,
|
||||||
|
YOffset: 0.0,
|
||||||
|
PlaceholderColor: goonerColor,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Coomer model (sphere)
|
||||||
|
sphere := createPrimitiveShape(1)
|
||||||
|
models = append(models, types.ModelAsset{
|
||||||
|
Model: sphere,
|
||||||
|
YOffset: -4.0,
|
||||||
|
PlaceholderColor: coomerColor,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Shreke model (cylinder)
|
||||||
|
cylinder := createPrimitiveShape(2)
|
||||||
|
models = append(models, types.ModelAsset{
|
||||||
|
Model: cylinder,
|
||||||
|
YOffset: 0.0,
|
||||||
|
PlaceholderColor: shrekeColor,
|
||||||
|
})
|
||||||
|
|
||||||
|
return models, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// The rest of the function with normal model loading
|
||||||
|
// Load Goonion model with error handling
|
||||||
|
var goonerModel rl.Model
|
||||||
|
var success bool
|
||||||
|
var modelColor rl.Color
|
||||||
|
|
||||||
|
goonerModel, success, modelColor = SafeLoadModel("resources/models/gooner/walk_no_y_transform.glb", 0, goonerColor)
|
||||||
|
|
||||||
|
// Create animations only if model was loaded successfully
|
||||||
|
var goonerAnims types.AnimationSet
|
||||||
|
if success {
|
||||||
|
goonerAnims, _ = loadModelAnimations(map[string]string{
|
||||||
|
"idle": "resources/models/gooner/idle_no_y_transform.glb",
|
||||||
|
"walk": "resources/models/gooner/walk_no_y_transform.glb",
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always add a model (real or placeholder)
|
||||||
|
models = append(models, types.ModelAsset{
|
||||||
|
Model: goonerModel,
|
||||||
|
Animation: append(goonerAnims.Idle, goonerAnims.Walk...),
|
||||||
|
AnimFrames: int32(len(goonerAnims.Idle) + len(goonerAnims.Walk)),
|
||||||
|
Animations: goonerAnims,
|
||||||
|
YOffset: 0.0,
|
||||||
|
PlaceholderColor: modelColor,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Coomer model with safe loading - using a sphere shape
|
||||||
|
var coomerModel rl.Model
|
||||||
|
coomerModel, success, modelColor = SafeLoadModel("resources/models/coomer/idle_notransy.glb", 1, coomerColor)
|
||||||
|
|
||||||
|
if success {
|
||||||
|
// Only load animations if the model loaded successfully
|
||||||
|
coomerAnims, _ := loadModelAnimations(map[string]string{
|
||||||
|
"idle": "resources/models/coomer/idle_notransy.glb",
|
||||||
|
"walk": "resources/models/coomer/unsteadywalk_notransy.glb",
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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))
|
||||||
|
coomerModel.Transform = transform
|
||||||
|
|
||||||
|
models = append(models, types.ModelAsset{
|
||||||
|
Model: coomerModel,
|
||||||
|
Animation: append(coomerAnims.Idle, coomerAnims.Walk...),
|
||||||
|
AnimFrames: int32(len(coomerAnims.Idle) + len(coomerAnims.Walk)),
|
||||||
|
Animations: coomerAnims,
|
||||||
|
YOffset: -4.0,
|
||||||
|
PlaceholderColor: rl.Color{}, // Not a placeholder
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Add a placeholder with different shape/color
|
||||||
|
models = append(models, types.ModelAsset{
|
||||||
|
Model: coomerModel,
|
||||||
|
YOffset: -4.0,
|
||||||
|
PlaceholderColor: modelColor,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shreke model with safe loading - using a cylinder shape
|
||||||
|
var shrekeModel rl.Model
|
||||||
|
shrekeModel, success, modelColor = SafeLoadModel("resources/models/shreke.obj", 2, shrekeColor)
|
||||||
|
|
||||||
|
if success {
|
||||||
|
// Only proceed with texture if model loaded
|
||||||
shrekeTexture := rl.LoadTexture("resources/models/shreke.png")
|
shrekeTexture := rl.LoadTexture("resources/models/shreke.png")
|
||||||
|
if shrekeTexture.ID <= 0 {
|
||||||
|
rl.TraceLog(rl.LogWarning, "Failed to load shreke texture")
|
||||||
|
} else {
|
||||||
rl.SetMaterialTexture(shrekeModel.Materials, rl.MapDiffuse, shrekeTexture)
|
rl.SetMaterialTexture(shrekeModel.Materials, rl.MapDiffuse, shrekeTexture)
|
||||||
|
|
||||||
return []types.ModelAsset{
|
models = append(models, types.ModelAsset{
|
||||||
{Model: goonerModel, Texture: goonerTexture},
|
Model: shrekeModel,
|
||||||
{Model: coomerModel, Texture: coomerTexture},
|
Texture: shrekeTexture,
|
||||||
{Model: shrekeModel, Texture: shrekeTexture},
|
YOffset: 0.0,
|
||||||
}, nil
|
PlaceholderColor: rl.Color{}, // Not a placeholder
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Add another placeholder with different shape/color
|
||||||
|
models = append(models, types.ModelAsset{
|
||||||
|
Model: shrekeModel,
|
||||||
|
YOffset: 0.0,
|
||||||
|
PlaceholderColor: modelColor,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(models) == 0 {
|
||||||
|
return nil, fmt.Errorf("failed to load any models")
|
||||||
|
}
|
||||||
|
|
||||||
|
return models, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadMusic(filename string) (rl.Music, error) {
|
func LoadMusic(filename string) (rl.Music, error) {
|
||||||
return rl.LoadMusicStream(filename), nil
|
defer func() {
|
||||||
}
|
// Recover from any panics during music loading
|
||||||
|
if r := recover(); r != nil {
|
||||||
func UnloadModels(models []types.ModelAsset) {
|
rl.TraceLog(rl.LogError, "Panic in LoadMusic: %v", r)
|
||||||
for _, model := range models {
|
|
||||||
rl.UnloadModel(model.Model)
|
|
||||||
rl.UnloadTexture(model.Texture)
|
|
||||||
}
|
}
|
||||||
}
|
}()
|
||||||
|
|
||||||
func UnloadMusic(music rl.Music) {
|
// Skip loading music if environment variable is set
|
||||||
rl.UnloadMusicStream(music)
|
if os.Getenv("GOONSCAPE_DISABLE_AUDIO") == "1" {
|
||||||
|
rl.TraceLog(rl.LogInfo, "Audio disabled, skipping music loading")
|
||||||
|
return rl.Music{}, fmt.Errorf("audio disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
music := rl.LoadMusicStream(filename)
|
||||||
|
if music.Stream.Buffer == nil {
|
||||||
|
return music, fmt.Errorf("failed to load music: %s", filename)
|
||||||
|
}
|
||||||
|
return music, nil
|
||||||
}
|
}
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
# Blender 3.6.0 MTL File: 'None'
|
|
||||||
# www.blender.org
|
|
||||||
|
|
||||||
newmtl Material.001
|
|
||||||
Ns 250.000000
|
|
||||||
Ka 1.000000 1.000000 1.000000
|
|
||||||
Ks 0.500000 0.500000 0.500000
|
|
||||||
Ke 0.000000 0.000000 0.000000
|
|
||||||
Ni 1.450000
|
|
||||||
d 1.000000
|
|
||||||
illum 2
|
|
||||||
map_Kd coomer.png
|
|
Before Width: | Height: | Size: 2.2 MiB |
@ -1,12 +0,0 @@
|
|||||||
# Blender 3.6.0 MTL File: 'None'
|
|
||||||
# www.blender.org
|
|
||||||
|
|
||||||
newmtl Material.001
|
|
||||||
Ns 250.000000
|
|
||||||
Ka 1.000000 1.000000 1.000000
|
|
||||||
Ks 0.500000 0.500000 0.500000
|
|
||||||
Ke 0.000000 0.000000 0.000000
|
|
||||||
Ni 1.450000
|
|
||||||
d 1.000000
|
|
||||||
illum 2
|
|
||||||
map_Kd goonion.png
|
|
Before Width: | Height: | Size: 2.5 MiB |
@ -1,12 +0,0 @@
|
|||||||
# Blender 3.6.0 MTL File: 'None'
|
|
||||||
# www.blender.org
|
|
||||||
|
|
||||||
newmtl Material.001
|
|
||||||
Ns 250.000000
|
|
||||||
Ka 1.000000 1.000000 1.000000
|
|
||||||
Ks 0.500000 0.500000 0.500000
|
|
||||||
Ke 0.000000 0.000000 0.000000
|
|
||||||
Ni 1.450000
|
|
||||||
d 1.000000
|
|
||||||
illum 2
|
|
||||||
map_Kd shreke.png
|
|
Before Width: | Height: | Size: 4.8 MiB |
Before Width: | Height: | Size: 104 KiB |
@ -1,12 +0,0 @@
|
|||||||
# Blender 3.6.0 MTL File: 'None'
|
|
||||||
# www.blender.org
|
|
||||||
|
|
||||||
newmtl Material.001
|
|
||||||
Ns 250.000000
|
|
||||||
Ka 1.000000 1.000000 1.000000
|
|
||||||
Ks 0.500000 0.500000 0.500000
|
|
||||||
Ke 0.000000 0.000000 0.000000
|
|
||||||
Ni 1.450000
|
|
||||||
d 1.000000
|
|
||||||
illum 2
|
|
||||||
map_Kd coomer.png
|
|
Before Width: | Height: | Size: 2.2 MiB |
@ -1,12 +0,0 @@
|
|||||||
# Blender 3.6.0 MTL File: 'None'
|
|
||||||
# www.blender.org
|
|
||||||
|
|
||||||
newmtl Material.001
|
|
||||||
Ns 250.000000
|
|
||||||
Ka 1.000000 1.000000 1.000000
|
|
||||||
Ks 0.500000 0.500000 0.500000
|
|
||||||
Ke 0.000000 0.000000 0.000000
|
|
||||||
Ni 1.450000
|
|
||||||
d 1.000000
|
|
||||||
illum 2
|
|
||||||
map_Kd goonion.png
|
|
Before Width: | Height: | Size: 2.5 MiB |
@ -1,12 +0,0 @@
|
|||||||
# Blender 3.6.0 MTL File: 'None'
|
|
||||||
# www.blender.org
|
|
||||||
|
|
||||||
newmtl Material.001
|
|
||||||
Ns 250.000000
|
|
||||||
Ka 1.000000 1.000000 1.000000
|
|
||||||
Ks 0.500000 0.500000 0.500000
|
|
||||||
Ke 0.000000 0.000000 0.000000
|
|
||||||
Ni 1.450000
|
|
||||||
d 1.000000
|
|
||||||
illum 2
|
|
||||||
map_Kd shreke.png
|
|
Before Width: | Height: | Size: 4.8 MiB |
Before Width: | Height: | Size: 104 KiB |
21
constants.go
@ -2,20 +2,11 @@ package main
|
|||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
|
// Game world constants
|
||||||
const (
|
const (
|
||||||
MapWidth = 50
|
// Server-related constants
|
||||||
MapHeight = 50
|
ServerTickRate = 600 * time.Millisecond // RuneScape-style tick rate
|
||||||
TileSize = 32
|
ClientTickRate = 50 * time.Millisecond // Client runs at higher rate for smooth rendering
|
||||||
TileHeight = 2.0
|
MaxTickDesync = 5 // Max ticks behind before forcing resync
|
||||||
TickRate = 600 * time.Millisecond // Server tick rate (600ms)
|
DefaultPort = "6969" // Default server port
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
@ -10,6 +10,7 @@ var (
|
|||||||
cameraDistance = float32(20.0)
|
cameraDistance = float32(20.0)
|
||||||
cameraYaw = float32(145.0)
|
cameraYaw = float32(145.0)
|
||||||
cameraPitch = float32(45.0)
|
cameraPitch = float32(45.0)
|
||||||
|
lastMousePos rl.Vector2 // Add this to track mouse movement
|
||||||
)
|
)
|
||||||
|
|
||||||
func UpdateCamera(camera *rl.Camera3D, player rl.Vector3, deltaTime float32) {
|
func UpdateCamera(camera *rl.Camera3D, player rl.Vector3, deltaTime float32) {
|
||||||
@ -32,6 +33,34 @@ func UpdateCamera(camera *rl.Camera3D, player rl.Vector3, deltaTime float32) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
if rl.IsKeyDown(rl.KeyRight) {
|
||||||
cameraYaw += 100 * deltaTime
|
cameraYaw += 100 * deltaTime
|
||||||
}
|
}
|
||||||
|
30
game/chat.go
@ -2,6 +2,8 @@ package game
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/types"
|
"gitea.boner.be/bdnugget/goonscape/types"
|
||||||
@ -25,6 +27,7 @@ type Chat struct {
|
|||||||
cursorPos int
|
cursorPos int
|
||||||
scrollOffset int
|
scrollOffset int
|
||||||
userData interface{}
|
userData interface{}
|
||||||
|
mutex sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewChat() *Chat {
|
func NewChat() *Chat {
|
||||||
@ -49,10 +52,20 @@ func (c *Chat) AddMessage(playerID int32, content string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
|
func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
if len(messages) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Processing %d chat messages", len(messages))
|
||||||
|
|
||||||
// Convert protobuf messages to our local type
|
// Convert protobuf messages to our local type
|
||||||
for _, msg := range messages {
|
for _, msg := range messages {
|
||||||
localMsg := types.ChatMessage{
|
localMsg := types.ChatMessage{
|
||||||
PlayerID: msg.PlayerId,
|
PlayerID: msg.PlayerId,
|
||||||
|
Username: msg.Username,
|
||||||
Content: msg.Content,
|
Content: msg.Content,
|
||||||
Time: time.Unix(0, msg.Timestamp),
|
Time: time.Unix(0, msg.Timestamp),
|
||||||
}
|
}
|
||||||
@ -63,6 +76,7 @@ func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
|
|||||||
c.messages = c.messages[1:]
|
c.messages = c.messages[1:]
|
||||||
}
|
}
|
||||||
c.messages = append(c.messages, localMsg)
|
c.messages = append(c.messages, localMsg)
|
||||||
|
log.Printf("Added chat message from %s: %s", msg.Username, msg.Content)
|
||||||
|
|
||||||
// Scroll to latest message if it's not already visible
|
// Scroll to latest message if it's not already visible
|
||||||
visibleMessages := int((chatHeight - inputHeight) / messageHeight)
|
visibleMessages := int((chatHeight - inputHeight) / messageHeight)
|
||||||
@ -86,6 +100,9 @@ func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
|
|||||||
ExpireTime: time.Now().Add(6 * time.Second),
|
ExpireTime: time.Now().Add(6 * time.Second),
|
||||||
}
|
}
|
||||||
otherPlayer.Unlock()
|
otherPlayer.Unlock()
|
||||||
|
log.Printf("Added floating message to other player %d", msg.PlayerId)
|
||||||
|
} else {
|
||||||
|
log.Printf("Could not find other player %d to add floating message", msg.PlayerId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -93,6 +110,9 @@ func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Chat) Draw(screenWidth, screenHeight int32) {
|
func (c *Chat) Draw(screenWidth, screenHeight int32) {
|
||||||
|
c.mutex.RLock()
|
||||||
|
defer c.mutex.RUnlock()
|
||||||
|
|
||||||
// Calculate chat window width based on screen width
|
// Calculate chat window width based on screen width
|
||||||
chatWindowWidth := screenWidth - (chatMargin * 2)
|
chatWindowWidth := screenWidth - (chatMargin * 2)
|
||||||
|
|
||||||
@ -117,8 +137,14 @@ func (c *Chat) Draw(screenWidth, screenHeight int32) {
|
|||||||
|
|
||||||
for i := startIdx; i < endIdx; i++ {
|
for i := startIdx; i < endIdx; i++ {
|
||||||
msg := c.messages[i]
|
msg := c.messages[i]
|
||||||
text := fmt.Sprintf("[%d]: %s", msg.PlayerID, msg.Content)
|
var color rl.Color
|
||||||
rl.DrawText(text, int32(chatX)+5, int32(messageY), 20, rl.White)
|
if msg.PlayerID == 0 { // System message
|
||||||
|
color = rl.Gold
|
||||||
|
} else {
|
||||||
|
color = rl.White
|
||||||
|
}
|
||||||
|
text := fmt.Sprintf("%s: %s", msg.Username, msg.Content)
|
||||||
|
rl.DrawText(text, int32(chatX)+5, int32(messageY), 20, color)
|
||||||
messageY += messageHeight
|
messageY += messageHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
248
game/game.go
@ -1,10 +1,12 @@
|
|||||||
package game
|
package game
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"fmt"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/assets"
|
"gitea.boner.be/bdnugget/goonscape/assets"
|
||||||
|
"gitea.boner.be/bdnugget/goonscape/network"
|
||||||
"gitea.boner.be/bdnugget/goonscape/types"
|
"gitea.boner.be/bdnugget/goonscape/types"
|
||||||
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
||||||
rl "github.com/gen2brain/raylib-go/raylib"
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
@ -18,20 +20,16 @@ type Game struct {
|
|||||||
Music rl.Music
|
Music rl.Music
|
||||||
Chat *Chat
|
Chat *Chat
|
||||||
MenuOpen bool
|
MenuOpen bool
|
||||||
QuitChan chan struct{} // Channel to signal shutdown
|
quitChan chan struct{}
|
||||||
|
loginScreen *LoginScreen
|
||||||
|
isLoggedIn bool
|
||||||
|
cleanupOnce sync.Once
|
||||||
|
frameCounter int // For periodic logging
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() *Game {
|
func New() *Game {
|
||||||
InitWorld()
|
InitWorld()
|
||||||
game := &Game{
|
game := &Game{
|
||||||
Player: &types.Player{
|
|
||||||
PosActual: rl.NewVector3(5*types.TileSize, 0, 5*types.TileSize),
|
|
||||||
PosTile: GetTile(5, 5),
|
|
||||||
Speed: 50.0,
|
|
||||||
TargetPath: []types.Tile{},
|
|
||||||
UserData: nil,
|
|
||||||
QuitDone: make(chan struct{}),
|
|
||||||
},
|
|
||||||
OtherPlayers: make(map[int32]*types.Player),
|
OtherPlayers: make(map[int32]*types.Player),
|
||||||
Camera: rl.Camera3D{
|
Camera: rl.Camera3D{
|
||||||
Position: rl.NewVector3(0, 10, 10),
|
Position: rl.NewVector3(0, 10, 10),
|
||||||
@ -41,29 +39,70 @@ func New() *Game {
|
|||||||
Projection: rl.CameraPerspective,
|
Projection: rl.CameraPerspective,
|
||||||
},
|
},
|
||||||
Chat: NewChat(),
|
Chat: NewChat(),
|
||||||
QuitChan: make(chan struct{}),
|
quitChan: make(chan struct{}),
|
||||||
|
loginScreen: NewLoginScreen(),
|
||||||
}
|
}
|
||||||
game.Player.UserData = game
|
|
||||||
game.Chat.userData = game
|
game.Chat.userData = game
|
||||||
return game
|
return game
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) LoadAssets() error {
|
func (g *Game) LoadAssets() error {
|
||||||
var err error
|
var loadErr error
|
||||||
g.Models, err = assets.LoadModels()
|
defer func() {
|
||||||
if err != nil {
|
if r := recover(); r != nil {
|
||||||
return err
|
loadErr = fmt.Errorf("panic during asset loading: %v", r)
|
||||||
|
// Cleanup any partially loaded assets
|
||||||
|
g.Cleanup()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Load models with better error handling
|
||||||
|
g.Models, loadErr = assets.LoadModels()
|
||||||
|
if loadErr != nil {
|
||||||
|
return fmt.Errorf("failed to load models: %v", loadErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
g.Music, err = assets.LoadMusic("resources/audio/GoonScape2.mp3")
|
// Verify model loading
|
||||||
if err != nil {
|
for i, model := range g.Models {
|
||||||
return err
|
if model.Model.Meshes == nil {
|
||||||
|
return fmt.Errorf("model %d failed to load properly", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load music with better error handling
|
||||||
|
g.Music, loadErr = assets.LoadMusic("resources/audio/GoonScape2.mp3")
|
||||||
|
if loadErr != nil {
|
||||||
|
return fmt.Errorf("failed to load music: %v", loadErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) Update(deltaTime float32) {
|
func (g *Game) Update(deltaTime float32) {
|
||||||
|
if !g.isLoggedIn {
|
||||||
|
username, password, isRegistering, submitted := g.loginScreen.Update()
|
||||||
|
if submitted {
|
||||||
|
conn, playerID, err := network.ConnectToServer(username, password, isRegistering)
|
||||||
|
if err != nil {
|
||||||
|
g.loginScreen.SetError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Handle ESC for menu
|
// Handle ESC for menu
|
||||||
if rl.IsKeyPressed(rl.KeyEscape) {
|
if rl.IsKeyPressed(rl.KeyEscape) {
|
||||||
g.MenuOpen = !g.MenuOpen
|
g.MenuOpen = !g.MenuOpen
|
||||||
@ -91,7 +130,28 @@ func (g *Game) Update(deltaTime float32) {
|
|||||||
g.Player.MoveTowards(g.Player.TargetPath[0], deltaTime, GetMapGrid())
|
g.Player.MoveTowards(g.Player.TargetPath[0], deltaTime, GetMapGrid())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Periodically log information about other players
|
||||||
|
g.frameCounter++
|
||||||
|
if g.frameCounter%300 == 0 {
|
||||||
|
rl.TraceLog(rl.LogInfo, "There are %d other players", len(g.OtherPlayers))
|
||||||
|
for id, other := range g.OtherPlayers {
|
||||||
|
rl.TraceLog(rl.LogInfo, "Other player ID: %d, Position: (%f, %f, %f), Has model: %v",
|
||||||
|
id, other.PosActual.X, other.PosActual.Y, other.PosActual.Z, other.Model.Meshes != nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process other players
|
||||||
for _, other := range g.OtherPlayers {
|
for _, other := range g.OtherPlayers {
|
||||||
|
if other == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure other players have models assigned
|
||||||
|
if other.Model.Meshes == nil {
|
||||||
|
g.AssignModelToPlayer(other)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update other player movement
|
||||||
if len(other.TargetPath) > 0 {
|
if len(other.TargetPath) > 0 {
|
||||||
other.MoveTowards(other.TargetPath[0], deltaTime, GetMapGrid())
|
other.MoveTowards(other.TargetPath[0], deltaTime, GetMapGrid())
|
||||||
}
|
}
|
||||||
@ -129,18 +189,55 @@ func (g *Game) DrawMap() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) DrawPlayer(player *types.Player, model rl.Model) {
|
func (g *Game) DrawPlayer(player *types.Player, model rl.Model) {
|
||||||
player.Lock()
|
// No need for lock in rendering, we'll use a "take snapshot" approach
|
||||||
defer player.Unlock()
|
// This avoids potential deadlocks and makes the rendering more consistent
|
||||||
|
|
||||||
|
// Check for invalid model
|
||||||
|
if model.Meshes == nil || model.Meshes.VertexCount <= 0 {
|
||||||
|
// Don't try to draw invalid models
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
grid := GetMapGrid()
|
grid := GetMapGrid()
|
||||||
|
modelIndex := int(player.ID) % len(g.Models)
|
||||||
|
if modelIndex < 0 || modelIndex >= len(g.Models) {
|
||||||
|
// Prevent out of bounds access
|
||||||
|
modelIndex = 0
|
||||||
|
}
|
||||||
|
modelAsset := g.Models[modelIndex]
|
||||||
|
|
||||||
|
const defaultHeight = 8.0 // Default height above tile, fine tune per model in types.ModelAsset
|
||||||
playerPos := rl.Vector3{
|
playerPos := rl.Vector3{
|
||||||
X: player.PosActual.X,
|
X: player.PosActual.X,
|
||||||
Y: grid[player.PosTile.X][player.PosTile.Y].Height*types.TileHeight + 16.0,
|
Y: grid[player.PosTile.X][player.PosTile.Y].Height*types.TileHeight + defaultHeight + modelAsset.YOffset,
|
||||||
Z: player.PosActual.Z,
|
Z: player.PosActual.Z,
|
||||||
}
|
}
|
||||||
|
|
||||||
rl.DrawModel(model, playerPos, 16, rl.White)
|
// 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
|
||||||
|
if anim.FrameCount > 0 {
|
||||||
|
currentFrame := player.AnimationFrame % anim.FrameCount
|
||||||
|
rl.UpdateModelAnimation(model, anim, currentFrame)
|
||||||
|
}
|
||||||
|
} else if len(modelAsset.Animations.Idle) > 0 {
|
||||||
|
anim := modelAsset.Animations.Idle[0] // Use first idle animation
|
||||||
|
if anim.FrameCount > 0 {
|
||||||
|
currentFrame := player.AnimationFrame % anim.FrameCount
|
||||||
|
rl.UpdateModelAnimation(model, anim, currentFrame)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use placeholder color if it's set, otherwise use white
|
||||||
|
var drawColor rl.Color = rl.White
|
||||||
|
if player.PlaceholderColor.A > 0 {
|
||||||
|
drawColor = player.PlaceholderColor
|
||||||
|
}
|
||||||
|
rl.DrawModel(model, playerPos, 16, drawColor)
|
||||||
|
|
||||||
|
// Draw floating messages and path indicators
|
||||||
if player.FloatingMessage != nil {
|
if player.FloatingMessage != nil {
|
||||||
screenPos := rl.GetWorldToScreen(rl.Vector3{
|
screenPos := rl.GetWorldToScreen(rl.Vector3{
|
||||||
X: playerPos.X,
|
X: playerPos.X,
|
||||||
@ -172,13 +269,45 @@ func (g *Game) DrawPlayer(player *types.Player, model rl.Model) {
|
|||||||
|
|
||||||
func (g *Game) Render() {
|
func (g *Game) Render() {
|
||||||
rl.BeginDrawing()
|
rl.BeginDrawing()
|
||||||
|
defer func() {
|
||||||
|
// This defer will catch any panics that might occur during rendering
|
||||||
|
// and ensure EndDrawing gets called to maintain proper graphics state
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
rl.TraceLog(rl.LogError, "Panic during rendering: %v", r)
|
||||||
|
}
|
||||||
|
rl.EndDrawing()
|
||||||
|
}()
|
||||||
|
|
||||||
rl.ClearBackground(rl.RayWhite)
|
rl.ClearBackground(rl.RayWhite)
|
||||||
|
|
||||||
|
if !g.isLoggedIn {
|
||||||
|
g.loginScreen.Draw()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
rl.BeginMode3D(g.Camera)
|
rl.BeginMode3D(g.Camera)
|
||||||
g.DrawMap()
|
g.DrawMap()
|
||||||
|
|
||||||
|
// Draw player only if valid
|
||||||
|
if g.Player != nil && g.Player.Model.Meshes != nil {
|
||||||
g.DrawPlayer(g.Player, g.Player.Model)
|
g.DrawPlayer(g.Player, g.Player.Model)
|
||||||
for id, other := range g.OtherPlayers {
|
}
|
||||||
g.DrawPlayer(other, g.Models[int(id)%len(g.Models)].Model)
|
|
||||||
|
// Draw other players with defensive checks
|
||||||
|
for _, other := range g.OtherPlayers {
|
||||||
|
if other == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure model is assigned
|
||||||
|
if other.Model.Meshes == nil {
|
||||||
|
g.AssignModelToPlayer(other)
|
||||||
|
// Skip this frame if assignment failed
|
||||||
|
if other.Model.Meshes == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.DrawPlayer(other, other.Model)
|
||||||
}
|
}
|
||||||
rl.EndMode3D()
|
rl.EndMode3D()
|
||||||
|
|
||||||
@ -203,13 +332,15 @@ func (g *Game) Render() {
|
|||||||
rl.DrawText(text, int32(pos.X)-textWidth/2, int32(pos.Y), 20, rl.Yellow)
|
rl.DrawText(text, int32(pos.X)-textWidth/2, int32(pos.Y), 20, rl.Yellow)
|
||||||
}
|
}
|
||||||
|
|
||||||
if g.Player.FloatingMessage != nil {
|
if g.Player != nil && g.Player.FloatingMessage != nil {
|
||||||
drawFloatingMessage(g.Player.FloatingMessage)
|
drawFloatingMessage(g.Player.FloatingMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, other := range g.OtherPlayers {
|
for _, other := range g.OtherPlayers {
|
||||||
|
if other != nil && other.FloatingMessage != nil {
|
||||||
drawFloatingMessage(other.FloatingMessage)
|
drawFloatingMessage(other.FloatingMessage)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Draw menu if open
|
// Draw menu if open
|
||||||
if g.MenuOpen {
|
if g.MenuOpen {
|
||||||
@ -217,17 +348,28 @@ func (g *Game) Render() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only draw chat if menu is not open
|
// Only draw chat if menu is not open
|
||||||
if !g.MenuOpen {
|
if !g.MenuOpen && g.Chat != nil {
|
||||||
g.Chat.Draw(int32(rl.GetScreenWidth()), int32(rl.GetScreenHeight()))
|
g.Chat.Draw(int32(rl.GetScreenWidth()), int32(rl.GetScreenHeight()))
|
||||||
}
|
}
|
||||||
|
|
||||||
rl.DrawFPS(10, 10)
|
rl.DrawFPS(10, 10)
|
||||||
rl.EndDrawing()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) Cleanup() {
|
func (g *Game) Cleanup() {
|
||||||
assets.UnloadModels(g.Models)
|
g.cleanupOnce.Do(func() {
|
||||||
assets.UnloadMusic(g.Music)
|
// Stop music first
|
||||||
|
if g.Music.Stream.Buffer != nil {
|
||||||
|
rl.StopMusicStream(g.Music)
|
||||||
|
rl.UnloadMusicStream(g.Music)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unload textures
|
||||||
|
for _, model := range g.Models {
|
||||||
|
if model.Texture.ID > 0 {
|
||||||
|
rl.UnloadTexture(model.Texture)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) HandleInput() {
|
func (g *Game) HandleInput() {
|
||||||
@ -307,8 +449,46 @@ func (g *Game) DrawMenu() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) Shutdown() {
|
func (g *Game) Shutdown() {
|
||||||
close(g.QuitChan)
|
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) {
|
||||||
|
if player == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defensive check for empty models array
|
||||||
|
if len(g.Models) == 0 {
|
||||||
|
rl.TraceLog(rl.LogWarning, "No models available to assign to player")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure model index is positive for consistent player appearances
|
||||||
|
// Use abs value of ID to ensure consistent appearance for negative IDs
|
||||||
|
modelIndex := abs(int(player.ID)) % len(g.Models)
|
||||||
|
if modelIndex < 0 || modelIndex >= len(g.Models) {
|
||||||
|
// Prevent out of bounds access
|
||||||
|
modelIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.TraceLog(rl.LogInfo, "Assigning model %d to player %d", modelIndex, player.ID)
|
||||||
|
modelAsset := g.Models[modelIndex]
|
||||||
|
|
||||||
|
// Validate model before assigning
|
||||||
|
if modelAsset.Model.Meshes == nil {
|
||||||
|
rl.TraceLog(rl.LogWarning, "Trying to assign invalid model to player %d", player.ID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
player.Model = modelAsset.Model
|
||||||
|
player.Texture = modelAsset.Texture
|
||||||
|
player.PlaceholderColor = modelAsset.PlaceholderColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) QuitChan() <-chan struct{} {
|
||||||
|
return g.quitChan
|
||||||
}
|
}
|
||||||
|
185
game/login.go
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LoginScreen struct {
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
errorMessage string
|
||||||
|
isRegistering bool
|
||||||
|
focusedField int // 0 = username, 1 = password
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLoginScreen() *LoginScreen {
|
||||||
|
return &LoginScreen{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LoginScreen) Draw() {
|
||||||
|
screenWidth := float32(rl.GetScreenWidth())
|
||||||
|
screenHeight := float32(rl.GetScreenHeight())
|
||||||
|
|
||||||
|
// Draw background
|
||||||
|
rl.DrawRectangle(0, 0, int32(screenWidth), int32(screenHeight), rl.RayWhite)
|
||||||
|
|
||||||
|
// Draw title
|
||||||
|
title := "GoonScape"
|
||||||
|
if l.isRegistering {
|
||||||
|
title += " - Register"
|
||||||
|
} else {
|
||||||
|
title += " - Login"
|
||||||
|
}
|
||||||
|
titleSize := int32(40)
|
||||||
|
titleWidth := rl.MeasureText(title, titleSize)
|
||||||
|
rl.DrawText(title, int32(screenWidth/2)-titleWidth/2, 100, titleSize, rl.Black)
|
||||||
|
|
||||||
|
// Draw input fields
|
||||||
|
inputWidth := float32(200)
|
||||||
|
inputHeight := float32(30)
|
||||||
|
inputX := screenWidth/2 - inputWidth/2
|
||||||
|
|
||||||
|
// Username field
|
||||||
|
rl.DrawRectangleRec(rl.Rectangle{
|
||||||
|
X: inputX, Y: 200,
|
||||||
|
Width: inputWidth, Height: inputHeight,
|
||||||
|
}, rl.LightGray)
|
||||||
|
rl.DrawText(l.username, int32(inputX)+5, 205, 20, rl.Black)
|
||||||
|
if l.focusedField == 0 {
|
||||||
|
rl.DrawRectangleLinesEx(rl.Rectangle{
|
||||||
|
X: inputX - 2, Y: 198,
|
||||||
|
Width: inputWidth + 4, Height: inputHeight + 4,
|
||||||
|
}, 2, rl.Blue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password field
|
||||||
|
rl.DrawRectangleRec(rl.Rectangle{
|
||||||
|
X: inputX, Y: 250,
|
||||||
|
Width: inputWidth, Height: inputHeight,
|
||||||
|
}, rl.LightGray)
|
||||||
|
masked := ""
|
||||||
|
for range l.password {
|
||||||
|
masked += "*"
|
||||||
|
}
|
||||||
|
rl.DrawText(masked, int32(inputX)+5, 255, 20, rl.Black)
|
||||||
|
if l.focusedField == 1 {
|
||||||
|
rl.DrawRectangleLinesEx(rl.Rectangle{
|
||||||
|
X: inputX - 2, Y: 248,
|
||||||
|
Width: inputWidth + 4, Height: inputHeight + 4,
|
||||||
|
}, 2, rl.Blue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw error message
|
||||||
|
if l.errorMessage != "" {
|
||||||
|
msgWidth := rl.MeasureText(l.errorMessage, 20)
|
||||||
|
rl.DrawText(l.errorMessage, int32(screenWidth/2)-msgWidth/2, 300, 20, rl.Red)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw buttons
|
||||||
|
buttonWidth := float32(100)
|
||||||
|
buttonHeight := float32(30)
|
||||||
|
buttonY := float32(350)
|
||||||
|
|
||||||
|
// Login/Register button
|
||||||
|
actionBtn := rl.Rectangle{
|
||||||
|
X: screenWidth/2 - buttonWidth - 10,
|
||||||
|
Y: buttonY,
|
||||||
|
Width: buttonWidth,
|
||||||
|
Height: buttonHeight,
|
||||||
|
}
|
||||||
|
rl.DrawRectangleRec(actionBtn, rl.Blue)
|
||||||
|
actionText := "Login"
|
||||||
|
if l.isRegistering {
|
||||||
|
actionText = "Register"
|
||||||
|
}
|
||||||
|
actionWidth := rl.MeasureText(actionText, 20)
|
||||||
|
rl.DrawText(actionText,
|
||||||
|
int32(actionBtn.X+actionBtn.Width/2)-actionWidth/2,
|
||||||
|
int32(actionBtn.Y+5),
|
||||||
|
20, rl.White)
|
||||||
|
|
||||||
|
// Switch mode button
|
||||||
|
switchBtn := rl.Rectangle{
|
||||||
|
X: screenWidth/2 + 10,
|
||||||
|
Y: buttonY,
|
||||||
|
Width: buttonWidth,
|
||||||
|
Height: buttonHeight,
|
||||||
|
}
|
||||||
|
rl.DrawRectangleRec(switchBtn, rl.DarkGray)
|
||||||
|
switchText := "Register"
|
||||||
|
if l.isRegistering {
|
||||||
|
switchText = "Login"
|
||||||
|
}
|
||||||
|
switchWidth := rl.MeasureText(switchText, 20)
|
||||||
|
rl.DrawText(switchText,
|
||||||
|
int32(switchBtn.X+switchBtn.Width/2)-switchWidth/2,
|
||||||
|
int32(switchBtn.Y+5),
|
||||||
|
20, rl.White)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LoginScreen) Update() (string, string, bool, bool) {
|
||||||
|
// Handle input field focus
|
||||||
|
if rl.IsMouseButtonPressed(rl.MouseLeftButton) {
|
||||||
|
mousePos := rl.GetMousePosition()
|
||||||
|
screenWidth := float32(rl.GetScreenWidth())
|
||||||
|
inputWidth := float32(200)
|
||||||
|
inputX := screenWidth/2 - inputWidth/2
|
||||||
|
|
||||||
|
// Check username field
|
||||||
|
if mousePos.X >= inputX && mousePos.X <= inputX+inputWidth &&
|
||||||
|
mousePos.Y >= 200 && mousePos.Y <= 230 {
|
||||||
|
l.focusedField = 0
|
||||||
|
}
|
||||||
|
// Check password field
|
||||||
|
if mousePos.X >= inputX && mousePos.X <= inputX+inputWidth &&
|
||||||
|
mousePos.Y >= 250 && mousePos.Y <= 280 {
|
||||||
|
l.focusedField = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check buttons
|
||||||
|
buttonWidth := float32(100)
|
||||||
|
if mousePos.Y >= 350 && mousePos.Y <= 380 {
|
||||||
|
// Action button
|
||||||
|
if mousePos.X >= screenWidth/2-buttonWidth-10 &&
|
||||||
|
mousePos.X <= screenWidth/2-10 {
|
||||||
|
return l.username, l.password, l.isRegistering, true
|
||||||
|
}
|
||||||
|
// Switch mode button
|
||||||
|
if mousePos.X >= screenWidth/2+10 &&
|
||||||
|
mousePos.X <= screenWidth/2+buttonWidth+10 {
|
||||||
|
l.isRegistering = !l.isRegistering
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle text input
|
||||||
|
key := rl.GetCharPressed()
|
||||||
|
for key > 0 {
|
||||||
|
if l.focusedField == 0 && len(l.username) < 12 {
|
||||||
|
l.username += string(key)
|
||||||
|
} else if l.focusedField == 1 && len(l.password) < 20 {
|
||||||
|
l.password += string(key)
|
||||||
|
}
|
||||||
|
key = rl.GetCharPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle backspace
|
||||||
|
if rl.IsKeyPressed(rl.KeyBackspace) {
|
||||||
|
if l.focusedField == 0 && len(l.username) > 0 {
|
||||||
|
l.username = l.username[:len(l.username)-1]
|
||||||
|
} else if l.focusedField == 1 && len(l.password) > 0 {
|
||||||
|
l.password = l.password[:len(l.password)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle tab to switch fields
|
||||||
|
if rl.IsKeyPressed(rl.KeyTab) {
|
||||||
|
l.focusedField = (l.focusedField + 1) % 2
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", "", false, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LoginScreen) SetError(msg string) {
|
||||||
|
l.errorMessage = msg
|
||||||
|
}
|
2
go.mod
@ -3,7 +3,7 @@ module gitea.boner.be/bdnugget/goonscape
|
|||||||
go 1.23.0
|
go 1.23.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
gitea.boner.be/bdnugget/goonserver v0.0.0-20250113131525-49e23114973c
|
gitea.boner.be/bdnugget/goonserver v1.1.0
|
||||||
github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b
|
github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b
|
||||||
google.golang.org/protobuf v1.36.3
|
google.golang.org/protobuf v1.36.3
|
||||||
)
|
)
|
||||||
|
148
main.go
@ -3,7 +3,11 @@ package main
|
|||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/game"
|
"gitea.boner.be/bdnugget/goonscape/game"
|
||||||
"gitea.boner.be/bdnugget/goonscape/network"
|
"gitea.boner.be/bdnugget/goonscape/network"
|
||||||
@ -11,61 +15,139 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
local := flag.Bool("local", false, "Use local server instead of remote")
|
// Set up panic recovery at the top level
|
||||||
addr := flag.String("addr", "boner.be:6969", "Server address (hostname:port or hostname)")
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Printf("Recovered from fatal panic in main: %v", r)
|
||||||
|
// Give the user a chance to see the error
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Parse command line flags
|
||||||
|
verbose := flag.Bool("v", false, "Also show info logs (spammy)")
|
||||||
|
local := flag.Bool("local", false, "Connect to local server")
|
||||||
|
addr := flag.String("addr", "", "Server address (host or host:port)")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *local && *addr != "boner.be:6969" {
|
if *verbose {
|
||||||
log.Fatal("Cannot use both -local and -addr flags")
|
rl.SetTraceLogLevel(rl.LogTrace)
|
||||||
|
} else {
|
||||||
|
rl.SetTraceLogLevel(rl.LogWarning)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set server address based on flags
|
||||||
if *local {
|
if *local {
|
||||||
|
if *addr != "" {
|
||||||
|
log.Fatal("Cannot use -local and -addr together")
|
||||||
|
}
|
||||||
network.SetServerAddr("localhost:6969")
|
network.SetServerAddr("localhost:6969")
|
||||||
} else if *addr != "" {
|
} else if *addr != "" {
|
||||||
// If only hostname is provided, append default port
|
// If port is not specified, append default port
|
||||||
if !strings.Contains(*addr, ":") {
|
if !strings.Contains(*addr, ":") {
|
||||||
*addr = *addr + ":6969"
|
*addr += ":6969"
|
||||||
}
|
}
|
||||||
network.SetServerAddr(*addr)
|
network.SetServerAddr(*addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize window with error handling
|
||||||
|
rl.SetConfigFlags(rl.FlagMsaa4xHint | rl.FlagWindowResizable) // Enable MSAA and make window resizable
|
||||||
rl.InitWindow(1024, 768, "GoonScape")
|
rl.InitWindow(1024, 768, "GoonScape")
|
||||||
|
|
||||||
rl.SetExitKey(0)
|
rl.SetExitKey(0)
|
||||||
defer rl.CloseWindow()
|
|
||||||
|
// Initialize audio with error handling
|
||||||
|
if !rl.IsAudioDeviceReady() {
|
||||||
rl.InitAudioDevice()
|
rl.InitAudioDevice()
|
||||||
defer rl.CloseAudioDevice()
|
if !rl.IsAudioDeviceReady() {
|
||||||
|
log.Println("Warning: Failed to initialize audio device, continuing without audio")
|
||||||
game := game.New()
|
|
||||||
if err := game.LoadAssets(); err != nil {
|
|
||||||
log.Fatalf("Failed to load assets: %v", err)
|
|
||||||
}
|
}
|
||||||
defer game.Cleanup()
|
|
||||||
|
|
||||||
conn, playerID, err := network.ConnectToServer()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to connect to server: %v", err)
|
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
game.Player.ID = playerID
|
// Use a maximum of 3 attempts to load assets
|
||||||
modelIndex := int(playerID) % len(game.Models)
|
var gameInstance *game.Game
|
||||||
game.Player.Model = game.Models[modelIndex].Model
|
var loadErr error
|
||||||
game.Player.Texture = game.Models[modelIndex].Texture
|
maxAttempts := 3
|
||||||
|
|
||||||
go network.HandleServerCommunication(conn, playerID, game.Player, game.OtherPlayers, game.QuitChan)
|
for attempt := 1; attempt <= maxAttempts; attempt++ {
|
||||||
|
gameInstance = game.New()
|
||||||
|
loadErr = gameInstance.LoadAssets()
|
||||||
|
if loadErr == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Attempt %d/%d: Failed to load assets: %v", attempt, maxAttempts, loadErr)
|
||||||
|
if attempt < maxAttempts {
|
||||||
|
log.Println("Retrying...")
|
||||||
|
gameInstance.Cleanup() // Cleanup before retrying
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if loadErr != nil {
|
||||||
|
log.Printf("Failed to load assets after %d attempts. Starting with default assets.", maxAttempts)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if gameInstance != nil {
|
||||||
|
gameInstance.Cleanup()
|
||||||
|
}
|
||||||
|
rl.CloseWindow()
|
||||||
|
if rl.IsAudioDeviceReady() {
|
||||||
|
rl.CloseAudioDevice()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
rl.PlayMusicStream(game.Music)
|
|
||||||
rl.SetMusicVolume(game.Music, 0.5)
|
|
||||||
rl.SetTargetFPS(60)
|
rl.SetTargetFPS(60)
|
||||||
|
|
||||||
for !rl.WindowShouldClose() {
|
// Play music if available
|
||||||
rl.UpdateMusicStream(game.Music)
|
if gameInstance.Music.Stream.Buffer != nil {
|
||||||
deltaTime := rl.GetFrameTime()
|
rl.PlayMusicStream(gameInstance.Music)
|
||||||
|
rl.SetMusicVolume(gameInstance.Music, 0.5)
|
||||||
game.Update(deltaTime)
|
|
||||||
game.Render()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for clean shutdown
|
// Handle OS signals for clean shutdown
|
||||||
<-game.QuitChan
|
sigChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
<-sigChan
|
||||||
|
if gameInstance != nil {
|
||||||
|
gameInstance.Shutdown()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Keep game loop in main thread for Raylib
|
||||||
|
for !rl.WindowShouldClose() {
|
||||||
|
deltaTime := rl.GetFrameTime()
|
||||||
|
|
||||||
|
// Update music if available
|
||||||
|
if gameInstance.Music.Stream.Buffer != nil {
|
||||||
|
rl.UpdateMusicStream(gameInstance.Music)
|
||||||
|
}
|
||||||
|
|
||||||
|
func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Printf("Recovered from panic in game update: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
gameInstance.Update(deltaTime)
|
||||||
|
}()
|
||||||
|
|
||||||
|
func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Printf("Recovered from panic in game render: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
gameInstance.Render()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Check if game requested shutdown
|
||||||
|
select {
|
||||||
|
case <-gameInstance.QuitChan():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,71 +3,122 @@ package network
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/game"
|
|
||||||
"gitea.boner.be/bdnugget/goonscape/types"
|
"gitea.boner.be/bdnugget/goonscape/types"
|
||||||
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
||||||
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
var serverAddr = "boner.be:6969"
|
const protoVersion = 1
|
||||||
|
|
||||||
|
var serverAddr = "boner.be:6969" // Default server address
|
||||||
|
|
||||||
func SetServerAddr(addr string) {
|
func SetServerAddr(addr string) {
|
||||||
serverAddr = addr
|
serverAddr = addr
|
||||||
|
log.Printf("Server address set to: %s", serverAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConnectToServer() (net.Conn, int32, error) {
|
func ConnectToServer(username, password string, isRegistering bool) (net.Conn, int32, error) {
|
||||||
conn, err := net.Dial("tcp", serverAddr)
|
conn, err := net.Dial("tcp", serverAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to dial server: %v", err)
|
log.Printf("Failed to dial server: %v", err)
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Connected to server. Waiting for player ID...")
|
log.Println("Connected to server. Authenticating...")
|
||||||
reader := bufio.NewReader(conn)
|
|
||||||
|
|
||||||
// Read message length (4 bytes)
|
// Send auth message
|
||||||
|
authAction := &pb.Action{
|
||||||
|
Type: pb.Action_LOGIN,
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
}
|
||||||
|
if isRegistering {
|
||||||
|
authAction.Type = pb.Action_REGISTER
|
||||||
|
}
|
||||||
|
|
||||||
|
authBatch := &pb.ActionBatch{
|
||||||
|
Actions: []*pb.Action{authAction},
|
||||||
|
ProtocolVersion: protoVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeMessage(conn, authBatch); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, 0, fmt.Errorf("failed to send auth: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read server response
|
||||||
|
reader := bufio.NewReader(conn)
|
||||||
lengthBuf := make([]byte, 4)
|
lengthBuf := make([]byte, 4)
|
||||||
if _, err := io.ReadFull(reader, lengthBuf); err != nil {
|
if _, err := io.ReadFull(reader, lengthBuf); err != nil {
|
||||||
log.Printf("Failed to read message length: %v", err)
|
conn.Close()
|
||||||
return nil, 0, err
|
return nil, 0, fmt.Errorf("failed to read auth response: %v", err)
|
||||||
}
|
}
|
||||||
messageLength := binary.BigEndian.Uint32(lengthBuf)
|
messageLength := binary.BigEndian.Uint32(lengthBuf)
|
||||||
|
|
||||||
// Read the full message
|
|
||||||
messageBuf := make([]byte, messageLength)
|
messageBuf := make([]byte, messageLength)
|
||||||
if _, err := io.ReadFull(reader, messageBuf); err != nil {
|
if _, err := io.ReadFull(reader, messageBuf); err != nil {
|
||||||
log.Printf("Failed to read message body: %v", err)
|
conn.Close()
|
||||||
return nil, 0, err
|
return nil, 0, fmt.Errorf("failed to read auth response body: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var response pb.ServerMessage
|
var response pb.ServerMessage
|
||||||
if err := proto.Unmarshal(messageBuf, &response); err != nil {
|
if err := proto.Unmarshal(messageBuf, &response); err != nil {
|
||||||
log.Printf("Failed to unmarshal server response: %v", err)
|
conn.Close()
|
||||||
return nil, 0, err
|
return nil, 0, fmt.Errorf("failed to unmarshal auth response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.ProtocolVersion > protoVersion {
|
||||||
|
conn.Close()
|
||||||
|
return nil, 0, fmt.Errorf("server requires newer protocol version (server: %d, client: %d)",
|
||||||
|
response.ProtocolVersion, protoVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !response.AuthSuccess {
|
||||||
|
conn.Close()
|
||||||
|
return nil, 0, fmt.Errorf(response.ErrorMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
playerID := response.GetPlayerId()
|
playerID := response.GetPlayerId()
|
||||||
log.Printf("Successfully connected with player ID: %d", playerID)
|
log.Printf("Successfully authenticated with player ID: %d", playerID)
|
||||||
return conn, playerID, nil
|
return conn, playerID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Player, otherPlayers map[int32]*types.Player, quitChan <-chan struct{}) {
|
func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Player, otherPlayers map[int32]*types.Player, quitChan <-chan struct{}) {
|
||||||
reader := bufio.NewReader(conn)
|
reader := bufio.NewReader(conn)
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Printf("Recovered from panic in HandleServerCommunication: %v", r)
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
if player.QuitDone != nil {
|
||||||
|
close(player.QuitDone)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
actionTicker := time.NewTicker(types.ClientTickRate)
|
actionTicker := time.NewTicker(types.ClientTickRate)
|
||||||
defer actionTicker.Stop()
|
defer actionTicker.Stop()
|
||||||
defer conn.Close()
|
|
||||||
defer close(player.QuitDone)
|
|
||||||
|
|
||||||
// Create a channel to signal when goroutines are done
|
// Create error channel for goroutine communication
|
||||||
|
errChan := make(chan error, 1)
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
|
|
||||||
|
// Start message sending goroutine
|
||||||
go func() {
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Printf("Recovered from panic in message sender: %v", r)
|
||||||
|
errChan <- fmt.Errorf("message sender panic: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-quitChan:
|
case <-quitChan:
|
||||||
@ -82,23 +133,23 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
|
|||||||
writeMessage(conn, disconnectMsg)
|
writeMessage(conn, disconnectMsg)
|
||||||
done <- struct{}{}
|
done <- struct{}{}
|
||||||
return
|
return
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
case <-actionTicker.C:
|
case <-actionTicker.C:
|
||||||
player.Lock()
|
player.Lock()
|
||||||
if len(player.ActionQueue) > 0 {
|
if len(player.ActionQueue) > 0 {
|
||||||
actions := make([]*pb.Action, len(player.ActionQueue))
|
actions := make([]*pb.Action, len(player.ActionQueue))
|
||||||
copy(actions, player.ActionQueue)
|
copy(actions, player.ActionQueue)
|
||||||
|
|
||||||
batch := &pb.ActionBatch{
|
batch := &pb.ActionBatch{
|
||||||
PlayerId: playerID,
|
PlayerId: playerID,
|
||||||
Actions: actions,
|
Actions: actions,
|
||||||
Tick: player.CurrentTick,
|
Tick: player.CurrentTick,
|
||||||
}
|
}
|
||||||
|
|
||||||
player.ActionQueue = player.ActionQueue[:0]
|
player.ActionQueue = player.ActionQueue[:0]
|
||||||
player.Unlock()
|
player.Unlock()
|
||||||
|
|
||||||
if err := writeMessage(conn, batch); err != nil {
|
if err := writeMessage(conn, batch); err != nil {
|
||||||
log.Printf("Failed to send actions to server: %v", err)
|
errChan <- err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -108,42 +159,50 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// Main message receiving loop
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Printf("Recovered from panic in message receiver: %v", r)
|
||||||
|
errChan <- fmt.Errorf("message receiver panic: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-quitChan:
|
case <-quitChan:
|
||||||
done := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
<-done
|
|
||||||
close(player.QuitDone)
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
case <-time.After(1 * time.Second):
|
|
||||||
log.Println("Shutdown timed out")
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
// Read message length (4 bytes)
|
|
||||||
lengthBuf := make([]byte, 4)
|
lengthBuf := make([]byte, 4)
|
||||||
if _, err := io.ReadFull(reader, lengthBuf); err != nil {
|
if _, err := io.ReadFull(reader, lengthBuf); err != nil {
|
||||||
log.Printf("Failed to read message length: %v", err)
|
if err != io.EOF {
|
||||||
|
log.Printf("Network read error: %v", err)
|
||||||
|
errChan <- fmt.Errorf("failed to read message length: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("Connection closed by server")
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
messageLength := binary.BigEndian.Uint32(lengthBuf)
|
messageLength := binary.BigEndian.Uint32(lengthBuf)
|
||||||
|
|
||||||
// Read the full message
|
// Sanity check message size to prevent potential memory issues
|
||||||
|
if messageLength > 1024*1024 { // 1MB max message size
|
||||||
|
log.Printf("Message size too large: %d bytes", messageLength)
|
||||||
|
errChan <- fmt.Errorf("message size too large: %d bytes", messageLength)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
messageBuf := make([]byte, messageLength)
|
messageBuf := make([]byte, messageLength)
|
||||||
if _, err := io.ReadFull(reader, messageBuf); err != nil {
|
if _, err := io.ReadFull(reader, messageBuf); err != nil {
|
||||||
log.Printf("Failed to read message body: %v", err)
|
log.Printf("Failed to read message body: %v", err)
|
||||||
|
errChan <- fmt.Errorf("failed to read message body: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var serverMessage pb.ServerMessage
|
var serverMessage pb.ServerMessage
|
||||||
if err := proto.Unmarshal(messageBuf, &serverMessage); err != nil {
|
if err := proto.Unmarshal(messageBuf, &serverMessage); err != nil {
|
||||||
log.Printf("Failed to unmarshal server message: %v", err)
|
log.Printf("Failed to unmarshal server message: %v", err)
|
||||||
continue
|
continue // Skip this message but don't quit
|
||||||
}
|
}
|
||||||
|
|
||||||
player.Lock()
|
player.Lock()
|
||||||
@ -160,22 +219,66 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
|
|||||||
}
|
}
|
||||||
player.Unlock()
|
player.Unlock()
|
||||||
|
|
||||||
|
// Process player states
|
||||||
|
validPlayerIds := make(map[int32]bool)
|
||||||
for _, state := range serverMessage.Players {
|
for _, state := range serverMessage.Players {
|
||||||
|
validPlayerIds[state.PlayerId] = true
|
||||||
|
|
||||||
if state.PlayerId == playerID {
|
if state.PlayerId == playerID {
|
||||||
|
player.Lock()
|
||||||
|
// Update initial position if not set
|
||||||
|
if player.PosActual.X == 0 && player.PosActual.Z == 0 {
|
||||||
|
player.PosActual = rl.Vector3{
|
||||||
|
X: float32(state.X * types.TileSize),
|
||||||
|
Y: 0,
|
||||||
|
Z: float32(state.Y * types.TileSize),
|
||||||
|
}
|
||||||
|
player.PosTile = types.Tile{X: int(state.X), Y: int(state.Y)}
|
||||||
|
}
|
||||||
|
player.Unlock()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update or create other players
|
||||||
if otherPlayer, exists := otherPlayers[state.PlayerId]; exists {
|
if otherPlayer, exists := otherPlayers[state.PlayerId]; exists {
|
||||||
otherPlayer.UpdatePosition(state, types.ServerTickRate)
|
otherPlayer.UpdatePosition(state, types.ServerTickRate)
|
||||||
} else {
|
} else {
|
||||||
|
log.Printf("Creating new player with ID: %d", state.PlayerId)
|
||||||
otherPlayers[state.PlayerId] = types.NewPlayer(state)
|
otherPlayers[state.PlayerId] = types.NewPlayer(state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if g, ok := player.UserData.(*game.Game); ok && len(serverMessage.ChatMessages) > 0 {
|
// Remove players no longer in the server state
|
||||||
g.Chat.HandleServerMessages(serverMessage.ChatMessages)
|
for id := range otherPlayers {
|
||||||
|
if id != playerID && !validPlayerIds[id] {
|
||||||
|
log.Printf("Removing player with ID: %d", id)
|
||||||
|
delete(otherPlayers, id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle chat messages
|
||||||
|
if handler, ok := player.UserData.(types.ChatMessageHandler); ok && len(serverMessage.ChatMessages) > 0 {
|
||||||
|
log.Printf("Received %d chat messages from server", len(serverMessage.ChatMessages))
|
||||||
|
handler.HandleServerMessages(serverMessage.ChatMessages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for error or quit signal
|
||||||
|
select {
|
||||||
|
case <-quitChan:
|
||||||
|
// Send disconnect message
|
||||||
|
disconnectMsg := &pb.ActionBatch{
|
||||||
|
PlayerId: playerID,
|
||||||
|
Actions: []*pb.Action{{
|
||||||
|
Type: pb.Action_DISCONNECT,
|
||||||
|
PlayerId: playerID,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
writeMessage(conn, disconnectMsg)
|
||||||
|
case err := <-errChan:
|
||||||
|
log.Printf("Network error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,3 +300,50 @@ func writeMessage(conn net.Conn, msg proto.Message) error {
|
|||||||
_, err = conn.Write(data)
|
_, err = conn.Write(data)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Connection struct {
|
||||||
|
conn net.Conn
|
||||||
|
playerID int32
|
||||||
|
quitChan chan struct{}
|
||||||
|
quitDone chan struct{}
|
||||||
|
closeOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConnection(username, password string, isRegistering bool) (*Connection, error) {
|
||||||
|
conn, playerID, err := ConnectToServer(username, password, isRegistering)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Connection{
|
||||||
|
conn: conn,
|
||||||
|
playerID: playerID,
|
||||||
|
quitChan: make(chan struct{}),
|
||||||
|
quitDone: make(chan struct{}),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Connection) Close() {
|
||||||
|
c.closeOnce.Do(func() {
|
||||||
|
close(c.quitChan)
|
||||||
|
// Wait with timeout for network cleanup
|
||||||
|
select {
|
||||||
|
case <-c.quitDone:
|
||||||
|
// Clean shutdown completed
|
||||||
|
case <-time.After(500 * time.Millisecond):
|
||||||
|
log.Println("Network cleanup timed out")
|
||||||
|
}
|
||||||
|
c.conn.Close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Connection) PlayerID() int32 {
|
||||||
|
return c.playerID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Connection) Start(player *types.Player, otherPlayers map[int32]*types.Player) {
|
||||||
|
go HandleServerCommunication(c.conn, c.playerID, player, otherPlayers, c.quitChan)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Connection) QuitChan() <-chan struct{} {
|
||||||
|
return c.quitChan
|
||||||
|
}
|
||||||
|
BIN
resources/models/coomer/Animation_Confused_Scratch_withSkin.glb
Normal file
BIN
resources/models/coomer/Animation_Idle_withSkin.glb
Normal file
BIN
resources/models/coomer/Animation_Running_withSkin.glb
Normal file
BIN
resources/models/coomer/Animation_Unsteady_Walk_withSkin.glb
Normal file
BIN
resources/models/coomer/Animation_Walking_withSkin.glb
Normal file
BIN
resources/models/coomer/idle_notransy.glb
Normal file
BIN
resources/models/coomer/unsteadywalk_notransy.glb
Normal file
BIN
resources/models/coomerAnim.zip
Normal file
BIN
resources/models/gooner/idle_no_y_transform.glb
Normal file
BIN
resources/models/gooner/walk_no_y_transform.glb
Normal file
@ -6,12 +6,15 @@ build() {
|
|||||||
local arch=$2
|
local arch=$2
|
||||||
local output=$3
|
local output=$3
|
||||||
|
|
||||||
# Set CGO flags for static linking
|
# Set GOOS and GOARCH for cross-compilation
|
||||||
export CGO_ENABLED=1
|
|
||||||
export GOOS=$os
|
export GOOS=$os
|
||||||
export GOARCH=$arch
|
export GOARCH=$arch
|
||||||
|
|
||||||
# Platform specific flags
|
# 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
|
if [ "$os" = "windows" ]; then
|
||||||
export CC=x86_64-w64-mingw32-gcc
|
export CC=x86_64-w64-mingw32-gcc
|
||||||
export CXX=x86_64-w64-mingw32-g++
|
export CXX=x86_64-w64-mingw32-g++
|
||||||
|
@ -1,16 +1,37 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
||||||
rl "github.com/gen2brain/raylib-go/raylib"
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *Player) MoveTowards(target Tile, deltaTime float32, mapGrid [][]Tile) {
|
type Player struct {
|
||||||
p.Lock()
|
sync.RWMutex // Keep this for network operations
|
||||||
defer p.Unlock()
|
Model rl.Model
|
||||||
|
Texture rl.Texture2D
|
||||||
|
PosActual rl.Vector3
|
||||||
|
PosTile Tile
|
||||||
|
TargetPath []Tile
|
||||||
|
Speed float32
|
||||||
|
ActionQueue []*pb.Action
|
||||||
|
ID int32
|
||||||
|
QuitDone chan struct{}
|
||||||
|
CurrentTick int64
|
||||||
|
UserData interface{}
|
||||||
|
FloatingMessage *FloatingMessage
|
||||||
|
IsMoving bool
|
||||||
|
AnimationFrame int32
|
||||||
|
LastAnimUpdate time.Time
|
||||||
|
LastUpdateTime time.Time
|
||||||
|
InterpolationProgress float32
|
||||||
|
PlaceholderColor rl.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Player) MoveTowards(target Tile, deltaTime float32, mapGrid [][]Tile) {
|
||||||
|
// No need for lock here as this is called from a single thread (game loop)
|
||||||
targetPos := rl.Vector3{
|
targetPos := rl.Vector3{
|
||||||
X: float32(target.X * TileSize),
|
X: float32(target.X * TileSize),
|
||||||
Y: mapGrid[target.X][target.Y].Height * TileHeight,
|
Y: mapGrid[target.X][target.Y].Height * TileHeight,
|
||||||
@ -19,6 +40,33 @@ func (p *Player) MoveTowards(target Tile, deltaTime float32, mapGrid [][]Tile) {
|
|||||||
|
|
||||||
direction := rl.Vector3Subtract(targetPos, p.PosActual)
|
direction := rl.Vector3Subtract(targetPos, p.PosActual)
|
||||||
distance := rl.Vector3Length(direction)
|
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.LogDebug, "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.LogDebug, "Idle frame update: %d -> %d (delta: %f)",
|
||||||
|
oldFrame, p.AnimationFrame, deltaTime)
|
||||||
|
}
|
||||||
|
|
||||||
if distance > 0 {
|
if distance > 0 {
|
||||||
direction = rl.Vector3Scale(direction, p.Speed*deltaTime/distance)
|
direction = rl.Vector3Scale(direction, p.Speed*deltaTime/distance)
|
||||||
}
|
}
|
||||||
@ -44,6 +92,11 @@ func NewPlayer(state *pb.PlayerState) *Player {
|
|||||||
PosTile: Tile{X: int(state.X), Y: int(state.Y)},
|
PosTile: Tile{X: int(state.X), Y: int(state.Y)},
|
||||||
Speed: 50.0,
|
Speed: 50.0,
|
||||||
ID: state.PlayerId,
|
ID: state.PlayerId,
|
||||||
|
IsMoving: false,
|
||||||
|
AnimationFrame: 0,
|
||||||
|
LastAnimUpdate: time.Now(),
|
||||||
|
LastUpdateTime: time.Now(),
|
||||||
|
InterpolationProgress: 1.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,6 +114,7 @@ func (p *Player) UpdatePosition(state *pb.PlayerState, tickRate time.Duration) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Player) ForceResync(state *pb.PlayerState) {
|
func (p *Player) ForceResync(state *pb.PlayerState) {
|
||||||
|
// Keep this lock since it's called from the network goroutine
|
||||||
p.Lock()
|
p.Lock()
|
||||||
defer p.Unlock()
|
defer p.Unlock()
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
||||||
@ -14,31 +13,27 @@ type Tile struct {
|
|||||||
Walkable bool
|
Walkable bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Player struct {
|
type AnimationSet struct {
|
||||||
sync.Mutex
|
Idle []rl.ModelAnimation
|
||||||
PosActual rl.Vector3
|
Walk []rl.ModelAnimation
|
||||||
PosTile Tile
|
// Can add more animation types later like:
|
||||||
TargetPath []Tile
|
// Attack []ModelAnimation
|
||||||
ActionQueue []*pb.Action
|
// Jump []ModelAnimation
|
||||||
Speed float32
|
|
||||||
Model rl.Model
|
|
||||||
Texture rl.Texture2D
|
|
||||||
ID int32
|
|
||||||
CurrentTick int64
|
|
||||||
LastUpdateTime time.Time
|
|
||||||
InterpolationProgress float32
|
|
||||||
UserData interface{}
|
|
||||||
FloatingMessage *FloatingMessage
|
|
||||||
QuitDone chan struct{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModelAsset struct {
|
type ModelAsset struct {
|
||||||
Model rl.Model
|
Model rl.Model
|
||||||
Texture rl.Texture2D
|
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)
|
||||||
|
PlaceholderColor rl.Color
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChatMessage struct {
|
type ChatMessage struct {
|
||||||
PlayerID int32
|
PlayerID int32
|
||||||
|
Username string
|
||||||
Content string
|
Content string
|
||||||
Time time.Time
|
Time time.Time
|
||||||
}
|
}
|
||||||
@ -49,6 +44,10 @@ type FloatingMessage struct {
|
|||||||
ScreenPos rl.Vector2 // Store the screen position for 2D rendering
|
ScreenPos rl.Vector2 // Store the screen position for 2D rendering
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ChatMessageHandler interface {
|
||||||
|
HandleServerMessages([]*pb.ChatMessage)
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MapWidth = 50
|
MapWidth = 50
|
||||||
MapHeight = 50
|
MapHeight = 50
|
||||||
|