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:
122
internal/save/save.go
Normal file
122
internal/save/save.go
Normal file
@@ -0,0 +1,122 @@
|
||||
// Package save läser och skriver sparfilen: kunskap, plånbok, debuffar,
|
||||
// progression och den pågående omgångens bana + seed. JSON med
|
||||
// versionsfält så att gamla filer kan migreras.
|
||||
package save
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/lang"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/progress"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/sim"
|
||||
)
|
||||
|
||||
// Version är sparfilens formatversion.
|
||||
const Version = 1
|
||||
|
||||
// AppDir är mappnamnet under användarens konfigmapp.
|
||||
const AppDir = "metro-puzzle-jp"
|
||||
|
||||
// RunRef pekar ut en pågående omgång så att den kan återupptas med samma
|
||||
// slump (minispel, händelser, läsning).
|
||||
type RunRef struct {
|
||||
LevelID string `json:"levelId"`
|
||||
Seed [2]uint64 `json:"seed"`
|
||||
Started time.Time `json:"started"`
|
||||
}
|
||||
|
||||
// State är allt som sparas.
|
||||
type State struct {
|
||||
Version int `json:"version"`
|
||||
SavedAt time.Time `json:"savedAt"`
|
||||
Know *lang.Knowledge `json:"knowledge"`
|
||||
Wallet *sim.Wallet `json:"wallet"`
|
||||
Profile *sim.Profile `json:"profile"`
|
||||
Progress *progress.Progress `json:"progress"`
|
||||
Current *RunRef `json:"current,omitempty"`
|
||||
}
|
||||
|
||||
// New är ett nytt spel.
|
||||
func New() *State {
|
||||
return &State{Version: Version, Know: lang.NewKnowledge(), Wallet: sim.NewWallet(), Profile: &sim.Profile{}, Progress: progress.New()}
|
||||
}
|
||||
|
||||
// DefaultPath är sparfilens plats: ~/.config/metro-puzzle-jp/save.json på
|
||||
// Linux, %AppData%\metro-puzzle-jp\save.json på Windows.
|
||||
func DefaultPath() (string, error) {
|
||||
dir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(dir, AppDir, "save.json"), nil
|
||||
}
|
||||
|
||||
// Load läser sparfilen. Saknas den returneras ett nytt spel (och exists=false).
|
||||
func Load(path string) (st *State, exists bool, err error) {
|
||||
b, err := os.ReadFile(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return New(), false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
var s State
|
||||
if err := json.Unmarshal(b, &s); err != nil {
|
||||
return nil, true, fmt.Errorf("sparfilen %s är trasig: %w", path, err)
|
||||
}
|
||||
if err := migrate(&s); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
return &s, true, nil
|
||||
}
|
||||
|
||||
// migrate lyfter äldre versioner till den aktuella och fyller i luckor.
|
||||
func migrate(s *State) error {
|
||||
if s.Version > Version {
|
||||
return fmt.Errorf("sparfilen är från en nyare version (%d > %d)", s.Version, Version)
|
||||
}
|
||||
// Version 0 (saknat fält) och 1 har samma form; här läggs framtida steg.
|
||||
s.Version = Version
|
||||
if s.Know == nil {
|
||||
s.Know = lang.NewKnowledge()
|
||||
}
|
||||
if s.Know.Words == nil {
|
||||
s.Know.Words = map[string]bool{}
|
||||
}
|
||||
if s.Know.Progress == nil {
|
||||
s.Know.Progress = map[string]int{}
|
||||
}
|
||||
if s.Wallet == nil {
|
||||
s.Wallet = sim.NewWallet()
|
||||
}
|
||||
if s.Profile == nil {
|
||||
s.Profile = &sim.Profile{}
|
||||
}
|
||||
if s.Progress == nil {
|
||||
s.Progress = progress.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Store skriver sparfilen atomiskt (tempfil + byte av namn).
|
||||
func Store(path string, s *State) error {
|
||||
s.Version = Version
|
||||
s.SavedAt = time.Now()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
b, err := json.MarshalIndent(s, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmp := path + ".tmp"
|
||||
if err := os.WriteFile(tmp, b, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmp, path)
|
||||
}
|
||||
Reference in New Issue
Block a user