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>
145 lines
4.0 KiB
Go
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)
|
|
}
|