Files
claude 86f8130735
All checks were successful
release / build-release (push) Successful in 1m35s
M12: ljud, inställningar och release v0.1.0
tools/gensound syntetiserar 21 ljud: fyra stationsmelodier i klockklang
(en per station via melody-fältet eller banans id), ankomstjingel,
ding-dong vid inrullning, dörrsignal vid avgång, spärrens pip och
nekande, kiosk, toalett, lärt ord, bekräfta/varning, byte, personal,
vinst, förlust, flygplan, sorl och menymusik. sfx.go spelar effekter
utifrån vad som hänt i sim.Run sedan förra bildrutan. Ljudlagret har
huvudvolym × effekter/musik och byter inte slinga i onödan.
Inställningar (S på bankartan, sparas): volymer, språk sv/en som laddas
om direkt, fönsterskala, fullskärm (F11 överallt) och tangentöversikt.
Fönsterikon från genart. CHANGELOG.md. Tester för ljudfiler, melodival
och inställningar. Docs, wiki och skärmdumpar uppdaterade.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-25 08:31:38 +01:00

187 lines
5.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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"
"sort"
"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"`
NavMode string `json:"navMode,omitempty"` // "list" (standard) eller "signs"
Endless []EndlessRecord `json:"endless,omitempty"` // oändligt läge: bästa omgångarna, högst poäng först
Settings *Settings `json:"settings,omitempty"`
}
// Settings är spelarens inställningar (M12).
type Settings struct {
Master int `json:"master"` // 0–10
Effects int `json:"effects"` // 0–10
Music int `json:"music"` // 0–10 (musik och sorl)
Lang string `json:"lang"` // "sv" eller "en"
Scale int `json:"scale"` // fönsterskala 1–6, 0 = flaggan -scale
Fullscreen bool `json:"fullscreen"`
}
// DefaultSettings är standardinställningarna.
func DefaultSettings() *Settings { return &Settings{Master: 8, Effects: 8, Music: 6, Lang: "sv"} }
// normalize ser till att inställningarna är rimliga (saknade = standard).
func (s *Settings) normalize() {
clamp := func(v *int) { *v = max(0, min(10, *v)) }
clamp(&s.Master)
clamp(&s.Effects)
clamp(&s.Music)
if s.Lang != "sv" && s.Lang != "en" {
s.Lang = "sv"
}
s.Scale = max(0, min(6, s.Scale))
}
// EndlessRecord är en avslutad omgång i oändligt läge.
type EndlessRecord struct {
Score int `json:"score"`
Goals int `json:"goals"` // antal klarade mål
Seed [2]uint64 `json:"seed"`
When time.Time `json:"when"`
}
// MaxEndlessRecords är hur många omgångar som sparas i listan.
const MaxEndlessRecords = 5
// AddEndless lägger in en omgång i listan (sorterad på poäng, högst först)
// och rapporterar om den är ny bästa.
func (s *State) AddEndless(r EndlessRecord) (best bool) {
best = len(s.Endless) == 0 || r.Score > s.Endless[0].Score
s.Endless = append(s.Endless, r)
sort.SliceStable(s.Endless, func(i, j int) bool { return s.Endless[i].Score > s.Endless[j].Score })
if len(s.Endless) > MaxEndlessRecords {
s.Endless = s.Endless[:MaxEndlessRecords]
}
return best
}
// BestEndless är bästa omgången (nil om ingen).
func (s *State) BestEndless() *EndlessRecord {
if len(s.Endless) == 0 {
return nil
}
return &s.Endless[0]
}
// New är ett nytt spel.
func New() *State {
return &State{Version: Version, Know: lang.NewKnowledge(), Wallet: sim.NewWallet(), Profile: &sim.Profile{}, Progress: progress.New(), Settings: DefaultSettings()}
}
// 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.Settings == nil {
s.Settings = DefaultSettings()
}
s.Settings.normalize()
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)
}