Files
metro-puzzle-jp/internal/game/scene_items.go
claude 915c684dc2
All checks were successful
release / build-release (push) Successful in 1m46s
M8: progression, items, bankarta och sparfil
- 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>
2026-10-24 11:29:30 +02:00

145 lines
4.0 KiB
Go

package game
import (
"fmt"
"image/color"
"sort"
"strings"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/vector"
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/progress"
)
// ItemsScene är garderoben: ägda items per plats, Enter bär/tar av.
type ItemsScene struct {
back Scene
items []progress.Item
sel int
top int
msg string
}
const itemRows = 8
// NewItemsScene listar ägda items, plats för plats.
func NewItemsScene(g *Game, back Scene) *ItemsScene {
var owned []progress.Item
for _, it := range g.Catalog.Items {
if g.Progress.Own(it.ID) {
owned = append(owned, it)
}
}
slotOrder := map[string]int{}
for i, s := range progress.Slots {
slotOrder[s] = i
}
sort.SliceStable(owned, func(i, j int) bool {
if owned[i].Slot != owned[j].Slot {
return slotOrder[owned[i].Slot] < slotOrder[owned[j].Slot]
}
return owned[i].Price < owned[j].Price
})
return &ItemsScene{back: back, items: owned}
}
func (s *ItemsScene) Update(g *Game) error {
if backJustPressed() || itemsJustPressed() {
g.SetScene(s.back)
return nil
}
if n := len(s.items); n > 0 {
s.sel = (s.sel + menuMove() + n) % n
if s.sel < s.top {
s.top = s.sel
}
if s.sel >= s.top+itemRows {
s.top = s.sel - itemRows + 1
}
if confirmJustPressed() {
it := s.items[s.sel]
if g.Progress.Equipped[it.Slot] == it.ID {
g.Progress.Unequip(it.Slot)
s.msg = fmt.Sprintf(g.Assets.T("items.removed"), it.Name.Get(UILang))
} else {
g.Progress.Equip(g.Catalog, it.ID)
s.msg = fmt.Sprintf(g.Assets.T("items.worn"), it.Name.Get(UILang))
}
g.Save()
}
}
return nil
}
// effectText beskriver en effekt kort.
func effectText(a *Assets, e progress.Effect) string {
var parts []string
keys := make([]string, 0, len(e.Stats))
for k := range e.Stats {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
parts = append(parts, fmt.Sprintf("%s %+.0f%%", a.T("stat."+k), (e.Stats[k]-1)*100))
}
if e.Kana {
parts = append(parts, a.T("effect.kana"))
}
if e.FogRadius > 0 {
parts = append(parts, fmt.Sprintf(a.T("effect.fog"), e.FogRadius))
}
if e.Landmarks {
parts = append(parts, a.T("effect.landmarks"))
}
if e.FreeGates {
parts = append(parts, a.T("effect.free_gates"))
}
if e.RevealLevel != "" {
parts = append(parts, fmt.Sprintf(a.T("effect.reveal"), e.RevealLevel))
}
if e.Battery {
parts = append(parts, a.T("effect.battery"))
}
return strings.Join(parts, ", ")
}
func (s *ItemsScene) Draw(g *Game, screen *ebiten.Image) {
a := g.Assets
p := g.Progress
screen.Fill(color.RGBA{24, 20, 37, 255})
vector.DrawFilledRect(screen, 0, 0, ScreenW, 22, color.RGBA{38, 43, 68, 255}, false)
a.DrawBig(screen, a.T("items.title"), 8, 5, colText)
hdr := fmt.Sprintf("%d/%d %s %s %d¥", len(s.items), len(g.Catalog.Items), a.T("items.owned"), a.T("hud.cash"), g.Wallet.Cash)
a.Draw(screen, hdr, ScreenW-8-a.Width(hdr), 6, colKnown)
y0 := 30
for i := s.top; i < len(s.items) && i < s.top+itemRows; i++ {
it := s.items[i]
y := y0 + (i-s.top)*LineH
if i == s.sel {
vector.DrawFilledRect(screen, 4, float32(y-1), ScreenW-8, LineH, colBar, false)
}
col, status := colText, ""
if p.Equipped[it.Slot] == it.ID {
col, status = colGood, a.T("items.wearing")
}
a.Draw(screen, a.T("slot."+it.Slot), 8, y, colDim)
a.Draw(screen, it.Name.Get(UILang), 60, y, col)
a.Draw(screen, effectText(a, it.Effect), 150, y, colDim)
a.Draw(screen, status, ScreenW-8-a.Width(status), y, col)
}
if len(s.items) == 0 {
a.Draw(screen, a.T("items.none"), 8, y0, colDim)
}
eff := g.Effects()
sum := fmt.Sprintf("%s: %s %.0f%% %s %.0f%% %s %.0f%% %s %.0f%% %s %.0f%%", a.T("items.total"),
a.T("stat.speed"), 100*eff.Stats[0], a.T("stat.thirst"), 100*eff.Stats[1], a.T("stat.focus"), 100*eff.Stats[2],
a.T("stat.toilet"), 100*eff.Stats[3], a.T("stat.luck"), 100*eff.Stats[4])
a.Draw(screen, sum, 8, 134, colDim)
if s.msg != "" {
a.Draw(screen, s.msg, 8, ScreenH-26, colKnown)
}
a.Draw(screen, a.T("items.hint"), 8, ScreenH-13, colDim)
}