All checks were successful
release / build-release (push) Successful in 1m37s
- sim/stats.go: Stats (fart, törst, fokus, toalett, tur), debuffar som multiplikatorer, Profile som följer med mellan banor och räknar ned - sim.Run: tidsgräns och maxantal missade tåg förlorar banan; poängformel (tid mot par, pengar, sällskapets mätare, utforskat, avdrag); debuffar tjänas av många steg, lång tid, tom matmätare och seedad otur vid kiosken; stats påverkar fart, mätartakter, läsning och hjälptid - egna kort (individualCards): Member.Card, spärren kräver allas saldo och drar från alla, automaten frågar vems kort, biljetter köps åt alla - sittplatsval (seatSelection): Ticket får ett sittplatssteg med seedade önskemål (fönster/gång/sida/bredvid); missnöjda ger mätar- och poängavdrag - level: maxMissedTrains, individualCards, seatSelection; bana 003 Shibuya (svårighet 3, fyra i sällskapet) och 002 med tidsgräns och sittplatser; ordboken utökad; generatorn är spec-driven - game: tid kvar/missade i HUD, debuffar i meny/HUD/resultat, poäng och förlustorsaker på resultatet, vyer för personval och vagn, skript-pick, flaggan -debuffs; HUD och resultat komprimerade för fyra i sällskapet - plan (Björn 2026-10-24): flera våningar med trappor som M9a, egna kort och sittplatsval som svårighetsinställningar i M7 – i spelbeskrivning, roadmap, testplan - docs/README/wiki med sittplatsbilden Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
145 lines
4.4 KiB
Go
145 lines
4.4 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
|
||
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)
|
||
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
|
||
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()
|
||
}
|