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>
169 lines
5.1 KiB
Go
169 lines
5.1 KiB
Go
// 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
|
||
}
|