// Package game innehåller Ebitengine-spelloopen. Detta är det enda paketet // (tillsammans med cmd/) som får känna till Ebitengine; all spellogik ska // ligga i paket som går att testa utan fönster. package game import ( "fmt" "image/color" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/ebitenutil" "github.com/hajimehoshi/ebiten/v2/inpututil" "gitea.brasse-pc.eu/brasse/jp-subway-pussel/internal/buildinfo" "gitea.brasse-pc.eu/brasse/jp-subway-pussel/internal/level" "gitea.brasse-pc.eu/brasse/jp-subway-pussel/internal/res" ) // Intern upplösning. Allt ritas i denna storlek och skalas sedan upp med // heltalsfaktor av Ebitengine så pixlarna förblir skarpa. const ( ScreenW = 320 ScreenH = 180 TileSize = 8 ) // Game är Ebitengine-tillståndet för hela programmet. type Game struct { lvl *level.Level player *ebiten.Image tiles map[level.Tile]*ebiten.Image resDir string smokeLeft int // >0: avsluta automatiskt efter så många uppdateringar (för smoke-test) frames int levelError error } // Options styr uppstarten. type Options struct { LevelID string // bana att visa, t.ex. "001-flygplatsen" SmokeFrames int // >0 avslutar efter N uppdateringar } // New laddar resurser och bygger spelet. func New(opt Options) (*Game, error) { dir, err := res.Dir() if err != nil { return nil, err } g := &Game{resDir: dir, smokeLeft: opt.SmokeFrames} img, err := res.LoadImage("sprites", "placeholder.png") if err != nil { return nil, err } g.player = ebiten.NewImageFromImage(img) var raw []byte if raw, err = res.ReadFile("data", "levels", opt.LevelID+".json"); err == nil { g.lvl, err = level.Parse(raw) } if err != nil { g.levelError = err } g.tiles = map[level.Tile]*ebiten.Image{} for t, c := range map[level.Tile]color.RGBA{ level.Wall: {38, 43, 68, 255}, level.Floor: {139, 155, 180, 255}, level.Spawn: {99, 199, 77, 255}, level.Platform: {254, 174, 52, 255}, level.WrongPlatform: {228, 59, 68, 255}, level.Gate: {44, 232, 245, 255}, level.TicketMachine: {247, 118, 34, 255}, level.Sign: {255, 255, 255, 255}, } { im := ebiten.NewImage(TileSize, TileSize) im.Fill(c) g.tiles[t] = im } return g, nil } // Update körs 60 ggr/s. func (g *Game) Update() error { g.frames++ if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { return ebiten.Termination } if g.smokeLeft > 0 { g.smokeLeft-- if g.smokeLeft == 0 { return ebiten.Termination } } return nil } // Draw ritar aktuell bild. func (g *Game) Draw(screen *ebiten.Image) { screen.Fill(color.RGBA{24, 20, 37, 255}) if g.lvl != nil { // Centrera banan på skärmen (riktiga banor får kamera senare). ox := (ScreenW - g.lvl.Width()*TileSize) / 2 oy := (ScreenH - g.lvl.Height()*TileSize) / 2 var op ebiten.DrawImageOptions for y := 0; y < g.lvl.Height(); y++ { for x := 0; x < g.lvl.Width(); x++ { op.GeoM.Reset() op.GeoM.Translate(float64(ox+x*TileSize), float64(oy+y*TileSize)) screen.DrawImage(g.tiles[g.lvl.At(x, y)], &op) } } s := g.lvl.SpawnPoint() op.GeoM.Reset() op.GeoM.Translate(float64(ox+s.X*TileSize)-4, float64(oy+s.Y*TileSize)-8) screen.DrawImage(g.player, &op) } name := "(ingen bana)" if g.lvl != nil { name = g.lvl.Name.Get("sv") + " -> " + g.lvl.Destination.Get("ja") } else if g.levelError != nil { name = "FEL: " + g.levelError.Error() } ebitenutil.DebugPrint(screen, fmt.Sprintf( "JP Subway Pussel %s (%s) %.0f fps\nBana: %s\nResurser: %s\nESC avslutar", buildinfo.Version, buildinfo.Mode, ebiten.ActualFPS(), name, g.resDir)) } // Layout talar om för Ebitengine vilken intern upplösning vi ritar i. func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { return ScreenW, ScreenH }