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) } }