Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added client-go/assets/ThaleahFat.ttf
Binary file not shown.
Binary file added client-go/assets/block.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client-go/assets/player-sheet.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client-go/assets/tilemap.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client-go/client
Binary file not shown.
280 changes: 280 additions & 0 deletions client-go/cmd/client/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
package main

import (
"fmt"
"os"
"path/filepath"
"strconv"
"time"

rl "github.com/gen2brain/raylib-go/raylib"
"tinyhold/client/internal/network"
"tinyhold/client/internal/render"
"tinyhold/client/internal/ui"
"tinyhold/client/internal/world"
)

const (
ScreenWidth = 1280
ScreenHeight = 720
TargetFPS = 60
)

type GameState int

const (
GameStateMenu GameState = iota
GameStateGame
GameStateExiting
)

func main() {
// Initialize raylib
rl.InitWindow(ScreenWidth, ScreenHeight, "Tinyhold - Go Client")
defer rl.CloseWindow()

rl.SetTargetFPS(TargetFPS)

// Set working directory to client-go
os.Chdir(filepath.Dir(os.Args[0]))

// Create world
gameWorld := world.NewWorld(0)

// Create renderer
renderer := render.NewRenderer(gameWorld)
if err := renderer.Init(); err != nil {
fmt.Printf("Failed to initialize renderer: %v\n", err)
return
}
defer renderer.Cleanup()

// Create menu
menu := ui.NewMainMenu()

// Create network client
netClient := network.NewClient()

// Game state
gameState := GameStateMenu

// Local server
var localServer *network.LocalServer

// Setup network callbacks
netClient.SetHandshakeCallback(func(playerID uint16, seed int64) {
fmt.Printf("Handshake received: playerID=%d, seed=%d\n", playerID, seed)
gameWorld.SetSeed(seed)
gameWorld.SetNetworkClient(netClient)

// Spawn local player
gameWorld.AddPlayer(playerID, 0, 0, true)

// Request initial chunks
go func() {
time.Sleep(100 * time.Millisecond)
gameWorld.RequestMissingChunks()
}()
})

netClient.SetPlayerStateCallback(func(state network.PlayerState) {
if state.ID == netClient.PlayerID() {
// Update local player
if p := gameWorld.GetPlayer(state.ID); p != nil {
p.X = state.X
p.Y = state.Y
}
} else {
// Update or create remote player
if p := gameWorld.GetPlayer(state.ID); p != nil {
p.X = state.X
p.Y = state.Y
} else {
gameWorld.AddPlayer(state.ID, state.X, state.Y, false)
}
}
})

netClient.SetWorldChunkCallback(func(chunk network.ChunkData) {
gameWorld.ApplyChunk(chunk.CX, chunk.CY, chunk.Tiles)
})

netClient.SetBlockUpdateCallback(func(update network.BlockUpdate) {
gameWorld.ApplyBlock(update.X, update.Y, update.Type)
})

netClient.SetDisconnectCallback(func() {
fmt.Println("Disconnected from server")
gameState = GameStateMenu
menu.SetState(ui.MenuStateMain)

// Cleanup
if localServer != nil {
localServer.Stop()
localServer = nil
}

// Reset world
gameWorld = world.NewWorld(0)
renderer = render.NewRenderer(gameWorld)
if err := renderer.Init(); err != nil {
fmt.Printf("Failed to reinitialize renderer: %v\n", err)
}
})

// Main game loop
for !rl.WindowShouldClose() {
// Get delta time
dt := rl.GetFrameTime()

// Handle window close
if rl.IsKeyPressed(rl.KeyQ) && rl.IsKeyDown(rl.KeyLeftControl) {
gameState = GameStateExiting
}

switch gameState {
case GameStateMenu:
menu.Update(dt)
menu.Draw()

// Handle menu actions
if menu.IsSingleplayerSelected() {
// Start local server
port := 42069
serverPath := "../server/build/tinyhold-server"
if _, err := os.Stat(serverPath); err != nil {
serverPath = "../../server/build/tinyhold-server"
}

var err error
localServer, err = network.StartLocalServer(serverPath, port)
if err != nil {
fmt.Printf("Failed to start local server: %v\n", err)
menu.SetError(fmt.Sprintf("Failed to start server: %v", err))
break
}

// Connect to local server
if err := netClient.Connect("127.0.0.1", port); err != nil {
fmt.Printf("Failed to connect: %v\n", err)
menu.SetError(fmt.Sprintf("Failed to connect: %v", err))
localServer.Stop()
localServer = nil
break
}

gameState = GameStateGame
menu.SetState(ui.MenuStateConnecting)
}

if menu.IsConnectSelected() {
ip, portStr := menu.GetConnectionInfo()
port, err := strconv.Atoi(portStr)
if err != nil {
port = 42069
}

if err := netClient.Connect(ip, port); err != nil {
fmt.Printf("Failed to connect: %v\n", err)
menu.SetError(fmt.Sprintf("Failed to connect: %v", err))
break
}

gameState = GameStateGame
menu.SetState(ui.MenuStateConnecting)
}

if menu.IsBackSelected() {
menu.SetState(ui.MenuStateMain)
}

case GameStateGame:
// Update game
if netClient.IsConnected() {
// Handle input
handleInput(netClient, renderer)

// Update camera
renderer.UpdateCamera()

// Request chunks as player moves
gameWorld.RequestMissingChunks()

// Draw game
renderer.Draw()

// Check if disconnected
if !netClient.IsConnected() {
gameState = GameStateMenu
menu.SetState(ui.MenuStateMain)
}
} else {
// Connection failed or disconnected
gameState = GameStateMenu
menu.SetState(ui.MenuStateMain)
}

// Handle escape to return to menu
if rl.IsKeyPressed(rl.KeyEscape) {
netClient.Disconnect()
if localServer != nil {
localServer.Stop()
localServer = nil
}
gameState = GameStateMenu
menu.SetState(ui.MenuStateMain)

// Reset world
gameWorld = world.NewWorld(0)
renderer = render.NewRenderer(gameWorld)
if err := renderer.Init(); err != nil {
fmt.Printf("Failed to reinitialize renderer: %v\n", err)
}
}

case GameStateExiting:
return
}
}

// Cleanup local server if exiting
if localServer != nil {
localServer.Stop()
}
}

