All checks were successful
release / build-release (push) Successful in 1m42s
- level.Schedule.Phase: away/approaching/arriving/dwelling/closing/leaving ur tidtabellen med progress; Boardable (öppna dörrar) och Present; validering av minsta uppehåll/borta-tid; TrackTile för rälsen - sim.Run: ombordstigning bara med öppna dörrar, TrainVisible via Present, PlatformPhase och NearTrack (närmaste perrong inom två rutor) - game/trains.go: rälsrutor längs kantraden (roterade för vertikala spår), tre vagnar som bromsar in och accelererar ut längs rälsen, strålkastare under inrullningen, blinkande lampor vid kanten för den som står nära, dammpartiklar längs perrongen; kameran får en rutas marginal så att kanträlsen syns - game/audio.go: ebiten/v2/audio med avgångsmelodi (approaching) och dörrsignal (closing), volym efter avstånd, -mute; tools/gensound syntetiserar placeholder-WAV:ar - HUD: fasberoende perrongtext och "tåg på väg in" nära rälsen; skriptkommandot waitphase; skärmdump av ankomsten - plan (Björn 2026-10-24): räls/väg-grafik och melodierna i M7b; folk/NPC:er, vänster-/högertrafik, bredare gångar, reklam- och ljudröra som nytt steg M9b – i spelbeskrivning, roadmap, testplan - docs/README/wiki uppdaterade Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
150 lines
4.5 KiB
Go
150 lines
4.5 KiB
Go
// 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"
|
||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/sim"
|
||
)
|
||
|
||
// 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)
|
||
Wallet *sim.Wallet // pengar, kort, telefon, biljett – följer med mellan banor
|
||
Profile *sim.Profile // aktiva debuffar – följer med mellan banor
|
||
Audio *Audio // nil = tyst
|
||
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)
|
||
Phone bool // kortet i telefonen från start
|
||
Debuffs []string // debuffar från start (för tester/skärmdumpar)
|
||
Mute bool // inget ljud
|
||
CardYen int // >=0: kortets startsaldo (för tester/skärmdumpar), -1 = standard
|
||
}
|
||
|
||
// 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 g.Audio, err = newAudio(opt.Mute); err != nil {
|
||
return nil, err
|
||
}
|
||
g.Wallet = sim.NewWallet()
|
||
g.Profile = &sim.Profile{}
|
||
for _, d := range opt.Debuffs {
|
||
g.Profile.Add(sim.DebuffKind(d), 1)
|
||
}
|
||
g.Wallet.Phone = opt.Phone
|
||
if opt.CardYen >= 0 {
|
||
g.Wallet.Card = opt.CardYen
|
||
}
|
||
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, 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()
|
||
}
|
||
|
||
// levelEnded appliceras exakt en gång när en bana avgjorts: banans nya
|
||
// debuffar läggs på profilen och alla räknas ned en bana.
|
||
func (g *Game) levelEnded(run *sim.Run) {
|
||
for _, d := range run.NewDebuffs {
|
||
g.Profile.Add(d.Kind, d.LevelsLeft)
|
||
}
|
||
g.Profile.EndLevel()
|
||
}
|