Compare commits
	
		
			25 Commits
		
	
	
		
			feature/ch
			...
			e45066b2a8
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| e45066b2a8 | |||
| bb01dccf2b | |||
| 0f56916295 | |||
| a1ddbadea0 | |||
| e4d0b98945 | |||
| 509bc8b20b | |||
| c40e4ae7ac | |||
| 863f5a939c | |||
| cd68581429 | |||
| b9d0d46bd6 | |||
| b96c7ada7a | |||
| d86cbe15a3 | |||
| fb018e2a7d | |||
| 5ca973fdf1 | |||
| 2a0f9348e9 | |||
| d6d0f36cee | |||
| e8d062c4b7 | |||
| 0cd3145d28 | |||
| 0b6ab17ad5 | |||
| 50952309f4 | |||
| afc44710f2 | |||
| 1a7b0eff42 | |||
| bf7bf12a53 | |||
| e661320508 | |||
| 567ec40c3d | 
							
								
								
									
										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 | ||||
| ``` | ||||
							
								
								
									
										
											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 | 
| @ -13,6 +13,14 @@ var ( | ||||
| ) | ||||
|  | ||||
| func UpdateCamera(camera *rl.Camera3D, player rl.Vector3, deltaTime float32) { | ||||
| 	// Adjust target position to be at character's torso height (about half character height) | ||||
| 	characterHeight := float32(32.0) // Assuming character is roughly 32 units tall | ||||
| 	targetPos := rl.Vector3{ | ||||
| 		X: player.X, | ||||
| 		Y: player.Y + characterHeight*0.5, // Focus on middle of character | ||||
| 		Z: player.Z, | ||||
| 	} | ||||
|  | ||||
| 	wheelMove := rl.GetMouseWheelMove() | ||||
| 	if wheelMove != 0 { | ||||
| 		cameraDistance += -wheelMove * 5 | ||||
| @ -47,9 +55,9 @@ func UpdateCamera(camera *rl.Camera3D, player rl.Vector3, deltaTime float32) { | ||||
| 	cameraPitchRad := float64(cameraPitch) * rl.Deg2rad | ||||
|  | ||||
| 	camera.Position = 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)), | ||||
| 		X: targetPos.X + cameraDistance*float32(math.Cos(cameraYawRad))*float32(math.Cos(cameraPitchRad)), | ||||
| 		Y: targetPos.Y + cameraDistance*float32(math.Sin(cameraPitchRad)), | ||||
| 		Z: targetPos.Z + cameraDistance*float32(math.Sin(cameraYawRad))*float32(math.Cos(cameraPitchRad)), | ||||
| 	} | ||||
| 	camera.Target = player | ||||
| 	camera.Target = targetPos | ||||
| } | ||||
|  | ||||
							
								
								
									
										51
									
								
								game/chat.go
									
									
									
									
									
								
							
							
						
						| @ -11,10 +11,11 @@ import ( | ||||
|  | ||||
| const ( | ||||
| 	maxMessages   = 50 | ||||
| 	chatWindowWidth = 400 | ||||
| 	chatMargin    = 10 // Margin from screen edges | ||||
| 	chatHeight    = 200 | ||||
| 	messageHeight = 20 | ||||
| 	inputHeight   = 30 | ||||
| 	runeLimit     = 256 | ||||
| ) | ||||
|  | ||||
| type Chat struct { | ||||
| @ -29,7 +30,7 @@ type Chat struct { | ||||
| func NewChat() *Chat { | ||||
| 	return &Chat{ | ||||
| 		messages:    make([]types.ChatMessage, 0, maxMessages), | ||||
| 		inputBuffer: make([]rune, 0, 256), | ||||
| 		inputBuffer: make([]rune, 0, runeLimit), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -52,6 +53,7 @@ func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) { | ||||
| 	for _, msg := range messages { | ||||
| 		localMsg := types.ChatMessage{ | ||||
| 			PlayerID: msg.PlayerId, | ||||
| 			Username: msg.Username, | ||||
| 			Content:  msg.Content, | ||||
| 			Time:     time.Unix(0, msg.Timestamp), | ||||
| 		} | ||||
| @ -63,6 +65,12 @@ func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) { | ||||
| 			} | ||||
| 			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 { | ||||
| @ -86,19 +94,31 @@ func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) { | ||||
| } | ||||
|  | ||||
| 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(10) | ||||
| 	chatY := float32(screenHeight - chatHeight - 10) | ||||
| 	chatX := float32(chatMargin) | ||||
| 	chatY := float32(screenHeight - chatHeight - chatMargin) | ||||
| 	rl.DrawRectangle(int32(chatX), int32(chatY), chatWindowWidth, chatHeight, rl.ColorAlpha(rl.Black, 0.5)) | ||||
|  | ||||
| 	// Draw messages | ||||
| 	// Draw messages from oldest to newest | ||||
| 	messageY := chatY + 5 | ||||
| 	startIdx := len(c.messages) - 1 - c.scrollOffset | ||||
| 	endIdx := max(0, startIdx-int((chatHeight-inputHeight)/messageHeight)) | ||||
| 	visibleMessages := int((chatHeight - inputHeight) / messageHeight) | ||||
|  | ||||
| 	for i := startIdx; i >= endIdx && i >= 0; i-- { | ||||
| 	// 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) | ||||
| 		text := fmt.Sprintf("%s: %s", msg.Username, msg.Content) | ||||
| 		rl.DrawText(text, int32(chatX)+5, int32(messageY), 20, rl.White) | ||||
| 		messageY += messageHeight | ||||
| 	} | ||||
| @ -136,14 +156,14 @@ func (c *Chat) Update() (string, bool) { | ||||
|  | ||||
| 	key := rl.GetCharPressed() | ||||
| 	for key > 0 { | ||||
| 		if len(c.inputBuffer) < 256 { | ||||
| 		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) { | ||||
| 	if rl.IsKeyPressed(rl.KeyEnter) || rl.IsKeyPressed(rl.KeyKpEnter) { | ||||
| 		if len(c.inputBuffer) > 0 { | ||||
| 			message := string(c.inputBuffer) | ||||
| 			c.inputBuffer = c.inputBuffer[:0] | ||||
| @ -154,7 +174,7 @@ func (c *Chat) Update() (string, bool) { | ||||
| 		c.isTyping = false | ||||
| 	} | ||||
|  | ||||
| 	if rl.IsKeyPressed(rl.KeyEscape) { | ||||
| 	if rl.IsKeyPressed(rl.KeyEscape) && c.isTyping { | ||||
| 		c.inputBuffer = c.inputBuffer[:0] | ||||
| 		c.cursorPos = 0 | ||||
| 		c.isTyping = false | ||||
| @ -183,6 +203,13 @@ func max(a, b int) int { | ||||
| 	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 | ||||
|  | ||||
							
								
								
									
										155
									
								
								game/game.go
									
									
									
									
									
								
							
							
						
						| @ -1,9 +1,11 @@ | ||||
| package game | ||||
|  | ||||
| import ( | ||||
| 	"os" | ||||
| 	"time" | ||||
|  | ||||
| 	"gitea.boner.be/bdnugget/goonscape/assets" | ||||
| 	"gitea.boner.be/bdnugget/goonscape/network" | ||||
| 	"gitea.boner.be/bdnugget/goonscape/types" | ||||
| 	pb "gitea.boner.be/bdnugget/goonserver/actions" | ||||
| 	rl "github.com/gen2brain/raylib-go/raylib" | ||||
| @ -16,18 +18,15 @@ type Game struct { | ||||
| 	Models       []types.ModelAsset | ||||
| 	Music        rl.Music | ||||
| 	Chat         *Chat | ||||
| 	MenuOpen     bool | ||||
| 	QuitChan     chan struct{} // Channel to signal shutdown | ||||
| 	loginScreen  *LoginScreen | ||||
| 	isLoggedIn   bool | ||||
| } | ||||
|  | ||||
| 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, | ||||
| 		}, | ||||
| 		OtherPlayers: make(map[int32]*types.Player), | ||||
| 		Camera: rl.Camera3D{ | ||||
| 			Position:   rl.NewVector3(0, 10, 10), | ||||
| @ -37,8 +36,9 @@ func New() *Game { | ||||
| 			Projection: rl.CameraPerspective, | ||||
| 		}, | ||||
| 		Chat:        NewChat(), | ||||
| 		QuitChan:    make(chan struct{}), | ||||
| 		loginScreen: NewLoginScreen(), | ||||
| 	} | ||||
| 	game.Player.UserData = game | ||||
| 	game.Chat.userData = game | ||||
| 	return game | ||||
| } | ||||
| @ -59,6 +59,46 @@ func (g *Game) LoadAssets() error { | ||||
| } | ||||
|  | ||||
| func (g *Game) Update(deltaTime float32) { | ||||
| 	if !g.isLoggedIn { | ||||
| 		username, password, isRegistering, submitted := g.loginScreen.Update() | ||||
| 		if submitted { | ||||
| 			conn, playerID, err := network.ConnectToServer(username, password, isRegistering) | ||||
| 			if err != nil { | ||||
| 				g.loginScreen.SetError(err.Error()) | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			// Assign model based on player ID | ||||
| 			modelIndex := int(playerID) % len(g.Models) | ||||
| 			g.Player = &types.Player{ | ||||
| 				Speed:      50.0, | ||||
| 				TargetPath: []types.Tile{}, | ||||
| 				UserData:   g, | ||||
| 				QuitDone:   make(chan struct{}), | ||||
| 				ID:         playerID, | ||||
| 				Model:      g.Models[modelIndex].Model, | ||||
| 				Texture:    g.Models[modelIndex].Texture, | ||||
| 			} | ||||
|  | ||||
| 			go network.HandleServerCommunication(conn, playerID, g.Player, g.OtherPlayers, g.QuitChan) | ||||
| 			g.isLoggedIn = true | ||||
| 			return | ||||
| 		} | ||||
| 		g.loginScreen.Draw() | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// Handle ESC for menu | ||||
| 	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{ | ||||
| @ -125,7 +165,7 @@ func (g *Game) DrawPlayer(player *types.Player, model rl.Model) { | ||||
|  | ||||
| 	rl.DrawModel(model, playerPos, 16, rl.White) | ||||
|  | ||||
| 	if player.FloatingMessage != nil && time.Now().Before(player.FloatingMessage.ExpireTime) { | ||||
| 	if player.FloatingMessage != nil { | ||||
| 		screenPos := rl.GetWorldToScreen(rl.Vector3{ | ||||
| 			X: playerPos.X, | ||||
| 			Y: playerPos.Y + 24.0, | ||||
| @ -133,8 +173,6 @@ func (g *Game) DrawPlayer(player *types.Player, model rl.Model) { | ||||
| 		}, g.Camera) | ||||
|  | ||||
| 		player.FloatingMessage.ScreenPos = screenPos | ||||
| 	} else if player.FloatingMessage != nil { | ||||
| 		player.FloatingMessage = nil | ||||
| 	} | ||||
|  | ||||
| 	if len(player.TargetPath) > 0 { | ||||
| @ -160,14 +198,27 @@ func (g *Game) Render() { | ||||
| 	rl.BeginDrawing() | ||||
| 	rl.ClearBackground(rl.RayWhite) | ||||
|  | ||||
| 	if !g.isLoggedIn { | ||||
| 		g.loginScreen.Draw() | ||||
| 		rl.EndDrawing() | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	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) | ||||
| 		if other.Model.Meshes == nil { | ||||
| 			// Assign model based on player ID for consistency | ||||
| 			modelIndex := int(id) % len(g.Models) | ||||
| 			other.Model = g.Models[modelIndex].Model | ||||
| 			other.Texture = g.Models[modelIndex].Texture | ||||
| 		} | ||||
| 		g.DrawPlayer(other, other.Model) | ||||
| 	} | ||||
| 	rl.EndMode3D() | ||||
|  | ||||
| 	// Draw floating messages | ||||
| 	drawFloatingMessage := func(msg *types.FloatingMessage) { | ||||
| 		if msg == nil || time.Now().After(msg.ExpireTime) { | ||||
| 			return | ||||
| @ -196,8 +247,17 @@ func (g *Game) Render() { | ||||
| 		drawFloatingMessage(other.FloatingMessage) | ||||
| 	} | ||||
|  | ||||
| 	rl.DrawFPS(10, 10) | ||||
| 	// 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() | ||||
| } | ||||
|  | ||||
| @ -223,3 +283,72 @@ func (g *Game) HandleInput() { | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| 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) | ||||
| } | ||||
|  | ||||
| func (g *Game) HandleServerMessages(messages []*pb.ChatMessage) { | ||||
| 	g.Chat.HandleServerMessages(messages) | ||||
| } | ||||
|  | ||||
							
								
								
									
										185
									
								
								game/login.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,185 @@ | ||||
| package game | ||||
|  | ||||
| import ( | ||||
| 	rl "github.com/gen2brain/raylib-go/raylib" | ||||
| ) | ||||
|  | ||||
| type LoginScreen struct { | ||||
| 	username      string | ||||
| 	password      string | ||||
| 	errorMessage  string | ||||
| 	isRegistering bool | ||||
| 	focusedField  int // 0 = username, 1 = password | ||||
| } | ||||
|  | ||||
| func NewLoginScreen() *LoginScreen { | ||||
| 	return &LoginScreen{} | ||||
| } | ||||
|  | ||||
| func (l *LoginScreen) Draw() { | ||||
| 	screenWidth := float32(rl.GetScreenWidth()) | ||||
| 	screenHeight := float32(rl.GetScreenHeight()) | ||||
|  | ||||
| 	// Draw background | ||||
| 	rl.DrawRectangle(0, 0, int32(screenWidth), int32(screenHeight), rl.RayWhite) | ||||
|  | ||||
| 	// Draw title | ||||
| 	title := "GoonScape" | ||||
| 	if l.isRegistering { | ||||
| 		title += " - Register" | ||||
| 	} else { | ||||
| 		title += " - Login" | ||||
| 	} | ||||
| 	titleSize := int32(40) | ||||
| 	titleWidth := rl.MeasureText(title, titleSize) | ||||
| 	rl.DrawText(title, int32(screenWidth/2)-titleWidth/2, 100, titleSize, rl.Black) | ||||
|  | ||||
| 	// Draw input fields | ||||
| 	inputWidth := float32(200) | ||||
| 	inputHeight := float32(30) | ||||
| 	inputX := screenWidth/2 - inputWidth/2 | ||||
|  | ||||
| 	// Username field | ||||
| 	rl.DrawRectangleRec(rl.Rectangle{ | ||||
| 		X: inputX, Y: 200, | ||||
| 		Width: inputWidth, Height: inputHeight, | ||||
| 	}, rl.LightGray) | ||||
| 	rl.DrawText(l.username, int32(inputX)+5, 205, 20, rl.Black) | ||||
| 	if l.focusedField == 0 { | ||||
| 		rl.DrawRectangleLinesEx(rl.Rectangle{ | ||||
| 			X: inputX - 2, Y: 198, | ||||
| 			Width: inputWidth + 4, Height: inputHeight + 4, | ||||
| 		}, 2, rl.Blue) | ||||
| 	} | ||||
|  | ||||
| 	// Password field | ||||
| 	rl.DrawRectangleRec(rl.Rectangle{ | ||||
| 		X: inputX, Y: 250, | ||||
| 		Width: inputWidth, Height: inputHeight, | ||||
| 	}, rl.LightGray) | ||||
| 	masked := "" | ||||
| 	for range l.password { | ||||
| 		masked += "*" | ||||
| 	} | ||||
| 	rl.DrawText(masked, int32(inputX)+5, 255, 20, rl.Black) | ||||
| 	if l.focusedField == 1 { | ||||
| 		rl.DrawRectangleLinesEx(rl.Rectangle{ | ||||
| 			X: inputX - 2, Y: 248, | ||||
| 			Width: inputWidth + 4, Height: inputHeight + 4, | ||||
| 		}, 2, rl.Blue) | ||||
| 	} | ||||
|  | ||||
| 	// Draw error message | ||||
| 	if l.errorMessage != "" { | ||||
| 		msgWidth := rl.MeasureText(l.errorMessage, 20) | ||||
| 		rl.DrawText(l.errorMessage, int32(screenWidth/2)-msgWidth/2, 300, 20, rl.Red) | ||||
| 	} | ||||
|  | ||||
| 	// Draw buttons | ||||
| 	buttonWidth := float32(100) | ||||
| 	buttonHeight := float32(30) | ||||
| 	buttonY := float32(350) | ||||
|  | ||||
| 	// Login/Register button | ||||
| 	actionBtn := rl.Rectangle{ | ||||
| 		X:      screenWidth/2 - buttonWidth - 10, | ||||
| 		Y:      buttonY, | ||||
| 		Width:  buttonWidth, | ||||
| 		Height: buttonHeight, | ||||
| 	} | ||||
| 	rl.DrawRectangleRec(actionBtn, rl.Blue) | ||||
| 	actionText := "Login" | ||||
| 	if l.isRegistering { | ||||
| 		actionText = "Register" | ||||
| 	} | ||||
| 	actionWidth := rl.MeasureText(actionText, 20) | ||||
| 	rl.DrawText(actionText, | ||||
| 		int32(actionBtn.X+actionBtn.Width/2)-actionWidth/2, | ||||
| 		int32(actionBtn.Y+5), | ||||
| 		20, rl.White) | ||||
|  | ||||
| 	// Switch mode button | ||||
| 	switchBtn := rl.Rectangle{ | ||||
| 		X:      screenWidth/2 + 10, | ||||
| 		Y:      buttonY, | ||||
| 		Width:  buttonWidth, | ||||
| 		Height: buttonHeight, | ||||
| 	} | ||||
| 	rl.DrawRectangleRec(switchBtn, rl.DarkGray) | ||||
| 	switchText := "Register" | ||||
| 	if l.isRegistering { | ||||
| 		switchText = "Login" | ||||
| 	} | ||||
| 	switchWidth := rl.MeasureText(switchText, 20) | ||||
| 	rl.DrawText(switchText, | ||||
| 		int32(switchBtn.X+switchBtn.Width/2)-switchWidth/2, | ||||
| 		int32(switchBtn.Y+5), | ||||
| 		20, rl.White) | ||||
| } | ||||
|  | ||||
| func (l *LoginScreen) Update() (string, string, bool, bool) { | ||||
| 	// Handle input field focus | ||||
| 	if rl.IsMouseButtonPressed(rl.MouseLeftButton) { | ||||
| 		mousePos := rl.GetMousePosition() | ||||
| 		screenWidth := float32(rl.GetScreenWidth()) | ||||
| 		inputWidth := float32(200) | ||||
| 		inputX := screenWidth/2 - inputWidth/2 | ||||
|  | ||||
| 		// Check username field | ||||
| 		if mousePos.X >= inputX && mousePos.X <= inputX+inputWidth && | ||||
| 			mousePos.Y >= 200 && mousePos.Y <= 230 { | ||||
| 			l.focusedField = 0 | ||||
| 		} | ||||
| 		// Check password field | ||||
| 		if mousePos.X >= inputX && mousePos.X <= inputX+inputWidth && | ||||
| 			mousePos.Y >= 250 && mousePos.Y <= 280 { | ||||
| 			l.focusedField = 1 | ||||
| 		} | ||||
|  | ||||
| 		// Check buttons | ||||
| 		buttonWidth := float32(100) | ||||
| 		if mousePos.Y >= 350 && mousePos.Y <= 380 { | ||||
| 			// Action button | ||||
| 			if mousePos.X >= screenWidth/2-buttonWidth-10 && | ||||
| 				mousePos.X <= screenWidth/2-10 { | ||||
| 				return l.username, l.password, l.isRegistering, true | ||||
| 			} | ||||
| 			// Switch mode button | ||||
| 			if mousePos.X >= screenWidth/2+10 && | ||||
| 				mousePos.X <= screenWidth/2+buttonWidth+10 { | ||||
| 				l.isRegistering = !l.isRegistering | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Handle text input | ||||
| 	key := rl.GetCharPressed() | ||||
| 	for key > 0 { | ||||
| 		if l.focusedField == 0 && len(l.username) < 12 { | ||||
| 			l.username += string(key) | ||||
| 		} else if l.focusedField == 1 && len(l.password) < 20 { | ||||
| 			l.password += string(key) | ||||
| 		} | ||||
| 		key = rl.GetCharPressed() | ||||
| 	} | ||||
|  | ||||
| 	// Handle backspace | ||||
| 	if rl.IsKeyPressed(rl.KeyBackspace) { | ||||
| 		if l.focusedField == 0 && len(l.username) > 0 { | ||||
| 			l.username = l.username[:len(l.username)-1] | ||||
| 		} else if l.focusedField == 1 && len(l.password) > 0 { | ||||
| 			l.password = l.password[:len(l.password)-1] | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Handle tab to switch fields | ||||
| 	if rl.IsKeyPressed(rl.KeyTab) { | ||||
| 		l.focusedField = (l.focusedField + 1) % 2 | ||||
| 	} | ||||
|  | ||||
| 	return "", "", false, false | ||||
| } | ||||
|  | ||||
| func (l *LoginScreen) SetError(msg string) { | ||||
| 	l.errorMessage = msg | ||||
| } | ||||
							
								
								
									
										12
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						| @ -3,15 +3,15 @@ module gitea.boner.be/bdnugget/goonscape | ||||
| go 1.23.0 | ||||
|  | ||||
| require ( | ||||
| 	gitea.boner.be/bdnugget/goonserver v0.0.0-20241011195320-f16e8647dc6b | ||||
| 	github.com/gen2brain/raylib-go/raylib v0.0.0-20240930075631-c66f9e2942fe | ||||
| 	google.golang.org/protobuf v1.35.1 | ||||
| 	gitea.boner.be/bdnugget/goonserver v0.0.0-20250113131525-49e23114973c | ||||
| 	github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b | ||||
| 	google.golang.org/protobuf v1.36.3 | ||||
| ) | ||||
|  | ||||
| require ( | ||||
| 	github.com/ebitengine/purego v0.8.0 // indirect | ||||
| 	golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect | ||||
| 	golang.org/x/sys v0.26.0 // indirect | ||||
| 	github.com/ebitengine/purego v0.8.2 // indirect | ||||
| 	golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect | ||||
| 	golang.org/x/sys v0.29.0 // indirect | ||||
| ) | ||||
|  | ||||
| replace gitea.boner.be/bdnugget/goonserver => ./goonserver | ||||
|  | ||||
							
								
								
									
										20
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						| @ -1,12 +1,12 @@ | ||||
| github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE= | ||||
| github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= | ||||
| github.com/gen2brain/raylib-go/raylib v0.0.0-20240930075631-c66f9e2942fe h1:mInjrbJkUglTM7tBmXG+epnPCE744aj15J7vjJwM4gs= | ||||
| github.com/gen2brain/raylib-go/raylib v0.0.0-20240930075631-c66f9e2942fe/go.mod h1:BaY76bZk7nw1/kVOSQObPY1v1iwVE1KHAGMfvI6oK1Q= | ||||
| github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= | ||||
| github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= | ||||
| github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b h1:JJfspevP3YOXcSKVABizYOv++yMpTJIdPUtoDzF/RWw= | ||||
| github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b/go.mod h1:BaY76bZk7nw1/kVOSQObPY1v1iwVE1KHAGMfvI6oK1Q= | ||||
| github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= | ||||
| 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-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= | ||||
| golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= | ||||
| golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||
| google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= | ||||
| google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= | ||||
| golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= | ||||
| golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= | ||||
| golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= | ||||
| golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||
| google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= | ||||
| google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= | ||||
|  | ||||
							
								
								
									
										45
									
								
								main.go
									
									
									
									
									
								
							
							
						
						| @ -1,7 +1,9 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"flag" | ||||
| 	"log" | ||||
| 	"strings" | ||||
|  | ||||
| 	"gitea.boner.be/bdnugget/goonscape/game" | ||||
| 	"gitea.boner.be/bdnugget/goonscape/network" | ||||
| @ -9,39 +11,50 @@ import ( | ||||
| ) | ||||
|  | ||||
| func main() { | ||||
| 	// Parse command line flags | ||||
| 	local := flag.Bool("local", false, "Connect to local server") | ||||
| 	addr := flag.String("addr", "", "Server address (host or host:port)") | ||||
| 	flag.Parse() | ||||
|  | ||||
| 	// Set server address based on flags | ||||
| 	if *local { | ||||
| 		if *addr != "" { | ||||
| 			log.Fatal("Cannot use -local and -addr together") | ||||
| 		} | ||||
| 		network.SetServerAddr("localhost:6969") | ||||
| 	} else if *addr != "" { | ||||
| 		// If port is not specified, append default port | ||||
| 		if !strings.Contains(*addr, ":") { | ||||
| 			*addr += ":6969" | ||||
| 		} | ||||
| 		network.SetServerAddr(*addr) | ||||
| 	} | ||||
|  | ||||
| 	rl.InitWindow(1024, 768, "GoonScape") | ||||
| 	rl.SetExitKey(0) | ||||
| 	defer rl.CloseWindow() | ||||
|  | ||||
| 	rl.InitAudioDevice() | ||||
| 	defer rl.CloseAudioDevice() | ||||
|  | ||||
| 	rl.SetTargetFPS(60) | ||||
|  | ||||
| 	game := game.New() | ||||
| 	if err := game.LoadAssets(); err != nil { | ||||
| 		log.Fatalf("Failed to load assets: %v", err) | ||||
| 	} | ||||
| 	defer game.Cleanup() | ||||
|  | ||||
| 	conn, playerID, err := network.ConnectToServer() | ||||
| 	if err != nil { | ||||
| 		log.Fatalf("Failed to connect to server: %v", err) | ||||
| 	} | ||||
| 	defer conn.Close() | ||||
|  | ||||
| 	game.Player.ID = playerID | ||||
| 	modelIndex := int(playerID) % len(game.Models) | ||||
| 	game.Player.Model = game.Models[modelIndex].Model | ||||
| 	game.Player.Texture = game.Models[modelIndex].Texture | ||||
|  | ||||
| 	go network.HandleServerCommunication(conn, playerID, game.Player, game.OtherPlayers) | ||||
|  | ||||
| 	rl.PlayMusicStream(game.Music) | ||||
| 	rl.SetMusicVolume(game.Music, 0.5) | ||||
| 	rl.SetTargetFPS(60) | ||||
|  | ||||
| 	for !rl.WindowShouldClose() { | ||||
| 		rl.UpdateMusicStream(game.Music) | ||||
| 		deltaTime := rl.GetFrameTime() | ||||
|  | ||||
| 		rl.UpdateMusicStream(game.Music) | ||||
| 		game.Update(deltaTime) | ||||
| 		game.Render() | ||||
| 	} | ||||
|  | ||||
| 	// Wait for clean shutdown | ||||
| 	<-game.QuitChan | ||||
| } | ||||
|  | ||||
| @ -1,50 +1,121 @@ | ||||
| package network | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"encoding/binary" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"log" | ||||
| 	"net" | ||||
| 	"time" | ||||
|  | ||||
| 	"gitea.boner.be/bdnugget/goonscape/game" | ||||
| 	"gitea.boner.be/bdnugget/goonscape/types" | ||||
| 	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) { | ||||
| 	conn, err := net.Dial("tcp", types.ServerAddr) | ||||
| const protoVersion = 1 | ||||
|  | ||||
| var serverAddr = "boner.be:6969" | ||||
|  | ||||
| func SetServerAddr(addr string) { | ||||
| 	serverAddr = addr | ||||
| } | ||||
|  | ||||
| func ConnectToServer(username, password string, isRegistering bool) (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...") | ||||
| 	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.Println("Connected to server. Authenticating...") | ||||
|  | ||||
| 	// Send auth message | ||||
| 	authAction := &pb.Action{ | ||||
| 		Type:     pb.Action_LOGIN, | ||||
| 		Username: username, | ||||
| 		Password: password, | ||||
| 	} | ||||
| 	if isRegistering { | ||||
| 		authAction.Type = pb.Action_REGISTER | ||||
| 	} | ||||
|  | ||||
| 	authBatch := &pb.ActionBatch{ | ||||
| 		Actions:         []*pb.Action{authAction}, | ||||
| 		ProtocolVersion: protoVersion, | ||||
| 	} | ||||
|  | ||||
| 	if err := writeMessage(conn, authBatch); err != nil { | ||||
| 		conn.Close() | ||||
| 		return nil, 0, fmt.Errorf("failed to send auth: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	// Read server response | ||||
| 	reader := bufio.NewReader(conn) | ||||
| 	lengthBuf := make([]byte, 4) | ||||
| 	if _, err := io.ReadFull(reader, lengthBuf); err != nil { | ||||
| 		conn.Close() | ||||
| 		return nil, 0, fmt.Errorf("failed to read auth response: %v", err) | ||||
| 	} | ||||
| 	messageLength := binary.BigEndian.Uint32(lengthBuf) | ||||
|  | ||||
| 	messageBuf := make([]byte, messageLength) | ||||
| 	if _, err := io.ReadFull(reader, messageBuf); err != nil { | ||||
| 		conn.Close() | ||||
| 		return nil, 0, fmt.Errorf("failed to read auth response body: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	var response pb.ServerMessage | ||||
| 	if err := proto.Unmarshal(buf[:n], &response); err != nil { | ||||
| 		log.Printf("Failed to unmarshal server response: %v", err) | ||||
| 		return nil, 0, err | ||||
| 	if err := proto.Unmarshal(messageBuf, &response); err != nil { | ||||
| 		conn.Close() | ||||
| 		return nil, 0, fmt.Errorf("failed to unmarshal auth response: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	if response.ProtocolVersion > protoVersion { | ||||
| 		conn.Close() | ||||
| 		return nil, 0, fmt.Errorf("server requires newer protocol version (server: %d, client: %d)", | ||||
| 			response.ProtocolVersion, protoVersion) | ||||
| 	} | ||||
|  | ||||
| 	if !response.AuthSuccess { | ||||
| 		conn.Close() | ||||
| 		return nil, 0, fmt.Errorf(response.ErrorMessage) | ||||
| 	} | ||||
|  | ||||
| 	playerID := response.GetPlayerId() | ||||
| 	log.Printf("Successfully connected with player ID: %d", playerID) | ||||
| 	log.Printf("Successfully authenticated with player ID: %d", playerID) | ||||
| 	return conn, playerID, nil | ||||
| } | ||||
|  | ||||
| func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Player, otherPlayers map[int32]*types.Player) { | ||||
| 	buf := make([]byte, 4096) | ||||
| 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 range actionTicker.C { | ||||
| 		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)) | ||||
| @ -59,13 +130,7 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play | ||||
| 					player.ActionQueue = player.ActionQueue[:0] | ||||
| 					player.Unlock() | ||||
|  | ||||
| 				data, err := proto.Marshal(batch) | ||||
| 				if err != nil { | ||||
| 					log.Printf("Failed to marshal action batch: %v", err) | ||||
| 					continue | ||||
| 				} | ||||
|  | ||||
| 				if _, err = conn.Write(data); err != nil { | ||||
| 					if err := writeMessage(conn, batch); err != nil { | ||||
| 						log.Printf("Failed to send actions to server: %v", err) | ||||
| 						return | ||||
| 					} | ||||
| @ -73,17 +138,43 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play | ||||
| 					player.Unlock() | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| 	for { | ||||
| 		n, err := conn.Read(buf) | ||||
| 		if err != nil { | ||||
| 			log.Printf("Failed to read from server: %v", err) | ||||
| 		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(buf[:n], &serverMessage); err != nil { | ||||
| 			if err := proto.Unmarshal(messageBuf, &serverMessage); err != nil { | ||||
| 				log.Printf("Failed to unmarshal server message: %v", err) | ||||
| 				continue | ||||
| 			} | ||||
| @ -104,6 +195,17 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play | ||||
|  | ||||
| 			for _, state := range serverMessage.Players { | ||||
| 				if state.PlayerId == playerID { | ||||
| 					player.Lock() | ||||
| 					// Update initial position if not set | ||||
| 					if player.PosActual.X == 0 && player.PosActual.Z == 0 { | ||||
| 						player.PosActual = rl.Vector3{ | ||||
| 							X: float32(state.X * types.TileSize), | ||||
| 							Y: 0, | ||||
| 							Z: float32(state.Y * types.TileSize), | ||||
| 						} | ||||
| 						player.PosTile = types.Tile{X: int(state.X), Y: int(state.Y)} | ||||
| 					} | ||||
| 					player.Unlock() | ||||
| 					continue | ||||
| 				} | ||||
|  | ||||
| @ -114,8 +216,28 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 		if g, ok := player.UserData.(*game.Game); ok && len(serverMessage.ChatMessages) > 0 { | ||||
| 			g.Chat.HandleServerMessages(serverMessage.ChatMessages) | ||||
| 			if handler, ok := player.UserData.(types.ChatMessageHandler); ok && len(serverMessage.ChatMessages) > 0 { | ||||
| 				handler.HandleServerMessages(serverMessage.ChatMessages) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Helper function to write length-prefixed messages | ||||
| func writeMessage(conn net.Conn, msg proto.Message) error { | ||||
| 	data, err := proto.Marshal(msg) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// Write length prefix | ||||
| 	lengthBuf := make([]byte, 4) | ||||
| 	binary.BigEndian.PutUint32(lengthBuf, uint32(len(data))) | ||||
| 	if _, err := conn.Write(lengthBuf); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// Write message body | ||||
| 	_, err = conn.Write(data) | ||||
| 	return err | ||||
| } | ||||
|  | ||||
							
								
								
									
										
											BIN
										
									
								
								resources/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  | ||||
| @ -27,8 +27,9 @@ type Player struct { | ||||
| 	CurrentTick           int64 | ||||
| 	LastUpdateTime        time.Time | ||||
| 	InterpolationProgress float32 | ||||
| 	UserData              interface{} // Used to store reference to game | ||||
| 	UserData              interface{} | ||||
| 	FloatingMessage       *FloatingMessage | ||||
| 	QuitDone              chan struct{} | ||||
| } | ||||
|  | ||||
| type ModelAsset struct { | ||||
| @ -38,6 +39,7 @@ type ModelAsset struct { | ||||
|  | ||||
| type ChatMessage struct { | ||||
| 	PlayerID int32 | ||||
| 	Username string | ||||
| 	Content  string | ||||
| 	Time     time.Time | ||||
| } | ||||
| @ -48,6 +50,10 @@ type FloatingMessage struct { | ||||
| 	ScreenPos  rl.Vector2 // Store the screen position for 2D rendering | ||||
| } | ||||
|  | ||||
| type ChatMessageHandler interface { | ||||
| 	HandleServerMessages([]*pb.ChatMessage) | ||||
| } | ||||
|  | ||||
| const ( | ||||
| 	MapWidth   = 50 | ||||
| 	MapHeight  = 50 | ||||
| @ -58,6 +64,4 @@ const ( | ||||
| 	ServerTickRate = 600 * time.Millisecond | ||||
| 	ClientTickRate = 50 * time.Millisecond | ||||
| 	MaxTickDesync  = 5 | ||||
| 	// ServerAddr     = "localhost:6969" | ||||
| 	ServerAddr = "boner.be:6969" | ||||
| ) | ||||
|  | ||||