Compare commits
	
		
			31 Commits
		
	
	
		
			develop
			...
			0f56916295
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 0f56916295 | |||
| a1ddbadea0 | |||
| e4d0b98945 | |||
| 509bc8b20b | |||
| c40e4ae7ac | |||
| 863f5a939c | |||
| cd68581429 | |||
| b9d0d46bd6 | |||
| b96c7ada7a | |||
| d86cbe15a3 | |||
| fb018e2a7d | |||
| 5ca973fdf1 | |||
| 2a0f9348e9 | |||
| d6d0f36cee | |||
| e8d062c4b7 | |||
| 0cd3145d28 | |||
| 0b6ab17ad5 | |||
| 50952309f4 | |||
| afc44710f2 | |||
| 1a7b0eff42 | |||
| bf7bf12a53 | |||
| e661320508 | |||
| 567ec40c3d | |||
| c01b8d1c59 | |||
| d301d597e8 | |||
| 91cdbab54a | |||
| 0a58e0453a | |||
| 8d70129c73 | |||
| 4012a2ed92 | |||
| 4f36c2ee1f | |||
| 63e3837441 | 
							
								
								
									
										3
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,3 @@ | |||||||
|  | [submodule "goonserver"] | ||||||
|  | 	path = goonserver | ||||||
|  | 	url = https://gitea.boner.be/bdnugget/goonserver | ||||||
							
								
								
									
										30
									
								
								Dockerfile.build
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,30 @@ | |||||||
|  | FROM golang:1.23 | ||||||
|  |  | ||||||
|  | # Install build dependencies | ||||||
|  | RUN apt-get update && apt-get install -y \ | ||||||
|  |     gcc-mingw-w64 \ | ||||||
|  |     cmake \ | ||||||
|  |     zip \ | ||||||
|  |     libasound2-dev \ | ||||||
|  |     mesa-common-dev \ | ||||||
|  |     libx11-dev \ | ||||||
|  |     libxrandr-dev \ | ||||||
|  |     libxi-dev \ | ||||||
|  |     xorg-dev \ | ||||||
|  |     libgl1-mesa-dev \ | ||||||
|  |     libglu1-mesa-dev \ | ||||||
|  |     libwayland-dev \ | ||||||
|  |     wayland-protocols \ | ||||||
|  |     libxkbcommon-dev \ | ||||||
|  |     && rm -rf /var/lib/apt/lists/* | ||||||
|  |  | ||||||
|  | WORKDIR /build | ||||||
|  |  | ||||||
|  | # Copy build scripts | ||||||
|  | COPY . /build/ | ||||||
|  |  | ||||||
|  | # Set execute permissions | ||||||
|  | RUN chmod +x /build/scripts/build.sh | ||||||
|  |  | ||||||
|  | # Build command | ||||||
|  | CMD ["make", "all"]  | ||||||
							
								
								
									
										30
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,30 @@ | |||||||
|  | .PHONY: all clean windows linux darwin | ||||||
|  |  | ||||||
|  | include scripts/platforms.mk | ||||||
|  |  | ||||||
|  | BINARY_NAME=goonscape | ||||||
|  | VERSION=1.0.0 | ||||||
|  | BUILD_DIR=build | ||||||
|  | ASSETS_DIR=resources | ||||||
|  |  | ||||||
|  | all: clean $(PLATFORMS) | ||||||
|  |  | ||||||
|  | $(PLATFORMS): | ||||||
|  | 	@echo "Building for $@..." | ||||||
|  | 	@mkdir -p $(BUILD_DIR)/$@ | ||||||
|  | 	@scripts/build.sh $(word 1,$(subst /, ,$@)) $(word 2,$(subst /, ,$@)) \ | ||||||
|  | 		$(BUILD_DIR)/$@/$(BINARY_NAME)$(if $(findstring windows,$@),.exe,) | ||||||
|  | 	@cp -r $(ASSETS_DIR) $(BUILD_DIR)/$@/ | ||||||
|  | 	@cd $(BUILD_DIR) && zip -r $(BINARY_NAME)-$(word 1,$(subst /, ,$@))-$(word 2,$(subst /, ,$@))-v$(VERSION).zip $@ | ||||||
|  | 	@echo "Done building for $@" | ||||||
|  |  | ||||||
|  | clean: | ||||||
|  | 	rm -rf $(BUILD_DIR) | ||||||
|  |  | ||||||
|  | # Development build for current platform | ||||||
|  | dev: | ||||||
|  | 	go build -o $(BINARY_NAME) | ||||||
|  |  | ||||||
|  | # Run tests | ||||||
|  | test: | ||||||
|  | 	go test ./...  | ||||||
							
								
								
									
										91
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,91 @@ | |||||||
|  | # 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 | ||||||
|  | - Raylib dependencies (see [raylib-go](https://github.com/gen2brain/raylib-go#requirements)) | ||||||
|  |  | ||||||
|  | ## Installation | ||||||
|  |  | ||||||
|  | 1. Clone the repository: | ||||||
|  | ```bash | ||||||
|  | git clone https://gitea.boner.be/bdnugget/goonscape.git | ||||||
|  | cd goonscape | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | 2. Install dependencies: | ||||||
|  | ```bash | ||||||
|  | go mod tidy | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | 3. Build and run: | ||||||
|  | ```bash | ||||||
|  | go run main.go | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## 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 | ||||||
|  | ``` | ||||||
							
								
								
									
										41
									
								
								assets/assets.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,41 @@ | |||||||
|  | package assets | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"gitea.boner.be/bdnugget/goonscape/types" | ||||||
|  | 	rl "github.com/gen2brain/raylib-go/raylib" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func LoadModels() ([]types.ModelAsset, 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 []types.ModelAsset{ | ||||||
|  | 		{Model: goonerModel, Texture: goonerTexture}, | ||||||
|  | 		{Model: coomerModel, Texture: coomerTexture}, | ||||||
|  | 		{Model: shrekeModel, Texture: shrekeTexture}, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func LoadMusic(filename string) (rl.Music, error) { | ||||||
|  | 	return rl.LoadMusicStream(filename), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func UnloadModels(models []types.ModelAsset) { | ||||||
|  | 	for _, model := range models { | ||||||
|  | 		rl.UnloadModel(model.Model) | ||||||
|  | 		rl.UnloadTexture(model.Texture) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func UnloadMusic(music rl.Music) { | ||||||
|  | 	rl.UnloadMusicStream(music) | ||||||
|  | } | ||||||
							
								
								
									
										
											BIN
										
									
								
								build/goonscape-linux-amd64-v1.0.0.zip
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								build/goonscape-windows-amd64-v1.0.0.zip
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								build/linux/amd64/goonscape
									
									
									
									
									
										Executable file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								build/linux/amd64/resources/audio/GoonScape1.mp3
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								build/linux/amd64/resources/audio/GoonScape2.mp3
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										12
									
								
								build/linux/amd64/resources/models/coomer.mtl
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,12 @@ | |||||||
|  | # Blender 3.6.0 MTL File: 'None' | ||||||
|  | # www.blender.org | ||||||
|  |  | ||||||
|  | newmtl Material.001 | ||||||
|  | Ns 250.000000 | ||||||
|  | Ka 1.000000 1.000000 1.000000 | ||||||
|  | Ks 0.500000 0.500000 0.500000 | ||||||
|  | Ke 0.000000 0.000000 0.000000 | ||||||
|  | Ni 1.450000 | ||||||
|  | d 1.000000 | ||||||
|  | illum 2 | ||||||
|  | map_Kd coomer.png | ||||||
							
								
								
									
										117248
									
								
								build/linux/amd64/resources/models/coomer.obj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								build/linux/amd64/resources/models/coomer.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.2 MiB | 
							
								
								
									
										12
									
								
								build/linux/amd64/resources/models/goonion.mtl
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,12 @@ | |||||||
|  | # Blender 3.6.0 MTL File: 'None' | ||||||
|  | # www.blender.org | ||||||
|  |  | ||||||
|  | newmtl Material.001 | ||||||
|  | Ns 250.000000 | ||||||
|  | Ka 1.000000 1.000000 1.000000 | ||||||
|  | Ks 0.500000 0.500000 0.500000 | ||||||
|  | Ke 0.000000 0.000000 0.000000 | ||||||
|  | Ni 1.450000 | ||||||
|  | d 1.000000 | ||||||
|  | illum 2 | ||||||
|  | map_Kd goonion.png | ||||||
							
								
								
									
										112531
									
								
								build/linux/amd64/resources/models/goonion.obj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								build/linux/amd64/resources/models/goonion.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.5 MiB | 
							
								
								
									
										12
									
								
								build/linux/amd64/resources/models/shreke.mtl
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,12 @@ | |||||||
|  | # Blender 3.6.0 MTL File: 'None' | ||||||
|  | # www.blender.org | ||||||
|  |  | ||||||
|  | newmtl Material.001 | ||||||
|  | Ns 250.000000 | ||||||
|  | Ka 1.000000 1.000000 1.000000 | ||||||
|  | Ks 0.500000 0.500000 0.500000 | ||||||
|  | Ke 0.000000 0.000000 0.000000 | ||||||
|  | Ni 1.450000 | ||||||
|  | d 1.000000 | ||||||
|  | illum 2 | ||||||
|  | map_Kd shreke.png | ||||||
							
								
								
									
										210035
									
								
								build/linux/amd64/resources/models/shreke.obj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								build/linux/amd64/resources/models/shreke.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.8 MiB | 
							
								
								
									
										
											BIN
										
									
								
								build/linux/amd64/resources/screenshot.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 104 KiB | 
							
								
								
									
										
											BIN
										
									
								
								build/windows/amd64/goonscape.exe
									
									
									
									
									
										Executable file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								build/windows/amd64/resources/audio/GoonScape1.mp3
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								build/windows/amd64/resources/audio/GoonScape2.mp3
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										12
									
								
								build/windows/amd64/resources/models/coomer.mtl
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,12 @@ | |||||||
|  | # Blender 3.6.0 MTL File: 'None' | ||||||
|  | # www.blender.org | ||||||
|  |  | ||||||
|  | newmtl Material.001 | ||||||
|  | Ns 250.000000 | ||||||
|  | Ka 1.000000 1.000000 1.000000 | ||||||
|  | Ks 0.500000 0.500000 0.500000 | ||||||
|  | Ke 0.000000 0.000000 0.000000 | ||||||
|  | Ni 1.450000 | ||||||
|  | d 1.000000 | ||||||
|  | illum 2 | ||||||
|  | map_Kd coomer.png | ||||||
							
								
								
									
										117248
									
								
								build/windows/amd64/resources/models/coomer.obj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								build/windows/amd64/resources/models/coomer.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.2 MiB | 
							
								
								
									
										12
									
								
								build/windows/amd64/resources/models/goonion.mtl
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,12 @@ | |||||||
|  | # Blender 3.6.0 MTL File: 'None' | ||||||
|  | # www.blender.org | ||||||
|  |  | ||||||
|  | newmtl Material.001 | ||||||
|  | Ns 250.000000 | ||||||
|  | Ka 1.000000 1.000000 1.000000 | ||||||
|  | Ks 0.500000 0.500000 0.500000 | ||||||
|  | Ke 0.000000 0.000000 0.000000 | ||||||
|  | Ni 1.450000 | ||||||
|  | d 1.000000 | ||||||
|  | illum 2 | ||||||
|  | map_Kd goonion.png | ||||||
							
								
								
									
										112531
									
								
								build/windows/amd64/resources/models/goonion.obj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								build/windows/amd64/resources/models/goonion.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.5 MiB | 
							
								
								
									
										12
									
								
								build/windows/amd64/resources/models/shreke.mtl
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,12 @@ | |||||||
|  | # Blender 3.6.0 MTL File: 'None' | ||||||
|  | # www.blender.org | ||||||
|  |  | ||||||
|  | newmtl Material.001 | ||||||
|  | Ns 250.000000 | ||||||
|  | Ka 1.000000 1.000000 1.000000 | ||||||
|  | Ks 0.500000 0.500000 0.500000 | ||||||
|  | Ke 0.000000 0.000000 0.000000 | ||||||
|  | Ni 1.450000 | ||||||
|  | d 1.000000 | ||||||
|  | illum 2 | ||||||
|  | map_Kd shreke.png | ||||||
							
								
								
									
										210035
									
								
								build/windows/amd64/resources/models/shreke.obj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								build/windows/amd64/resources/models/shreke.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.8 MiB | 
							
								
								
									
										
											BIN
										
									
								
								build/windows/amd64/resources/screenshot.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 104 KiB | 
							
								
								
									
										11
									
								
								constants.go
									
									
									
									
									
								
							
							
						
						| @ -7,6 +7,15 @@ const ( | |||||||
| 	MapHeight  = 50 | 	MapHeight  = 50 | ||||||
| 	TileSize   = 32 | 	TileSize   = 32 | ||||||
| 	TileHeight = 2.0 | 	TileHeight = 2.0 | ||||||
| 	TickRate   = 2600 * time.Millisecond // Server tick rate (600ms) | 	TickRate   = 600 * 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,4 +1,4 @@ | |||||||
| package main | package game | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"math" | 	"math" | ||||||
| @ -6,8 +6,21 @@ import ( | |||||||
| 	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) | ||||||
|  | ) | ||||||
|  | 
 | ||||||
| func UpdateCamera(camera *rl.Camera3D, player rl.Vector3, deltaTime float32) { | func UpdateCamera(camera *rl.Camera3D, player rl.Vector3, deltaTime float32) { | ||||||
| 	// Update camera based on mouse wheel | 	// 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() | 	wheelMove := rl.GetMouseWheelMove() | ||||||
| 	if wheelMove != 0 { | 	if wheelMove != 0 { | ||||||
| 		cameraDistance += -wheelMove * 5 | 		cameraDistance += -wheelMove * 5 | ||||||
| @ -19,7 +32,6 @@ func UpdateCamera(camera *rl.Camera3D, player rl.Vector3, deltaTime float32) { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Orbit camera around the player using arrow keys |  | ||||||
| 	if rl.IsKeyDown(rl.KeyRight) { | 	if rl.IsKeyDown(rl.KeyRight) { | ||||||
| 		cameraYaw += 100 * deltaTime | 		cameraYaw += 100 * deltaTime | ||||||
| 	} | 	} | ||||||
| @ -39,16 +51,13 @@ func UpdateCamera(camera *rl.Camera3D, player rl.Vector3, deltaTime float32) { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Calculate the new camera position using spherical coordinates |  | ||||||
| 	cameraYawRad := float64(cameraYaw) * rl.Deg2rad | 	cameraYawRad := float64(cameraYaw) * rl.Deg2rad | ||||||
| 	cameraPitchRad := float64(cameraPitch) * 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 = rl.Vector3{ | ||||||
| 	camera.Position = cameraPos | 		X: targetPos.X + cameraDistance*float32(math.Cos(cameraYawRad))*float32(math.Cos(cameraPitchRad)), | ||||||
| 	camera.Target = rl.NewVector3(player.X, player.Y, player.Z) | 		Y: targetPos.Y + cameraDistance*float32(math.Sin(cameraPitchRad)), | ||||||
|  | 		Z: targetPos.Z + cameraDistance*float32(math.Sin(cameraYawRad))*float32(math.Cos(cameraPitchRad)), | ||||||
|  | 	} | ||||||
|  | 	camera.Target = targetPos | ||||||
| } | } | ||||||
							
								
								
									
										220
									
								
								game/chat.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,220 @@ | |||||||
|  | package game | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"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 { | ||||||
|  | 	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) { | ||||||
|  | 	// Convert protobuf messages to our local type | ||||||
|  | 	for _, msg := range messages { | ||||||
|  | 		localMsg := types.ChatMessage{ | ||||||
|  | 			PlayerID: msg.PlayerId, | ||||||
|  | 			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[msg.PlayerId]; exists { | ||||||
|  | 					otherPlayer.Lock() | ||||||
|  | 					otherPlayer.FloatingMessage = &types.FloatingMessage{ | ||||||
|  | 						Content:    msg.Content, | ||||||
|  | 						ExpireTime: time.Now().Add(6 * time.Second), | ||||||
|  | 					} | ||||||
|  | 					otherPlayer.Unlock() | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Chat) Draw(screenWidth, screenHeight int32) { | ||||||
|  | 	// 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] | ||||||
|  | 		text := fmt.Sprintf("[%d]: %s", msg.PlayerID, msg.Content) | ||||||
|  | 		rl.DrawText(text, int32(chatX)+5, int32(messageY), 20, rl.White) | ||||||
|  | 		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 | ||||||
|  | } | ||||||
							
								
								
									
										314
									
								
								game/game.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,314 @@ | |||||||
|  | package game | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"os" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"gitea.boner.be/bdnugget/goonscape/assets" | ||||||
|  | 	"gitea.boner.be/bdnugget/goonscape/types" | ||||||
|  | 	pb "gitea.boner.be/bdnugget/goonserver/actions" | ||||||
|  | 	rl "github.com/gen2brain/raylib-go/raylib" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type Game struct { | ||||||
|  | 	Player       *types.Player | ||||||
|  | 	OtherPlayers map[int32]*types.Player | ||||||
|  | 	Camera       rl.Camera3D | ||||||
|  | 	Models       []types.ModelAsset | ||||||
|  | 	Music        rl.Music | ||||||
|  | 	Chat         *Chat | ||||||
|  | 	MenuOpen     bool | ||||||
|  | 	QuitChan     chan struct{} // Channel to signal shutdown | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func New() *Game { | ||||||
|  | 	InitWorld() | ||||||
|  | 	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), | ||||||
|  | 		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{}), | ||||||
|  | 	} | ||||||
|  | 	game.Player.UserData = game | ||||||
|  | 	game.Chat.userData = game | ||||||
|  | 	return game | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (g *Game) LoadAssets() error { | ||||||
|  | 	var err error | ||||||
|  | 	g.Models, err = assets.LoadModels() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	g.Music, err = assets.LoadMusic("resources/audio/GoonScape2.mp3") | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (g *Game) Update(deltaTime float32) { | ||||||
|  | 	// Handle ESC for menu | ||||||
|  | 	if rl.IsKeyPressed(rl.KeyEscape) { | ||||||
|  | 		g.MenuOpen = !g.MenuOpen | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Don't process other inputs if menu is open | ||||||
|  | 	if g.MenuOpen { | ||||||
|  | 		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.MoveTowards(g.Player.TargetPath[0], deltaTime, GetMapGrid()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, other := range g.OtherPlayers { | ||||||
|  | 		if len(other.TargetPath) > 0 { | ||||||
|  | 			other.MoveTowards(other.TargetPath[0], deltaTime, GetMapGrid()) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	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, model rl.Model) { | ||||||
|  | 	player.Lock() | ||||||
|  | 	defer player.Unlock() | ||||||
|  |  | ||||||
|  | 	grid := GetMapGrid() | ||||||
|  | 	playerPos := rl.Vector3{ | ||||||
|  | 		X: player.PosActual.X, | ||||||
|  | 		Y: grid[player.PosTile.X][player.PosTile.Y].Height*types.TileHeight + 16.0, | ||||||
|  | 		Z: player.PosActual.Z, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	rl.DrawModel(model, playerPos, 16, rl.White) | ||||||
|  |  | ||||||
|  | 	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() { | ||||||
|  | 	rl.BeginDrawing() | ||||||
|  | 	rl.ClearBackground(rl.RayWhite) | ||||||
|  |  | ||||||
|  | 	rl.BeginMode3D(g.Camera) | ||||||
|  | 	g.DrawMap() | ||||||
|  | 	g.DrawPlayer(g.Player, g.Player.Model) | ||||||
|  | 	for id, other := range g.OtherPlayers { | ||||||
|  | 		g.DrawPlayer(other, g.Models[int(id)%len(g.Models)].Model) | ||||||
|  | 	} | ||||||
|  | 	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) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, other := range g.OtherPlayers { | ||||||
|  | 		drawFloatingMessage(other.FloatingMessage) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Draw menu if open | ||||||
|  | 	if g.MenuOpen { | ||||||
|  | 		g.DrawMenu() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Only draw chat if menu is not open | ||||||
|  | 	if !g.MenuOpen { | ||||||
|  | 		g.Chat.Draw(int32(rl.GetScreenWidth()), int32(rl.GetScreenHeight())) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	rl.DrawFPS(10, 10) | ||||||
|  | 	rl.EndDrawing() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (g *Game) Cleanup() { | ||||||
|  | 	assets.UnloadModels(g.Models) | ||||||
|  | 	assets.UnloadMusic(g.Music) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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 = 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) | ||||||
|  | } | ||||||
							
								
								
									
										31
									
								
								game/input.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,31 @@ | |||||||
|  | package game | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  |  | ||||||
|  | 	"gitea.boner.be/bdnugget/goonscape/types" | ||||||
|  | 	rl "github.com/gen2brain/raylib-go/raylib" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func (g *Game) GetTileAtMouse() (types.Tile, bool) { | ||||||
|  | 	if !rl.IsMouseButtonPressed(rl.MouseLeftButton) { | ||||||
|  | 		return types.Tile{}, false | ||||||
|  | 	} | ||||||
|  | 	mouse := rl.GetMousePosition() | ||||||
|  | 	ray := rl.GetMouseRay(mouse, g.Camera) | ||||||
|  |  | ||||||
|  | 	for x := 0; x < types.MapWidth; x++ { | ||||||
|  | 		for y := 0; y < types.MapHeight; y++ { | ||||||
|  | 			tile := GetTile(x, y) | ||||||
|  | 			tilePos := rl.NewVector3(float32(x*types.TileSize), tile.Height*types.TileHeight, float32(y*types.TileSize)) | ||||||
|  | 			boxMin := rl.Vector3Subtract(tilePos, rl.NewVector3(types.TileSize/2, types.TileHeight/2, types.TileSize/2)) | ||||||
|  | 			boxMax := rl.Vector3Add(tilePos, rl.NewVector3(types.TileSize/2, types.TileHeight/2, types.TileSize/2)) | ||||||
|  |  | ||||||
|  | 			if RayIntersectsBox(ray, boxMin, boxMax) { | ||||||
|  | 				fmt.Printf("Clicked: %d, %d\n", tile.X, tile.Y) | ||||||
|  | 				return tile, true | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return types.Tile{}, false | ||||||
|  | } | ||||||
| @ -1,14 +1,18 @@ | |||||||
| package main | package game | ||||||
| 
 | 
 | ||||||
| import "fmt" | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 
 | ||||||
|  | 	"gitea.boner.be/bdnugget/goonscape/types" | ||||||
|  | ) | ||||||
| 
 | 
 | ||||||
| type Node struct { | type Node struct { | ||||||
| 	Tile    Tile | 	Tile    types.Tile | ||||||
| 	Parent  *Node | 	Parent  *Node | ||||||
| 	G, H, F float32 | 	G, H, F float32 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func FindPath(start, end Tile) []Tile { | func FindPath(start, end types.Tile) []types.Tile { | ||||||
| 	openList := []*Node{} | 	openList := []*Node{} | ||||||
| 	closedList := make(map[[2]int]bool) | 	closedList := make(map[[2]int]bool) | ||||||
| 
 | 
 | ||||||
| @ -17,7 +21,6 @@ func FindPath(start, end Tile) []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 { | ||||||
| @ -27,23 +30,20 @@ func FindPath(start, end Tile) []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 := []Tile{} | 			path := []types.Tile{} | ||||||
| 			node := current | 			node := current | ||||||
| 			for node != nil { | 			for node != nil { | ||||||
| 				path = append([]Tile{node.Tile}, path...) | 				path = append([]types.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,32 +75,30 @@ func FindPath(start, end Tile) []Tile { | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 |  | ||||||
| 	// No path found |  | ||||||
| 	fmt.Println("No path found") |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func heuristic(a, b Tile) float32 { | func heuristic(a, b types.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 Tile) float32 { | func distance(a, b types.Tile) float32 { | ||||||
| 	_ = a |  | ||||||
| 	_ = b |  | ||||||
| 	return 1.0 // uniform cost for now | 	return 1.0 // uniform cost for now | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func GetNeighbors(tile Tile) []Tile { | func GetNeighbors(tile types.Tile) []types.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 := []Tile{} | 	neighbors := []types.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 < MapWidth && ny >= 0 && ny < MapHeight { | 		if nx >= 0 && nx < types.MapWidth && ny >= 0 && ny < types.MapHeight { | ||||||
| 			neighbors = append(neighbors, mapGrid[nx][ny]) | 			if grid[nx][ny].Walkable { | ||||||
|  | 				neighbors = append(neighbors, grid[nx][ny]) | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	return neighbors | 	return neighbors | ||||||
							
								
								
									
										45
									
								
								game/utils.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,45 @@ | |||||||
|  | package game | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	rl "github.com/gen2brain/raylib-go/raylib" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func RayIntersectsBox(ray rl.Ray, boxMin, boxMax rl.Vector3) bool { | ||||||
|  | 	tmin := (boxMin.X - ray.Position.X) / ray.Direction.X | ||||||
|  | 	tmax := (boxMax.X - ray.Position.X) / ray.Direction.X | ||||||
|  |  | ||||||
|  | 	if tmin > tmax { | ||||||
|  | 		tmin, tmax = tmax, tmin | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	tymin := (boxMin.Z - ray.Position.Z) / ray.Direction.Z | ||||||
|  | 	tymax := (boxMax.Z - ray.Position.Z) / ray.Direction.Z | ||||||
|  |  | ||||||
|  | 	if tymin > tymax { | ||||||
|  | 		tymin, tymax = tymax, tymin | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if (tmin > tymax) || (tymin > tmax) { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if tymin > tmin { | ||||||
|  | 		tmin = tymin | ||||||
|  | 	} | ||||||
|  | 	if tymax < tmax { | ||||||
|  | 		tmax = tymax | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	tzmin := (boxMin.Y - ray.Position.Y) / ray.Direction.Y | ||||||
|  | 	tzmax := (boxMax.Y - ray.Position.Y) / ray.Direction.Y | ||||||
|  |  | ||||||
|  | 	if tzmin > tzmax { | ||||||
|  | 		tzmin, tzmax = tzmax, tzmin | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if (tmin > tzmax) || (tzmin > tmax) { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return true | ||||||
|  | } | ||||||
							
								
								
									
										39
									
								
								game/world.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,39 @@ | |||||||
|  | package game | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"gitea.boner.be/bdnugget/goonscape/types" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	mapGrid [][]types.Tile | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func GetMapGrid() [][]types.Tile { | ||||||
|  | 	return mapGrid | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func InitWorld() { | ||||||
|  | 	mapGrid = make([][]types.Tile, types.MapWidth) | ||||||
|  | 	for x := 0; x < types.MapWidth; x++ { | ||||||
|  | 		mapGrid[x] = make([]types.Tile, types.MapHeight) | ||||||
|  | 		for y := 0; y < types.MapHeight; y++ { | ||||||
|  | 			mapGrid[x][y] = types.Tile{ | ||||||
|  | 				X:        x, | ||||||
|  | 				Y:        y, | ||||||
|  | 				Height:   1.0 + float32(x%5), | ||||||
|  | 				Walkable: true, | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func GetTile(x, y int) types.Tile { | ||||||
|  | 	if x >= 0 && x < types.MapWidth && y >= 0 && y < types.MapHeight { | ||||||
|  | 		return mapGrid[x][y] | ||||||
|  | 	} | ||||||
|  | 	return types.Tile{} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func GetTileHeight(x, y int) float32 { | ||||||
|  | 	return mapGrid[x][y].Height | ||||||
|  | } | ||||||
							
								
								
									
										16
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						| @ -1,15 +1,17 @@ | |||||||
| module goonscape | module gitea.boner.be/bdnugget/goonscape | ||||||
|  |  | ||||||
| go 1.23.0 | go 1.23.0 | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	gitea.boner.be/bdnugget/goonserver v0.0.0-20241011195320-f16e8647dc6b | 	gitea.boner.be/bdnugget/goonserver v0.0.0-20250113131525-49e23114973c | ||||||
| 	github.com/gen2brain/raylib-go/raylib v0.0.0-20240930075631-c66f9e2942fe | 	github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b | ||||||
| 	google.golang.org/protobuf v1.35.1 | 	google.golang.org/protobuf v1.36.3 | ||||||
| ) | ) | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	github.com/ebitengine/purego v0.8.0 // indirect | 	github.com/ebitengine/purego v0.8.2 // indirect | ||||||
| 	golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect | 	golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect | ||||||
| 	golang.org/x/sys v0.26.0 // indirect | 	golang.org/x/sys v0.29.0 // indirect | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | replace gitea.boner.be/bdnugget/goonserver => ./goonserver | ||||||
|  | |||||||
							
								
								
									
										22
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						| @ -1,14 +1,12 @@ | |||||||
| gitea.boner.be/bdnugget/goonserver v0.0.0-20241011195320-f16e8647dc6b h1:hdhCZH0YGqCsnSl6ru+8I7rxvCyOj5pCtf92urwyruA= | github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= | ||||||
| gitea.boner.be/bdnugget/goonserver v0.0.0-20241011195320-f16e8647dc6b/go.mod h1:inR1bKrr/vcTba+G1KzmmY6vssMq9oGNOk836VwPa4c= | github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= | ||||||
| github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE= | github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b h1:JJfspevP3YOXcSKVABizYOv++yMpTJIdPUtoDzF/RWw= | ||||||
| github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= | github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b/go.mod h1:BaY76bZk7nw1/kVOSQObPY1v1iwVE1KHAGMfvI6oK1Q= | ||||||
| github.com/gen2brain/raylib-go/raylib v0.0.0-20240930075631-c66f9e2942fe h1:mInjrbJkUglTM7tBmXG+epnPCE744aj15J7vjJwM4gs= |  | ||||||
| github.com/gen2brain/raylib-go/raylib v0.0.0-20240930075631-c66f9e2942fe/go.mod h1:BaY76bZk7nw1/kVOSQObPY1v1iwVE1KHAGMfvI6oK1Q= |  | ||||||
| github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= | ||||||
| github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | ||||||
| golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= | golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= | ||||||
| golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= | golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= | ||||||
| golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= | ||||||
| golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||||
| google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= | google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= | ||||||
| google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= | google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= | ||||||
|  | |||||||
							
								
								
									
										1
									
								
								goonserver
									
									
									
									
									
										Submodule
									
								
							
							
								
								
								
								
								
							
						
						
							
								
								
									
										88
									
								
								input.go
									
									
									
									
									
								
							
							
						
						| @ -1,88 +0,0 @@ | |||||||
| 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 |  | ||||||
| } |  | ||||||
							
								
								
									
										159
									
								
								main.go
									
									
									
									
									
								
							
							
						
						| @ -1,150 +1,71 @@ | |||||||
| package main | package main | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"flag" | ||||||
| 	"log" | 	"log" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"gitea.boner.be/bdnugget/goonscape/game" | ||||||
|  | 	"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() { | ||||||
|  | 	local := flag.Bool("local", false, "Use local server instead of remote") | ||||||
|  | 	addr := flag.String("addr", "boner.be:6969", "Server address (hostname:port or hostname)") | ||||||
|  | 	flag.Parse() | ||||||
|  |  | ||||||
|  | 	if *local && *addr != "boner.be:6969" { | ||||||
|  | 		log.Fatal("Cannot use both -local and -addr flags") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if *local { | ||||||
|  | 		network.SetServerAddr("localhost:6969") | ||||||
|  | 	} else if *addr != "" { | ||||||
|  | 		// If only hostname is provided, append default port | ||||||
|  | 		if !strings.Contains(*addr, ":") { | ||||||
|  | 			*addr = *addr + ":6969" | ||||||
|  | 		} | ||||||
|  | 		network.SetServerAddr(*addr) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	rl.InitWindow(1024, 768, "GoonScape") | 	rl.InitWindow(1024, 768, "GoonScape") | ||||||
|  | 	rl.SetExitKey(0) | ||||||
| 	defer rl.CloseWindow() | 	defer rl.CloseWindow() | ||||||
| 	rl.InitAudioDevice() | 	rl.InitAudioDevice() | ||||||
| 	defer rl.CloseAudioDevice() | 	defer rl.CloseAudioDevice() | ||||||
|  |  | ||||||
| 	player := Player{ | 	game := game.New() | ||||||
| 		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 game.Cleanup() | ||||||
|  |  | ||||||
| 	camera := rl.Camera3D{ | 	conn, playerID, err := network.ConnectToServer() | ||||||
| 		Position:   rl.NewVector3(0, 10, 10), // Will be updated every frame |  | ||||||
| 		Target:     player.PosActual, |  | ||||||
| 		Up:         rl.NewVector3(0, 1, 0), // Y is up in 3D |  | ||||||
| 		Fovy:       45.0, |  | ||||||
| 		Projection: rl.CameraPerspective, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	conn, playerID, err := ConnectToServer() |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatalf("Failed to connect to server: %v", err) | 		log.Fatalf("Failed to connect to server: %v", err) | ||||||
| 	} | 	} | ||||||
| 	log.Printf("Player ID: %d", playerID) |  | ||||||
| 	player.ID = playerID |  | ||||||
| 	defer conn.Close() | 	defer conn.Close() | ||||||
|  |  | ||||||
| 	otherPlayers := make(map[int32]*Player) | 	game.Player.ID = playerID | ||||||
|  | 	modelIndex := int(playerID) % len(game.Models) | ||||||
|  | 	game.Player.Model = game.Models[modelIndex].Model | ||||||
|  | 	game.Player.Texture = game.Models[modelIndex].Texture | ||||||
|  |  | ||||||
| 	go HandleServerCommunication(conn, playerID, &player, otherPlayers) | 	go network.HandleServerCommunication(conn, playerID, game.Player, game.OtherPlayers, game.QuitChan) | ||||||
|  |  | ||||||
| 	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.PlayMusicStream(game.Music) | ||||||
|  | 	rl.SetMusicVolume(game.Music, 0.5) | ||||||
| 	rl.SetTargetFPS(60) | 	rl.SetTargetFPS(60) | ||||||
|  |  | ||||||
| 	for !rl.WindowShouldClose() { | 	for !rl.WindowShouldClose() { | ||||||
|  | 		rl.UpdateMusicStream(game.Music) | ||||||
| 		rl.UpdateMusicStream(music) |  | ||||||
|  |  | ||||||
| 		// Time management |  | ||||||
| 		deltaTime := rl.GetFrameTime() | 		deltaTime := rl.GetFrameTime() | ||||||
|  |  | ||||||
| 		// Handle input | 		game.Update(deltaTime) | ||||||
| 		HandleInput(&player, &camera) | 		game.Render() | ||||||
|  |  | ||||||
| 		// Update player |  | ||||||
| 		if len(player.TargetPath) > 0 { |  | ||||||
| 			player.MoveTowards(player.TargetPath[0], deltaTime) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 		// Update camera | 	// Wait for clean shutdown | ||||||
| 		UpdateCamera(&camera, player.PosActual, deltaTime) | 	<-game.QuitChan | ||||||
|  |  | ||||||
| 		// 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
									
									
									
									
									
								
							
							
						
						| @ -1,48 +0,0 @@ | |||||||
| 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
									
									
									
									
									
								
							
							
						
						| @ -1,121 +0,0 @@ | |||||||
| 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, |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
							
								
								
									
										199
									
								
								network/network.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,199 @@ | |||||||
|  | package network | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bufio" | ||||||
|  | 	"encoding/binary" | ||||||
|  | 	"io" | ||||||
|  | 	"log" | ||||||
|  | 	"net" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"gitea.boner.be/bdnugget/goonscape/game" | ||||||
|  | 	"gitea.boner.be/bdnugget/goonscape/types" | ||||||
|  | 	pb "gitea.boner.be/bdnugget/goonserver/actions" | ||||||
|  | 	"google.golang.org/protobuf/proto" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var serverAddr = "boner.be:6969" | ||||||
|  |  | ||||||
|  | func SetServerAddr(addr string) { | ||||||
|  | 	serverAddr = addr | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func ConnectToServer() (net.Conn, int32, error) { | ||||||
|  | 	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...") | ||||||
|  | 	reader := bufio.NewReader(conn) | ||||||
|  |  | ||||||
|  | 	// Read message length (4 bytes) | ||||||
|  | 	lengthBuf := make([]byte, 4) | ||||||
|  | 	if _, err := io.ReadFull(reader, lengthBuf); err != nil { | ||||||
|  | 		log.Printf("Failed to read message length: %v", err) | ||||||
|  | 		return nil, 0, err | ||||||
|  | 	} | ||||||
|  | 	messageLength := binary.BigEndian.Uint32(lengthBuf) | ||||||
|  |  | ||||||
|  | 	// Read the full message | ||||||
|  | 	messageBuf := make([]byte, messageLength) | ||||||
|  | 	if _, err := io.ReadFull(reader, messageBuf); err != nil { | ||||||
|  | 		log.Printf("Failed to read message body: %v", err) | ||||||
|  | 		return nil, 0, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var response pb.ServerMessage | ||||||
|  | 	if err := proto.Unmarshal(messageBuf, &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 *types.Player, otherPlayers map[int32]*types.Player, quitChan <-chan struct{}) { | ||||||
|  | 	reader := bufio.NewReader(conn) | ||||||
|  |  | ||||||
|  | 	actionTicker := time.NewTicker(types.ClientTickRate) | ||||||
|  | 	defer actionTicker.Stop() | ||||||
|  | 	defer conn.Close() | ||||||
|  | 	defer close(player.QuitDone) | ||||||
|  |  | ||||||
|  | 	// Create a channel to signal when goroutines are done | ||||||
|  | 	done := make(chan struct{}) | ||||||
|  |  | ||||||
|  | 	go func() { | ||||||
|  | 		for { | ||||||
|  | 			select { | ||||||
|  | 			case <-quitChan: | ||||||
|  | 				// Send disconnect message to server | ||||||
|  | 				disconnectMsg := &pb.ActionBatch{ | ||||||
|  | 					PlayerId: playerID, | ||||||
|  | 					Actions: []*pb.Action{{ | ||||||
|  | 						Type:     pb.Action_DISCONNECT, | ||||||
|  | 						PlayerId: playerID, | ||||||
|  | 					}}, | ||||||
|  | 				} | ||||||
|  | 				writeMessage(conn, disconnectMsg) | ||||||
|  | 				done <- struct{}{} | ||||||
|  | 				return | ||||||
|  | 			case <-actionTicker.C: | ||||||
|  | 				player.Lock() | ||||||
|  | 				if len(player.ActionQueue) > 0 { | ||||||
|  | 					actions := make([]*pb.Action, len(player.ActionQueue)) | ||||||
|  | 					copy(actions, player.ActionQueue) | ||||||
|  |  | ||||||
|  | 					batch := &pb.ActionBatch{ | ||||||
|  | 						PlayerId: playerID, | ||||||
|  | 						Actions:  actions, | ||||||
|  | 						Tick:     player.CurrentTick, | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					player.ActionQueue = player.ActionQueue[:0] | ||||||
|  | 					player.Unlock() | ||||||
|  |  | ||||||
|  | 					if err := writeMessage(conn, batch); err != nil { | ||||||
|  | 						log.Printf("Failed to send actions to server: %v", err) | ||||||
|  | 						return | ||||||
|  | 					} | ||||||
|  | 				} else { | ||||||
|  | 					player.Unlock() | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	for { | ||||||
|  | 		select { | ||||||
|  | 		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 | ||||||
|  | 		default: | ||||||
|  | 			// Read message length (4 bytes) | ||||||
|  | 			lengthBuf := make([]byte, 4) | ||||||
|  | 			if _, err := io.ReadFull(reader, lengthBuf); err != nil { | ||||||
|  | 				log.Printf("Failed to read message length: %v", err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			messageLength := binary.BigEndian.Uint32(lengthBuf) | ||||||
|  |  | ||||||
|  | 			// Read the full message | ||||||
|  | 			messageBuf := make([]byte, messageLength) | ||||||
|  | 			if _, err := io.ReadFull(reader, messageBuf); err != nil { | ||||||
|  | 				log.Printf("Failed to read message body: %v", err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			var serverMessage pb.ServerMessage | ||||||
|  | 			if err := proto.Unmarshal(messageBuf, &serverMessage); err != nil { | ||||||
|  | 				log.Printf("Failed to unmarshal server message: %v", err) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			player.Lock() | ||||||
|  | 			player.CurrentTick = serverMessage.CurrentTick | ||||||
|  |  | ||||||
|  | 			tickDiff := serverMessage.CurrentTick - player.CurrentTick | ||||||
|  | 			if tickDiff > types.MaxTickDesync { | ||||||
|  | 				for _, state := range serverMessage.Players { | ||||||
|  | 					if state.PlayerId == playerID { | ||||||
|  | 						player.ForceResync(state) | ||||||
|  | 						break | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			player.Unlock() | ||||||
|  |  | ||||||
|  | 			for _, state := range serverMessage.Players { | ||||||
|  | 				if state.PlayerId == playerID { | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				if otherPlayer, exists := otherPlayers[state.PlayerId]; exists { | ||||||
|  | 					otherPlayer.UpdatePosition(state, types.ServerTickRate) | ||||||
|  | 				} else { | ||||||
|  | 					otherPlayers[state.PlayerId] = types.NewPlayer(state) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if g, ok := player.UserData.(*game.Game); ok && len(serverMessage.ChatMessages) > 0 { | ||||||
|  | 				g.Chat.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 | ||||||
|  | } | ||||||
							
								
								
									
										63
									
								
								player.go
									
									
									
									
									
								
							
							
						
						| @ -1,63 +0,0 @@ | |||||||
| 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 |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
							
								
								
									
										
											BIN
										
									
								
								resources/screenshot.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 104 KiB | 
							
								
								
									
										24
									
								
								scripts/build.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						| @ -0,0 +1,24 @@ | |||||||
|  | #!/bin/bash | ||||||
|  |  | ||||||
|  | # Main build process | ||||||
|  | build() { | ||||||
|  |     local os=$1 | ||||||
|  |     local arch=$2 | ||||||
|  |     local output=$3 | ||||||
|  |  | ||||||
|  |     # Set CGO flags for static linking | ||||||
|  |     export CGO_ENABLED=1 | ||||||
|  |     export GOOS=$os | ||||||
|  |     export GOARCH=$arch | ||||||
|  |      | ||||||
|  |     # Platform specific flags | ||||||
|  |     if [ "$os" = "windows" ]; then | ||||||
|  |         export CC=x86_64-w64-mingw32-gcc | ||||||
|  |         export CXX=x86_64-w64-mingw32-g++ | ||||||
|  |     fi | ||||||
|  |  | ||||||
|  |     go build -buildvcs=false -ldflags="-s -w" -o $output | ||||||
|  | } | ||||||
|  |  | ||||||
|  | # Call build with provided arguments | ||||||
|  | build "$1" "$2" "$3"  | ||||||
							
								
								
									
										1
									
								
								scripts/platforms.mk
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1 @@ | |||||||
|  | PLATFORMS=windows/amd64 linux/amd64  | ||||||
							
								
								
									
										31
									
								
								types.go
									
									
									
									
									
								
							
							
						
						| @ -1,31 +0,0 @@ | |||||||
| 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 |  | ||||||
| } |  | ||||||
							
								
								
									
										76
									
								
								types/player.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,76 @@ | |||||||
|  | package types | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	pb "gitea.boner.be/bdnugget/goonserver/actions" | ||||||
|  | 	rl "github.com/gen2brain/raylib-go/raylib" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | 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 > 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, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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 | ||||||
|  | } | ||||||
							
								
								
									
										62
									
								
								types/types.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,62 @@ | |||||||
|  | package types | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"sync" | ||||||
|  | 	"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 Player struct { | ||||||
|  | 	sync.Mutex | ||||||
|  | 	PosActual             rl.Vector3 | ||||||
|  | 	PosTile               Tile | ||||||
|  | 	TargetPath            []Tile | ||||||
|  | 	ActionQueue           []*pb.Action | ||||||
|  | 	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 { | ||||||
|  | 	Model   rl.Model | ||||||
|  | 	Texture rl.Texture2D | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type ChatMessage struct { | ||||||
|  | 	PlayerID int32 | ||||||
|  | 	Content  string | ||||||
|  | 	Time     time.Time | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type FloatingMessage struct { | ||||||
|  | 	Content    string | ||||||
|  | 	ExpireTime time.Time | ||||||
|  | 	ScreenPos  rl.Vector2 // Store the screen position for 2D rendering | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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 | ||||||
|  | ) | ||||||