Compare commits
	
		
			10 Commits
		
	
	
		
			0f56916295
			...
			feature/ma
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| d5bb464d9f | |||
| 4549ee7517 | |||
| 31ae9c525f | |||
| 06913a5217 | |||
| 49663c9094 | |||
| a843680b09 | |||
| 7183df4a8b | |||
| 33e355200d | |||
| e45066b2a8 | |||
| bb01dccf2b | 
							
								
								
									
										13
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,13 @@ | |||||||
|  | # Build artifacts | ||||||
|  | build/ | ||||||
|  | goonscape | ||||||
|  | goonscape.exe | ||||||
|  |  | ||||||
|  | # IDE files | ||||||
|  | .vscode/ | ||||||
|  | .idea/ | ||||||
|  | *.swp | ||||||
|  |  | ||||||
|  | # OS files | ||||||
|  | .DS_Store | ||||||
|  | Thumbs.db  | ||||||
							
								
								
									
										25
									
								
								.woodpecker.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,25 @@ | |||||||
|  | pipeline: | ||||||
|  |   build: | ||||||
|  |     image: golang:1.23 | ||||||
|  |     commands: | ||||||
|  |       # Install build dependencies | ||||||
|  |       - apt-get update && apt-get install -y gcc-mingw-w64 cmake zip libasound2-dev mesa-common-dev libx11-dev libxrandr-dev libxi-dev xorg-dev libgl1-mesa-dev libglu1-mesa-dev libwayland-dev wayland-protocols libxkbcommon-dev | ||||||
|  |        | ||||||
|  |       # Build for all platforms | ||||||
|  |       - make all | ||||||
|  |      | ||||||
|  |     when: | ||||||
|  |       event: tag | ||||||
|  |       tag: v* | ||||||
|  |  | ||||||
|  |   # Optional: Create Gitea release with built artifacts | ||||||
|  |   release: | ||||||
|  |     image: plugins/gitea-release | ||||||
|  |     settings: | ||||||
|  |       api_key:  | ||||||
|  |         from_secret: gitea_token | ||||||
|  |       base_url: https://gitea.boner.be | ||||||
|  |       files: build/*.zip | ||||||
|  |     when: | ||||||
|  |       event: tag | ||||||
|  |       tag: v*  | ||||||
							
								
								
									
										2
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						| @ -3,7 +3,7 @@ | |||||||
| include scripts/platforms.mk | include scripts/platforms.mk | ||||||
|  |  | ||||||
| BINARY_NAME=goonscape | BINARY_NAME=goonscape | ||||||
| VERSION=1.0.0 | VERSION=1.1.0 | ||||||
| BUILD_DIR=build | BUILD_DIR=build | ||||||
| ASSETS_DIR=resources | ASSETS_DIR=resources | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										55
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @ -16,24 +16,57 @@ A multiplayer isometric game inspired by Oldschool RuneScape, built with Go and | |||||||
| ## Prerequisites | ## Prerequisites | ||||||
|  |  | ||||||
| - Go 1.23 or higher | - Go 1.23 or higher | ||||||
|  | - GCC (for CGO/SQLite support) | ||||||
|  | - OpenGL development libraries | ||||||
| - Raylib dependencies (see [raylib-go](https://github.com/gen2brain/raylib-go#requirements)) | - Raylib dependencies (see [raylib-go](https://github.com/gen2brain/raylib-go#requirements)) | ||||||
|  |  | ||||||
| ## Installation | ## Installation | ||||||
|  |  | ||||||
| 1. Clone the repository: | ### Pre-built Binaries | ||||||
|  | The easiest way to get started is to download the latest release from: | ||||||
|  | ``` | ||||||
|  | https://gitea.boner.be/bdnugget/goonscape/releases | ||||||
|  | ``` | ||||||
|  | Choose the appropriate zip file for your platform: | ||||||
|  | - Windows: `goonscape-windows-amd64-v1.1.0.zip` | ||||||
|  | - Linux: `goonscape-linux-amd64-v1.1.0.zip` | ||||||
|  |  | ||||||
|  | Extract the zip and run the executable. | ||||||
|  |  | ||||||
|  | ### Quick Start | ||||||
|  | For development: | ||||||
|  | ```bash | ||||||
|  | # Run directly (recommended for development) | ||||||
|  | go run main.go | ||||||
|  |  | ||||||
|  | # Run with local server | ||||||
|  | go run main.go -local | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Server Setup | ||||||
|  | The server requires CGO for SQLite support: | ||||||
|  | ```bash | ||||||
|  | # Enable CGO | ||||||
|  | go env -w CGO_ENABLED=1 | ||||||
|  |  | ||||||
|  | # Clone and build server | ||||||
|  | git clone https://gitea.boner.be/bdnugget/goonserver.git | ||||||
|  | cd goonserver | ||||||
|  | go build | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Client Installation | ||||||
|  | Then install or build: | ||||||
|  | ```bash | ||||||
|  | # Install the client | ||||||
|  | go install gitea.boner.be/bdnugget/goonscape@latest | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Or build from source: | ||||||
| ```bash | ```bash | ||||||
| git clone https://gitea.boner.be/bdnugget/goonscape.git | git clone https://gitea.boner.be/bdnugget/goonscape.git | ||||||
| cd goonscape | cd goonscape | ||||||
| ``` | go build | ||||||
|  |  | ||||||
| 2. Install dependencies: |  | ||||||
| ```bash |  | ||||||
| go mod tidy |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| 3. Build and run: |  | ||||||
| ```bash |  | ||||||
| go run main.go |  | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ## Controls | ## Controls | ||||||
|  | |||||||
| @ -1,12 +0,0 @@ | |||||||
| # Blender 3.6.0 MTL File: 'None' |  | ||||||
| # www.blender.org |  | ||||||
|  |  | ||||||
| newmtl Material.001 |  | ||||||
| Ns 250.000000 |  | ||||||
| Ka 1.000000 1.000000 1.000000 |  | ||||||
| Ks 0.500000 0.500000 0.500000 |  | ||||||
| Ke 0.000000 0.000000 0.000000 |  | ||||||
| Ni 1.450000 |  | ||||||
| d 1.000000 |  | ||||||
| illum 2 |  | ||||||
| map_Kd coomer.png |  | ||||||
| Before Width: | Height: | Size: 2.2 MiB | 
| @ -1,12 +0,0 @@ | |||||||
| # Blender 3.6.0 MTL File: 'None' |  | ||||||
| # www.blender.org |  | ||||||
|  |  | ||||||
| newmtl Material.001 |  | ||||||
| Ns 250.000000 |  | ||||||
| Ka 1.000000 1.000000 1.000000 |  | ||||||
| Ks 0.500000 0.500000 0.500000 |  | ||||||
| Ke 0.000000 0.000000 0.000000 |  | ||||||
| Ni 1.450000 |  | ||||||
| d 1.000000 |  | ||||||
| illum 2 |  | ||||||
| map_Kd goonion.png |  | ||||||
| Before Width: | Height: | Size: 2.5 MiB | 
| @ -1,12 +0,0 @@ | |||||||
| # Blender 3.6.0 MTL File: 'None' |  | ||||||
| # www.blender.org |  | ||||||
|  |  | ||||||
| newmtl Material.001 |  | ||||||
| Ns 250.000000 |  | ||||||
| Ka 1.000000 1.000000 1.000000 |  | ||||||
| Ks 0.500000 0.500000 0.500000 |  | ||||||
| Ke 0.000000 0.000000 0.000000 |  | ||||||
| Ni 1.450000 |  | ||||||
| d 1.000000 |  | ||||||
| illum 2 |  | ||||||
| map_Kd shreke.png |  | ||||||
| Before Width: | Height: | Size: 4.8 MiB | 
| Before Width: | Height: | Size: 104 KiB | 
| @ -1,12 +0,0 @@ | |||||||
| # Blender 3.6.0 MTL File: 'None' |  | ||||||
| # www.blender.org |  | ||||||
|  |  | ||||||
| newmtl Material.001 |  | ||||||
| Ns 250.000000 |  | ||||||
| Ka 1.000000 1.000000 1.000000 |  | ||||||
| Ks 0.500000 0.500000 0.500000 |  | ||||||
| Ke 0.000000 0.000000 0.000000 |  | ||||||
| Ni 1.450000 |  | ||||||
| d 1.000000 |  | ||||||
| illum 2 |  | ||||||
| map_Kd coomer.png |  | ||||||
| Before Width: | Height: | Size: 2.2 MiB | 
| @ -1,12 +0,0 @@ | |||||||
| # Blender 3.6.0 MTL File: 'None' |  | ||||||
| # www.blender.org |  | ||||||
|  |  | ||||||
| newmtl Material.001 |  | ||||||
| Ns 250.000000 |  | ||||||
| Ka 1.000000 1.000000 1.000000 |  | ||||||
| Ks 0.500000 0.500000 0.500000 |  | ||||||
| Ke 0.000000 0.000000 0.000000 |  | ||||||
| Ni 1.450000 |  | ||||||
| d 1.000000 |  | ||||||
| illum 2 |  | ||||||
| map_Kd goonion.png |  | ||||||
| Before Width: | Height: | Size: 2.5 MiB | 
| @ -1,12 +0,0 @@ | |||||||
| # Blender 3.6.0 MTL File: 'None' |  | ||||||
| # www.blender.org |  | ||||||
|  |  | ||||||
| newmtl Material.001 |  | ||||||
| Ns 250.000000 |  | ||||||
| Ka 1.000000 1.000000 1.000000 |  | ||||||
| Ks 0.500000 0.500000 0.500000 |  | ||||||
| Ke 0.000000 0.000000 0.000000 |  | ||||||
| Ni 1.450000 |  | ||||||
| d 1.000000 |  | ||||||
| illum 2 |  | ||||||
| map_Kd shreke.png |  | ||||||
| Before Width: | Height: | Size: 4.8 MiB | 
| Before Width: | Height: | Size: 104 KiB | 
							
								
								
									
										11
									
								
								game/chat.go
									
									
									
									
									
								
							
							
						
						| @ -53,6 +53,7 @@ func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) { | |||||||
| 	for _, msg := range messages { | 	for _, msg := range messages { | ||||||
| 		localMsg := types.ChatMessage{ | 		localMsg := types.ChatMessage{ | ||||||
| 			PlayerID: msg.PlayerId, | 			PlayerID: msg.PlayerId, | ||||||
|  | 			Username: msg.Username, | ||||||
| 			Content:  msg.Content, | 			Content:  msg.Content, | ||||||
| 			Time:     time.Unix(0, msg.Timestamp), | 			Time:     time.Unix(0, msg.Timestamp), | ||||||
| 		} | 		} | ||||||
| @ -117,8 +118,14 @@ func (c *Chat) Draw(screenWidth, screenHeight int32) { | |||||||
|  |  | ||||||
| 	for i := startIdx; i < endIdx; i++ { | 	for i := startIdx; i < endIdx; i++ { | ||||||
| 		msg := c.messages[i] | 		msg := c.messages[i] | ||||||
| 		text := fmt.Sprintf("[%d]: %s", msg.PlayerID, msg.Content) | 		var color rl.Color | ||||||
| 		rl.DrawText(text, int32(chatX)+5, int32(messageY), 20, rl.White) | 		if msg.PlayerID == 0 { // System message | ||||||
|  | 			color = rl.Gold | ||||||
|  | 		} else { | ||||||
|  | 			color = rl.White | ||||||
|  | 		} | ||||||
|  | 		text := fmt.Sprintf("%s: %s", msg.Username, msg.Content) | ||||||
|  | 		rl.DrawText(text, int32(chatX)+5, int32(messageY), 20, color) | ||||||
| 		messageY += messageHeight | 		messageY += messageHeight | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										64
									
								
								game/game.go
									
									
									
									
									
								
							
							
						
						| @ -5,6 +5,7 @@ import ( | |||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"gitea.boner.be/bdnugget/goonscape/assets" | 	"gitea.boner.be/bdnugget/goonscape/assets" | ||||||
|  | 	"gitea.boner.be/bdnugget/goonscape/network" | ||||||
| 	"gitea.boner.be/bdnugget/goonscape/types" | 	"gitea.boner.be/bdnugget/goonscape/types" | ||||||
| 	pb "gitea.boner.be/bdnugget/goonserver/actions" | 	pb "gitea.boner.be/bdnugget/goonserver/actions" | ||||||
| 	rl "github.com/gen2brain/raylib-go/raylib" | 	rl "github.com/gen2brain/raylib-go/raylib" | ||||||
| @ -19,19 +20,13 @@ type Game struct { | |||||||
| 	Chat         *Chat | 	Chat         *Chat | ||||||
| 	MenuOpen     bool | 	MenuOpen     bool | ||||||
| 	QuitChan     chan struct{} // Channel to signal shutdown | 	QuitChan     chan struct{} // Channel to signal shutdown | ||||||
|  | 	loginScreen  *LoginScreen | ||||||
|  | 	isLoggedIn   bool | ||||||
| } | } | ||||||
|  |  | ||||||
| func New() *Game { | func New() *Game { | ||||||
| 	InitWorld() | 	InitWorld() | ||||||
| 	game := &Game{ | 	game := &Game{ | ||||||
| 		Player: &types.Player{ |  | ||||||
| 			PosActual:  rl.NewVector3(5*types.TileSize, 0, 5*types.TileSize), |  | ||||||
| 			PosTile:    GetTile(5, 5), |  | ||||||
| 			Speed:      50.0, |  | ||||||
| 			TargetPath: []types.Tile{}, |  | ||||||
| 			UserData:   nil, |  | ||||||
| 			QuitDone:   make(chan struct{}), |  | ||||||
| 		}, |  | ||||||
| 		OtherPlayers: make(map[int32]*types.Player), | 		OtherPlayers: make(map[int32]*types.Player), | ||||||
| 		Camera: rl.Camera3D{ | 		Camera: rl.Camera3D{ | ||||||
| 			Position:   rl.NewVector3(0, 10, 10), | 			Position:   rl.NewVector3(0, 10, 10), | ||||||
| @ -40,10 +35,10 @@ func New() *Game { | |||||||
| 			Fovy:       45.0, | 			Fovy:       45.0, | ||||||
| 			Projection: rl.CameraPerspective, | 			Projection: rl.CameraPerspective, | ||||||
| 		}, | 		}, | ||||||
| 		Chat:     NewChat(), | 		Chat:        NewChat(), | ||||||
| 		QuitChan: make(chan struct{}), | 		QuitChan:    make(chan struct{}), | ||||||
|  | 		loginScreen: NewLoginScreen(), | ||||||
| 	} | 	} | ||||||
| 	game.Player.UserData = game |  | ||||||
| 	game.Chat.userData = game | 	game.Chat.userData = game | ||||||
| 	return game | 	return game | ||||||
| } | } | ||||||
| @ -64,6 +59,35 @@ func (g *Game) LoadAssets() error { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (g *Game) Update(deltaTime float32) { | func (g *Game) Update(deltaTime float32) { | ||||||
|  | 	if !g.isLoggedIn { | ||||||
|  | 		username, password, isRegistering, submitted := g.loginScreen.Update() | ||||||
|  | 		if submitted { | ||||||
|  | 			conn, playerID, err := network.ConnectToServer(username, password, isRegistering) | ||||||
|  | 			if err != nil { | ||||||
|  | 				g.loginScreen.SetError(err.Error()) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// 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 | 	// Handle ESC for menu | ||||||
| 	if rl.IsKeyPressed(rl.KeyEscape) { | 	if rl.IsKeyPressed(rl.KeyEscape) { | ||||||
| 		g.MenuOpen = !g.MenuOpen | 		g.MenuOpen = !g.MenuOpen | ||||||
| @ -174,11 +198,23 @@ func (g *Game) Render() { | |||||||
| 	rl.BeginDrawing() | 	rl.BeginDrawing() | ||||||
| 	rl.ClearBackground(rl.RayWhite) | 	rl.ClearBackground(rl.RayWhite) | ||||||
|  |  | ||||||
|  | 	if !g.isLoggedIn { | ||||||
|  | 		g.loginScreen.Draw() | ||||||
|  | 		rl.EndDrawing() | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	rl.BeginMode3D(g.Camera) | 	rl.BeginMode3D(g.Camera) | ||||||
| 	g.DrawMap() | 	g.DrawMap() | ||||||
| 	g.DrawPlayer(g.Player, g.Player.Model) | 	g.DrawPlayer(g.Player, g.Player.Model) | ||||||
| 	for id, other := range g.OtherPlayers { | 	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() | 	rl.EndMode3D() | ||||||
|  |  | ||||||
| @ -312,3 +348,7 @@ func (g *Game) Shutdown() { | |||||||
| 	rl.CloseWindow() | 	rl.CloseWindow() | ||||||
| 	os.Exit(0) | 	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 | ||||||
|  | } | ||||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						| @ -3,7 +3,7 @@ module gitea.boner.be/bdnugget/goonscape | |||||||
| go 1.23.0 | go 1.23.0 | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	gitea.boner.be/bdnugget/goonserver v0.0.0-20250113131525-49e23114973c | 	gitea.boner.be/bdnugget/goonserver v1.1.0 | ||||||
| 	github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b | 	github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b | ||||||
| 	google.golang.org/protobuf v1.36.3 | 	google.golang.org/protobuf v1.36.3 | ||||||
| ) | ) | ||||||
|  | |||||||
							
								
								
									
										37
									
								
								main.go
									
									
									
									
									
								
							
							
						
						| @ -11,20 +11,21 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| func main() { | func main() { | ||||||
| 	local := flag.Bool("local", false, "Use local server instead of remote") | 	// Parse command line flags | ||||||
| 	addr := flag.String("addr", "boner.be:6969", "Server address (hostname:port or hostname)") | 	local := flag.Bool("local", false, "Connect to local server") | ||||||
|  | 	addr := flag.String("addr", "", "Server address (host or host:port)") | ||||||
| 	flag.Parse() | 	flag.Parse() | ||||||
|  |  | ||||||
| 	if *local && *addr != "boner.be:6969" { | 	// Set server address based on flags | ||||||
| 		log.Fatal("Cannot use both -local and -addr flags") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if *local { | 	if *local { | ||||||
|  | 		if *addr != "" { | ||||||
|  | 			log.Fatal("Cannot use -local and -addr together") | ||||||
|  | 		} | ||||||
| 		network.SetServerAddr("localhost:6969") | 		network.SetServerAddr("localhost:6969") | ||||||
| 	} else if *addr != "" { | 	} else if *addr != "" { | ||||||
| 		// If only hostname is provided, append default port | 		// If port is not specified, append default port | ||||||
| 		if !strings.Contains(*addr, ":") { | 		if !strings.Contains(*addr, ":") { | ||||||
| 			*addr = *addr + ":6969" | 			*addr += ":6969" | ||||||
| 		} | 		} | ||||||
| 		network.SetServerAddr(*addr) | 		network.SetServerAddr(*addr) | ||||||
| 	} | 	} | ||||||
| @ -32,36 +33,24 @@ func main() { | |||||||
| 	rl.InitWindow(1024, 768, "GoonScape") | 	rl.InitWindow(1024, 768, "GoonScape") | ||||||
| 	rl.SetExitKey(0) | 	rl.SetExitKey(0) | ||||||
| 	defer rl.CloseWindow() | 	defer rl.CloseWindow() | ||||||
|  |  | ||||||
| 	rl.InitAudioDevice() | 	rl.InitAudioDevice() | ||||||
| 	defer rl.CloseAudioDevice() | 	defer rl.CloseAudioDevice() | ||||||
|  |  | ||||||
|  | 	rl.SetTargetFPS(60) | ||||||
|  |  | ||||||
| 	game := game.New() | 	game := game.New() | ||||||
| 	if err := game.LoadAssets(); err != nil { | 	if err := game.LoadAssets(); err != nil { | ||||||
| 		log.Fatalf("Failed to load assets: %v", err) | 		log.Fatalf("Failed to load assets: %v", err) | ||||||
| 	} | 	} | ||||||
| 	defer game.Cleanup() | 	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, game.QuitChan) |  | ||||||
|  |  | ||||||
| 	rl.PlayMusicStream(game.Music) | 	rl.PlayMusicStream(game.Music) | ||||||
| 	rl.SetMusicVolume(game.Music, 0.5) | 	rl.SetMusicVolume(game.Music, 0.5) | ||||||
| 	rl.SetTargetFPS(60) |  | ||||||
|  |  | ||||||
| 	for !rl.WindowShouldClose() { | 	for !rl.WindowShouldClose() { | ||||||
| 		rl.UpdateMusicStream(game.Music) |  | ||||||
| 		deltaTime := rl.GetFrameTime() | 		deltaTime := rl.GetFrameTime() | ||||||
|  | 		rl.UpdateMusicStream(game.Music) | ||||||
| 		game.Update(deltaTime) | 		game.Update(deltaTime) | ||||||
| 		game.Render() | 		game.Render() | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -3,56 +3,89 @@ package network | |||||||
| import ( | import ( | ||||||
| 	"bufio" | 	"bufio" | ||||||
| 	"encoding/binary" | 	"encoding/binary" | ||||||
|  | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
| 	"log" | 	"log" | ||||||
| 	"net" | 	"net" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"gitea.boner.be/bdnugget/goonscape/game" |  | ||||||
| 	"gitea.boner.be/bdnugget/goonscape/types" | 	"gitea.boner.be/bdnugget/goonscape/types" | ||||||
| 	pb "gitea.boner.be/bdnugget/goonserver/actions" | 	pb "gitea.boner.be/bdnugget/goonserver/actions" | ||||||
|  | 	rl "github.com/gen2brain/raylib-go/raylib" | ||||||
| 	"google.golang.org/protobuf/proto" | 	"google.golang.org/protobuf/proto" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | const protoVersion = 1 | ||||||
|  |  | ||||||
| var serverAddr = "boner.be:6969" | var serverAddr = "boner.be:6969" | ||||||
|  |  | ||||||
| func SetServerAddr(addr string) { | func SetServerAddr(addr string) { | ||||||
| 	serverAddr = addr | 	serverAddr = addr | ||||||
| } | } | ||||||
|  |  | ||||||
| func ConnectToServer() (net.Conn, int32, error) { | func ConnectToServer(username, password string, isRegistering bool) (net.Conn, int32, error) { | ||||||
| 	conn, err := net.Dial("tcp", serverAddr) | 	conn, err := net.Dial("tcp", serverAddr) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Printf("Failed to dial server: %v", err) | 		log.Printf("Failed to dial server: %v", err) | ||||||
| 		return nil, 0, err | 		return nil, 0, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	log.Println("Connected to server. Waiting for player ID...") | 	log.Println("Connected to server. Authenticating...") | ||||||
| 	reader := bufio.NewReader(conn) |  | ||||||
|  |  | ||||||
| 	// Read message length (4 bytes) | 	// Send auth message | ||||||
|  | 	authAction := &pb.Action{ | ||||||
|  | 		Type:     pb.Action_LOGIN, | ||||||
|  | 		Username: username, | ||||||
|  | 		Password: password, | ||||||
|  | 	} | ||||||
|  | 	if isRegistering { | ||||||
|  | 		authAction.Type = pb.Action_REGISTER | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	authBatch := &pb.ActionBatch{ | ||||||
|  | 		Actions:         []*pb.Action{authAction}, | ||||||
|  | 		ProtocolVersion: protoVersion, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := writeMessage(conn, authBatch); err != nil { | ||||||
|  | 		conn.Close() | ||||||
|  | 		return nil, 0, fmt.Errorf("failed to send auth: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Read server response | ||||||
|  | 	reader := bufio.NewReader(conn) | ||||||
| 	lengthBuf := make([]byte, 4) | 	lengthBuf := make([]byte, 4) | ||||||
| 	if _, err := io.ReadFull(reader, lengthBuf); err != nil { | 	if _, err := io.ReadFull(reader, lengthBuf); err != nil { | ||||||
| 		log.Printf("Failed to read message length: %v", err) | 		conn.Close() | ||||||
| 		return nil, 0, err | 		return nil, 0, fmt.Errorf("failed to read auth response: %v", err) | ||||||
| 	} | 	} | ||||||
| 	messageLength := binary.BigEndian.Uint32(lengthBuf) | 	messageLength := binary.BigEndian.Uint32(lengthBuf) | ||||||
|  |  | ||||||
| 	// Read the full message |  | ||||||
| 	messageBuf := make([]byte, messageLength) | 	messageBuf := make([]byte, messageLength) | ||||||
| 	if _, err := io.ReadFull(reader, messageBuf); err != nil { | 	if _, err := io.ReadFull(reader, messageBuf); err != nil { | ||||||
| 		log.Printf("Failed to read message body: %v", err) | 		conn.Close() | ||||||
| 		return nil, 0, err | 		return nil, 0, fmt.Errorf("failed to read auth response body: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var response pb.ServerMessage | 	var response pb.ServerMessage | ||||||
| 	if err := proto.Unmarshal(messageBuf, &response); err != nil { | 	if err := proto.Unmarshal(messageBuf, &response); err != nil { | ||||||
| 		log.Printf("Failed to unmarshal server response: %v", err) | 		conn.Close() | ||||||
| 		return nil, 0, err | 		return nil, 0, fmt.Errorf("failed to unmarshal auth response: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if response.ProtocolVersion > protoVersion { | ||||||
|  | 		conn.Close() | ||||||
|  | 		return nil, 0, fmt.Errorf("server requires newer protocol version (server: %d, client: %d)", | ||||||
|  | 			response.ProtocolVersion, protoVersion) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if !response.AuthSuccess { | ||||||
|  | 		conn.Close() | ||||||
|  | 		return nil, 0, fmt.Errorf(response.ErrorMessage) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	playerID := response.GetPlayerId() | 	playerID := response.GetPlayerId() | ||||||
| 	log.Printf("Successfully connected with player ID: %d", playerID) | 	log.Printf("Successfully authenticated with player ID: %d", playerID) | ||||||
| 	return conn, playerID, nil | 	return conn, playerID, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| @ -67,6 +100,9 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play | |||||||
| 	// Create a channel to signal when goroutines are done | 	// Create a channel to signal when goroutines are done | ||||||
| 	done := make(chan struct{}) | 	done := make(chan struct{}) | ||||||
|  |  | ||||||
|  | 	// Create a set of current players to track disconnects | ||||||
|  | 	currentPlayers := make(map[int32]bool) | ||||||
|  |  | ||||||
| 	go func() { | 	go func() { | ||||||
| 		for { | 		for { | ||||||
| 			select { | 			select { | ||||||
| @ -161,7 +197,19 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play | |||||||
| 			player.Unlock() | 			player.Unlock() | ||||||
|  |  | ||||||
| 			for _, state := range serverMessage.Players { | 			for _, state := range serverMessage.Players { | ||||||
|  | 				currentPlayers[state.PlayerId] = true | ||||||
| 				if state.PlayerId == playerID { | 				if state.PlayerId == playerID { | ||||||
|  | 					player.Lock() | ||||||
|  | 					// Update initial position if not set | ||||||
|  | 					if player.PosActual.X == 0 && player.PosActual.Z == 0 { | ||||||
|  | 						player.PosActual = rl.Vector3{ | ||||||
|  | 							X: float32(state.X * types.TileSize), | ||||||
|  | 							Y: 0, | ||||||
|  | 							Z: float32(state.Y * types.TileSize), | ||||||
|  | 						} | ||||||
|  | 						player.PosTile = types.Tile{X: int(state.X), Y: int(state.Y)} | ||||||
|  | 					} | ||||||
|  | 					player.Unlock() | ||||||
| 					continue | 					continue | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
| @ -172,8 +220,15 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play | |||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if g, ok := player.UserData.(*game.Game); ok && len(serverMessage.ChatMessages) > 0 { | 			// Remove players that are no longer in the server state | ||||||
| 				g.Chat.HandleServerMessages(serverMessage.ChatMessages) | 			for id := range otherPlayers { | ||||||
|  | 				if !currentPlayers[id] { | ||||||
|  | 					delete(otherPlayers, id) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if handler, ok := player.UserData.(types.ChatMessageHandler); ok && len(serverMessage.ChatMessages) > 0 { | ||||||
|  | 				handler.HandleServerMessages(serverMessage.ChatMessages) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -6,12 +6,15 @@ build() { | |||||||
|     local arch=$2 |     local arch=$2 | ||||||
|     local output=$3 |     local output=$3 | ||||||
|  |  | ||||||
|     # Set CGO flags for static linking |     # Set GOOS and GOARCH for cross-compilation | ||||||
|     export CGO_ENABLED=1 |  | ||||||
|     export GOOS=$os |     export GOOS=$os | ||||||
|     export GOARCH=$arch |     export GOARCH=$arch | ||||||
|  |  | ||||||
|     # Platform specific flags |     # Disable CGO only for cross-compilation | ||||||
|  |     if [ "$os" != "$(go env GOOS)" ] || [ "$arch" != "$(go env GOARCH)" ]; then | ||||||
|  |         export CGO_ENABLED=0 | ||||||
|  |     fi | ||||||
|  |  | ||||||
|     if [ "$os" = "windows" ]; then |     if [ "$os" = "windows" ]; then | ||||||
|         export CC=x86_64-w64-mingw32-gcc |         export CC=x86_64-w64-mingw32-gcc | ||||||
|         export CXX=x86_64-w64-mingw32-g++ |         export CXX=x86_64-w64-mingw32-g++ | ||||||
|  | |||||||
| @ -39,6 +39,7 @@ type ModelAsset struct { | |||||||
|  |  | ||||||
| type ChatMessage struct { | type ChatMessage struct { | ||||||
| 	PlayerID int32 | 	PlayerID int32 | ||||||
|  | 	Username string | ||||||
| 	Content  string | 	Content  string | ||||||
| 	Time     time.Time | 	Time     time.Time | ||||||
| } | } | ||||||
| @ -49,6 +50,10 @@ type FloatingMessage struct { | |||||||
| 	ScreenPos  rl.Vector2 // Store the screen position for 2D rendering | 	ScreenPos  rl.Vector2 // Store the screen position for 2D rendering | ||||||
| } | } | ||||||
|  |  | ||||||
|  | type ChatMessageHandler interface { | ||||||
|  | 	HandleServerMessages([]*pb.ChatMessage) | ||||||
|  | } | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
| 	MapWidth   = 50 | 	MapWidth   = 50 | ||||||
| 	MapHeight  = 50 | 	MapHeight  = 50 | ||||||
|  | |||||||