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

@@ -150,6 +150,9 @@ func TestAllShippedLevels(t *testing.T) {
if l.Party > 1 && (len(l.Find(Toilet)) == 0 || len(l.Find(Kiosk)) == 0) {
t.Errorf("banor med sällskap behöver minst en toalett (W) och en kiosk (K)")
}
if l.Kind == "main" && l.Order == 0 {
t.Errorf("huvudbanor behöver order för bankartan")
}
for _, sg := range l.Signs {
if sg.Text["sv"] == "" || sg.Text["en"] == "" {
t.Errorf("skylt (%d,%d) saknar sv/en-text", sg.X, sg.Y)
@@ -384,3 +387,46 @@ func TestTrackTile(t *testing.T) {
t.Fatal("räls till höger om perrongen är vertikal")
}
}
func TestKindOrderParentAndLoadAll(t *testing.T) {
l := mustParse(t, sample)
if l.Kind != "main" || l.IsSide() {
t.Fatal("standard är main")
}
if _, err := Parse([]byte(strings.Replace(sample, `"difficulty": 1`, `"difficulty": 1, "kind": "side"`, 1))); err == nil {
t.Fatal("sidobana utan parent ska ge fel")
}
if _, err := Parse([]byte(strings.Replace(sample, `"difficulty": 1`, `"difficulty": 1, "parent": "x"`, 1))); err == nil {
t.Fatal("huvudbana med parent ska ge fel")
}
if _, err := Parse([]byte(strings.Replace(sample, `"difficulty": 1`, `"difficulty": 1, "kind": "bus"`, 1))); err == nil {
t.Fatal("okänd kind ska ge fel")
}
side := mustParse(t, strings.Replace(sample, `"difficulty": 1`, `"difficulty": 1, "kind": "side", "parent": "001", "rewards": {"items": ["skin_kimono"], "skillPoints": 1}`, 1))
if !side.IsSide() || side.Parent != "001" || side.Rewards.SkillPoints != 1 || len(side.Rewards.Items) != 1 {
t.Fatal("sidobanans fält ska läsas")
}
dir, _ := res.Path("data", "levels")
all, err := LoadAll(dir)
if err != nil || len(all) < 4 {
t.Fatalf("LoadAll: %d banor, %v", len(all), err)
}
sides := 0
for _, l := range all {
if l.IsSide() {
sides++
found := false
for _, m := range all {
if m.ID == l.Parent && !m.IsSide() {
found = true
}
}
if !found {
t.Errorf("%s: föräldern %q finns inte bland huvudbanorna", l.ID, l.Parent)
}
}
}
if sides == 0 {
t.Fatal("det ska finnas minst en sidobana")
}
}