M13: titelskärm, språkval, tutorial, tips och pausmeny
All checks were successful
release / build-release (push) Successful in 1m40s

Björns feedback efter v0.1.0. Titelskärm med premissen i text och menyn
Fortsätt/Starta, Tutorial, Inställningar, Avsluta (Enter två gånger);
språkval första gången spelet startar. Tutorial i fem sidor med riktiga
sprites och en exempelskylt där resmålet markeras och resten tonas ned,
plus nyckeln 番線/方面/改札 och biljettipset. Kontextuella tips i spelet
tills första banan är klarad, och resmålets tecken markeras på skyltar
och avgångstavlor under tiden. 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; Esc på bankartan går till titeln. Kortare lägestexter så att
bankartans rader ryms på svenska. Test för markeringen, docs, wiki och
skärmdumpar.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 17:13:32 +01:00
parent 86f8130735
commit f0a0bc1522
26 changed files with 622 additions and 32 deletions

View File

@@ -1,5 +1,11 @@
# Ändringslogg # Ä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 ## v0.1.0 – 2026-10-25
Första versionerade releasen: hela storyn från Narita till Narita. Första versionerade releasen: hela storyn från Narita till Narita.

View File

@@ -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 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, 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 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). [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) ![Bankartan](doc/img/map.png)
![Skylt på japanska med delvis översättning](doc/img/sign.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 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 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 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, med standardlayout fungerar: d-pad/vänster spak, A = välj, B = tillbaka,
Y = hjälp, X = ordlista, Back/Select = karta. Y = hjälp, X = ordlista, Back/Select = karta.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 17 KiB

BIN
doc/img/lang.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 17 KiB

BIN
doc/img/pause.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 17 KiB

BIN
doc/img/tips-sign.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
doc/img/tips.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
doc/img/title.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
doc/img/tutorial.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -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/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/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/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/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/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 | | `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 | | ~~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 | | ~~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 | | ~~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 | | 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 | | ~~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) | | ~~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) ### 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). - [ ] Fönstret kan skalas; pixlarna är skarpa (heltalsskalning).
- [ ] Bana 001 går att klara i både listläge och skyltläge. - [ ] Bana 001 går att klara i både listläge och skyltläge.
- [ ] Fel tåg ger tydlig förlust med förklaring. - [ ] Fel tåg ger tydlig förlust med förklaring.

View File

@@ -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 (`sim.TestOneWayGatesNeedStaff` går vägen till första spärren i stället
för ett fast steg), och levelcheck är grön. ✓ 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) ### 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. - Slumpade mål från seed, poäng per mål, egen poänglista. NG+ behåller skills.

View File

@@ -50,6 +50,20 @@ otydligt står tolkningen i [§15](#15-tolkningar-av-originalet).
- Spela om storyn och behåll alla dina skills. - 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 ## 4. En bana – kärnloopen
1. Du startar på en station (eller kliver av ett tåg). 1. Du startar på en station (eller kliver av ett tåg).

View File

@@ -162,8 +162,10 @@ func New(opt Options) (*Game, error) {
return nil, err return nil, err
} }
g.scene = s g.scene = s
case !exists:
g.scene = &LangScene{} // första starten: välj språk, sedan titelskärmen
default: default:
g.scene = NewMapScene(g) g.scene = NewTitleScene()
} }
return g, nil return g, nil
} }
@@ -284,8 +286,8 @@ func (g *Game) Save() {
// till banan och tystnar när man lämnar den. // till banan och tystnar när man lämnar den.
func (g *Game) SetScene(s Scene) { func (g *Game) SetScene(s Scene) {
switch s.(type) { switch s.(type) {
case *LevelScene, *ResultScene: // banans sorl fortsätter case *LevelScene, *ResultScene, *PauseScene: // banans sorl fortsätter
case *MapScene, *SettingsScene, *EndingScene, *WordsScene, *SkillsScene, *ItemsScene: case *MapScene, *SettingsScene, *EndingScene, *WordsScene, *SkillsScene, *ItemsScene, *TitleScene, *LangScene, *TutorialScene:
g.Audio.Loop(SoundMusicMap, 0.6) // menymusiken (fortsätter om den redan spelas) g.Audio.Loop(SoundMusicMap, 0.6) // menymusiken (fortsätter om den redan spelas)
default: default:
g.Audio.StopLoop() g.Audio.StopLoop()

View File

@@ -38,14 +38,17 @@ const (
// LevelScene är själva spelandet av en bana. All spellogik ligger i sim.Run; // LevelScene är själva spelandet av en bana. All spellogik ligger i sim.Run;
// scenen översätter input och ritar. // scenen översätter input och ritar.
type LevelScene struct { type LevelScene struct {
id string id string
level *level.Level // aktuell delbana (byts när resan fortsätter på nästa station) level *level.Level // aktuell delbana (byts när resan fortsätter på nästa station)
run *sim.Run run *sim.Run
endless bool // målet är ett lottat mål i oändligt läge (inte storyn) endless bool // målet är ett lottat mål i oändligt läge (inte storyn)
sfx sfxState sfx sfxState
cam *sim.Camera tips tipState
animT float64 highlight bool // tutorialen: resmålets tecken markeras på skyltar, resten tonas ned
hideMap bool // M döljer/visar minikartan 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) shop *shop // öppen butik i kiosken (pausar banan)
outcome LevelOutcome // progression från banan (fylls i när den avgjorts) 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.updateEffects(g)
s.updateSfx(g) s.updateSfx(g)
s.updateTips(g)
s.highlight = s.tutorialActive(g)
if s.run.Mini != nil { if s.run.Mini != nil {
// Minispel: all input går dit; Esc avbryter minispelet, inte banan. // Minispel: all input går dit; Esc avbryter minispelet, inte banan.
mx, my := miniMoveXY() mx, my := miniMoveXY()
@@ -199,7 +204,7 @@ func (s *LevelScene) Update(g *Game) error {
return nil return nil
} }
if backJustPressed() { if backJustPressed() {
g.SetScene(NewMapScene(g)) g.SetScene(NewPauseScene(s)) // Esc pausar; att lämna banan kräver ett val i menyn
return nil return nil
} }
if shopJustPressed() && !s.run.Player.Moving() && s.run.Level.At(s.run.Player.Tile) == level.Kiosk { 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.drawIndicator(screen, ox, oy)
s.drawHUD(g, screen) 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 { 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") txt := g.Assets.T("hud.transfer_done")
w := g.Assets.Width(txt) + 16 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) box = image.Rect(0, y, ScreenW, y+h)
} }
s.infoBox = box
if !s.hideMap { if !s.hideMap {
s.drawMinimap(g, screen, box, hasBox) 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 { if tr, at, secs := r.PlatformHere(); tr != nil {
// Avgångstavlan är en skylt som alla andra: japanska, sedd genom kunskapen. // Avgångstavlan är en skylt som alla andra: japanska, sedd genom kunskapen.
rd := lang.Render(a.Dict, r.Know, tr.BoardText(), UILang) 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)...) lines = append(lines, a.wrapLine(shownLine(rd.Partial, colText, false), maxW)...)
if english { if english {
lines = append(lines, a.wrapLine(plainLine(fmt.Sprintf("%s: Track %s %s for %s", a.T("hud.english"), tr.Track, 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 accent = colKnown
} }
rd := lang.Render(a.Dict, know, sg.Text["ja"], UILang) 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)...) lines = append(lines, a.wrapLine(shownLine(rd.Partial, colText, false), maxW)...)
if english { if english {
lines = append(lines, a.wrapLine(plainLine(a.T("hud.english")+": "+sg.Text.Get("en"), colEnglish), maxW)...) lines = append(lines, a.wrapLine(plainLine(a.T("hud.english")+": "+sg.Text.Get("en"), colEnglish), maxW)...)

View File

@@ -91,7 +91,8 @@ func (m *MapScene) current() *level.Level {
func (m *MapScene) Update(g *Game) error { func (m *MapScene) Update(g *Game) error {
switch { switch {
case backJustPressed(): case backJustPressed():
return ebiten.Termination g.SetScene(NewTitleScene())
return nil
case wordsJustPressed(): case wordsJustPressed():
g.SetScene(NewWordsScene(g.Assets, m)) g.SetScene(NewWordsScene(g.Assets, m))
return nil return nil

View File

@@ -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
}
}

View File

@@ -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)
}

View File

@@ -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):]
}
}

View File

@@ -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)
}
}

85
internal/game/tips.go Normal file
View File

@@ -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)
}

View File

@@ -15,7 +15,7 @@
"level.failed": "You missed too many trains…", "level.failed": "You missed too many trains…",
"level.cleared": "Arrived!", "level.cleared": "Arrived!",
"menu.choose_level": "Choose a level:", "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.time": "Time",
"hud.steps": "Steps", "hud.steps": "Steps",
"hud.controls": "Esc: menu M: map", "hud.controls": "Esc: menu M: map",
@@ -202,8 +202,8 @@
"hud.mode_signs": "Sign mode: work out the train yourself, +50 % score", "hud.mode_signs": "Sign mode: work out the train yourself, +50 % score",
"hud.terminal": "Lines from here (terminal):", "hud.terminal": "Lines from here (terminal):",
"menu.mode": "N: navigation %s", "menu.mode": "N: navigation %s",
"mode.list": "list mode (next train shown)", "mode.list": "list",
"mode.signs": "sign mode (+50 % score)", "mode.signs": "signs (+50 %)",
"result.bonus": "sign bonus", "result.bonus": "sign bonus",
"result.lost_wrong_direction": "Wrong direction.", "result.lost_wrong_direction": "Wrong direction.",
"result.lost_too_far": "Too far away.", "result.lost_too_far": "Too far away.",
@@ -235,8 +235,8 @@
"hud.staff_busy": "Staff on the way...", "hud.staff_busy": "Staff on the way...",
"hud.staff_unlocked": "The staff unlocked the cards - you can leave.", "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": "O: endless",
"map.endless_best": "best %d points, %d goals", "map.endless_best": "best %d pts, %d goals",
"map.ngplus": "G: New Game+", "map.ngplus": "G: New Game+",
"map.ngplus_level": "NG+%d", "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.", "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.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.", "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.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."
} }

View File

@@ -15,7 +15,7 @@
"level.failed": "Du missade för många tåg…", "level.failed": "Du missade för många tåg…",
"level.cleared": "Framme!", "level.cleared": "Framme!",
"menu.choose_level": "Välj bana:", "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.time": "Tid",
"hud.steps": "Steg", "hud.steps": "Steg",
"hud.controls": "Esc: meny M: karta", "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.mode_signs": "Skyltläge: lista ut tåget själv, +50 % poäng",
"hud.terminal": "Linjer härifrån (terminal):", "hud.terminal": "Linjer härifrån (terminal):",
"menu.mode": "N: navigering %s", "menu.mode": "N: navigering %s",
"mode.list": "listläge (nästa tåg visas)", "mode.list": "lista",
"mode.signs": "skyltläge (+50 % poäng)", "mode.signs": "skyltar (+50 %)",
"result.bonus": "skyltbonus", "result.bonus": "skyltbonus",
"result.lost_wrong_direction": "Fel håll.", "result.lost_wrong_direction": "Fel håll.",
"result.lost_too_far": "För långt bort.", "result.lost_too_far": "För långt bort.",
@@ -235,8 +235,8 @@
"hud.staff_busy": "Personalen kommer...", "hud.staff_busy": "Personalen kommer...",
"hud.staff_unlocked": "Personalen låste upp korten - du kan gå ut.", "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": "O: oändligt",
"map.endless_best": "bästa %d poäng, %d mål", "map.endless_best": "bästa %d p, %d mål",
"map.ngplus": "G: New Game+", "map.ngplus": "G: New Game+",
"map.ngplus_level": "NG+%d", "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.", "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.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.", "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.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."
} }

View File

@@ -55,7 +55,7 @@ type shot struct {
// bilderna överlever ändringar i banorna. // bilderna överlever ändringar i banorna.
var shots []shot var shots []shot
var demo string var demo, fresh string
func buildShots(levelsDir string) error { func buildShots(levelsDir string) error {
l, err := level.LoadFile(filepath.Join(levelsDir, "001-flygplatsen.json")) 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"}}, {"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; 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; 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"}}, {"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 return nil
@@ -252,6 +254,8 @@ func main() {
demo = filepath.Join(root, "build", "demo-save.json") demo = filepath.Join(root, "build", "demo-save.json")
must(os.MkdirAll(filepath.Dir(demo), 0o755)) must(os.MkdirAll(filepath.Dir(demo), 0o755))
must(demoSave(demo)) 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"))) must(buildShots(filepath.Join(root, "resources", "data", "levels")))
build := exec.Command("go", "run", "./tools/build", "-mode", "debug") build := exec.Command("go", "run", "./tools/build", "-mode", "debug")
@@ -280,7 +284,7 @@ func main() {
args = append(args, "-level", s.level) args = append(args, "-level", s.level)
} }
for _, a := range s.args { 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 := exec.Command(exe, args...)
run.Dir, run.Stdout, run.Stderr = filepath.Dir(exe), os.Stdout, os.Stderr run.Dir, run.Stdout, run.Stderr = filepath.Dir(exe), os.Stdout, os.Stderr