Introduce core components for "Escape From Teacher" game: server, client, physics, asset system, and protocol definitions. Add Docker-Compose setup for Redis and NATS infrastructure.
This commit is contained in:
328
cmd/client/main.go
Normal file
328
cmd/client/main.go
Normal file
@@ -0,0 +1,328 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"image/color"
|
||||
_ "image/png"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||
"github.com/hajimehoshi/ebiten/v2/text"
|
||||
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||
"github.com/nats-io/nats.go"
|
||||
"golang.org/x/image/font/basicfont"
|
||||
|
||||
"git.zb-server.de/ZB-Server/EscapeFromTeacher/pkg/game"
|
||||
)
|
||||
|
||||
// --- KONFIGURATION ---
|
||||
const (
|
||||
ScreenWidth = 1280
|
||||
ScreenHeight = 720
|
||||
StateMenu = 0
|
||||
StateGame = 1
|
||||
RefFloorY = 540
|
||||
)
|
||||
|
||||
var (
|
||||
ColText = color.White
|
||||
ColBtnNormal = color.RGBA{40, 44, 52, 255}
|
||||
ColBtnHover = color.RGBA{60, 66, 78, 255}
|
||||
ColSky = color.RGBA{135, 206, 235, 255}
|
||||
ColGrass = color.RGBA{34, 139, 34, 255}
|
||||
ColDirt = color.RGBA{101, 67, 33, 255}
|
||||
)
|
||||
|
||||
// --- GAME STRUCT ---
|
||||
type Game struct {
|
||||
appState int
|
||||
conn *nats.EncodedConn
|
||||
gameState game.GameState
|
||||
stateMutex sync.Mutex
|
||||
connected bool
|
||||
world *game.World
|
||||
assetsImages map[string]*ebiten.Image
|
||||
|
||||
// Spieler Info
|
||||
playerName string
|
||||
roomID string // <-- NEU: Raum ID
|
||||
activeField string // "name" oder "room"
|
||||
|
||||
// Kamera
|
||||
camX float64
|
||||
|
||||
// Touch State
|
||||
joyBaseX, joyBaseY float64
|
||||
joyStickX, joyStickY float64
|
||||
joyActive bool
|
||||
joyTouchID ebiten.TouchID
|
||||
btnJumpActive bool
|
||||
}
|
||||
|
||||
func NewGame() *Game {
|
||||
g := &Game{
|
||||
appState: StateMenu,
|
||||
world: game.NewWorld(),
|
||||
assetsImages: make(map[string]*ebiten.Image),
|
||||
gameState: game.GameState{Players: make(map[string]game.PlayerState)},
|
||||
|
||||
playerName: "Student",
|
||||
roomID: "room1", // Standard Raum
|
||||
activeField: "name",
|
||||
|
||||
joyBaseX: 150, joyBaseY: ScreenHeight - 150,
|
||||
joyStickX: 150, joyStickY: ScreenHeight - 150,
|
||||
}
|
||||
g.loadAssets()
|
||||
return g
|
||||
}
|
||||
|
||||
func (g *Game) loadAssets() {
|
||||
// Pfad anpassen: Wir suchen im relativen Pfad
|
||||
baseDir := "./cmd/client/assets"
|
||||
manifestPath := filepath.Join(baseDir, "assets.json")
|
||||
|
||||
data, err := ioutil.ReadFile(manifestPath)
|
||||
if err == nil {
|
||||
var m game.AssetManifest
|
||||
json.Unmarshal(data, &m)
|
||||
g.world.Manifest = m
|
||||
fmt.Println("✅ Assets Manifest geladen:", len(m.Assets), "Einträge")
|
||||
} else {
|
||||
log.Println("⚠️ assets.json NICHT gefunden! Pfad:", manifestPath)
|
||||
// Fallback: Leeres Manifest, damit das Spiel nicht abstürzt
|
||||
g.world.Manifest = game.AssetManifest{Assets: make(map[string]game.AssetDefinition)}
|
||||
}
|
||||
|
||||
// Bilder vorladen
|
||||
for id, def := range g.world.Manifest.Assets {
|
||||
if def.Filename != "" {
|
||||
path := filepath.Join(baseDir, def.Filename)
|
||||
img, _, err := ebitenutil.NewImageFromFile(path)
|
||||
if err == nil {
|
||||
g.assetsImages[id] = img
|
||||
} else {
|
||||
// log.Println("Fehler beim Laden von Bild:", def.Filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- UPDATE ---
|
||||
func (g *Game) Update() error {
|
||||
switch g.appState {
|
||||
case StateMenu:
|
||||
g.handleMenuInput() // Text Eingabe Logik
|
||||
|
||||
// Button & Felder Layout
|
||||
btnW, btnH := 200, 50
|
||||
btnX := ScreenWidth/2 - btnW/2
|
||||
btnY := ScreenHeight/2 + 80
|
||||
|
||||
// Feld 1: Name
|
||||
fieldW, fieldH := 250, 40
|
||||
nameX := ScreenWidth/2 - fieldW/2
|
||||
nameY := ScreenHeight/2 - 100
|
||||
|
||||
// Feld 2: Raum (NEU)
|
||||
roomX := ScreenWidth/2 - fieldW/2
|
||||
roomY := ScreenHeight/2 - 20
|
||||
|
||||
// Klick Checks (Maus & Touch)
|
||||
if isHit(nameX, nameY, fieldW, fieldH) {
|
||||
g.activeField = "name"
|
||||
} else if isHit(roomX, roomY, fieldW, fieldH) {
|
||||
g.activeField = "room"
|
||||
} else if isHit(btnX, btnY, btnW, btnH) {
|
||||
// START
|
||||
if g.playerName == "" {
|
||||
g.playerName = "Player"
|
||||
}
|
||||
if g.roomID == "" {
|
||||
g.roomID = "room1"
|
||||
}
|
||||
g.appState = StateGame
|
||||
go g.connectAndStart()
|
||||
} else if isHit(0, 0, ScreenWidth, ScreenHeight) {
|
||||
// Klick ins Leere -> Fokus weg
|
||||
g.activeField = ""
|
||||
}
|
||||
|
||||
case StateGame:
|
||||
g.UpdateGame() // In game_render.go
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- DRAW ---
|
||||
func (g *Game) Draw(screen *ebiten.Image) {
|
||||
switch g.appState {
|
||||
case StateMenu:
|
||||
g.DrawMenu(screen)
|
||||
case StateGame:
|
||||
g.DrawGame(screen) // In game_render.go
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Game) DrawMenu(screen *ebiten.Image) {
|
||||
screen.Fill(color.RGBA{20, 20, 30, 255})
|
||||
text.Draw(screen, "ESCAPE FROM TEACHER", basicfont.Face7x13, ScreenWidth/2-60, ScreenHeight/2-140, ColText)
|
||||
|
||||
// Helper zum Zeichnen von Textfeldern
|
||||
drawField := func(label, value, fieldID string, x, y, w, h int) {
|
||||
col := color.RGBA{50, 50, 60, 255}
|
||||
if g.activeField == fieldID {
|
||||
col = color.RGBA{70, 70, 80, 255}
|
||||
}
|
||||
vector.DrawFilledRect(screen, float32(x), float32(y), float32(w), float32(h), col, false)
|
||||
vector.StrokeRect(screen, float32(x), float32(y), float32(w), float32(h), 1, color.White, false)
|
||||
|
||||
display := value
|
||||
if g.activeField == fieldID && (time.Now().UnixMilli()/500)%2 == 0 {
|
||||
display += "|"
|
||||
}
|
||||
text.Draw(screen, label+": "+display, basicfont.Face7x13, x+10, y+25, ColText)
|
||||
}
|
||||
|
||||
fieldW := 250
|
||||
drawField("Name", g.playerName, "name", ScreenWidth/2-fieldW/2, ScreenHeight/2-100, fieldW, 40)
|
||||
drawField("Room Code", g.roomID, "room", ScreenWidth/2-fieldW/2, ScreenHeight/2-20, fieldW, 40)
|
||||
|
||||
// Start Button
|
||||
btnW, btnH := 200, 50
|
||||
btnX := ScreenWidth/2 - btnW/2
|
||||
btnY := ScreenHeight/2 + 80
|
||||
vector.DrawFilledRect(screen, float32(btnX), float32(btnY), float32(btnW), float32(btnH), ColBtnNormal, false)
|
||||
vector.StrokeRect(screen, float32(btnX), float32(btnY), float32(btnW), float32(btnH), 2, color.White, false)
|
||||
text.Draw(screen, "JOIN GAME", basicfont.Face7x13, btnX+65, btnY+30, ColText)
|
||||
|
||||
text.Draw(screen, "WASD / Arrows to Move - SPACE to Jump\nMobile: Touch Controls", basicfont.Face7x13, ScreenWidth/2-120, ScreenHeight-50, color.Gray{150})
|
||||
}
|
||||
|
||||
func (g *Game) Layout(w, h int) (int, int) { return ScreenWidth, ScreenHeight }
|
||||
|
||||
// --- HELPER ---
|
||||
|
||||
func isHit(x, y, w, h int) bool {
|
||||
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
|
||||
mx, my := ebiten.CursorPosition()
|
||||
if mx >= x && mx <= x+w && my >= y && my <= y+h {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, id := range inpututil.JustPressedTouchIDs() {
|
||||
tx, ty := ebiten.TouchPosition(id)
|
||||
if tx >= x && tx <= x+w && ty >= y && ty <= y+h {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (g *Game) handleMenuInput() {
|
||||
if g.activeField == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Text Eingabe
|
||||
var target *string
|
||||
if g.activeField == "name" {
|
||||
target = &g.playerName
|
||||
}
|
||||
if g.activeField == "room" {
|
||||
target = &g.roomID
|
||||
}
|
||||
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyEnter) {
|
||||
g.activeField = ""
|
||||
} else if inpututil.IsKeyJustPressed(ebiten.KeyBackspace) {
|
||||
if len(*target) > 0 {
|
||||
*target = (*target)[:len(*target)-1]
|
||||
}
|
||||
} else {
|
||||
*target += string(ebiten.InputChars())
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Game) connectAndStart() {
|
||||
// URL: Wasm -> WS, Desktop -> TCP
|
||||
serverURL := "nats://localhost:4222"
|
||||
if runtime.GOARCH == "wasm" || runtime.GOOS == "js" {
|
||||
serverURL = "ws://localhost:9222"
|
||||
}
|
||||
|
||||
nc, err := nats.Connect(serverURL)
|
||||
if err != nil {
|
||||
log.Println("❌ NATS Connect Fehler:", err)
|
||||
return
|
||||
}
|
||||
ec, _ := nats.NewEncodedConn(nc, nats.JSON_ENCODER)
|
||||
g.conn = ec
|
||||
|
||||
sub, err := g.conn.Subscribe("game.update", func(state *game.GameState) {
|
||||
g.stateMutex.Lock()
|
||||
g.gameState = *state
|
||||
g.stateMutex.Unlock()
|
||||
log.Printf("📦 State empfangen: Players=%d, Chunks=%d, Status=%s", len(state.Players), len(state.WorldChunks), state.Status)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Println("❌ Fehler beim Subscribe:", err)
|
||||
return
|
||||
}
|
||||
log.Printf("👂 Subscription aktiv (Valid: %v)", sub.IsValid())
|
||||
|
||||
// Kurze Pause, damit Subscription aktiv ist
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// JOIN MIT ROOM ID SENDEN
|
||||
joinReq := game.JoinRequest{
|
||||
Name: g.playerName,
|
||||
RoomID: g.roomID,
|
||||
}
|
||||
log.Printf("📤 Sende JOIN Request: Name=%s, RoomID=%s", g.playerName, g.roomID)
|
||||
err = g.conn.Publish("game.join", joinReq)
|
||||
if err != nil {
|
||||
log.Println("❌ Fehler beim Publish:", err)
|
||||
return
|
||||
}
|
||||
g.connected = true
|
||||
log.Printf("✅ JOIN gesendet. Warte auf Server-Antwort...")
|
||||
}
|
||||
|
||||
func (g *Game) SendCommand(cmdType string) {
|
||||
if !g.connected {
|
||||
return
|
||||
}
|
||||
// ID Suche (Fallback Name)
|
||||
myID := ""
|
||||
g.stateMutex.Lock()
|
||||
for id, p := range g.gameState.Players {
|
||||
if p.Name == g.playerName {
|
||||
myID = id
|
||||
break
|
||||
}
|
||||
}
|
||||
g.stateMutex.Unlock()
|
||||
if myID == "" {
|
||||
myID = g.playerName
|
||||
}
|
||||
|
||||
g.conn.Publish("game.input", game.ClientInput{PlayerID: myID, Type: cmdType})
|
||||
}
|
||||
|
||||
func main() {
|
||||
ebiten.SetWindowSize(ScreenWidth, ScreenHeight)
|
||||
ebiten.SetWindowTitle("Escape From Teacher")
|
||||
if err := ebiten.RunGame(NewGame()); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user