M8: progression, items, bankarta och sparfil
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>
This commit is contained in:
2026-10-24 11:29:30 +02:00
parent d466333f01
commit 915c684dc2
46 changed files with 3058 additions and 316 deletions

View File

@@ -172,9 +172,20 @@ type SignText struct {
Text Text `json:"text"`
}
// Rewards är vad en klarad bana ger utöver poäng.
type Rewards struct {
Items []string `json:"items"` // item-id:n (t.ex. skins) som delas ut första gången
SkillPoints int `json:"skillPoints"` // extra skillpoäng första gången
}
// Level är en bana.
type Level struct {
ID string `json:"id"`
Kind string `json:"kind"` // "main" (standard) eller "side"
Order int `json:"order"` // huvudbanornas ordning på bankartan (1, 2, …); sidobanor ärver förälderns
Parent string `json:"parent"` // sidobanans huvudbana – låses upp när den är klarad
Rewards Rewards `json:"rewards"`
Shop []string `json:"shop"` // items som kiosken säljer på banan (tom = seedat urval)
Name Text `json:"name"`
Difficulty int `json:"difficulty"` // 1 (lätt) … 5 (svår)
Destination Text `json:"destination"` // dit spelaren ska
@@ -247,6 +258,22 @@ func (l *Level) Validate() error {
if l.ID == "" {
return fmt.Errorf("level: saknar id")
}
switch l.Kind {
case "":
l.Kind = "main"
case "main", "side":
default:
return fmt.Errorf("level %s: kind måste vara main eller side", l.ID)
}
if l.Kind == "side" && l.Parent == "" {
return fmt.Errorf("level %s: sidobana utan parent", l.ID)
}
if l.Kind == "main" && l.Parent != "" {
return fmt.Errorf("level %s: huvudbana med parent", l.ID)
}
if l.Order < 0 {
return fmt.Errorf("level %s: negativ order", l.ID)
}
if len(l.Grid) == 0 {
return fmt.Errorf("level %s: tomt rutnät", l.ID)
}
@@ -534,6 +561,26 @@ func LoadFile(path string) (*Level, error) {
return Parse(b)
}
// IsSide rapporterar om banan är en sidobana.
func (l *Level) IsSide() bool { return l.Kind == "side" }
// LoadAll läser alla banor i en mapp, sorterade på id.
func LoadAll(dir string) ([]*Level, error) {
ids, err := List(dir)
if err != nil {
return nil, err
}
var out []*Level
for _, id := range ids {
l, err := LoadFile(filepath.Join(dir, id+".json"))
if err != nil {
return nil, err
}
out = append(out, l)
}
return out, nil
}
// List returnerar id:n (filnamn utan .json) för alla banor i en mapp, sorterade.
func List(dir string) ([]string, error) {
files, err := filepath.Glob(filepath.Join(dir, "*.json"))