M11: oändligt läge och New Game+
All checks were successful
release / build-release (push) Successful in 1m34s
All checks were successful
release / build-release (push) Successful in 1m34s
internal/endless lottar mål ur en seed: en av storyns nätkopplade stationer (aldrig samma två gånger i rad) med ett resmål som minst ett tåg går rakt till men inte alla; banan kopieras, bedöms om mot nätet, sällskapet växer var tredje mål och tidsgränsen krymper. O på bankartan (eller -endless) startar en omgång; klarade mål ger poäng och XP, Enter ger nästa mål, förlust eller Esc avslutar och omgången hamnar i sparfilens poänglista (fem bästa). G två gånger startar New Game+: items, banresultat, pengar och debuffar nollställs, skills, XP och japanskan behålls, sällskapet är en person större per varv. Docs, wiki, tester och skärmdumpar uppdaterade. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
15
README.md
15
README.md
@@ -19,8 +19,11 @@ sällskapet i behåll. Klarar man finalen (Tokyo → Nippori → Narita) visas
|
||||
**hemresan**: planet lyfter, resan summeras och eftertexterna rullar. Med
|
||||
egna kort väljer man **vem som betalar** i kiosken, och på de svåraste
|
||||
stationerna släpper spärren inte ut en som checkat in: man får **kalla på
|
||||
personal** med Enter vid spärren. Nästa är M11 (oändligt läge och New
|
||||
Game+). Se [doc/roadmap.md](doc/roadmap.md).
|
||||
personal** med Enter vid spärren. Efter storyn finns **oändligt läge** (O på
|
||||
bankartan: lottade resmål på storyns stationer tills man förlorar, egen
|
||||
poänglista) och **New Game+** (G två gånger: om från början med skills kvar
|
||||
och ett större sällskap). Nästa är M12 (ljud, polish, release) och M10b
|
||||
(bussar). Se [doc/roadmap.md](doc/roadmap.md).
|
||||
|
||||