func handleInput(client *network.Client, renderer *render.Renderer) {
var keys uint8

if rl.IsKeyDown(rl.KeyW) || rl.IsKeyDown(rl.KeyUp) {
keys |= 1 // Up
}
if rl.IsKeyDown(rl.KeyS) || rl.IsKeyDown(rl.KeyDown) {
keys |= 2 // Down
}
if rl.IsKeyDown(rl.KeyA) || rl.IsKeyDown(rl.KeyLeft) {
keys |= 4 // Left
}
if rl.IsKeyDown(rl.KeyD) || rl.IsKeyDown(rl.KeyRight) {
keys |= 8 // Right
}

if keys != 0 {
client.SendPlayerInput(keys)
}

// Handle block placement with mouse
if rl.IsMouseButtonPressed(rl.MouseLeftButton) {
mousePos := rl.GetMousePosition()
camera := renderer.Camera()

// Convert screen position to world position
worldPos := rl.GetScreenToWorld2D(mousePos, camera)

// Convert to tile coordinates
tileX := int32(worldPos.X * 100.0 / 16.0)
tileY := int32(worldPos.Y * 100.0 / 16.0)

client.SendBlockPlace(tileX, tileY)
}
}
12 changes: 12 additions & 0 deletions client-go/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module tinyhold/client

go 1.23

require github.com/gen2brain/raylib-go/raylib v0.55.1

require (
github.com/ebitengine/purego v0.10.0 // indirect
github.com/jupiterrider/ffi v0.7.0 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/sys v0.20.0 // indirect
)
12 changes: 12 additions & 0 deletions client-go/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/gen2brain/raylib-go/raylib v0.55.1 h1:1rdc10WvvYjtj7qijHnV9T38/WuvlT6IIL+PaZ6cNA8=
github.com/gen2brain/raylib-go/raylib v0.55.1/go.mod h1:BaY76bZk7nw1/kVOSQObPY1v1iwVE1KHAGMfvI6oK1Q=
github.com/gen2brain/raylib-go/raylib v0.60.0 h1:KsP7W3EMkmb9zztivdgbh1bXR78pEuqP6jeUR8GCbEA=
github.com/gen2brain/raylib-go/raylib v0.60.0/go.mod h1:puAMU7Zcx6VJ6pcZSSs3gGFPyFvJuTwQlfm4KzeoXy8=
github.com/jupiterrider/ffi v0.7.0 h1:RKsl6Ascal+3kyAqR5Qcbp83LceQMLc1VZbPfHWoNzs=
github.com/jupiterrider/ffi v0.7.0/go.mod h1:9dauhpOfNqrqk28fxuu0kkdeFtT9Qr4vbfigiuIXN7c=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
Loading