M10: byten, hemresan och levelcheck
All checks were successful
release / build-release (push) Successful in 1m37s

Resor med byten: en bana kan ha next som pekar på en delbana (kind leg,
syns inte på bankartan). När man klivit av rätt tåg fortsätter samma
omgång på nästa station med klocka, plånbok, sällskap och räknare;
par-tiden adderas, dimma och folk är nya, en Byte!-banner visas och
HUD:en visar resmålet via bytesstationen. Tidsgräns och missade tåg
gäller hela resan. level.ValidateChains kräver sammanhängande kedjor.
Nya resor: 007 Akihabara -> Ueno -> Asakusa och finalen 008 Tokyo (två
våningar) -> Nippori -> Narita flygplats. Slutscen när sista huvudbanan
klaras första gången: planet lyfter, resan summeras, eftertexter;
Progress.StoryCleared sparas; flaggan -ending. tools/levelcheck ritar
banorna och rapporterar problem. HUD-nivån kortas när mätarna tar plats;
poängraden radbryts. Skärmdumpar och docs uppdaterade.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 13:37:48 +02:00
parent f762f9554e
commit 4ae6a1d24c
36 changed files with 1681 additions and 74 deletions

View File

@@ -71,6 +71,7 @@ type Options struct {
Mute bool // inget ljud
SavePath string // sparfil ("" = standardplatsen)
NoSave bool // läs sparfilen men skriv aldrig
Ending bool // öppna slutscenen direkt (tester/skärmdumpar)
Mode string // navigeringsläge från start: "list" eller "signs" (tomt = sparfilens)
}
@@ -137,18 +138,40 @@ func New(opt Options) (*Game, error) {
g.Wallet.Card = opt.CardYen
}
if opt.LevelID != "" {
switch {
case opt.Ending:
g.scene = NewEndingScene(g)
case opt.LevelID != "":
s, err := g.startLevel(opt.LevelID)
if err != nil {
return nil, err
}
g.scene = s
} else {
default:
g.scene = NewMapScene(g)
}
return g, nil
}
// chainAfter är resans fortsättning efter banan id (delbanorna via next).
func (g *Game) chainAfter(id string) []*level.Level {
if c := level.ChainOf(g.Levels, id); len(c) > 1 {
return c[1:]
}
return nil
}
// isLastMain rapporterar om banan är storyns sista huvudbana.
func (g *Game) isLastMain(id string) bool {
var last *level.Level
for _, l := range g.Levels {
if !l.IsSide() && !l.IsLeg() && (last == nil || l.Order > last.Order) {
last = l
}
}
return last != nil && last.ID == id
}
func loadCatalog() (*progress.Catalog, error) {
sk, err := res.ReadFile("data", "skills.json")
if err != nil {
@@ -240,7 +263,7 @@ func (g *Game) runOptions(id string, seed rng.Seed) sim.RunOptions {
return sim.RunOptions{
Dict: g.Assets.Dict, Know: g.Know, Wallet: g.Wallet, Debuffs: g.Profile.Debuffs, Seed: seed,
Extra: eff.Stats, FogRadius: eff.FogRadius, Landmarks: eff.Landmarks, FreeGates: eff.FreeGates,
RevealAll: eff.Reveal[id], InfiniteBattery: eff.Battery, Mode: g.Mode,
RevealAll: eff.Reveal[id], InfiniteBattery: eff.Battery, Mode: g.Mode, Chain: g.chainAfter(id),
}
}
@@ -281,12 +304,13 @@ func (g *Game) startLevel(id string) (*LevelScene, error) {
// LevelOutcome är vad en avslutad bana gav i progression (för resultatskärmen).
type LevelOutcome struct {
XP int
LevelUps int
FirstClear bool
NewItems []string
NewPoints int
Unlocked []string // banor som öppnades
XP int
LevelUps int
FirstClear bool
NewItems []string
NewPoints int
Unlocked []string // banor som öppnades
StoryCleared bool // sista huvudbanan klarades för första gången: visa slutet
}
// levelEnded appliceras exakt en gång när en bana avgjorts: debuffar,
@@ -323,6 +347,10 @@ func (g *Game) levelEnded(run *sim.Run, id string) LevelOutcome {
out.Unlocked = append(out.Unlocked, l.ID)
}
}
if g.isLastMain(id) && !g.Progress.StoryCleared {
g.Progress.StoryCleared = true
out.StoryCleared = true
}
}
g.State.Current = nil
g.Save()