package progress import ( "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/sim" ) // XP: nivå n+1 nås när man samlat 150·n(n+1)/2 XP (150, 450, 900, …). // Varje level-up ger en skillpoäng. const XPPerLevelStep = 150 // LevelResult är bästa resultatet på en bana. type LevelResult struct { Cleared bool `json:"cleared"` Stars int `json:"stars"` Score int `json:"score"` Attempts int `json:"attempts"` } // Progress är det som växer mellan banor. type Progress struct { XP int `json:"xp"` SkillPoints int `json:"skillPoints"` Skills map[string]bool `json:"skills"` Items map[string]bool `json:"items"` Equipped map[string]string `json:"equipped"` // slot -> item-id Levels map[string]*LevelResult `json:"levels"` StoryCleared bool `json:"storyCleared"` // sista huvudbanan klarad: slutet har visats, oändligt läge/NG+ (M11) låses upp } // New börjar från noll. func New() *Progress { return &Progress{Skills: map[string]bool{}, Items: map[string]bool{}, Equipped: map[string]string{}, Levels: map[string]*LevelResult{}} } func (p *Progress) ensure() { if p.Skills == nil { p.Skills = map[string]bool{} } if p.Items == nil { p.Items = map[string]bool{} } if p.Equipped == nil { p.Equipped = map[string]string{} } if p.Levels == nil { p.Levels = map[string]*LevelResult{} } } // LevelFor är nivån (från 1) för en XP-summa. func LevelFor(xp int) int { n := 0 for XPPerLevelStep*(n+1)*(n+2)/2 <= xp { n++ } return n + 1 } // XPToNext är hur mycket som saknas till nästa nivå. func XPToNext(xp int) int { n := LevelFor(xp) // nästa nivå är n+1, som kräver 150·n(n+1)/2 return XPPerLevelStep*n*(n+1)/2 - xp } // XPFor ger XP för en klarad bana. func XPFor(score, stars int) int { return score/10 + 25*stars } // AddXP lägger till XP och ger en skillpoäng per level-up. Returnerar antalet level-ups. func (p *Progress) AddXP(n int) int { before := LevelFor(p.XP) p.XP += max(0, n) ups := LevelFor(p.XP) - before p.SkillPoints += ups return ups } // Level är spelarens nivå. func (p *Progress) Level() int { return LevelFor(p.XP) } // Has rapporterar om skillen är upplåst. func (p *Progress) Has(skill string) bool { return p.Skills[skill] } // CanUnlock: har poängen, kraven uppfyllda och inte redan upplåst. func (p *Progress) CanUnlock(c *Catalog, id string) bool { s := c.Skill(id) if s == nil || p.Has(id) || p.SkillPoints < s.Cost { return false } for _, r := range s.Requires { if !p.Has(r) { return false } } return true } // Unlock låser upp en skill mot skillpoäng. func (p *Progress) Unlock(c *Catalog, id string) bool { if !p.CanUnlock(c, id) { return false } p.ensure() p.SkillPoints -= c.Skill(id).Cost p.Skills[id] = true return true } // Own rapporterar om spelaren har itemet. func (p *Progress) Own(item string) bool { return p.Items[item] } // Give ger ett item (belöning). Bärs direkt om platsen är ledig. Returnerar // false om spelaren redan hade det. func (p *Progress) Give(c *Catalog, id string) bool { it := c.Item(id) if it == nil || p.Own(id) { return false } p.ensure() p.Items[id] = true if p.Equipped[it.Slot] == "" { p.Equipped[it.Slot] = id } return true } // Buy köper ett item kontant via pay. Returnerar false om det inte gick. func (p *Progress) Buy(c *Catalog, id string, pay func(yen int) bool) bool { it := c.Item(id) if it == nil || it.Price <= 0 || p.Own(id) || !pay(it.Price) { return false } return p.Give(c, id) } // Equip bär ett ägt item på sin plats (ersätter det som satt där). func (p *Progress) Equip(c *Catalog, id string) bool { it := c.Item(id) if it == nil || !p.Own(id) { return false } p.ensure() p.Equipped[it.Slot] = id return true } // Unequip tar av det som bärs på platsen. func (p *Progress) Unequip(slot string) { delete(p.Equipped, slot) } // Worn är itemet på platsen, eller nil. func (p *Progress) Worn(c *Catalog, slot string) *Item { if id := p.Equipped[slot]; id != "" { return c.Item(id) } return nil } // Aggregate är de sammanlagda effekterna av skills och burna items. type Aggregate struct { Stats sim.Stats Kana bool FogRadius int Landmarks bool FreeGates bool Battery bool Reveal map[string]bool Tint [3]float32 } // Effects räknar ihop allt spelaren har. func (p *Progress) Effects(c *Catalog) Aggregate { a := Aggregate{Stats: sim.DefaultStats(), Reveal: map[string]bool{}, Tint: [3]float32{1, 1, 1}} apply := func(e Effect) { for k, v := range e.Stats { a.Stats[statIndex[k]] *= v } a.Kana = a.Kana || e.Kana a.FogRadius += e.FogRadius a.Landmarks = a.Landmarks || e.Landmarks a.FreeGates = a.FreeGates || e.FreeGates a.Battery = a.Battery || e.Battery if e.RevealLevel != "" { a.Reveal[e.RevealLevel] = true } } for id := range p.Skills { if s := c.Skill(id); s != nil { apply(s.Effect) } } for _, slot := range Slots { if it := p.Worn(c, slot); it != nil { apply(it.Effect) if len(it.Tint) == 3 { for i := range a.Tint { a.Tint[i] *= float32(it.Tint[i]) } } } } return a } // RecordLevel sparar ett resultat; bästa stjärnor/poäng behålls. Returnerar // true första gången banan klaras. func (p *Progress) RecordLevel(id string, won bool, stars, score int) (first bool) { p.ensure() r := p.Levels[id] if r == nil { r = &LevelResult{} p.Levels[id] = r } r.Attempts++ if !won { return false } first = !r.Cleared r.Cleared = true r.Stars = max(r.Stars, stars) r.Score = max(r.Score, score) return first } // Cleared rapporterar om banan är klarad. func (p *Progress) Cleared(id string) bool { r := p.Levels[id] return r != nil && r.Cleared } // Unlocked avgör om en bana går att spela: huvudbana 1 alltid, huvudbana n // när huvudbana n−1 är klarad, sidobana när föräldern är klarad. func (p *Progress) Unlocked(all []*level.Level, id string) bool { var me *level.Level for _, l := range all { if l.ID == id { me = l } } if me == nil || me.IsLeg() { return false // delbanor nås bara via sin resa } if me.IsSide() { return p.Cleared(me.Parent) } if me.Order <= 1 { return true } for _, l := range all { if !l.IsSide() && l.Order == me.Order-1 { return p.Cleared(l.ID) } } return false } // TotalStars är summan av bästa stjärnor. func (p *Progress) TotalStars() int { n := 0 for _, r := range p.Levels { n += r.Stars } return n }