Compare commits
6 Commits
d3de2401f3
...
feature/mu
Author | SHA1 | Date | |
---|---|---|---|
be569be8a6 | |||
de75b8ef52 | |||
6c8993981c | |||
92b0b13add | |||
a9c6298da8 | |||
d4e299c72b |
12
go.mod
12
go.mod
@ -2,10 +2,14 @@ module goonscape
|
|||||||
|
|
||||||
go 1.23.0
|
go 1.23.0
|
||||||
|
|
||||||
require github.com/gen2brain/raylib-go/raylib v0.0.0-20240916050633-6bc3d79c96ad
|
require (
|
||||||
|
gitea.boner.be/bdnugget/goonserver v0.0.0-20241011122434-4bd5303cfd46
|
||||||
|
github.com/gen2brain/raylib-go/raylib v0.0.0-20240930075631-c66f9e2942fe
|
||||||
|
google.golang.org/protobuf v1.35.1
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/ebitengine/purego v0.7.1 // indirect
|
github.com/ebitengine/purego v0.8.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
|
||||||
golang.org/x/sys v0.20.0 // indirect
|
golang.org/x/sys v0.26.0 // indirect
|
||||||
)
|
)
|
||||||
|
22
go.sum
22
go.sum
@ -1,8 +1,14 @@
|
|||||||
github.com/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA=
|
gitea.boner.be/bdnugget/goonserver v0.0.0-20241011122434-4bd5303cfd46 h1:T2D4QcmvBqzGoHO0VJGNUd1k2lLmUcyg6Rc/vN4/Im8=
|
||||||
github.com/ebitengine/purego v0.7.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
|
gitea.boner.be/bdnugget/goonserver v0.0.0-20241011122434-4bd5303cfd46/go.mod h1:inR1bKrr/vcTba+G1KzmmY6vssMq9oGNOk836VwPa4c=
|
||||||
github.com/gen2brain/raylib-go/raylib v0.0.0-20240916050633-6bc3d79c96ad h1:kmIjqc2wOOn+MXw/pyuoYhhzfRFsZ+ESfkMWVt+WE7Y=
|
github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE=
|
||||||
github.com/gen2brain/raylib-go/raylib v0.0.0-20240916050633-6bc3d79c96ad/go.mod h1:BaY76bZk7nw1/kVOSQObPY1v1iwVE1KHAGMfvI6oK1Q=
|
github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
github.com/gen2brain/raylib-go/raylib v0.0.0-20240930075631-c66f9e2942fe h1:mInjrbJkUglTM7tBmXG+epnPCE744aj15J7vjJwM4gs=
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
github.com/gen2brain/raylib-go/raylib v0.0.0-20240930075631-c66f9e2942fe/go.mod h1:BaY76bZk7nw1/kVOSQObPY1v1iwVE1KHAGMfvI6oK1Q=
|
||||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
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=
|
||||||
|
220
main.go
220
main.go
@ -2,9 +2,14 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"math"
|
"math"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
||||||
rl "github.com/gen2brain/raylib-go/raylib"
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -12,6 +17,8 @@ const (
|
|||||||
MapHeight = 50
|
MapHeight = 50
|
||||||
TileSize = 32
|
TileSize = 32
|
||||||
TileHeight = 2.0
|
TileHeight = 2.0
|
||||||
|
TickRate = 2600 * time.Millisecond // Server tick rate (600ms)
|
||||||
|
serverAddr = "localhost:6969"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -26,11 +33,23 @@ type Tile struct {
|
|||||||
Walkable bool
|
Walkable bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ActionType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
MoveAction ActionType = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
type Action struct {
|
||||||
|
Type ActionType
|
||||||
|
X, Y int // Target position for movement
|
||||||
|
}
|
||||||
|
|
||||||
type Player struct {
|
type Player struct {
|
||||||
PosActual rl.Vector3
|
PosActual rl.Vector3
|
||||||
PosTile Tile
|
PosTile Tile
|
||||||
TargetPath []Tile
|
TargetPath []Tile
|
||||||
Speed float32
|
Speed float32
|
||||||
|
ActionQueue []Action // Queue for player actions
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the map with some height data
|
// Initialize the map with some height data
|
||||||
@ -78,14 +97,75 @@ func DrawMap(mapGrid [][]Tile) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func DrawPlayer(player Player, mapGrid [][]Tile) {
|
func DrawPlayer(player Player, model *rl.Model, mapGrid [][]Tile) {
|
||||||
// Draw the player based on its actual position (PosActual) and current tile height
|
// Draw the player based on its actual position (PosActual) and current tile height
|
||||||
playerPos := rl.Vector3{
|
playerPos := rl.Vector3{
|
||||||
X: player.PosActual.X,
|
X: player.PosActual.X,
|
||||||
Y: mapGrid[player.PosTile.X][player.PosTile.Y].Height * TileHeight,
|
Y: mapGrid[player.PosTile.X][player.PosTile.Y].Height*TileHeight + 16.0,
|
||||||
Z: player.PosActual.Z,
|
Z: player.PosActual.Z,
|
||||||
}
|
}
|
||||||
rl.DrawCube(playerPos, 16, 16, 16, rl.Green) // Draw player cube
|
// 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) {
|
func GetTileAtMouse(mapGrid [][]Tile, camera *rl.Camera3D) (Tile, bool) {
|
||||||
@ -93,30 +173,23 @@ func GetTileAtMouse(mapGrid [][]Tile, camera *rl.Camera3D) (Tile, bool) {
|
|||||||
return Tile{}, false
|
return Tile{}, false
|
||||||
}
|
}
|
||||||
mouse := rl.GetMousePosition()
|
mouse := rl.GetMousePosition()
|
||||||
|
|
||||||
// Unproject mouse coordinates to 3D ray
|
|
||||||
ray := rl.GetMouseRay(mouse, *camera)
|
ray := rl.GetMouseRay(mouse, *camera)
|
||||||
|
|
||||||
// Calculate the distance from the camera to the ray's intersection with the ground plane (Y=0)
|
for x := 0; x < MapWidth; x++ {
|
||||||
if ray.Direction.Y == 0 {
|
for y := 0; y < MapHeight; y++ {
|
||||||
return Tile{}, false
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
dist := -ray.Position.Y / ray.Direction.Y
|
|
||||||
|
|
||||||
// Calculate the intersection point
|
|
||||||
intersection := rl.NewVector3(
|
|
||||||
ray.Position.X+ray.Direction.X*dist,
|
|
||||||
ray.Position.Y+ray.Direction.Y*dist,
|
|
||||||
ray.Position.Z+ray.Direction.Z*dist,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Convert the intersection point to tile coordinates
|
|
||||||
tileX := int(intersection.X / float32(TileSize))
|
|
||||||
tileY := int(intersection.Z / float32(TileSize))
|
|
||||||
|
|
||||||
if tileX >= 0 && tileX < MapWidth && tileY >= 0 && tileY < MapHeight {
|
|
||||||
fmt.Println("Clicked:", tileX, tileY)
|
|
||||||
return mapGrid[tileX][tileY], true
|
|
||||||
}
|
}
|
||||||
return Tile{}, false
|
return Tile{}, false
|
||||||
}
|
}
|
||||||
@ -155,6 +228,7 @@ func HandleInput(player *Player, mapGrid [][]Tile, camera *rl.Camera) {
|
|||||||
// Exclude the first tile (current position)
|
// Exclude the first tile (current position)
|
||||||
if len(path) > 1 {
|
if len(path) > 1 {
|
||||||
player.TargetPath = path[1:]
|
player.TargetPath = path[1:]
|
||||||
|
player.ActionQueue = append(player.ActionQueue, Action{Type: MoveAction, X: clickedTile.X, Y: clickedTile.Y})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -208,7 +282,7 @@ func UpdateCamera(camera *rl.Camera3D, player rl.Vector3, deltaTime float32) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
rl.InitWindow(800, 600, "goonscape")
|
rl.InitWindow(1024, 768, "GoonScape")
|
||||||
defer rl.CloseWindow()
|
defer rl.CloseWindow()
|
||||||
rl.InitAudioDevice()
|
rl.InitAudioDevice()
|
||||||
defer rl.CloseAudioDevice()
|
defer rl.CloseAudioDevice()
|
||||||
@ -230,6 +304,27 @@ func main() {
|
|||||||
Projection: rl.CameraPerspective,
|
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)
|
||||||
|
|
||||||
|
playerModel := rl.LoadModel("resources/models/goonion.obj")
|
||||||
|
defer rl.UnloadModel(playerModel)
|
||||||
|
playerTexture := rl.LoadTexture("resources/models/goonion.png")
|
||||||
|
defer rl.UnloadTexture(playerTexture)
|
||||||
|
rl.SetMaterialTexture(playerModel.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)
|
||||||
|
|
||||||
rl.SetTargetFPS(60)
|
rl.SetTargetFPS(60)
|
||||||
|
|
||||||
// Music
|
// Music
|
||||||
@ -239,6 +334,7 @@ func main() {
|
|||||||
defer rl.UnloadMusicStream(music)
|
defer rl.UnloadMusicStream(music)
|
||||||
|
|
||||||
for !rl.WindowShouldClose() {
|
for !rl.WindowShouldClose() {
|
||||||
|
|
||||||
rl.UpdateMusicStream(music)
|
rl.UpdateMusicStream(music)
|
||||||
|
|
||||||
// Time management
|
// Time management
|
||||||
@ -260,12 +356,84 @@ func main() {
|
|||||||
rl.ClearBackground(rl.RayWhite)
|
rl.ClearBackground(rl.RayWhite)
|
||||||
rl.BeginMode3D(camera)
|
rl.BeginMode3D(camera)
|
||||||
DrawMap(mapGrid)
|
DrawMap(mapGrid)
|
||||||
DrawPlayer(player, mapGrid)
|
DrawPlayer(player, &playerModel, mapGrid)
|
||||||
|
|
||||||
|
rl.DrawModel(coomerModel, rl.NewVector3(5*TileSize+32, 32, 5*TileSize+32), 16, rl.White)
|
||||||
|
|
||||||
|
rl.DrawFPS(10, 10)
|
||||||
|
|
||||||
rl.EndMode3D()
|
rl.EndMode3D()
|
||||||
rl.EndDrawing()
|
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
|
// pathfinding
|
||||||
type Node struct {
|
type Node struct {
|
||||||
Tile Tile
|
Tile Tile
|
||||||
|
12
resources/models/coomer.mtl
Normal file
12
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
resources/models/coomer.obj
Normal file
117248
resources/models/coomer.obj
Normal file
File diff suppressed because it is too large
Load Diff
BIN
resources/models/coomer.png
Normal file
BIN
resources/models/coomer.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 MiB |
@ -9,4 +9,4 @@ Ke 0.000000 0.000000 0.000000
|
|||||||
Ni 1.450000
|
Ni 1.450000
|
||||||
d 1.000000
|
d 1.000000
|
||||||
illum 2
|
illum 2
|
||||||
map_Kd Minion_full_body_A__1002112559.png
|
map_Kd goonion.png
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# Blender 3.6.0
|
# Blender 3.6.0
|
||||||
# www.blender.org
|
# www.blender.org
|
||||||
mtllib Minion_full_body_A__1002112559.mtl
|
mtllib goonion.mtl
|
||||||
o Minion_full_body_A__1002112559
|
o goonion
|
||||||
v -0.441385 -0.290769 -0.044101
|
v -0.441385 -0.290769 -0.044101
|
||||||
v -0.448696 -0.291602 -0.052330
|
v -0.448696 -0.291602 -0.052330
|
||||||
v -0.444993 -0.298477 -0.046398
|
v -0.444993 -0.298477 -0.046398
|
||||||
|
Reference in New Issue
Block a user