329 lines
8.2 KiB
Go
329 lines
8.2 KiB
Go
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)
|
|
}
|
|
}
|