Files
metro-puzzle-jp/internal/game/game.go
claude 9fc7e94074
All checks were successful
release / build-release (push) Successful in 1m41s
M6: plånbok, biljetter, kortet i telefonen och seedade minispel
- sim.Wallet: IC-kort, kontanter, pappersbiljett, telefon med batteri,
  spenderat; följer med mellan banor (Game.Wallet)
- internal/minigame: gemensamt Game-gränssnitt (Update/Done/Success), all
  slump från del-seeds: TopUp (hitta チャージ, belopp, mata sedlar),
  Ticket (taxetavla med resmål på japanska), Crowd (tidsfönster, två
  försök), Ride (stationerna passerar, kliv av vid resmålet)
- sim.Run: spärren kräver biljett på biljettbanor annars användbart kort;
  telefonen ger ×1,1 fart men 15 % seedad omskanning (frysning 1,5 s),
  dött batteri gör kortet oanvändbart; automaten startar påfyllning eller
  biljettköp; tåget kontrollerar biljetten; fulla tåg kräver trängsel;
  resan avgör vinst/förlust (fel station, åkte förbi, fel tåg); StepMini
  låter klockan och sällskapet ticka medan rörelsen pausar
- level: ticketRequired, rideStops, Train.Crowded; 002 är biljettbana med
  fullt tåg och fyra stopp; ordboken har stationer (kind) + sju nya namn
- game: vyer för minispelen, plånbok i HUD, P i menyn = kort i telefonen,
  förlustorsaker och spenderat på resultatet, flaggor -phone/-card,
  skriptkommandon hold/waitstop; inforuta/minikarta döljs under minispel
- plan: M7b (Björn) – tågvind, ankomstindikator nära rälsen,
  in-/utkörningsanimation för tåg och bussar – i spelbeskrivning, roadmap,
  testplan
- docs/README/wiki med tre nya bilder (automat, biljettavla, tågresa)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-24 09:50:12 +02:00

130 lines
3.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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
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
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.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.Know, g.Wallet, 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()
}