package main import ( "fmt" "log" "math" "net" "time" pb "gitea.boner.be/bdnugget/goonserver/actions" rl "github.com/gen2brain/raylib-go/raylib" "google.golang.org/protobuf/proto" ) const ( MapWidth = 50 MapHeight = 50 TileSize = 32 TileHeight = 2.0 TickRate = 2600 * time.Millisecond // Server tick rate (600ms) serverAddr = "localhost:6969" ) var ( cameraDistance = float32(20.0) cameraYaw = float32(145.0) cameraPitch = float32(45.0) // Adjusted for a more overhead view ) 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 } // 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(mapGrid [][]Tile) { 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 } } } func DrawPlayer(player Player, model *rl.Model, mapGrid [][]Tile) { // 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.DrawCube(playerPos, 16, 16, 16, rl.Green) // Draw player cube 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) } } // 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 } func GetTileAtMouse(mapGrid [][]Tile, 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 (player *Player) MoveTowards(target Tile, deltaTime float32, mapGrid [][]Tile) { // 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 player.TargetPath = player.TargetPath[1:] // Move to next tile in path if any } } func HandleInput(player *Player, mapGrid [][]Tile, camera *rl.Camera) { clickedTile, clicked := GetTileAtMouse(mapGrid, camera) if clicked { path := FindPath(mapGrid, 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}) } } } } func UpdateCamera(camera *rl.Camera3D, player rl.Vector3, deltaTime float32) { // Update camera based on mouse wheel wheelMove := rl.GetMouseWheelMove() if wheelMove != 0 { cameraDistance += -wheelMove * 5 if cameraDistance < 10 { cameraDistance = 10 } if cameraDistance > 250 { cameraDistance = 250 } } // Orbit camera around the player using arrow keys if rl.IsKeyDown(rl.KeyRight) { cameraYaw += 100 * deltaTime } if rl.IsKeyDown(rl.KeyLeft) { cameraYaw -= 100 * deltaTime } if rl.IsKeyDown(rl.KeyUp) { cameraPitch -= 50 * deltaTime if cameraPitch < 20 { cameraPitch = 20 } } if rl.IsKeyDown(rl.KeyDown) { cameraPitch += 50 * deltaTime if cameraPitch > 85 { cameraPitch = 85 } } // Calculate the new camera position using spherical coordinates cameraYawRad := float64(cameraYaw) * rl.Deg2rad cameraPitchRad := float64(cameraPitch) * rl.Deg2rad cameraPos := rl.Vector3{ X: player.X + cameraDistance*float32(math.Cos(cameraYawRad))*float32(math.Cos(cameraPitchRad)), Y: player.Y + cameraDistance*float32(math.Sin(cameraPitchRad)), Z: player.Z + cameraDistance*float32(math.Sin(cameraYawRad))*float32(math.Cos(cameraPitchRad)), } // Update the camera's position and target camera.Position = cameraPos camera.Target = rl.NewVector3(player.X, player.Y, player.Z) } func main() { rl.InitWindow(1024, 768, "GoonScape") defer rl.CloseWindow() rl.InitAudioDevice() defer rl.CloseAudioDevice() mapGrid := InitMap() player := Player{ PosActual: rl.NewVector3(5*TileSize, 0, 5*TileSize), PosTile: mapGrid[5][5], Speed: 50.0, TargetPath: []Tile{}, } camera := rl.Camera3D{ 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 { log.Fatalf("Failed to connect to server: %v", err) } log.Printf("Player ID: %d", playerID) defer conn.Close() go HandleServerCommunication(conn, playerID, &player) goonerModel := rl.LoadModel("resources/models/goonion.obj") defer rl.UnloadModel(goonerModel) playerTexture := rl.LoadTexture("resources/models/goonion.png") defer rl.UnloadTexture(playerTexture) rl.SetMaterialTexture(goonerModel.Materials, rl.MapDiffuse, playerTexture) coomerModel := rl.LoadModel("resources/models/coomer.obj") defer rl.UnloadModel(coomerModel) coomerTexture := rl.LoadTexture("resources/models/coomer.png") defer rl.UnloadTexture(coomerTexture) rl.SetMaterialTexture(coomerModel.Materials, rl.MapDiffuse, coomerTexture) shrekeModel := rl.LoadModel("resources/models/shreke.obj") defer rl.UnloadModel(shrekeModel) shrekeTexture := rl.LoadTexture("resources/models/shreke.png") defer rl.UnloadTexture(shrekeTexture) rl.SetMaterialTexture(shrekeModel.Materials, rl.MapDiffuse, shrekeTexture) models := []struct { Model rl.Model Texture rl.Texture2D }{ {Model: goonerModel, Texture: playerTexture}, {Model: coomerModel, Texture: coomerTexture}, {Model: shrekeModel, Texture: shrekeTexture}, } modelIndex := int(playerID) % len(models) player.Model = models[modelIndex].Model player.Texture = models[modelIndex].Texture rl.SetTargetFPS(60) // Music music := rl.LoadMusicStream("resources/audio/GoonScape2.mp3") rl.PlayMusicStream(music) rl.SetMusicVolume(music, 0.5) defer rl.UnloadMusicStream(music) for !rl.WindowShouldClose() { rl.UpdateMusicStream(music) // Time management deltaTime := rl.GetFrameTime() // Handle input HandleInput(&player, mapGrid, &camera) // Update player if len(player.TargetPath) > 0 { player.MoveTowards(player.TargetPath[0], deltaTime, mapGrid) } // Update camera UpdateCamera(&camera, player.PosActual, deltaTime) // Rendering rl.BeginDrawing() rl.ClearBackground(rl.RayWhite) rl.BeginMode3D(camera) DrawMap(mapGrid) DrawPlayer(player, &player.Model, mapGrid) rl.DrawFPS(10, 10) rl.EndMode3D() rl.EndDrawing() } } 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) { for { // Check if there are actions in the player's queue 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 based on the server's tick rate time.Sleep(TickRate) } } // pathfinding type Node struct { Tile Tile Parent *Node G, H, F float32 } func FindPath(mapGrid [][]Tile, start, end Tile) []Tile { openList := []*Node{} closedList := make(map[[2]int]bool) startNode := &Node{Tile: start, G: 0, H: heuristic(start, end)} startNode.F = startNode.G + startNode.H openList = append(openList, startNode) for len(openList) > 0 { // Find node with lowest F current := openList[0] currentIndex := 0 for i, node := range openList { if node.F < current.F { current = node currentIndex = i } } // Move current to closed list openList = append(openList[:currentIndex], openList[currentIndex+1:]...) 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 { path := []Tile{} node := current for node != nil { path = append([]Tile{node.Tile}, path...) node = node.Parent } fmt.Printf("Path found: %v\n", path) return path } // Generate neighbors neighbors := GetNeighbors(mapGrid, current.Tile) for _, neighbor := range neighbors { if !neighbor.Walkable || closedList[[2]int{neighbor.X, neighbor.Y}] { continue } tentativeG := current.G + distance(current.Tile, neighbor) inOpen := false var existingNode *Node for _, node := range openList { if node.Tile.X == neighbor.X && node.Tile.Y == neighbor.Y { existingNode = node inOpen = true break } } if !inOpen || tentativeG < existingNode.G { newNode := &Node{ Tile: neighbor, Parent: current, G: tentativeG, H: heuristic(neighbor, end), } newNode.F = newNode.G + newNode.H if !inOpen { openList = append(openList, newNode) } } } } // No path found fmt.Println("No path found") return nil } func heuristic(a, b Tile) float32 { return float32(abs(a.X-b.X) + abs(a.Y-b.Y)) } func distance(a, b Tile) float32 { _ = a _ = b return 1.0 //uniform cost for now } func GetNeighbors(mapGrid [][]Tile, tile Tile) []Tile { directions := [][2]int{ {1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1}, {-1, -1}, {1, -1}, {-1, 1}, } neighbors := []Tile{} for _, dir := range directions { nx, ny := tile.X+dir[0], tile.Y+dir[1] if nx >= 0 && nx < MapWidth && ny >= 0 && ny < MapHeight { neighbors = append(neighbors, mapGrid[nx][ny]) } } return neighbors } func abs(x int) int { if x < 0 { return -x } return x }