|
||||
|
||||
@@ -82,6 +85,8 @@ labyrintgolv, samma klocka och samma hungriga sällskap.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
På de svåraste banorna har alla i sällskapet egna kort som måste ha saldo,
|
||||
alla behöver biljett, och när biljetten är köpt ska sällskapet placeras i
|
||||
vagnen efter sina önskemål: fönster eller gång, vänster eller höger sida,
|
||||
@@ -182,6 +187,7 @@ metro-puzzle-jp -save /sökväg/save.json # annan sparfil (standard ~/.config/
|
||||
metro-puzzle-jp -nosave # läs sparfilen men skriv aldrig (tester/skärmdumpar)
|
||||
metro-puzzle-jp -mode signs # navigeringsläge från start: list eller signs (annars sparfilens; N på bankartan växlar)
|
||||
metro-puzzle-jp -ending # öppna slutscenen (hemresan) direkt (tester/skärmdumpar)
|
||||
metro-puzzle-jp -endless -seed 7 # starta oändligt läge direkt med en viss målföljd (tester/skärmdumpar)
|
||||
metro-puzzle-jp -script "move right 4; shot bild.png; quit" # skriptläge, se internal/game/script.go
|
||||
metro-puzzle-jp -version
|
||||
```
|
||||
@@ -194,7 +200,9 @@ som inte släpper ut en; i minispelen väljer pilarna och
|
||||
Enter bekräftar (håll Enter för att mata in sedlar). H frågar om hjälp vid en
|
||||
skylt, L öppnar ordlistan, T skillträdet, I garderoben, B butiken vid en kiosk, M
|
||||
döljer/visar minikartan, P på kartan lägger kortet i telefonen, N på kartan
|
||||
växlar navigeringsläge (lista/skyltar), Esc går tillbaka, R spelar om. Gamepad
|
||||
växlar navigeringsläge (lista/skyltar), O på kartan startar oändligt läge
|
||||
och G (två gånger) New Game+ när storyn är klar, Esc går tillbaka, R spelar
|
||||
om. Gamepad
|
||||
med standardlayout fungerar: d-pad/vänster spak, A = välj, B = tillbaka,
|
||||
Y = hjälp, X = ordlista, Back/Select = karta.
|
||||
|
||||
@@ -240,6 +248,7 @@ internal/progress/ XP, skillträd, items, banresultat, upplåsning (ren Go)
|
||||
internal/save/ sparfil (versionerad JSON)
|
||||
internal/level/ banformat, validering, framkomlighet, koppling till linjenätet
|
||||
internal/nav/ linjenätet: stationer, linjer, rätt/fel håll/för långt bort (ren Go)
|
||||
internal/endless/ oändligt läge: seedade mål på storyns stationer (ren Go)
|
||||
internal/res/ hittar och läser resources/
|
||||
internal/rng/ seedad slump
|
||||
tools/ build/ genart/ shots/
|
||||
|
||||
@@ -30,6 +30,7 @@ func main() {
|
||||
noSave := flag.Bool("nosave", false, "läs sparfilen men skriv aldrig (tester/skärmdumpar)")
|
||||
mode := flag.String("mode", "", "navigeringsläge från start: list eller signs (tomt = sparfilens)")
|
||||
ending := flag.Bool("ending", false, "öppna slutscenen direkt (tester/skärmdumpar)")
|
||||
endlessFlag := flag.Bool("endless", false, "starta oändligt läge direkt (tester/skärmdumpar)")
|
||||
version := flag.Bool("version", false, "skriv ut version och avsluta")
|
||||
flag.Parse()
|
||||
|
||||
@@ -38,7 +39,7 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
g, err := game.New(game.Options{LevelID: *levelID, SmokeFrames: *smoke, Script: *script, KnownWords: splitWords(*know), Points: *points, Seed: *seed, Phone: *phone, CardYen: *card, Debuffs: splitWords(*debuffs), Mute: *mute, SavePath: *savePath, NoSave: *noSave, Mode: *mode, Ending: *ending})
|
||||
g, err := game.New(game.Options{LevelID: *levelID, SmokeFrames: *smoke, Script: *script, KnownWords: splitWords(*know), Points: *points, Seed: *seed, Phone: *phone, CardYen: *card, Debuffs: splitWords(*debuffs), Mute: *mute, SavePath: *savePath, NoSave: *noSave, Mode: *mode, Ending: *ending, Endless: *endlessFlag})
|
||||
if err != nil {
|
||||
log.Fatalf("kunde inte starta: %v", err)
|
||||
}
|
||||
|
||||
BIN
doc/img/endless.png
Normal file
BIN
doc/img/endless.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
BIN
doc/img/map.png
BIN
doc/img/map.png
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
@@ -136,6 +136,9 @@ att bygga på Linux som på Windows.
|
||||
| `internal/sim` | `TestKioskAsksWhoPays`, `TestOneWayGatesNeedStaff` | **M10c:** kiosken frågar vem som betalar med egna kort, vald följares kort dras, kontant fungerar, avbrott drar inget; avgift vid inpassering i betalt område, spärren släpper inte ut incheckad, personal låser upp efter 10 s, ny inpassering kostar igen |
|
||||
| `internal/level` | `TestOneWayGatesAndInside` | **M10c:** `oneWayGates` läses, `InsideGates` = rutorna innanför spärrarna |
|
||||
| `internal/level` | `TestPathOutsideAvoidsOtherPaidAreas` + regel i `TestAllShippedLevels` | **M10d:** vägar som undviker andra perrongers betalda områden; varje perrong nås utan att gå genom ett annat betalt område |
|
||||
| `internal/endless` | `TestPlanIsDeterministicAndValid`, `TestDestinationsNeedTrapAndRoute` | **M11:** samma seed → samma följd, annan seed → annan; varje mål har 1–4 rätta tåg och en fälla, nytt resmål, inga byten, aldrig samma station två gånger i rad; sällskapet växer och tidsgränsen krymper |
|
||||
| `internal/progress` | `TestNewGamePlusKeepsSkillsResetsItems` | **M11:** NG+ behåller skills, XP, skillpoäng och klarad story; nollställer items, utrustning och banresultat |
|
||||
| `internal/save` | `TestEndlessRecords` | **M11:** poänglistan sorterad, kapad till fem, "ny bästa" bara när toppen slås |
|
||||
| `internal/level` | `TestToiletSides` | **M10d:** 005 och 008b har toaletter bara innanför, 007b på båda sidor, 007 och 008 bara utanför; innanför bara på banor med spärrar utan återvändo |
|
||||
| `internal/sim` | `TestJourneyWithTransfer`, `TestJourneyWrongTrainOnSecondLeg`, `TestChainLevelsLoad` | **M10:** bytet fortsätter omgången på nästa station med klocka, plånbok, sällskap och par-summa; vinst först på sista delbanan; fel håll på bytesstationen förlorar; delbanor laddas enskilt |
|
||||
| `internal/minigame` | `TestCarPickAlightingAndChoice`, `TestCrowdLevelNarrowsZone`, `TestRideCrowdLetPass` | Folk av först (för tidigt räknas), seedad trängsel med minst en lugn vagn, val; trängsel smalnar zonen; resan: kortare tid per station, passagerare som vill förbi, blockerade räknas |
|
||||
@@ -163,7 +166,7 @@ att bygga på Linux som på Windows.
|
||||
| ~~M10c Vem betalar, spärrar utan återvändo~~ ✓ | Väljaren betalar från rätt källa och nekar den utan pengar; spärren håller kvar, personalen låser upp, ny inpassering kostar – finns, se tabellen ovan |
|
||||
| ~~M10d Toaletter innanför/utanför~~ ✓ | Toaletterna ligger på rätt sida av spärrarna per bana – finns, se tabellen ovan |
|
||||
| M10b Bussar och vägar | Bussar följer tidtabell och vägrutor; hållplatser bedöms som perronger; överstimulerande UI-reglaget påverkar bara presentationen |
|
||||
| M11 Oändligt läge & NG+ | Målsekvens deterministisk per seed; NG+ behåller skills men nollställer items |
|
||||
| ~~M11 Oändligt läge & NG+~~ ✓ | Målsekvens deterministisk per seed, giltiga mål med fälla, NG+ behåller skills men nollställer items, poänglista – finns, se tabellen ovan |
|
||||
| M12 Ljud & polish | Alla ljudfiler som refereras finns och går att avkoda; Windows-binär startar (manuellt) |
|
||||
|
||||
### Acceptanskriterier för "klar"
|
||||
|
||||
@@ -646,10 +646,32 @@ på **brasse-linux01-runnern** (label `linux-amd64`), aldrig på Pi5:n.
|
||||
(`sim.TestOneWayGatesNeedStaff` går vägen till första spärren i stället
|
||||
för ett fast steg), och levelcheck är grön. ✓
|
||||
|
||||
### M11 – Oändligt läge och New Game+
|
||||
### M11 – Oändligt läge och New Game+ ✓ (klar 2026-10-24)
|
||||
|
||||
- Slumpade mål från seed, poäng per mål, egen poänglista. NG+ behåller skills.
|
||||
- Låses upp när storyn är klar.
|
||||
- **Så blev det:** `internal/endless` lottar mål ur en seed: stationen är
|
||||
en av storyns nätkopplade banor (alla huvud-, sido- och delbanor, aldrig
|
||||
samma två gånger i rad) och resmålet lottas bland stationer som minst ett
|
||||
av banans tåg går rakt till men inte alla – samma labyrint och samma tåg,
|
||||
men vilket tåg som är rätt beror på resmålet. Banan kopieras, får nytt
|
||||
`to`/`destination`, inga byten, sällskap som växer med en person var
|
||||
tredje mål (max 5) och en tidsgräns som krymper 5 % per mål (golv 180 s),
|
||||
och bedöms om mot nätet. Samma seed ger samma följd. **Omgången:** O på
|
||||
bankartan (efter storyn) eller `-endless`; HUD:en visar "#3 Ueno
|
||||
station"; klarat mål ger poäng (summeras) och XP, Enter ger nästa mål;
|
||||
förlust (eller Esc) avslutar omgången som hamnar i poänglistan (fem
|
||||
bästa i sparfilen, "Ny bästa!" när toppen slås). **New Game+:** G två
|
||||
gånger på bankartan: banresultat, items, utrustning, pengar och debuffar
|
||||
nollställs, skills, XP och språkkunskap behålls, och sällskapet är en
|
||||
person större på alla banor per NG+-varv (`RunOptions.ExtraParty`).
|
||||
- **Klar när:** målföljden är deterministisk per seed och giltig
|
||||
(`endless.TestPlanIsDeterministicAndValid`, `TestDestinationsNeedTrapAndRoute`),
|
||||
NG+ behåller skills men nollställer items
|
||||
(`progress.TestNewGamePlusKeepsSkillsResetsItems`), poänglistan sorterar
|
||||
och kapar (`save.TestEndlessRecords`). ✓
|
||||
- **Kvar till senare:** oändligt läge med byten (resor över två stationer),
|
||||
stationer som genereras vid körning (generatorn in i repot).
|
||||
|
||||
### M12 – Ljud, polish och release
|
||||
|
||||
|
||||
@@ -283,4 +283,6 @@ från skills i skill tree, och från unika skills man får av sidouppdrag.
|
||||
| "flera stopp och byten" | En resa är en kedja av banfiler (`next` → delbana med `kind: "leg"`). Samma omgång fortsätter på nästa station med klocka, plånbok och sällskap; resmålet i HUD:en visas "via" bytesstationen. Vilken bytesstation som gäller bestämmer banan – spelaren väljer tåg, inte rutt | Fri ruttplanering (välj själv var man byter) är en möjlig utbyggnad |
|
||||
| "slutar när man ska tillbaka till flygplatsen" | Sista huvudbanan (008) går Tokyo → Nippori → Narita flygplats med Keisei. När den klaras första gången visas hemresan: planet lyfter, resan summeras, eftertexter | Slutet går att se igen via `-ending` |
|
||||
| Buss | Uppskjuten till M10b (vägrutor, hållplatser, egen sprite) | |
|
||||
| "Oändligt läge … målen är slumpade" | Målen är storyns stationer med lottade resmål (samma labyrint, samma tåg, nytt resmål avgör vilket tåg som är rätt), seedat; sällskapet växer och tidsgränsen krymper per mål; omgången slutar vid första förlust och hamnar i en egen poänglista | Slumpade labyrinter vid körning kan komma senare |
|
||||
| "New Game+ … behåll alla dina skills" | Skills, XP och språkkunskap behålls; items, stjärnor, pengar och debuffar nollställs; sällskapet är en person större per NG+-varv | |
|
||||
| Repo-namnet | Arbetsnamnet var "jp-subway-pussel"; Björn döpte Gitea-repot till **metro-puzzle-jp** (2026-10-23), så repo, modul, binär och titel heter så | Spelets visningsnamn "Metro Puzzle JP" kan ändras fritt i `resources/text/*.json` |
|
||||
|
||||
115
internal/endless/endless.go
Normal file
115
internal/endless/endless.go
Normal file
@@ -0,0 +1,115 @@
|
||||
// Package endless är oändligt läge: storyns stationer med slumpade resmål,
|
||||
// mål efter mål tills man förlorar. Allt lottas ur en seed så att samma
|
||||
// seed ger samma följd. Ren Go.
|
||||
package endless
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/nav"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/rng"
|
||||
)
|
||||
|
||||
// Goal är ett mål: en station (en kopia av storyns bana) med nytt resmål.
|
||||
type Goal struct {
|
||||
Step int // 0-baserat
|
||||
Base string // ursprungliga banans id
|
||||
Level *level.Level // kopian, bedömd mot nätet
|
||||
}
|
||||
|
||||
// Svårighetsramp och standardvärden.
|
||||
const (
|
||||
MaxParty = 5 // sällskapet växer med ett var tredje mål, upp till så här många
|
||||
PartyStepEvery = 3 // mål per extra person
|
||||
DefaultTimeLimit = 420 // s, för banor utan egen tidsgräns
|
||||
MinTimeLimit = 180 // s, golvet när tidsgränsen krymper
|
||||
TimeLimitShrinkPct = 5 // procent kortare tidsgräns per mål
|
||||
)
|
||||
|
||||
// Pool är stationerna som kan bli mål: alla nätkopplade banor (även
|
||||
// delbanor och sidobanor), sorterade på id.
|
||||
func Pool(levels []*level.Level) []*level.Level {
|
||||
var out []*level.Level
|
||||
for _, l := range levels {
|
||||
if l.Networked() && l.Net != nil {
|
||||
out = append(out, l)
|
||||
}
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
|
||||
return out
|
||||
}
|
||||
|
||||
// Destinations är stationerna som fungerar som resmål från banan: minst
|
||||
// ett tåg går rakt dit, minst ett gör det inte (en fälla), och inte fler
|
||||
// än nav.MaxRoutes rätta. Sorterade på id.
|
||||
func Destinations(l *level.Level) []string {
|
||||
var out []string
|
||||
for _, s := range l.Net.Stations {
|
||||
if s.ID == l.Station {
|
||||
continue
|
||||
}
|
||||
correct := 0
|
||||
for _, tr := range l.Trains {
|
||||
if l.Net.Judge(tr.LineID, l.Station, tr.Terminus, s.ID) == nav.Direct {
|
||||
correct++
|
||||
}
|
||||
}
|
||||
if correct >= 1 && correct <= nav.MaxRoutes && correct < len(l.Trains) {
|
||||
out = append(out, s.ID)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// Plan ger målet för ett steg ur seeden. Stationen väljs så att den aldrig
|
||||
// är samma som föregående stegs; resmålet lottas bland stationens möjliga.
|
||||
func Plan(levels []*level.Level, seed rng.Seed, step int) (*Goal, error) {
|
||||
pool := Pool(levels)
|
||||
if len(pool) == 0 {
|
||||
return nil, fmt.Errorf("endless: inga nätkopplade banor")
|
||||
}
|
||||
idx := baseIndex(len(pool), seed, step)
|
||||
base := pool[idx]
|
||||
dests := Destinations(base)
|
||||
if len(dests) == 0 {
|
||||
return nil, fmt.Errorf("endless: %s har inga möjliga resmål", base.ID)
|
||||
}
|
||||
r := seed.Derive(fmt.Sprintf("endless:dest:%d", step)).New()
|
||||
dest := dests[r.IntN(len(dests))]
|
||||
|
||||
c := *base
|
||||
c.Trains = append([]level.Train(nil), base.Trains...)
|
||||
c.Next = ""
|
||||
c.To = dest
|
||||
name := base.Net.Station(dest).Name
|
||||
c.Destination = level.Text{"ja": name["ja"], "romaji": nav.Name(name, "romaji"), "en": nav.Name(name, "en"), "sv": nav.Name(name, "sv")}
|
||||
c.Party = min(MaxParty, max(1, base.Party)+step/PartyStepEvery)
|
||||
limit := base.TimeLimitSec
|
||||
if limit == 0 {
|
||||
limit = DefaultTimeLimit
|
||||
}
|
||||
for i := 0; i < step; i++ {
|
||||
limit = limit * (100 - TimeLimitShrinkPct) / 100
|
||||
}
|
||||
c.TimeLimitSec = max(MinTimeLimit, limit)
|
||||
if err := c.Annotate(base.Net); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Goal{Step: step, Base: base.ID, Level: &c}, nil
|
||||
}
|
||||
|
||||
// baseIndex väljer station för ett steg: ett seedat hopp från föregående
|
||||
// stegs station, så att två mål i rad aldrig är samma station.
|
||||
func baseIndex(n int, seed rng.Seed, step int) int {
|
||||
idx := seed.Derive("endless:base:0").New().IntN(n)
|
||||
for s := 1; s <= step; s++ {
|
||||
r := seed.Derive(fmt.Sprintf("endless:base:%d", s)).New()
|
||||
if n > 1 {
|
||||
idx = (idx + 1 + r.IntN(n-1)) % n
|
||||
}
|
||||
}
|
||||
return idx
|
||||
}
|
||||
115
internal/endless/endless_test.go
Normal file
115
internal/endless/endless_test.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package endless
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/nav"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/res"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/rng"
|
||||
)
|
||||
|
||||
func loadLevels(t *testing.T) []*level.Level {
|
||||
t.Helper()
|
||||
dir, err := res.Path("data", "levels")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
all, err := level.LoadAll(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return all
|
||||
}
|
||||
|
||||
// TestPlanIsDeterministicAndValid: samma seed ger samma följd, andra seeds
|
||||
// en annan; varje mål har rätt tåg, en fälla, nytt resmål och ingen kedja;
|
||||
// två mål i rad är aldrig samma station; sällskapet växer.
|
||||
func TestPlanIsDeterministicAndValid(t *testing.T) {
|
||||
all := loadLevels(t)
|
||||
if len(Pool(all)) < 5 {
|
||||
t.Fatalf("poolen ska ha storyns stationer, har %d", len(Pool(all)))
|
||||
}
|
||||
seed := rng.FromInt(11)
|
||||
prev := ""
|
||||
var bases, dests []string
|
||||
for step := 0; step < 12; step++ {
|
||||
g, err := Plan(all, seed, step)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
again, _ := Plan(all, seed, step)
|
||||
if again.Base != g.Base || again.Level.To != g.Level.To {
|
||||
t.Fatalf("steg %d: inte deterministiskt", step)
|
||||
}
|
||||
l := g.Level
|
||||
if l.Next != "" || l.To == l.Station || l.Net == nil || l.Destination.Get("ja") == "" {
|
||||
t.Fatalf("steg %d: %+v", step, l)
|
||||
}
|
||||
correct, wrong := 0, 0
|
||||
for _, tr := range l.Trains {
|
||||
if tr.Verdict == nav.Unjudged {
|
||||
t.Fatalf("steg %d: obedömt tåg", step)
|
||||
}
|
||||
if tr.Correct {
|
||||
correct++
|
||||
} else {
|
||||
wrong++
|
||||
}
|
||||
}
|
||||
if correct < 1 || correct > nav.MaxRoutes || wrong == 0 {
|
||||
t.Fatalf("steg %d (%s → %s): rätt=%d fel=%d", step, g.Base, l.To, correct, wrong)
|
||||
}
|
||||
if g.Base == prev {
|
||||
t.Fatalf("steg %d: samma station två gånger i rad (%s)", step, g.Base)
|
||||
}
|
||||
if l.TimeLimitSec < MinTimeLimit || l.Party < 1 || l.Party > MaxParty {
|
||||
t.Fatalf("steg %d: tidsgräns %d sällskap %d", step, l.TimeLimitSec, l.Party)
|
||||
}
|
||||
prev = g.Base
|
||||
bases = append(bases, g.Base)
|
||||
dests = append(dests, l.To)
|
||||
}
|
||||
// Rampen: sällskapet är större längre fram än i början.
|
||||
first, _ := Plan(all, seed, 0)
|
||||
late, _ := Plan(all, seed, 9)
|
||||
if late.Level.Party <= first.Level.Party && first.Level.Party < MaxParty {
|
||||
t.Fatalf("sällskapet ska växa: %d → %d", first.Level.Party, late.Level.Party)
|
||||
}
|
||||
if late.Level.TimeLimitSec >= first.Level.TimeLimitSec && first.Level.TimeLimitSec > MinTimeLimit {
|
||||
t.Fatalf("tidsgränsen ska krympa: %d → %d", first.Level.TimeLimitSec, late.Level.TimeLimitSec)
|
||||
}
|
||||
// Annan seed ger en annan följd.
|
||||
diff := false
|
||||
for step := 0; step < 12 && !diff; step++ {
|
||||
g, err := Plan(all, rng.FromInt(12), step)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
diff = g.Base != bases[step] || g.Level.To != dests[step]
|
||||
}
|
||||
if !diff {
|
||||
t.Fatal("två seeds gav samma följd")
|
||||
}
|
||||
// Originalbanorna rörs inte.
|
||||
for _, l := range all {
|
||||
if l.ID == bases[0] && l.To == dests[0] && l.Next == "" && l.IsLeg() == false {
|
||||
// kan råka sammanfalla om lotten valde originalets resmål – kontrollera att Trains är orörda
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDestinationsNeedTrapAndRoute(t *testing.T) {
|
||||
all := loadLevels(t)
|
||||
for _, l := range Pool(all) {
|
||||
d := Destinations(l)
|
||||
if len(d) == 0 {
|
||||
t.Errorf("%s: inga möjliga resmål", l.ID)
|
||||
}
|
||||
for _, id := range d {
|
||||
if id == l.Station {
|
||||
t.Errorf("%s: resmål = stationen själv", l.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,11 @@ package game
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/endless"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/lang"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/nav"
|
||||
@@ -47,6 +49,7 @@ type Game struct {
|
||||
Levels []*level.Level // alla banor, sorterade på id
|
||||
Audio *Audio // nil = tyst
|
||||
Mode nav.Mode // navigeringsläge (= State.NavMode), växlas på bankartan
|
||||
Endless *EndlessRun // pågående omgång i oändligt läge (nil = ingen)
|
||||
|
||||
savePath string // "" = spara inte
|
||||
fixedSeed uint64 // >0: alla banor startar med denna seed (tester/skärmdumpar)
|
||||
@@ -72,6 +75,7 @@ type Options struct {
|
||||
SavePath string // sparfil ("" = standardplatsen)
|
||||
NoSave bool // läs sparfilen men skriv aldrig
|
||||
Ending bool // öppna slutscenen direkt (tester/skärmdumpar)
|
||||
Endless bool // starta oändligt läge direkt (tester/skärmdumpar)
|
||||
Mode string // navigeringsläge från start: "list" eller "signs" (tomt = sparfilens)
|
||||
}
|
||||
|
||||
@@ -141,6 +145,12 @@ func New(opt Options) (*Game, error) {
|
||||
switch {
|
||||
case opt.Ending:
|
||||
g.scene = NewEndingScene(g)
|
||||
case opt.Endless:
|
||||
s, err := g.startEndless(g.seed())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g.scene = s
|
||||
case opt.LevelID != "":
|
||||
s, err := g.startLevel(opt.LevelID)
|
||||
if err != nil {
|
||||
@@ -153,6 +163,77 @@ func New(opt Options) (*Game, error) {
|
||||
return g, nil
|
||||
}
|
||||
|
||||
// EndlessRun är en pågående omgång i oändligt läge.
|
||||
type EndlessRun struct {
|
||||
Seed rng.Seed
|
||||
Step int // 0-baserat: målet som spelas nu
|
||||
Score int // summan av klarade måls poäng
|
||||
Goals int // klarade mål
|
||||
}
|
||||
|
||||
// startEndless börjar en omgång i oändligt läge med första målet.
|
||||
func (g *Game) startEndless(seed rng.Seed) (*LevelScene, error) {
|
||||
g.Endless = &EndlessRun{Seed: seed}
|
||||
return g.endlessGoalScene()
|
||||
}
|
||||
|
||||
// endlessGoalScene bygger scenen för omgångens aktuella mål.
|
||||
func (g *Game) endlessGoalScene() (*LevelScene, error) {
|
||||
e := g.Endless
|
||||
goal, err := endless.Plan(g.Levels, e.Seed, e.Step)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := newLevelSceneFor(g, goal.Level, goal.Base, e.Seed.Derive(fmt.Sprintf("run:%d", e.Step)))
|
||||
s.endless = true
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// endlessEnded bokför ett avgjort mål: vinst ger poäng och nästa mål,
|
||||
// förlust avslutar omgången och lägger in den i poänglistan.
|
||||
func (g *Game) endlessEnded(run *sim.Run) LevelOutcome {
|
||||
var out LevelOutcome
|
||||
e := g.Endless
|
||||
if e == nil {
|
||||
return out
|
||||
}
|
||||
for _, d := range run.NewDebuffs {
|
||||
g.Profile.Add(d.Kind, d.LevelsLeft)
|
||||
}
|
||||
g.Profile.EndLevel()
|
||||
if run.Outcome == sim.Won {
|
||||
e.Score += run.Score.Total
|
||||
e.Goals++
|
||||
e.Step++
|
||||
out.XP = progress.XPFor(run.Score.Total, run.Stars)
|
||||
out.LevelUps = g.Progress.AddXP(out.XP)
|
||||
} else {
|
||||
out.EndlessBest = g.State.AddEndless(save.EndlessRecord{Score: e.Score, Goals: e.Goals, Seed: [2]uint64{e.Seed.A, e.Seed.B}, When: time.Now()})
|
||||
out.EndlessOver = true
|
||||
}
|
||||
g.Save()
|
||||
return out
|
||||
}
|
||||
|
||||
// endEndless avslutar en pågående omgång i förtid (Esc på resultatet).
|
||||
func (g *Game) endEndless() {
|
||||
if e := g.Endless; e != nil && e.Goals > 0 {
|
||||
g.State.AddEndless(save.EndlessRecord{Score: e.Score, Goals: e.Goals, Seed: [2]uint64{e.Seed.A, e.Seed.B}, When: time.Now()})
|
||||
g.Save()
|
||||
}
|
||||
g.Endless = nil
|
||||
}
|
||||
|
||||
// newGamePlus börjar om storyn med skills kvar: items, banresultat, pengar
|
||||
// och pågående omgång nollställs; sällskapet blir större på alla banor.
|
||||
func (g *Game) newGamePlus() {
|
||||
g.Progress.NewGamePlus()
|
||||
*g.Wallet = *sim.NewWallet()
|
||||
g.Profile.Debuffs = nil
|
||||
g.State.Current = nil
|
||||
g.Save()
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -264,6 +345,7 @@ func (g *Game) runOptions(id string, seed rng.Seed) 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, Chain: g.chainAfter(id),
|
||||
ExtraParty: g.Progress.NGPlus,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,6 +393,9 @@ type LevelOutcome struct {
|
||||
NewPoints int
|
||||
Unlocked []string // banor som öppnades
|
||||
StoryCleared bool // sista huvudbanan klarades för första gången: visa slutet
|
||||
|
||||
EndlessOver bool // oändligt läge: omgången tog slut
|
||||
EndlessBest bool // … och blev ny bästa
|
||||
}
|
||||
|
||||
// levelEnded appliceras exakt en gång när en bana avgjorts: debuffar,
|
||||
|
||||
@@ -126,6 +126,20 @@ func shopJustPressed() bool { return virtual.pressed["b"] || inpututil.IsKeyJust
|
||||
// phoneJustPressed: P = kortet i telefonen av/på (meny).
|
||||
func phoneJustPressed() bool { return virtual.pressed["p"] || inpututil.IsKeyJustPressed(ebiten.KeyP) }
|
||||
|
||||
// endlessJustPressed: O startar oändligt läge (när storyn är klar).
|
||||
func endlessJustPressed() bool {
|
||||
return virtual.pressed["o"] || inpututil.IsKeyJustPressed(ebiten.KeyO)
|
||||
}
|
||||
|
||||
// ngPlusJustPressed: G (två gånger) startar New Game+.
|
||||
func ngPlusJustPressed() bool { return virtual.pressed["g"] || inpututil.IsKeyJustPressed(ebiten.KeyG) }
|
||||
|
||||
// miniMoveXYAny rapporterar om någon pil just trycktes (för att avbryta en bekräftelse).
|
||||
func miniMoveXYAny() bool {
|
||||
x, y := miniMoveXY()
|
||||
return x != 0 || y != 0
|
||||
}
|
||||
|
||||
// modeJustPressed: N växlar navigeringsläge (lista/skyltar) på bankartan.
|
||||
func modeJustPressed() bool { return virtual.pressed["n"] || inpututil.IsKeyJustPressed(ebiten.KeyN) }
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ type LevelScene struct {
|
||||
id string
|
||||
level *level.Level // aktuell delbana (byts när resan fortsätter på nästa station)
|
||||
run *sim.Run
|
||||
endless bool // målet är ett lottat mål i oändligt läge (inte storyn)
|
||||
cam *sim.Camera
|
||||
animT float64
|
||||
hideMap bool // M döljer/visar minikartan
|
||||
@@ -66,10 +67,25 @@ func NewLevelScene(g *Game, id string, seed rng.Seed) (*LevelScene, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newLevelSceneFor(g, lvl, id, seed), nil
|
||||
}
|
||||
|
||||
// newLevelSceneFor startar en redan laddad (eller lottad) bana.
|
||||
func newLevelSceneFor(g *Game, lvl *level.Level, id string, seed rng.Seed) *LevelScene {
|
||||
s := &LevelScene{id: id, run: sim.NewRun(lvl, g.runOptions(id, seed))}
|
||||
s.rng = rand.New(rand.NewPCG(1, 2))
|
||||
s.rebind(g)
|
||||
return s, nil
|
||||
return s
|
||||
}
|
||||
|
||||
// ended bokför banans utgång: storyn via levelEnded, oändligt läge via
|
||||
// endlessEnded.
|
||||
func (s *LevelScene) ended(g *Game) {
|
||||
if s.endless {
|
||||
s.outcome = g.endlessEnded(s.run)
|
||||
return
|
||||
}
|
||||
s.outcome = g.levelEnded(s.run, s.id)
|
||||
}
|
||||
|
||||
// rebind bygger om allt som hör till den aktuella delbanan: kamera, räls,
|
||||
@@ -404,6 +420,9 @@ func (s *LevelScene) drawHUD(g *Game, screen *ebiten.Image) {
|
||||
if r.Legs() > 1 {
|
||||
name += fmt.Sprintf(" %d/%d", r.LegNo(), r.Legs())
|
||||
}
|
||||
if s.endless && g.Endless != nil {
|
||||
name = fmt.Sprintf("#%d %s", g.Endless.Step+1, name)
|
||||
}
|
||||
a.Draw(screen, name, 4, 0, colText)
|
||||
destX := 4 + a.Width(name) + 12
|
||||
a.Draw(screen, a.T("hud.destination")+": "+dest, destX, 0, colKnown)
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
// linje från flygplatsen, sidobanorna som avstickare ovanför. Låsta banor
|
||||
// är grå, klarade visar stjärnor.
|
||||
type MapScene struct {
|
||||
confirmNG bool // G tryckt en gång: nästa G startar New Game+
|
||||
mains []*level.Level
|
||||
sides map[string][]*level.Level // parent -> sidobanor
|
||||
sel int // vald huvudbana
|
||||
@@ -105,6 +106,26 @@ func (m *MapScene) Update(g *Game) error {
|
||||
g.Save()
|
||||
case modeJustPressed():
|
||||
g.toggleMode()
|
||||
case endlessJustPressed() && g.Progress.StoryCleared:
|
||||
s, err := g.startEndless(g.seed())
|
||||
if err != nil {
|
||||
m.msg = err.Error()
|
||||
return nil
|
||||
}
|
||||
g.SetScene(s)
|
||||
return nil
|
||||
case ngPlusJustPressed() && g.Progress.StoryCleared:
|
||||
if !m.confirmNG {
|
||||
m.confirmNG = true
|
||||
m.msg = g.Assets.T("map.ngplus_confirm")
|
||||
return nil
|
||||
}
|
||||
g.newGamePlus()
|
||||
g.SetScene(NewMapScene(g))
|
||||
return nil
|
||||
}
|
||||
if m.confirmNG && (confirmJustPressed() || miniMoveXYAny()) {
|
||||
m.confirmNG, m.msg = false, "" // något annat tryckt: ingen NG+
|
||||
}
|
||||
if n := len(m.mains); n > 0 {
|
||||
mx, my := miniMoveXY()
|
||||
@@ -207,7 +228,9 @@ func (m *MapScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
if !p.Unlocked(g.Levels, cur.ID) {
|
||||
a.Draw(screen, a.T("map.locked"), 150, 30, colBad)
|
||||
} else if m.msg != "" {
|
||||
a.Draw(screen, m.msg, 150, 30, colBad)
|
||||
for i, l := range a.wrapLine(plainLine(m.msg, colBad), ScreenW-158) {
|
||||
a.drawLines(screen, []line{l}, 150, 30+i*LineH)
|
||||
}
|
||||
}
|
||||
}
|
||||
onoff := a.T("menu.off")
|
||||
@@ -218,12 +241,25 @@ func (m *MapScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
a.T("hud.cash"), g.Wallet.Cash)
|
||||
a.Draw(screen, foot, 8, 30, colDim)
|
||||
a.Draw(screen, fmt.Sprintf(a.T("menu.phone"), onoff)+" "+fmt.Sprintf(a.T("menu.mode"), a.T("mode."+g.Mode.String())), 8, 42, colDim)
|
||||
y := 54
|
||||
if p.StoryCleared { // efter storyn: oändligt läge och New Game+
|
||||
line := a.T("map.endless")
|
||||
if b := g.State.BestEndless(); b != nil {
|
||||
line += fmt.Sprintf(" (%s)", fmt.Sprintf(a.T("map.endless_best"), b.Score, b.Goals))
|
||||
}
|
||||
line += " " + a.T("map.ngplus")
|
||||
if p.NGPlus > 0 {
|
||||
line += fmt.Sprintf(" (%s)", fmt.Sprintf(a.T("map.ngplus_level"), p.NGPlus))
|
||||
}
|
||||
a.Draw(screen, line, 8, y, colKnown)
|
||||
y += LineH
|
||||
}
|
||||
if len(g.Profile.Debuffs) > 0 {
|
||||
var parts []string
|
||||
for _, d := range g.Profile.Debuffs {
|
||||
parts = append(parts, fmt.Sprintf("%s (%d)", a.T("debuff."+string(d.Kind)), d.LevelsLeft))
|
||||
}
|
||||
a.Draw(screen, a.T("hud.debuffs")+": "+strings.Join(parts, ", "), 8, 54, colBad)
|
||||
a.Draw(screen, a.T("hud.debuffs")+": "+strings.Join(parts, ", "), 8, y, colBad)
|
||||
}
|
||||
a.Draw(screen, a.T("map.hint"), 8, ScreenH-24, colDim)
|
||||
a.Draw(screen, fmt.Sprintf("%s (%s)", buildinfo.Version, buildinfo.Mode), 8, ScreenH-12, colDim)
|
||||
|
||||
@@ -38,6 +38,17 @@ func (r *ResultScene) Update(g *Game) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// hint är tangentraden längst ner: oändligt läge har inget "spela om".
|
||||
func (r *ResultScene) hint(a *Assets) string {
|
||||
if r.from.endless {
|
||||
if r.from.outcome.EndlessOver {
|
||||
return a.T("result.hint_endless_over")
|
||||
}
|
||||
return a.T("result.hint_endless")
|
||||
}
|
||||
return a.T("result.hint")
|
||||
}
|
||||
|
||||
func (r *ResultScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
r.from.Draw(g, screen) // banan i bakgrunden
|
||||
run := r.from.run
|
||||
@@ -158,5 +169,5 @@ func (r *ResultScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
a.Draw(screen, fmt.Sprintf("%s Lv%d %d %s %s %d (%d %s)", a.T("hud.japanese"), run.Know.Level(), run.Know.Count(),
|
||||
a.T("hud.words"), a.T("words.points"), run.Know.Points, run.Know.Helps, a.T("result.helps")), 28, y, colDim)
|
||||
}
|
||||
a.Draw(screen, a.T("result.hint"), 28, 161, colDim)
|
||||
a.Draw(screen, r.hint(a), 28, 161, colDim)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@ type Progress struct {
|
||||
Items map[string]bool `json:"items"`
|
||||
Equipped map[string]string `json:"equipped"` // slot -> item-id
|
||||
Levels map[string]*LevelResult `json:"levels"`
|
||||
StoryCleared bool `json:"storyCleared"` // sista huvudbanan klarad: slutet har visats, oändligt läge/NG+ (M11) låses upp
|
||||
StoryCleared bool `json:"storyCleared"` // sista huvudbanan klarad: slutet har visats, oändligt läge/NG+ låses upp
|
||||
NGPlus int `json:"ngPlus"` // antal New Game+ som startats (svårighetspåslag)
|
||||
}
|
||||
|
||||
// New börjar från noll.
|
||||
@@ -253,6 +254,18 @@ func (p *Progress) Unlocked(all []*level.Level, id string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// NewGamePlus börjar om storyn med skills och XP kvar: banresultat, items
|
||||
// och det man har på sig nollställs. Storyn förblir "klar" så att oändligt
|
||||
// läge är kvar. Returnerar det nya NG+-numret.
|
||||
func (p *Progress) NewGamePlus() int {
|
||||
p.ensure()
|
||||
p.Levels = map[string]*LevelResult{}
|
||||
p.Items = map[string]bool{}
|
||||
p.Equipped = map[string]string{}
|
||||
p.NGPlus++
|
||||
return p.NGPlus
|
||||
}
|
||||
|
||||
// TotalStars är summan av bästa stjärnor.
|
||||
func (p *Progress) TotalStars() int {
|
||||
n := 0
|
||||
|
||||
@@ -214,3 +214,21 @@ func TestLevelRecordsAndUnlocking(t *testing.T) {
|
||||
t.Fatal("okänd bana")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewGamePlusKeepsSkillsResetsItems(t *testing.T) {
|
||||
p := New()
|
||||
p.XP, p.SkillPoints, p.StoryCleared = 900, 2, true
|
||||
p.Skills["fog_1"] = true
|
||||
p.Items["geta"] = true
|
||||
p.Equipped["feet"] = "geta"
|
||||
p.RecordLevel("001", true, 3, 500)
|
||||
if n := p.NewGamePlus(); n != 1 || p.NGPlus != 1 {
|
||||
t.Fatalf("NG+ nummer %d", n)
|
||||
}
|
||||
if !p.Skills["fog_1"] || p.XP != 900 || p.SkillPoints != 2 || !p.StoryCleared {
|
||||
t.Fatal("skills, XP, skillpoäng och klarad story ska vara kvar")
|
||||
}
|
||||
if len(p.Items) != 0 || len(p.Equipped) != 0 || len(p.Levels) != 0 || p.TotalStars() != 0 {
|
||||
t.Fatal("items, utrustning och banresultat ska nollställas")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/lang"
|
||||
@@ -40,6 +41,38 @@ type State struct {
|
||||
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
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
||||
@@ -76,3 +76,19 @@ func TestDefaultPath(t *testing.T) {
|
||||
t.Fatalf("path=%q err=%v", p, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEndlessRecords(t *testing.T) {
|
||||
s := New()
|
||||
if s.BestEndless() != nil {
|
||||
t.Fatal("tom lista")
|
||||
}
|
||||
if !s.AddEndless(EndlessRecord{Score: 100, Goals: 1}) || s.AddEndless(EndlessRecord{Score: 50, Goals: 1}) || !s.AddEndless(EndlessRecord{Score: 300, Goals: 3}) {
|
||||
t.Fatal("ny bästa ska rapporteras bara när poängen slår listans topp")
|
||||
}
|
||||
for i := 0; i < 10; i++ {
|
||||
s.AddEndless(EndlessRecord{Score: 10 + i})
|
||||
}
|
||||
if len(s.Endless) != MaxEndlessRecords || s.BestEndless().Score != 300 || s.Endless[1].Score != 100 {
|
||||
t.Fatalf("listan: %+v", s.Endless)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,6 +252,8 @@ type RunOptions struct {
|
||||
Mode nav.Mode // navigeringsläge (nollvärdet = listläge)
|
||||
|
||||
Chain []*level.Level // resans fortsättning: delbanorna efter den första (byten), i ordning
|
||||
|
||||
ExtraParty int // fler i sällskapet än banan säger (New Game+)
|
||||
}
|
||||
|
||||
// DefaultSpeed är spelarens grundfart i rutor per sekund.
|
||||
@@ -306,7 +308,7 @@ func NewRun(l *level.Level, o RunOptions) *Run {
|
||||
TransferAt: -1,
|
||||
inside: l.InsideGates(),
|
||||
NoExitAt: -1,
|
||||
Party: NewParty(l.Party, l.SpawnPoint(), o.Seed.Derive("party")),
|
||||
Party: NewParty(l.Party+o.ExtraParty, l.SpawnPoint(), o.Seed.Derive("party")),
|
||||
baseSpeed: o.Speed,
|
||||
NoticeAt: -1,
|
||||
Stats: CombinedStats(o.Debuffs),
|
||||
|
||||
@@ -234,5 +234,15 @@
|
||||
"hud.staff_prompt": "Enter: call the staff to get out (%.0f s)",
|
||||
"hud.staff_busy": "Staff on the way...",
|
||||
"hud.staff_unlocked": "The staff unlocked the cards - you can leave.",
|
||||
"hud.unlocked_prompt": "Cards unlocked: you can leave. Entering again costs again."
|
||||
"hud.unlocked_prompt": "Cards unlocked: you can leave. Entering again costs again.",
|
||||
"map.endless": "O: endless mode",
|
||||
"map.endless_best": "best %d points, %d goals",
|
||||
"map.ngplus": "G: New Game+",
|
||||
"map.ngplus_level": "NG+%d",
|
||||
"map.ngplus_confirm": "Press G again: the story restarts with skills and XP kept. Items, stars and money reset, the party grows.",
|
||||
"result.endless_won": "Goal %d done! %d points in total.",
|
||||
"result.endless_lost": "Endless mode over after %d goals: %d points (best %d).",
|
||||
"result.endless_best": "New best!",
|
||||
"result.hint_endless": "Enter: next goal L: words Esc: end the run",
|
||||
"result.hint_endless_over": "Enter/Esc: level map L: words"
|
||||
}
|
||||
|
||||
@@ -234,5 +234,15 @@
|
||||
"hud.staff_prompt": "Enter: kalla på personal för att komma ut (%.0f s)",
|
||||
"hud.staff_busy": "Personalen kommer...",
|
||||
"hud.staff_unlocked": "Personalen låste upp korten - du kan gå ut.",
|
||||
"hud.unlocked_prompt": "Korten är upplåsta: du kan gå ut. Ny inpassering kostar igen."
|
||||
"hud.unlocked_prompt": "Korten är upplåsta: du kan gå ut. Ny inpassering kostar igen.",
|
||||
"map.endless": "O: oändligt läge",
|
||||
"map.endless_best": "bästa %d poäng, %d mål",
|
||||
"map.ngplus": "G: New Game+",
|
||||
"map.ngplus_level": "NG+%d",
|
||||
"map.ngplus_confirm": "Tryck G igen: storyn börjar om med skills och XP kvar. Items, stjärnor och pengar nollställs, sällskapet blir större.",
|
||||
"result.endless_won": "Mål %d klart! Totalt %d poäng.",
|
||||
"result.endless_lost": "Oändligt läge slut efter %d mål: %d poäng (bästa %d).",
|
||||
"result.endless_best": "Ny bästa!",
|
||||
"result.hint_endless": "Enter: nästa mål L: ordlista Esc: avsluta omgången",
|
||||
"result.hint_endless_over": "Enter/Esc: bankartan L: ordlista"
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ func demoSave(path string) error {
|
||||
st.Progress.Equipped["body"] = "skin_salaryman"
|
||||
st.Progress.Equipped["feet"] = "geta"
|
||||
st.Progress.Skills["focus1"] = true
|
||||
st.Progress.StoryCleared = true // bankartan visar oändligt läge och New Game+
|
||||
st.AddEndless(save.EndlessRecord{Score: 2140, Goals: 4})
|
||||
return save.Store(path, st)
|
||||
}
|
||||
|
||||
@@ -165,6 +167,7 @@ func buildShots(levelsDir string) error {
|
||||
[]string{"-seed", "7", "-mode", "list", "-know", "上野,番線,方面,乗り換え"}},
|
||||
{"", "wait 150; shot $OUT/ending.png; press enter; wait 5; shot $OUT/ending-summary.png; press enter; wait 250; shot $OUT/credits.png; quit", []string{"-ending", "-save", "$DEMO"}},
|
||||
{"007-akihabara", toKiosk7 + "; urge Ville; press enter; wait 5; press right; wait 3; shot $OUT/payer.png; quit", []string{"-seed", "7"}},
|
||||
{"", "wait 10; shot $OUT/endless.png; quit", []string{"-endless", "-seed", "7", "-mode", "list", "-save", "$DEMO"}},
|
||||
{"005-ueno", toPlatform5 + "; " + toGateBack + "; wait 5; push " + pushOut + " 30; wait 3; shot $OUT/no-exit.png; press enter; wait 150; shot $OUT/staff.png; quit", []string{"-seed", "7"}},
|
||||
}
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user