Files
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

169 lines
5.1 KiB
Go
Raw Permalink 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 progress är spelarens progression: XP och nivåer, skill tree,
// items med buffar, klarade banor och upplåsning av bankartan. Ren Go;
// katalogerna (skills.json, items.json) läses från resources/data.
package progress
import (
"encoding/json"
"fmt"
"sort"
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/sim"
)
// Effect är vad en skill eller ett item gör. Allt är valfritt.
type Effect struct {
Stats map[string]float64 `json:"stats"` // speed/thirst/focus/toilet/luck → multiplikator
Kana bool `json:"kana"` // kan läsa kana
FogRadius int `json:"fogRadius"` // extra siktsteg i gångarna
Landmarks bool `json:"landmarks"` // toaletter/kiosker/automater syns alltid på minikartan
FreeGates bool `json:"freeGates"` // spärrarna kostar inget (genväg)
RevealLevel string `json:"revealLevel"` // hela minikartan känd på banan
Battery bool `json:"battery"` // telefonen laddar inte ur
}
// Skill är en nod i språk-/skillträdet.
type Skill struct {
ID string `json:"id"`
Branch string `json:"branch"`
Name level.Text `json:"name"`
Desc level.Text `json:"desc"`
Cost int `json:"cost"`
Requires []string `json:"requires"`
Effect Effect `json:"effect"`
}
// Item är kläder, masker, hattar, prylar och skins.
type Item struct {
ID string `json:"id"`
Kind string `json:"kind"`
Slot string `json:"slot"`
Name level.Text `json:"name"`
Price int `json:"price"` // 0 = bara belöning
Effect Effect `json:"effect"`
Tint []float64 `json:"tint"` // färgton på spelaren (r, g, b) när det bärs
}
// Slots är kroppsplatserna; ett item per plats bärs.
var Slots = []string{"head", "face", "body", "feet", "bag"}
var statIndex = map[string]int{"speed": sim.StatSpeed, "thirst": sim.StatThirst, "focus": sim.StatFocus, "toilet": sim.StatToilet, "luck": sim.StatLuck}
// Catalog är alla skills och items.
type Catalog struct {
Skills []Skill
Items []Item
skills map[string]*Skill
items map[string]*Item
}
// LoadCatalog läser och validerar skills.json och items.json.
func LoadCatalog(skillsJSON, itemsJSON []byte) (*Catalog, error) {
var sk struct {
Skills []Skill `json:"skills"`
}
if err := json.Unmarshal(skillsJSON, &sk); err != nil {
return nil, fmt.Errorf("skills.json: %w", err)
}
var it struct {
Items []Item `json:"items"`
}
if err := json.Unmarshal(itemsJSON, &it); err != nil {
return nil, fmt.Errorf("items.json: %w", err)
}
c := &Catalog{Skills: sk.Skills, Items: it.Items, skills: map[string]*Skill{}, items: map[string]*Item{}}
for i := range c.Skills {
s := &c.Skills[i]
if s.ID == "" || s.Cost < 1 || s.Name["sv"] == "" || s.Name["en"] == "" {
return nil, fmt.Errorf("skill %q: saknar id/cost/namn", s.ID)
}
if _, dup := c.skills[s.ID]; dup {
return nil, fmt.Errorf("skill %q: dubblett", s.ID)
}
if err := s.Effect.validate(); err != nil {
return nil, fmt.Errorf("skill %q: %w", s.ID, err)
}
c.skills[s.ID] = s
}
for _, s := range c.Skills {
for _, r := range s.Requires {
if _, ok := c.skills[r]; !ok {
return nil, fmt.Errorf("skill %q kräver okänd skill %q", s.ID, r)
}
}
}
for i := range c.Items {
it := &c.Items[i]
if it.ID == "" || it.Name["sv"] == "" || it.Name["en"] == "" {
return nil, fmt.Errorf("item %q: saknar id/namn", it.ID)
}
if _, dup := c.items[it.ID]; dup {
return nil, fmt.Errorf("item %q: dubblett", it.ID)
}
if !validSlot(it.Slot) {
return nil, fmt.Errorf("item %q: okänd slot %q", it.ID, it.Slot)
}
if it.Price < 0 || (len(it.Tint) != 0 && len(it.Tint) != 3) {
return nil, fmt.Errorf("item %q: pris/tint", it.ID)
}
if err := it.Effect.validate(); err != nil {
return nil, fmt.Errorf("item %q: %w", it.ID, err)
}
c.items[it.ID] = it
}
return c, nil
}
func (e Effect) validate() error {
for k, v := range e.Stats {
if _, ok := statIndex[k]; !ok {
return fmt.Errorf("okänd stat %q", k)
}
if v <= 0 || v > 3 {
return fmt.Errorf("stat %s=%v utanför 0..3", k, v)
}
}
return nil
}
func validSlot(s string) bool {
for _, x := range Slots {
if x == s {
return true
}
}
return false
}
// Skill slår upp en skill (nil om okänd).
func (c *Catalog) Skill(id string) *Skill { return c.skills[id] }
// Item slår upp ett item (nil om okänt).
func (c *Catalog) Item(id string) *Item { return c.items[id] }
// Purchasable är alla items med pris, billigast först.
func (c *Catalog) Purchasable() []Item {
var out []Item
for _, it := range c.Items {
if it.Price > 0 {
out = append(out, it)
}
}
sort.Slice(out, func(i, j int) bool { return out[i].Price < out[j].Price })
return out
}
// ThirdCost är vad den billigaste tredjedelen av alla köpbara items kostar
// tillsammans – designregeln säger att det ska vara mer än en omgångs
// startkassa, så att man knappt får tag på 33 % under en omgång.
func (c *Catalog) ThirdCost() int {
p := c.Purchasable()
n := (len(p) + 2) / 3
sum := 0
for _, it := range p[:n] {
sum += it.Price
}
return sum
}