// Package game innehåller Ebitengine-spelloopen och scenerna. Detta är det // enda paketet (tillsammans med cmd/) som får känna till Ebitengine; all // spellogik ligger i paket som går att testa utan fönster (internal/sim, // internal/level …). package game import ( "github.com/hajimehoshi/ebiten/v2" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/lang" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/rng" ) // 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 = 16 dt = 1.0 / 60 // Ebitengine kör Update med fast 60 TPS ) // Scene är ett spelläge: meny, bana, resultat … type Scene interface { Update(g *Game) error Draw(g *Game, screen *ebiten.Image) } // Game är Ebitengine-tillståndet för hela programmet. type Game struct { Assets *Assets Know *lang.Knowledge // spelarens japanska – följer med mellan banor (sparas i M8) fixedSeed uint64 // >0: alla banor startar med denna seed (tester/skärmdumpar) scene Scene next Scene smokeLeft int // >0: avsluta automatiskt efter så många uppdateringar script *script frame *ebiten.Image // allt ritas hit och blittas sedan till skärmen (gör skärmdumpar möjliga) } // Options styr uppstarten. type Options struct { LevelID string // hoppa direkt in i en bana (tom = meny) SmokeFrames int // >0 avslutar efter N uppdateringar (smoke-test) Script string // skriptade steg för skärmdumpar/tester, se script.go KnownWords []string // ord spelaren kan från start (för tester/skärmdumpar) Points int // språkpoäng från start (för tester/skärmdumpar) Seed uint64 // >0: fast seed för banorna (för tester/skärmdumpar) } // New laddar resurser och bygger spelet. func New(opt Options) (*Game, error) { a, err := LoadAssets() if err != nil { return nil, err } g := &Game{Assets: a, Know: lang.NewKnowledge(), smokeLeft: opt.SmokeFrames, frame: ebiten.NewImage(ScreenW, ScreenH)} for _, w := range opt.KnownWords { g.Know.Learn(w) } g.Know.Points = opt.Points g.fixedSeed = opt.Seed if opt.Script != "" { if g.script, err = parseScript(opt.Script); err != nil { return nil, err } } if opt.LevelID != "" { s, err := NewLevelScene(a, opt.LevelID, g.Know, g.seed()) if err != nil { return nil, err } g.scene = s } else { g.scene, err = NewMenuScene(a) if err != nil { return nil, err } } return g, nil } // SetScene byter scen från och med nästa uppdatering. func (g *Game) SetScene(s Scene) { g.next = s } // Update körs 60 ggr/s. func (g *Game) Update() error { if g.smokeLeft > 0 { g.smokeLeft-- if g.smokeLeft == 0 { return ebiten.Termination } } if g.next != nil { g.scene, g.next = g.next, nil } if g.script != nil { if err := g.script.tick(g, g.frame); err != nil { return err } } return g.scene.Update(g) } // Draw ritar aktuell scen till den interna bilden och sedan till skärmen. func (g *Game) Draw(screen *ebiten.Image) { g.scene.Draw(g, g.frame) screen.DrawImage(g.frame, nil) } // 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 } // seed ger seeden för en ny banomgång: fast om -seed angetts, annars färsk. func (g *Game) seed() rng.Seed { if g.fixedSeed > 0 { return rng.FromInt(g.fixedSeed) } return rng.NewSeed() }