// 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 }