M8: progression, items, bankarta och sparfil
All checks were successful
release / build-release (push) Successful in 1m46s
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:
@@ -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"))
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user