diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ed4d6a..08a7d9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Ändringslogg +## Oreleaset + +- Titelskärm med premissen, språkval första gången, tutorial i fem sidor + (skyltläsning med markering av resmålet), kontextuella tips i första + banan, pausmeny på Esc, Avsluta i menyerna, kortare texter på bankartan. + ## v0.1.0 – 2026-10-25 Första versionerade releasen: hela storyn från Narita till Narita. diff --git a/README.md b/README.md index 3cc141b..dbdfa92 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,16 @@ 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). Version 0.1.0 har ljud (stationsmelodier, effekter, menymusik), inställningar (S: volymer, språk sv/en, skala, fullskärm) och -fönsterikon. Kvar i roadmapen är M10b (bussar). Se +fönsterikon. Efter Björns speltest kom titelskärm med premissen, språkval +första gången, en tutorial som lär ut hur man läser skyltarna (resmålet +markerat, resten nedtonat) med tips i första banan, pausmeny på Esc och +ett sätt att avsluta spelet. Kvar i roadmapen är M10b (bussar). Se [doc/roadmap.md](doc/roadmap.md) och [CHANGELOG.md](CHANGELOG.md). +![Titelskärmen](doc/img/title.png) + +![Tutorial: så läser du en skylt](doc/img/tutorial.png) + ![Bankartan](doc/img/map.png) ![Skylt på japanska med delvis översättning](doc/img/sign.png) @@ -207,7 +214,9 @@ döljer/visar minikartan, P på kartan lägger kortet i telefonen, N på kartan 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, S på kartan öppnar inställningarna (volymer, språk, skala, fullskärm), F11 växlar fullskärm -överallt, Esc går tillbaka, R spelar om. Gamepad +överallt. Esc pausar banan (Fortsätt, Lämna banan, Inställningar, Avsluta +spelet), på bankartan går Esc till titelskärmen, och Avsluta kräver Enter +två gånger. R spelar om från resultatskärmen. Gamepad med standardlayout fungerar: d-pad/vänster spak, A = välj, B = tillbaka, Y = hjälp, X = ordlista, Back/Select = karta. diff --git a/doc/img/items.png b/doc/img/items.png index a219c8e..b59784b 100644 Binary files a/doc/img/items.png and b/doc/img/items.png differ diff --git a/doc/img/lang.png b/doc/img/lang.png new file mode 100644 index 0000000..81452da Binary files /dev/null and b/doc/img/lang.png differ diff --git a/doc/img/map.png b/doc/img/map.png index 3a6e5fb..2beb89b 100644 Binary files a/doc/img/map.png and b/doc/img/map.png differ diff --git a/doc/img/pause.png b/doc/img/pause.png new file mode 100644 index 0000000..3f8e45f Binary files /dev/null and b/doc/img/pause.png differ diff --git a/doc/img/skills.png b/doc/img/skills.png index 33ad79e..32438e1 100644 Binary files a/doc/img/skills.png and b/doc/img/skills.png differ diff --git a/doc/img/tips-sign.png b/doc/img/tips-sign.png new file mode 100644 index 0000000..d1de1bd Binary files /dev/null and b/doc/img/tips-sign.png differ diff --git a/doc/img/tips.png b/doc/img/tips.png new file mode 100644 index 0000000..9b7d4cb Binary files /dev/null and b/doc/img/tips.png differ diff --git a/doc/img/title.png b/doc/img/title.png new file mode 100644 index 0000000..80cb06c Binary files /dev/null and b/doc/img/title.png differ diff --git a/doc/img/tutorial.png b/doc/img/tutorial.png new file mode 100644 index 0000000..e72bc8d Binary files /dev/null and b/doc/img/tutorial.png differ diff --git a/doc/img/words.png b/doc/img/words.png index 1910f4c..952d23d 100644 Binary files a/doc/img/words.png and b/doc/img/words.png differ diff --git a/doc/mal-och-tester.md b/doc/mal-och-tester.md index 625a7a7..e59ab66 100644 --- a/doc/mal-och-tester.md +++ b/doc/mal-och-tester.md @@ -139,6 +139,7 @@ att bygga på Linux som på Windows. | `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/game` | `TestHighlightDestination` | **M13:** resmålets tecken får markeringsfärg, resten nedtonas, flera träffar, ingen träff | | `internal/game` | `TestAllSoundsDecode`, `TestMelodyForIsStableAndKnown` | **M12:** alla ljud i `AllSounds` finns, avkodas och är minst 50 ms; melodin per bana är stabil och känd; ljudlagret är nil-säkert | | `internal/save` | `TestSettingsDefaultsAndNormalize` | **M12:** standardinställningar, klampning av volymer/skala, okänt språk blir sv, saknade inställningar fylls i | | `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 | @@ -167,6 +168,7 @@ att bygga på Linux som på Windows. | ~~M10 Story-innehåll~~ ✓ | Kedjorna valideras, byten hänger ihop, storyn börjar och slutar på flygplatsen, bytet för med sig tid/plånbok/sällskap – finns, se tabellen ovan; `tools/levelcheck` som manuell kontroll | | ~~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 | +| ~~M13 Titel, tutorial, trygghet~~ ✓ | Markeringen av resmålet i skylttext (`game.TestHighlightDestination`); titel/språkval/paus/tutorial/tips med skärmdumpar; karttexter ryms | | 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, 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, stationsmelodi per bana, inställningar normaliseras – finns, se tabellen ovan; Windows-binär startar (manuellt, kvar) | @@ -182,7 +184,9 @@ En milstolpe är klar när: ### Manuell testchecklista (speltest) -- [ ] Spelet startar till meny på under 2 s; ESC/avsluta fungerar. +- [ ] Spelet startar till titelskärmen på under 2 s; första gången frågas efter språk; Avsluta kräver Enter två gånger. +- [ ] Esc i en bana öppnar pausmenyn och lämnar aldrig banan av misstag; Esc på bankartan går till titeln. +- [ ] Tutorialen går att läsa igenom; tipsen i bana 001 dyker upp vid start, skylt, spärr och perrong. - [ ] Fönstret kan skalas; pixlarna är skarpa (heltalsskalning). - [ ] Bana 001 går att klara i både listläge och skyltläge. - [ ] Fel tåg ger tydlig förlust med förklaring. diff --git a/doc/roadmap.md b/doc/roadmap.md index 846aa92..51043aa 100644 --- a/doc/roadmap.md +++ b/doc/roadmap.md @@ -646,6 +646,44 @@ 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. ✓ +### M13 – Titelskärm, tutorial och trygghet ✓ (klar 2026-10-25) + +- **Mål:** Björns feedback efter v0.1.0 (2026-10-25): spelet ska gå att + förstå och använda utan att läsa dokumentationen. +- **Titelskärm** som beskriver premissen i text, med Fortsätt/Starta, + Tutorial, Inställningar och **Avsluta** (ett sätt att stänga spelet + inifrån). Första gången spelet startar får man **välja språk**. +- **Tutorial** om grundmekaniken: målet, skyltarna, kort/biljetter/spärrar, + sällskapet, tåget, lägena och tangenterna. Tips och trix: **hur man läser + en skylt** och vad man ska memorera (resmålets tecken i HUD:en) för att + köpa rätt biljett – med resmålet **markerat** och all annan text + **nedtonad**. Dessutom kontextuella tips i spelet tills första banan är + klarad, och samma markering på riktiga skyltar under tiden. +- **Esc-skydd:** Esc i en bana öppnar en pausmeny (Fortsätt, Lämna banan, + Inställningar, Avsluta spelet) i stället för att lämna banan direkt; + många Esc i rad växlar bara menyn. Esc på bankartan går till + titelskärmen; Avsluta kräver Enter två gånger. +- **Karttexten** får plats på bredden på svenska (kortare lägestexter). +- **Teknik:** `TitleScene`, `LangScene`, `PauseScene`, `TutorialScene`, + `tips.go`, `highlightLine` i `text.go`. +- **Så blev det:** `TitleScene` (premiss, rullande tåg på räls, meny + Fortsätt/Starta – Tutorial – Inställningar – Avsluta med Enter två + gånger, version), `LangScene` första gången sparfilen saknas (svenska/ + engelska), `PauseScene` på Esc i banan (Fortsätt, Lämna banan, + Inställningar, Avsluta spelet; Esc igen fortsätter – många Esc i rad + växlar bara menyn), Esc på bankartan går till titeln. `TutorialScene` i + fem sidor med riktiga sprites och en exempelskylt där resmålet är + **markerat** och resten **nedtonad** (`highlightLine`), plus 番線/方面/ + 改札-nyckeln och biljettipset. I spelet visas fem kontextuella tips + (start, första skylten, sällskapets behov, spärren, perrongen) tills bana + 001 är klarad, och under tiden markeras resmålets tecken på skyltar och + avgångstavlor. Lägestexterna på bankartan kortades så att raderna ryms. +- **Klar när:** titel, språkval, pausmeny och tutorial finns med + skärmdumpar; markeringen är testad (`game.TestHighlightDestination`); + karttexterna ryms (skärmdump). ✓ +- **Kvar till senare:** en spelbar tutorialbana med styrda steg, + tips som kan slås av i inställningarna. + ### 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. diff --git a/doc/spelbeskrivning.md b/doc/spelbeskrivning.md index 2ae16de..2b6bb2b 100644 --- a/doc/spelbeskrivning.md +++ b/doc/spelbeskrivning.md @@ -50,6 +50,20 @@ otydligt står tolkningen i [§15](#15-tolkningar-av-originalet). - Spela om storyn och behåll alla dina skills. +### 3.4 Titelskärm, tutorial och trygghet (tillägg 2026-10-25) + +- En **titelskärm** med spelets premiss i text, och val: fortsätt/starta, + tutorial, inställningar, avsluta. **Första gången** spelet startas väljer + man språk. +- En **tutorial** om grundmekaniken med tips och trix: **hur man läser + skyltar** och **vad man bör memorera** (resmålets tecken) för att kunna + köpa rätt biljett – gärna med **markering** av det viktiga och + **nedtoning** av all annan information. +- **Esc-skydd:** trycker man Esc (flera gånger) ska en fråga dyka upp så att + man inte råkar lämna/starta om banan av misstag. Det ska också gå att + **stänga spelet** inifrån. +- Texten på bankartan ska få plats på bredden på svenska. + ## 4. En bana – kärnloopen 1. Du startar på en station (eller kliver av ett tåg). diff --git a/internal/game/game.go b/internal/game/game.go index 530ce7f..1136ca9 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -162,8 +162,10 @@ func New(opt Options) (*Game, error) { return nil, err } g.scene = s + case !exists: + g.scene = &LangScene{} // första starten: välj språk, sedan titelskärmen default: - g.scene = NewMapScene(g) + g.scene = NewTitleScene() } return g, nil } @@ -284,8 +286,8 @@ func (g *Game) Save() { // till banan och tystnar när man lämnar den. func (g *Game) SetScene(s Scene) { switch s.(type) { - case *LevelScene, *ResultScene: // banans sorl fortsätter - case *MapScene, *SettingsScene, *EndingScene, *WordsScene, *SkillsScene, *ItemsScene: + case *LevelScene, *ResultScene, *PauseScene: // banans sorl fortsätter + case *MapScene, *SettingsScene, *EndingScene, *WordsScene, *SkillsScene, *ItemsScene, *TitleScene, *LangScene, *TutorialScene: g.Audio.Loop(SoundMusicMap, 0.6) // menymusiken (fortsätter om den redan spelas) default: g.Audio.StopLoop() diff --git a/internal/game/scene_level.go b/internal/game/scene_level.go index 7952c2f..73c44db 100644 --- a/internal/game/scene_level.go +++ b/internal/game/scene_level.go @@ -38,14 +38,17 @@ const ( // LevelScene är själva spelandet av en bana. All spellogik ligger i sim.Run; // scenen översätter input och ritar. 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) - sfx sfxState - cam *sim.Camera - animT float64 - hideMap bool // M döljer/visar minikartan + 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) + sfx sfxState + tips tipState + highlight bool // tutorialen: resmålets tecken markeras på skyltar, resten tonas ned + infoBox image.Rectangle // inforutans läge denna bildruta (tom = ingen), så att tipsen inte täcker den + cam *sim.Camera + animT float64 + hideMap bool // M döljer/visar minikartan shop *shop // öppen butik i kiosken (pausar banan) outcome LevelOutcome // progression från banan (fylls i när den avgjorts) @@ -185,6 +188,8 @@ func (s *LevelScene) Update(g *Game) error { } s.updateEffects(g) s.updateSfx(g) + s.updateTips(g) + s.highlight = s.tutorialActive(g) if s.run.Mini != nil { // Minispel: all input går dit; Esc avbryter minispelet, inte banan. mx, my := miniMoveXY() @@ -199,7 +204,7 @@ func (s *LevelScene) Update(g *Game) error { return nil } if backJustPressed() { - g.SetScene(NewMapScene(g)) + g.SetScene(NewPauseScene(s)) // Esc pausar; att lämna banan kräver ett val i menyn return nil } if shopJustPressed() && !s.run.Player.Moving() && s.run.Level.At(s.run.Player.Tile) == level.Kiosk { @@ -353,6 +358,7 @@ func (s *LevelScene) Draw(g *Game, screen *ebiten.Image) { s.drawIndicator(screen, ox, oy) s.drawHUD(g, screen) + s.drawTip(g, screen) if r := s.run; r.TransferAt >= 0 && r.Time-r.TransferAt < transferBannerSec && r.Mini == nil && r.Outcome == sim.Ongoing { txt := g.Assets.T("hud.transfer_done") w := g.Assets.Width(txt) + 16 @@ -517,6 +523,7 @@ func (s *LevelScene) drawHUD(g *Game, screen *ebiten.Image) { } box = image.Rect(0, y, ScreenW, y+h) } + s.infoBox = box if !s.hideMap { s.drawMinimap(g, screen, box, hasBox) } @@ -553,7 +560,11 @@ func (s *LevelScene) infoLines(a *Assets) (lines []line, accent color.Color, bar if tr, at, secs := r.PlatformHere(); tr != nil { // Avgångstavlan är en skylt som alla andra: japanska, sedd genom kunskapen. rd := lang.Render(a.Dict, r.Know, tr.BoardText(), UILang) - lines = append(lines, a.wrapLine(shownLine(rd.Original, colText, r.Know.ShowsReadings()), maxW)...) + if s.highlight { + lines = append(lines, a.wrapLine(highlightLine(tr.BoardText(), lvl.Destination.Get("ja"), colGood, colDim), maxW)...) + } else { + lines = append(lines, a.wrapLine(shownLine(rd.Original, colText, r.Know.ShowsReadings()), maxW)...) + } lines = append(lines, a.wrapLine(shownLine(rd.Partial, colText, false), maxW)...) if english { lines = append(lines, a.wrapLine(plainLine(fmt.Sprintf("%s: Track %s %s for %s", a.T("hud.english"), tr.Track, @@ -594,7 +605,11 @@ func (s *LevelScene) infoLines(a *Assets) (lines []line, accent color.Color, bar accent = colKnown } rd := lang.Render(a.Dict, know, sg.Text["ja"], UILang) - lines = append(lines, a.wrapLine(shownLine(rd.Original, colText, r.Know.ShowsReadings()), maxW)...) + if s.highlight { + lines = append(lines, a.wrapLine(highlightLine(sg.Text["ja"], lvl.Destination.Get("ja"), colGood, colDim), maxW)...) + } else { + lines = append(lines, a.wrapLine(shownLine(rd.Original, colText, r.Know.ShowsReadings()), maxW)...) + } lines = append(lines, a.wrapLine(shownLine(rd.Partial, colText, false), maxW)...) if english { lines = append(lines, a.wrapLine(plainLine(a.T("hud.english")+": "+sg.Text.Get("en"), colEnglish), maxW)...) diff --git a/internal/game/scene_map.go b/internal/game/scene_map.go index 5ed23a0..4278426 100644 --- a/internal/game/scene_map.go +++ b/internal/game/scene_map.go @@ -91,7 +91,8 @@ func (m *MapScene) current() *level.Level { func (m *MapScene) Update(g *Game) error { switch { case backJustPressed(): - return ebiten.Termination + g.SetScene(NewTitleScene()) + return nil case wordsJustPressed(): g.SetScene(NewWordsScene(g.Assets, m)) return nil diff --git a/internal/game/scene_title.go b/internal/game/scene_title.go new file mode 100644 index 0000000..3339b50 --- /dev/null +++ b/internal/game/scene_title.go @@ -0,0 +1,211 @@ +package game + +import ( + "fmt" + "image/color" + "math" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/vector" + + "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/buildinfo" +) + +// TitleScene är titelskärmen: premissen i text och menyn Fortsätt/Starta, +// Tutorial, Inställningar, Avsluta (Enter två gånger). +type TitleScene struct { + sel int + t float64 + confirm bool // Avsluta valt en gång +} + +// NewTitleScene öppnar titelskärmen. +func NewTitleScene() *TitleScene { return &TitleScene{} } + +const titleItems = 4 + +func (s *TitleScene) Update(g *Game) error { + s.t += dt + if mv := menuMove(); mv != 0 { + s.sel = (s.sel + mv + titleItems) % titleItems + s.confirm = false + } + if backJustPressed() { + s.confirm = false + } + if !confirmJustPressed() { + return nil + } + g.Audio.Play(SoundConfirm, 0.6) + switch s.sel { + case 0: + g.SetScene(NewMapScene(g)) + case 1: + g.SetScene(NewTutorialScene(s)) + case 2: + g.SetScene(NewSettingsScene(s)) + case 3: + if s.confirm { + return ebiten.Termination + } + s.confirm = true + } + return nil +} + +func (s *TitleScene) Draw(g *Game, screen *ebiten.Image) { + a := g.Assets + screen.Fill(color.RGBA{24, 20, 37, 255}) + // Räls längs nederkanten med ett tåg som rullar förbi. + railY := ScreenH - 28 + for x := 0; x < ScreenW; x += TileSize { + var op ebiten.DrawImageOptions + op.GeoM.Translate(float64(x), float64(railY)) + screen.DrawImage(a.Rails, &op) + } + tx := math.Mod(s.t*40, float64(ScreenW+6*TileSize)) - 3*TileSize + for i := 0; i < 3; i++ { + var op ebiten.DrawImageOptions + op.GeoM.Translate(tx+float64(i*TileSize), float64(railY-TileSize+4)) + screen.DrawImage(a.Train, &op) + } + a.DrawBig(screen, a.T("game.title"), 16, 8, colKnown) + ver := fmt.Sprintf("%s (%s)", buildinfo.Version, buildinfo.Mode) + a.Draw(screen, ver, ScreenW-8-a.Width(ver), 10, colDim) + y := 30 + for _, l := range a.wrapLine(plainLine(a.T("title.premise"), colText), ScreenW-32) { + a.drawLines(screen, []line{l}, 16, y) + y += LineH + } + start := a.T("menu.continue") + if g.Progress.TotalStars() == 0 && len(g.Progress.Levels) == 0 { + start = a.T("menu.start") + } + items := [titleItems]string{start, a.T("title.tutorial"), a.T("menu.settings"), a.T("menu.quit")} + y += 6 + for i, it := range items { + col := colText + if i == s.sel { + vector.DrawFilledRect(screen, 12, float32(y-2), 130, LineH, colBar, false) + col = colKnown + a.Draw(screen, "▶", 14, y, colKnown) + } + a.Draw(screen, it, 26, y, col) + y += LineH + } + if s.confirm { + a.Draw(screen, a.T("title.quit_confirm"), 150, y-LineH, colWarn) + } + a.Draw(screen, a.T("title.hint"), 8, ScreenH-12, colDim) +} + +// LangScene visas första gången spelet startar: välj språk. +type LangScene struct{ sel int } + +var langChoices = []string{"sv", "en"} + +func (s *LangScene) Update(g *Game) error { + if mv := menuMove(); mv != 0 { + s.sel = (s.sel + mv + len(langChoices)) % len(langChoices) + } + if confirmJustPressed() { + g.State.Settings.Lang = langChoices[s.sel] + g.applySettings() + g.Save() + g.Audio.Play(SoundConfirm, 0.6) + g.SetScene(NewTitleScene()) + } + return nil +} + +func (s *LangScene) Draw(g *Game, screen *ebiten.Image) { + a := g.Assets + screen.Fill(color.RGBA{24, 20, 37, 255}) + a.DrawBig(screen, a.T("game.title"), 16, 10, colKnown) + a.Draw(screen, "Välj språk / Choose language", 16, 50, colText) + y := 72 + for i, l := range langChoices { + col := colText + if i == s.sel { + vector.DrawFilledRect(screen, 12, float32(y-2), 130, LineH+2, colBar, false) + col = colKnown + a.Draw(screen, "▶", 14, y, colKnown) + } + a.Draw(screen, a.T("lang."+l), 26, y, col) + y += LineH + 1 + } + a.Draw(screen, "Pilar + Enter / Arrows + Enter", 16, y+8, colDim) +} + +// PauseScene är pausmenyn i en bana (Esc): Fortsätt, Lämna banan, +// Inställningar, Avsluta spelet (Enter två gånger). Esc igen fortsätter. +type PauseScene struct { + from *LevelScene + sel int + confirm bool +} + +const pauseItems = 4 + +// NewPauseScene pausar banan. +func NewPauseScene(from *LevelScene) *PauseScene { return &PauseScene{from: from} } + +func (s *PauseScene) Update(g *Game) error { + if backJustPressed() { + g.SetScene(s.from) + return nil + } + if mv := menuMove(); mv != 0 { + s.sel = (s.sel + mv + pauseItems) % pauseItems + s.confirm = false + } + if !confirmJustPressed() { + return nil + } + g.Audio.Play(SoundConfirm, 0.6) + switch s.sel { + case 0: + g.SetScene(s.from) + case 1: + if s.from.endless { + g.endEndless() + } + g.SetScene(NewMapScene(g)) + case 2: + g.SetScene(NewSettingsScene(s)) + case 3: + if s.confirm { + return ebiten.Termination + } + s.confirm = true + } + return nil +} + +func (s *PauseScene) Draw(g *Game, screen *ebiten.Image) { + s.from.Draw(g, screen) + a := g.Assets + vector.DrawFilledRect(screen, 0, 0, ScreenW, ScreenH, color.RGBA{0, 0, 0, 120}, false) + vector.DrawFilledRect(screen, 40, 40, ScreenW-80, 104, colPanel, false) + vector.DrawFilledRect(screen, 40, 40, ScreenW-80, 2, colWarn, false) + a.DrawBig(screen, a.T("pause.title"), 48, 46, colText) + items := [pauseItems]string{a.T("pause.resume"), a.T("pause.leave"), a.T("menu.settings"), a.T("pause.quit")} + y := 68 + for i, it := range items { + col := colText + if i == s.sel { + vector.DrawFilledRect(screen, 44, float32(y-2), ScreenW-88, LineH+2, colBar, false) + col = colKnown + } + a.Draw(screen, it, 50, y, col) + y += LineH + 2 + } + msg := a.T("pause.hint") + if s.confirm { + msg = a.T("pause.quit_confirm") + } + for _, l := range a.wrapLine(plainLine(msg, colDim), ScreenW-96) { + a.drawLines(screen, []line{l}, 48, y+2) + y += LineH + } +} diff --git a/internal/game/scene_tutorial.go b/internal/game/scene_tutorial.go new file mode 100644 index 0000000..45156a0 --- /dev/null +++ b/internal/game/scene_tutorial.go @@ -0,0 +1,92 @@ +package game + +import ( + "fmt" + "image/color" + "strings" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/vector" + + "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level" +) + +// TutorialScene är genomgången av grundmekaniken i fem sidor: målet och +// stationen, hur man läser en skylt (med resmålet markerat och resten +// nedtonat), kort/biljetter/spärrar, sällskapet och tåget, lägen och +// tangenter. ←/→ bläddrar, Esc går tillbaka. +type TutorialScene struct { + back Scene + page int +} + +const tutorialPages = 5 + +// NewTutorialScene öppnar tutorialen. +func NewTutorialScene(back Scene) *TutorialScene { return &TutorialScene{back: back} } + +func (s *TutorialScene) Update(g *Game) error { + if backJustPressed() { + g.SetScene(s.back) + return nil + } + dx, _ := miniMoveXY() + if confirmJustPressed() { + dx = 1 + } + if dx != 0 { + s.page += dx + if s.page >= tutorialPages { + g.SetScene(s.back) + return nil + } + if s.page < 0 { + s.page = 0 + } + g.Audio.Play(SoundConfirm, 0.4) + } + return nil +} + +func (s *TutorialScene) Draw(g *Game, screen *ebiten.Image) { + a := g.Assets + screen.Fill(color.RGBA{24, 20, 37, 255}) + vector.DrawFilledRect(screen, 0, 0, ScreenW, 22, color.RGBA{38, 43, 68, 255}, false) + key := fmt.Sprintf("tutorial.p%d", s.page+1) + a.DrawBig(screen, a.T(key+".title"), 8, 5, colText) + a.Draw(screen, fmt.Sprintf(a.T("tutorial.page"), s.page+1, tutorialPages), ScreenW-70, 8, colDim) + y := 28 + for _, para := range strings.Split(a.T(key), "\n") { + for _, l := range a.wrapLine(plainLine(para, colText), ScreenW-16) { + a.drawLines(screen, []line{l}, 8, y) + y += LineH + } + y += 3 + } + switch s.page { + case 0: // rutlegend med riktiga sprites + x := 12 + for _, t := range []struct { + tile level.Tile + key string + }{{level.Sign, "tile.sign"}, {level.Gate, "tile.gate"}, {level.TicketMachine, "tile.machine"}, {level.Toilet, "tile.toilet"}, {level.Kiosk, "tile.kiosk"}, {level.Platform, "tile.platform"}} { + var op ebiten.DrawImageOptions + op.GeoM.Scale(2, 2) + op.GeoM.Translate(float64(x), float64(y)) + screen.DrawImage(a.Tiles[t.tile], &op) + a.Draw(screen, a.T(t.key), x, y+2*TileSize+2, colDim) + x += 52 + } + case 1: // exempelskylten: resmålet markerat, resten nedtonat + sample := "14番線 山手線 渋谷方面 → 南東口 改札" + body := append([]line{highlightLine(sample, "渋谷", colGood, colDim)}, a.wrapLine(line{{"→ ", colDim}, {a.T("tutorial.p2.sample"), colText}}, ScreenW-28)...) + vector.DrawFilledRect(screen, 8, float32(y), ScreenW-16, float32(LineH*len(body)+8), colPanel, false) + a.drawLines(screen, body, 14, y+4) + y += LineH*len(body) + 12 + for _, l := range a.wrapLine(plainLine(a.T("tutorial.p2b"), colText), ScreenW-16) { + a.drawLines(screen, []line{l}, 8, y) + y += LineH + } + } + a.Draw(screen, a.T("tutorial.hint"), 8, ScreenH-12, colDim) +} diff --git a/internal/game/text.go b/internal/game/text.go index 547db1b..ecabef9 100644 --- a/internal/game/text.go +++ b/internal/game/text.go @@ -135,3 +135,26 @@ func (a *Assets) drawLines(dst drawTarget, lines []line, x, y int) { } } } + +// highlightLine är tutorialens sätt att läsa en skylt: resmålets japanska +// tecken i markeringsfärg, allt annat nedtonat. +func highlightLine(text, needle string, hi, dim color.Color) line { + if needle == "" || !strings.Contains(text, needle) { + return plainLine(text, dim) + } + var l line + for { + i := strings.Index(text, needle) + if i < 0 { + if text != "" { + l = append(l, span{text, dim}) + } + return l + } + if i > 0 { + l = append(l, span{text[:i], dim}) + } + l = append(l, span{needle, hi}) + text = text[i+len(needle):] + } +} diff --git a/internal/game/text_test.go b/internal/game/text_test.go new file mode 100644 index 0000000..e53ab6d --- /dev/null +++ b/internal/game/text_test.go @@ -0,0 +1,16 @@ +package game + +import "testing" + +func TestHighlightDestination(t *testing.T) { + l := highlightLine("14番線 山手線 渋谷方面 → 南東", "渋谷", colGood, colDim) + if len(l) != 3 || l[0].text != "14番線 山手線 " || l[1].text != "渋谷" || l[1].col != colGood || l[2].col != colDim { + t.Fatalf("spans = %+v", l) + } + if l := highlightLine("渋谷 渋谷", "渋谷", colGood, colDim); len(l) != 3 || l[0].col != colGood || l[2].col != colGood { + t.Fatalf("flera träffar: %+v", l) + } + if l := highlightLine("東京方面", "渋谷", colGood, colDim); len(l) != 1 || l[0].col != colDim { + t.Fatalf("ingen träff: allt nedtonat, fick %+v", l) + } +} diff --git a/internal/game/tips.go b/internal/game/tips.go new file mode 100644 index 0000000..9e81bf7 --- /dev/null +++ b/internal/game/tips.go @@ -0,0 +1,85 @@ +package game + +import ( + "strings" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/vector" +) + +// Tutorialtips i spelet: visas en gång var, i ordning när villkoret +// uppfylls, tills första banan är klarad. {dest} och {ja} byts mot +// resmålet i latinska bokstäver respektive japanska tecken. +type tipDef struct { + key string + when func(s *LevelScene) bool +} + +var tutorialTips = []tipDef{ + {"tip.start", func(s *LevelScene) bool { return true }}, + {"tip.sign", func(s *LevelScene) bool { return s.run.SignHere() != nil }}, + {"tip.party", func(s *LevelScene) bool { return len(s.run.Party.Urges()) > 0 }}, + {"tip.gate", func(s *LevelScene) bool { return s.run.DeniedAt >= 0 || s.run.GatePasses > 0 }}, + {"tip.platform", func(s *LevelScene) bool { tr, _, _ := s.run.PlatformHere(); return tr != nil }}, +} + +const tipShowSec = 7.0 + +type tipState struct { + shown map[string]bool + current string + since float64 +} + +// tutorialActive: tipsen och skyltmarkeringen gäller tills första banan är +// klarad (och aldrig i oändligt läge). +func (s *LevelScene) tutorialActive(g *Game) bool { + return !s.endless && !g.Progress.Cleared("001-flygplatsen") +} + +// updateTips väljer nästa tips som ska visas. +func (s *LevelScene) updateTips(g *Game) { + if !s.tutorialActive(g) { + return + } + r := s.run + t := &s.tips + if t.shown == nil { + t.shown = map[string]bool{} + } + if t.current != "" && r.Time-t.since < tipShowSec { + return + } + t.current = "" + for _, d := range tutorialTips { + if !t.shown[d.key] && d.when(s) { + t.shown[d.key] = true + t.current, t.since = d.key, r.Time + return + } + } +} + +// tipText fyller i resmålet i ett tips. +func (s *LevelScene) tipText(a *Assets, key string) string { + d := s.run.Level.Destination + return strings.NewReplacer("{dest}", d.Get("romaji"), "{ja}", d.Get("ja")).Replace(a.T(key)) +} + +// drawTip ritar det aktuella tipset under HUD:en. +func (s *LevelScene) drawTip(g *Game, screen *ebiten.Image) { + t := &s.tips + if t.current == "" || s.run.Mini != nil || s.run.Outcome != 0 { + return + } + a := g.Assets + lines := a.wrapLine(highlightLine(s.tipText(a, t.current), s.run.Level.Destination.Get("ja"), colGood, colText), ScreenW-16) + h := 6 + LineH*len(lines) + y := 26 + if s.infoBox.Dy() > 0 && s.infoBox.Min.Y < ScreenH/2 { + y = s.infoBox.Max.Y + 2 // inforutan ligger upptill: lägg tipset under den + } + vector.DrawFilledRect(screen, 4, float32(y), ScreenW-8, float32(h), colPanel, false) + vector.DrawFilledRect(screen, 4, float32(y), 2, float32(h), colKnown, false) + a.drawLines(screen, lines, 10, y+3) +} diff --git a/resources/text/en.json b/resources/text/en.json index 8265d4f..c26166e 100644 --- a/resources/text/en.json +++ b/resources/text/en.json @@ -15,7 +15,7 @@ "level.failed": "You missed too many trains…", "level.cleared": "Arrived!", "menu.choose_level": "Choose a level:", - "menu.hint": "Arrows/WASD: select Enter: start L: word list Esc: quit", + "menu.hint": "Arrows: choose Enter: select", "hud.time": "Time", "hud.steps": "Steps", "hud.controls": "Esc: menu M: map", @@ -202,8 +202,8 @@ "hud.mode_signs": "Sign mode: work out the train yourself, +50 % score", "hud.terminal": "Lines from here (terminal):", "menu.mode": "N: navigation %s", - "mode.list": "list mode (next train shown)", - "mode.signs": "sign mode (+50 % score)", + "mode.list": "list", + "mode.signs": "signs (+50 %)", "result.bonus": "sign bonus", "result.lost_wrong_direction": "Wrong direction.", "result.lost_too_far": "Too far away.", @@ -235,8 +235,8 @@ "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.", - "map.endless": "O: endless mode", - "map.endless_best": "best %d points, %d goals", + "map.endless": "O: endless", + "map.endless_best": "best %d pts, %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.", @@ -256,5 +256,40 @@ "settings.hint": "Arrows: pick row / change Enter: toggle Esc: back", "settings.keys": "Arrows/WASD walk, Enter select, Esc back, H help, L words, T skills, I wardrobe, B shop, M map, N mode, O endless, G NG+, S settings, F11 fullscreen. Gamepad: A select, B back, Y help, X words, Back map.", "lang.sv": "Svenska", - "lang.en": "English" + "lang.en": "English", + "title.premise": "A few friends are on holiday in Japan. Every station is a maze where almost everything is written in Japanese: read the signs, keep the party fed, watered and out of the toilet queue, find the right platform and board the right train before it leaves. In the end you have to get back to the airport.", + "title.tutorial": "Tutorial", + "title.quit_confirm": "Enter again to quit, Esc cancels.", + "title.hint": "Arrows: choose Enter: select F11: fullscreen", + "pause.title": "Paused", + "pause.resume": "Continue", + "pause.leave": "Leave the level (progress on it is lost)", + "pause.quit": "Quit the game", + "pause.hint": "Esc: continue Enter: select", + "pause.quit_confirm": "Enter again to quit the game, Esc cancels.", + "tutorial.hint": "←/→ or Enter: next page Esc: back", + "tutorial.page": "page %d/%d", + "tutorial.p1.title": "The goal and the station", + "tutorial.p1": "Every level is a station. Your destination is shown in the HUD, in Latin letters and Japanese. Find the platform with the right train, pass the gate and board when the train is in.\nSigns (blue boards on a pole) show the way. This is what a sign, gate, machine, toilet, kiosk and platform look like:", + "tutorial.p2.title": "How to read a sign", + "tutorial.p2": "You do not need to know Japanese. Memorise the characters of your destination in the HUD and look for exactly those characters on the signs. Everything else can fade away:", + "tutorial.p2.sample": "track 14, Yamanote Line, for Shibuya, south-east exit, gate", + "tutorial.p2b": "Three words appear everywhere: 番線 = track, 方面 = for (direction), 改札 = gate. Standing on a sign you can press H to ask someone for help. Words you read often are learned and then translated automatically.", + "tutorial.p3.title": "Cards, tickets and gates", + "tutorial.p3": "The gate charges the fare from your card when you enter the paid area. Empty card? Top up at the machine (T): find the チャージ button and feed notes with Enter.\nOn ticket levels you buy a ticket at the machine: pick the row whose characters match the destination in the HUD, character by character. Everyone in the party needs a ticket.\nWith individual cards everyone needs a balance, and at the kiosk you choose who pays. At some stations the gate will not let you out again: Enter at the gate calls the staff.", + "tutorial.p4.title": "The party and the train", + "tutorial.p4": "Your friends follow you. The meters top right show food, drink and toilet: the kiosk (K) fills food and drink, the toilet (W) solves toilet needs. Hungry, thirsty and desperate friends slow you down.\nWhen the train is in: let people off first, pick a car with the arrows (few people gives a bonus, the pink car is women only) and press Enter at your destination during the ride. Do not ride past it!", + "tutorial.p5.title": "Modes and keys", + "tutorial.p5": "List mode shows which train to take. Sign mode lets you work it out yourself for +50 % score, but do not go the wrong way. The terminal at the machine shows the lines with neighbouring stations.\nArrows/WASD walk, Enter select, Esc pause, H help at a sign, L words, T skills, I wardrobe, B shop at a kiosk, M minimap. On the level map: N mode, S settings, O endless mode, G New Game+.", + "tile.sign": "sign", + "tile.gate": "gate", + "tile.machine": "machine", + "tile.toilet": "toilet", + "tile.kiosk": "kiosk", + "tile.platform": "platform", + "tip.start": "Tip: the destination is {dest}. On the signs it reads {ja} - look for those characters. Blue boards on a pole are signs.", + "tip.sign": "Tip: reading the sign - {ja} is the destination (highlighted), 番線 = track, 方面 = for. Press H to ask for help.", + "tip.party": "Tip: someone in the party needs something. The kiosk (K) gives food and drink, the toilet (W) solves toilet needs.", + "tip.gate": "Tip: the gate charges the fare from your card when you enter. Top up an empty card at the machine (T).", + "tip.platform": "Tip: wait for the train and press Enter when it is in. Let people off, pick a car with the arrows." } diff --git a/resources/text/sv.json b/resources/text/sv.json index 51eea77..db7e306 100644 --- a/resources/text/sv.json +++ b/resources/text/sv.json @@ -15,7 +15,7 @@ "level.failed": "Du missade för många tåg…", "level.cleared": "Framme!", "menu.choose_level": "Välj bana:", - "menu.hint": "Pilar/WASD: välj Enter: starta L: ordlista Esc: avsluta", + "menu.hint": "Pilar: välj Enter: välj", "hud.time": "Tid", "hud.steps": "Steg", "hud.controls": "Esc: meny M: karta", @@ -202,8 +202,8 @@ "hud.mode_signs": "Skyltläge: lista ut tåget själv, +50 % poäng", "hud.terminal": "Linjer härifrån (terminal):", "menu.mode": "N: navigering %s", - "mode.list": "listläge (nästa tåg visas)", - "mode.signs": "skyltläge (+50 % poäng)", + "mode.list": "lista", + "mode.signs": "skyltar (+50 %)", "result.bonus": "skyltbonus", "result.lost_wrong_direction": "Fel håll.", "result.lost_too_far": "För långt bort.", @@ -235,8 +235,8 @@ "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.", - "map.endless": "O: oändligt läge", - "map.endless_best": "bästa %d poäng, %d mål", + "map.endless": "O: oändligt", + "map.endless_best": "bästa %d p, %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.", @@ -256,5 +256,40 @@ "settings.hint": "Pilar: välj rad / ändra Enter: växla Esc: tillbaka", "settings.keys": "Pilar/WASD gå, Enter välj, Esc tillbaka, H hjälp, L ord, T skills, I garderob, B butik, M karta, N läge, O oändligt, G NG+, S inställningar, F11 fullskärm. Gamepad: A välj, B tillbaka, Y hjälp, X ord, Back karta.", "lang.sv": "Svenska", - "lang.en": "English" + "lang.en": "English", + "title.premise": "Några polare på semester i Japan. Varje station är en labyrint där nästan allt står på japanska: läs skyltarna, håll sällskapet mätt, torrt och på toa, hitta rätt perrong och kliv på rätt tåg innan det går. Till slut ska ni hem till flygplatsen.", + "title.tutorial": "Tutorial", + "title.quit_confirm": "Enter igen för att avsluta, Esc avbryter.", + "title.hint": "Pilar: välj Enter: välj F11: fullskärm", + "pause.title": "Paus", + "pause.resume": "Fortsätt", + "pause.leave": "Lämna banan (framsteg på banan försvinner)", + "pause.quit": "Avsluta spelet", + "pause.hint": "Esc: fortsätt Enter: välj", + "pause.quit_confirm": "Enter igen för att avsluta spelet, Esc avbryter.", + "tutorial.hint": "←/→ eller Enter: bläddra Esc: tillbaka", + "tutorial.page": "sida %d/%d", + "tutorial.p1.title": "Målet och stationen", + "tutorial.p1": "Varje bana är en station. Resmålet står uppe i HUD:en, på latinska bokstäver och japanska. Hitta perrongen med rätt tåg, passera spärren och kliv på när tåget står inne.\nSkyltarna (blå tavlor på stolpe) visar vägen. Så här ser skylt, spärr, automat, toalett, kiosk och perrong ut:", + "tutorial.p2.title": "Så läser du en skylt", + "tutorial.p2": "Du behöver inte kunna japanska. Memorera resmålets tecken i HUD:en och leta efter exakt samma tecken på skyltarna. Allt annat kan du tona bort:", + "tutorial.p2.sample": "spår 14, Yamanote-linjen, mot Shibuya, sydöstra utgången, spärr", + "tutorial.p2b": "Tre ord återkommer överallt: 番線 = spår, 方面 = mot (riktning), 改札 = spärr. Står du på en skylt kan du trycka H och fråga någon om hjälp. Ord du läser ofta lär du dig, och då översätts de automatiskt.", + "tutorial.p3.title": "Kort, biljetter och spärrar", + "tutorial.p3": "Spärren drar avgiften från tågkortet när du går in i det betalda området. Tomt kort? Fyll på i automaten (T): hitta knappen チャージ och mata in sedlar med Enter.\nPå biljettbanor köper du biljett i automaten: välj raden vars tecken matchar resmålet i HUD:en, tecken för tecken. Alla i sällskapet behöver biljett.\nMed egna kort måste alla ha saldo, och i kiosken väljer du vem som betalar. På vissa stationer släpper spärren inte ut dig igen: Enter vid spärren kallar på personal.", + "tutorial.p4.title": "Sällskapet och tåget", + "tutorial.p4": "Dina polare följer efter dig. Mätarna uppe till höger visar mat, dryck och toa: kiosken (K) fyller mat och dryck, toaletten (W) löser toabehov. Hungriga, törstiga och kissnödiga saktar ner dig.\nNär tåget står inne: låt folk kliva av först, välj vagn med pilarna (lite folk ger bonus, rosa vagn är bara för kvinnor) och tryck Enter vid ditt resmål under resan. Åk inte förbi!", + "tutorial.p5.title": "Lägen och tangenter", + "tutorial.p5": "Listläge visar vilket tåg du ska ta. Skyltläge låter dig lista ut det själv för +50 % poäng, men åk inte åt fel håll. Terminalen vid automaten visar linjerna med grannstationer.\nPilar/WASD gå, Enter välj, Esc paus, H hjälp vid skylt, L ordlista, T skills, I garderob, B butik vid kiosk, M minikarta. På bankartan: N läge, S inställningar, O oändligt läge, G New Game+.", + "tile.sign": "skylt", + "tile.gate": "spärr", + "tile.machine": "automat", + "tile.toilet": "toalett", + "tile.kiosk": "kiosk", + "tile.platform": "perrong", + "tip.start": "Tips: resmålet är {dest}. På skyltarna står det {ja} - leta efter de tecknen. Blå tavlor på stolpe är skyltar.", + "tip.sign": "Tips: så läser du skylten - {ja} är resmålet (markerat), 番線 = spår, 方面 = mot. Tryck H för att fråga om hjälp.", + "tip.party": "Tips: någon i sällskapet behöver något. Kiosken (K) ger mat och dryck, toaletten (W) löser toabehov.", + "tip.gate": "Tips: spärren drar avgiften från kortet när du går in. Tomt kort fylls på i automaten (T).", + "tip.platform": "Tips: vänta in tåget och tryck Enter när det står inne. Låt folk kliva av, välj vagn med pilarna." } diff --git a/tools/shots/main.go b/tools/shots/main.go index c594764..a6fd27e 100644 --- a/tools/shots/main.go +++ b/tools/shots/main.go @@ -55,7 +55,7 @@ type shot struct { // bilderna överlever ändringar i banorna. var shots []shot -var demo string +var demo, fresh string func buildShots(levelsDir string) error { l, err := level.LoadFile(filepath.Join(levelsDir, "001-flygplatsen.json")) @@ -169,6 +169,8 @@ func buildShots(levelsDir string) error { {"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"}}, {"", "wait 10; press s; wait 5; press down; wait 3; shot $OUT/settings.png; quit", []string{"-save", "$DEMO"}}, + {"", "wait 10; shot $OUT/lang.png; press enter; wait 30; shot $OUT/title.png; press down; press enter; wait 5; press right; wait 5; shot $OUT/tutorial.png; quit", []string{"-save", "$FRESH"}}, + {"001-flygplatsen", "wait 40; shot $OUT/tips.png; " + toSign + "; wait 5; shot $OUT/tips-sign.png; press esc; wait 5; shot $OUT/pause.png; quit", []string{"-seed", "7", "-save", "$FRESH"}}, {"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 @@ -252,6 +254,8 @@ func main() { demo = filepath.Join(root, "build", "demo-save.json") must(os.MkdirAll(filepath.Dir(demo), 0o755)) must(demoSave(demo)) + fresh = filepath.Join(root, "build", "fresh-save.json") // finns inte: första starten (språkval, titel, tips) + os.Remove(fresh) must(buildShots(filepath.Join(root, "resources", "data", "levels"))) build := exec.Command("go", "run", "./tools/build", "-mode", "debug") @@ -280,7 +284,7 @@ func main() { args = append(args, "-level", s.level) } for _, a := range s.args { - args = append(args, strings.ReplaceAll(a, "$DEMO", demo)) + args = append(args, strings.ReplaceAll(strings.ReplaceAll(a, "$DEMO", demo), "$FRESH", fresh)) } run := exec.Command(exe, args...) run.Dir, run.Stdout, run.Stderr = filepath.Dir(exe), os.Stdout, os.Stderr