M8: progression, items, bankarta och sparfil
All checks were successful
release / build-release (push) Successful in 1m46s

- internal/progress: katalog (skills.json 12 noder, items.json 25 items)
  med validering; XP/nivåer (150·n(n+1)/2) och skillpoäng; skillträd med
  krav och kostnad; items per plats med buffar, genvägar (JR Pass, kartor,
  powerbank) och färgton; banresultat och upplåsning (huvudbanor i ordning,
  sidospår efter föräldern); 33 %-regeln som test
- internal/save: versionerad JSON med kunskap, plånbok, debuffar,
  progression och pågående bana + seed; atomisk skrivning; migrering;
  ~/.config/metro-puzzle-jp/save.json resp. %AppData%
- sim: RunOptions får Extra/FogRadius/Landmarks/FreeGates/RevealAll/
  InfiniteBattery; luck påverkar trängselzonen; Fog.RevealAll och extra
  landmärken
- game: bankarta i spelbrädesstil ersätter menyn (⟳ återupptar med sparad
  seed), skillträd (T), garderob (I), butik i kiosken (B), XP/nivå/items/
  upplåsningar på resultatet, spelaren tonas av skins; autospar vid banstart,
  efter banan och vid köp; flaggor -save/-nosave
- level: kind/order/parent/rewards/shop; ny sidobana 002b Kabukicho som
  belönar skinnet Kimono; 001 belönar Salaryman-skinnet; butikssortiment
  på 002/003
- tools/shots: demo-sparfil (build/, aldrig den riktiga), bilder av karta,
  skills, garderob och butik; alla körningar med -nosave
- docs/README/wiki uppdaterade

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 11:29:30 +02:00
parent d466333f01
commit 915c684dc2
46 changed files with 3058 additions and 316 deletions

104
internal/game/shop.go Normal file
View File

@@ -0,0 +1,104 @@
package game
import (
"fmt"
"image"
"image/color"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/vector"
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/progress"
)
// Butiken i kiosken: banans sortiment (eller ett seedat urval), köps kontant.
type shop struct {
items []progress.Item
cursor int
msg string
}
const shopSize = 5
func (s *LevelScene) openShop(g *Game) {
sh := &shop{}
ids := s.run.Level.Shop
if len(ids) == 0 {
// Seedat urval ur allt köpbart, så att samma omgång ger samma butik.
all := g.Catalog.Purchasable()
r := s.run.Seed.Derive("shop").New()
r.Shuffle(len(all), func(i, j int) { all[i], all[j] = all[j], all[i] })
for _, it := range all {
ids = append(ids, it.ID)
if len(ids) >= shopSize {
break
}
}
}
for _, id := range ids {
if it := g.Catalog.Item(id); it != nil {
sh.items = append(sh.items, *it)
}
}
s.shop = sh
}
func (s *LevelScene) updateShop(g *Game) {
sh := s.shop
if backJustPressed() || shopJustPressed() {
s.shop = nil
return
}
if n := len(sh.items); n > 0 {
sh.cursor = (sh.cursor + menuMove() + n) % n
if confirmJustPressed() {
it := sh.items[sh.cursor]
switch {
case g.Progress.Own(it.ID):
sh.msg = g.Assets.T("shop.owned")
case g.Progress.Buy(g.Catalog, it.ID, g.Wallet.PayCash):
sh.msg = fmt.Sprintf(g.Assets.T("shop.bought"), it.Name.Get(UILang))
g.Save()
default:
sh.msg = g.Assets.T("shop.too_expensive")
}
}
}
}
func (s *LevelScene) drawShop(g *Game, screen *ebiten.Image) {
a := g.Assets
sh := s.shop
p := image.Rect(12, 28, ScreenW-12, ScreenH-10)
vector.DrawFilledRect(screen, float32(p.Min.X), float32(p.Min.Y), float32(p.Dx()), float32(p.Dy()), colPanel, false)
vector.DrawFilledRect(screen, float32(p.Min.X), float32(p.Min.Y), float32(p.Dx()), 3, colKnown, false)
x, y := p.Min.X+8, p.Min.Y+8
a.DrawBig(screen, "売店", x, y, colKnown)
a.Draw(screen, fmt.Sprintf("%s %s %d¥", a.T("shop.title"), a.T("hud.cash"), g.Wallet.Cash), x+36, y+2, colText)
y += 24
for i, it := range sh.items {
ry := y + i*LineH
if i == sh.cursor {
vector.DrawFilledRect(screen, float32(x-2), float32(ry-1), float32(p.Dx()-12), LineH, colBar, false)
}
col := colText
if g.Progress.Own(it.ID) {
col = colGood
} else if it.Price > g.Wallet.Cash {
col = colDim
}
a.Draw(screen, it.Name.Get(UILang), x, ry, col)
a.Draw(screen, effectText(a, it.Effect), x+110, ry, colDim)
price := fmt.Sprintf("%d円", it.Price)
if g.Progress.Own(it.ID) {
price = a.T("items.owned_short")
}
a.Draw(screen, price, p.Max.X-10-a.Width(price), ry, col)
}
if sh.msg != "" {
a.Draw(screen, sh.msg, x, p.Max.Y-28, colKnown)
}
a.Draw(screen, a.T("shop.hint"), x, p.Max.Y-16, colDim)
_ = color.RGBA{}
}