M6: plånbok, biljetter, kortet i telefonen och seedade minispel
All checks were successful
release / build-release (push) Successful in 1m41s

- 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>
This commit is contained in:
2026-10-24 09:50:12 +02:00
parent 3f56020777
commit 9fc7e94074
45 changed files with 1779 additions and 166 deletions

107
internal/minigame/topup.go Normal file
View File

@@ -0,0 +1,107 @@
package minigame
import "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/rng"
// TopUp är påfyllningsautomaten: hitta チャージ-knappen bland japanska
// knappar i seedad ordning, välj belopp, mata in sedlar genom att hålla
// Enter. Fel knapp kostar en straffsekund eller två.
type TopUp struct {
base
Stage TopUpStage
Buttons []string // knapparnas text, seedad ordning
Cursor int
Amounts []int
Mistakes int
Penalty float64 // sekunder kvar av frysning efter fel knapp
Fill float64 // 0..1 sedelinmatning
Amount int // valt belopp (resultat)
MaxCash int // vad spelaren har råd med
}
// TopUpStage är var i flödet man är.
type TopUpStage int
// Steg.
const (
TopUpPickButton TopUpStage = iota
TopUpPickAmount
TopUpFeed
)
// Knapparna på en japansk biljettautomat. チャージ är den rätta.
var topUpButtons = []string{"チャージ", "きっぷ", "定期券", "取消"}
// TopUpAmounts är beloppen man kan välja.
var TopUpAmounts = []int{1000, 2000, 3000, 5000}
const (
topUpPenaltySec = 1.5
topUpFeedPerSec = 0.8 // hålla Enter: fyller på 1/0.8 s
topUpDrainPerSec = 1.2
)
// NewTopUp skapar automaten. maxCash begränsar beloppen.
func NewTopUp(seed rng.Seed, maxCash int) *TopUp {
r := newRand(seed, "topup")
g := &TopUp{Buttons: append([]string(nil), topUpButtons...), MaxCash: maxCash}
r.shuffle(len(g.Buttons), func(i, j int) { g.Buttons[i], g.Buttons[j] = g.Buttons[j], g.Buttons[i] })
for _, a := range TopUpAmounts {
if a <= maxCash {
g.Amounts = append(g.Amounts, a)
}
}
if len(g.Amounts) == 0 {
g.done = true // inga pengar: inget att göra
}
return g
}
// Update för automaten framåt.
func (g *TopUp) Update(in Input, dt float64) {
if g.done {
return
}
if in.Cancel {
g.done = true
return
}
if g.Penalty > 0 {
g.Penalty -= dt
return
}
switch g.Stage {
case TopUpPickButton:
g.Cursor = wrap(g.Cursor+in.Move, len(g.Buttons))
if in.Confirm {
if g.Buttons[g.Cursor] == topUpButtons[0] {
g.Stage, g.Cursor = TopUpPickAmount, 0
} else {
g.Mistakes++
g.Penalty = topUpPenaltySec
}
}
case TopUpPickAmount:
g.Cursor = wrap(g.Cursor+in.Move, len(g.Amounts))
if in.Confirm {
g.Amount = g.Amounts[g.Cursor]
g.Stage = TopUpFeed
}
case TopUpFeed:
if in.Hold {
g.Fill += topUpFeedPerSec * dt
} else {
g.Fill = max(0, g.Fill-topUpDrainPerSec*dt)
}
if g.Fill >= 1 {
g.Fill = 1
g.done, g.success = true, true
}
}
}
func wrap(i, n int) int {
if n == 0 {
return 0
}
return ((i % n) + n) % n
}