Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5114115976 | |||
| 0c47b0ad00 | |||
| 87695b668b | |||
| f0a0bc1522 |
13
CHANGELOG.md
@@ -1,5 +1,18 @@
|
||||
# Ändringslogg
|
||||
|
||||
## Oreleaset
|
||||
|
||||
- Texterna ryms på skärmen på både svenska och engelska: kortare etiketter
|
||||
("Mål"/"To"), bankartans tangentrad på två rader, HUD-raden stryker
|
||||
delar som inte får plats, radbrytning i fler rutor; breddtester för alla
|
||||
enradstexter med den riktiga fonten.
|
||||
- Bussar: flygbussterminalen (sidobana 001b) med tre bussar på väg, egen
|
||||
sprite, hållplatsskylt och tuta; sidobanan 003b Harajuku; överstimulerande
|
||||
UI med reklamticker på Ueno och Tokyo.
|
||||
- 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.
|
||||
|
||||
287
README.md
@@ -1,271 +1,52 @@
|
||||
# Metro Puzzle JP
|
||||
|
||||
Pixel art-pusselspel: leda ett sällskap – *lämmeltåget* – genom japanska
|
||||
tågstationer byggda som labyrinter, hitta rätt tåg och komma fram till rätt
|
||||
resmål. Nästan alla skyltar är på japanska; ju mer du lär dig, desto mer kan
|
||||
du läsa. Sällskapet behöver mat, dryck och toapauser, tågkortet måste ha
|
||||
saldo, telefonen batteri, och på svåra banor tickar klockan.
|
||||
Ett pixel art-pusselspel om att hitta rätt tåg i Tokyo. Du leder ett
|
||||
sällskap – *lämmeltåget* – genom japanska tågstationer byggda som
|
||||
labyrinter. 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. Storyn följer några polare på semester, från landningen på Narita till
|
||||
hemresan; efteråt väntar ett oändligt läge och New Game+.
|
||||
|
||||
Storyn följer några polare på semester i Japan, från landningen på
|
||||
flygplatsen till avresan. Efter storyn låses ett oändligt läge och New Game+
|
||||
upp. Hela speldesignen finns i [doc/spelbeskrivning.md](doc/spelbeskrivning.md).
|
||||

|
||||
|
||||
**Status:** M10 klar (2026-10-24) – hela storyn från flygplats till
|
||||
flygplats: åtta huvudbanor och ett sidospår på en bankarta i spelbrädesstil,
|
||||
med flera våningar, folk, reklam, sorl, vagnval och ett riktigt linjenät med
|
||||
list- och skyltläge. De två sista resorna har **byten**: man kliver av på en
|
||||
mellanstation och fortsätter i samma omgång, med klockan, plånboken och
|
||||
sällskapet i behåll. Klarar man finalen (Tokyo → Nippori → Narita) visas
|
||||
**hemresan**: planet lyfter, resan summeras och eftertexterna rullar. Med
|
||||
egna kort väljer man **vem som betalar** i kiosken, och på de svåraste
|
||||
stationerna släpper spärren inte ut en som checkat in: man får **kalla på
|
||||
personal** med Enter vid spärren. Efter storyn finns **oändligt läge** (O på
|
||||
bankartan: lottade resmål på storyns stationer tills man förlorar, egen
|
||||
poänglista) och **New Game+** (G två gånger: om från början med skills kvar
|
||||
och ett större sällskap). 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
|
||||
[doc/roadmap.md](doc/roadmap.md) och [CHANGELOG.md](CHANGELOG.md).
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
## Spela
|
||||
|
||||
Så fungerar det: du startar på den gröna dörrmattan och letar dig genom
|
||||
stationen. Skyltarna är på japanska. Ord du kan markeras och översätts i
|
||||
raden under, okända ord står kvar. Trycker du H frågar du en förbipasserande,
|
||||
väntar några sekunder och får hela skylten översatt plus ett nytt ord.
|
||||
Spärrarna släpper bara igenom dig om kortet har saldo. På perrongen (gul
|
||||
ledlinje) är avgångstavlan också japansk. Står tåget inne trycker du Enter
|
||||
för att kliva på. Rätt tåg tar dig till resmålet, fel tåg avslutar banan.
|
||||
På tåget passerar stationerna en i taget; kliv av med Enter vid ditt resmål.
|
||||
Klarar du banan under par får du stjärnor, språkpoäng och resmålets namn.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
Spärren drar från tågkortet. Tar saldot slut fyller du på vid automaten,
|
||||
där du först måste hitta rätt knapp bland de japanska. På svårare banor
|
||||
krävs i stället en pappersbiljett till rätt resmål, köpt från en taxetavla
|
||||
på japanska.
|
||||
|
||||

|
||||
|
||||
I kioskens butik (B) köper du hattar, masker, kläder, skor och prylar med
|
||||
buffar; några är genvägar, som JR Pass som öppnar alla spärrar. Klarade
|
||||
banor ger XP, nivåer ger skillpoäng till skillträdet (T), och garderoben (I)
|
||||
visar vad du bär.
|
||||
|
||||

|
||||
|
||||
Stationer kan ha flera våningar. Du ser bara den våning du står på; trappor
|
||||
tar dig upp och ner, och minikartan visar vilket plan du är på.
|
||||
|
||||

|
||||
|
||||
På svåra banor är stationen full av folk, väggarna av reklam och luften av
|
||||
sorl. Tågen har flera vagnar: låt folk kliva av först, välj en vagn med lite
|
||||
folk för extra poäng, och undvik den rosa kvinnovagnen om det finns en man i
|
||||
sällskapet.
|
||||
|
||||

|
||||
|
||||
I skyltläget får du bara resmålet. Skyltarna säger var perrongerna är,
|
||||
terminalen (biljettautomaten) visar linjerna med deras närmaste stationer, och
|
||||
du avgör själv vilka tåg som faktiskt tar dig dit – på Ikebukuro är två av
|
||||
fyra rätt, ett går åt fel håll och ett tar omvägen via city.
|
||||
|
||||

|
||||
|
||||
Resorna med byten fortsätter på nästa station när du klivit av: nytt
|
||||
labyrintgolv, samma klocka och samma hungriga sällskap.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
På de svåraste banorna har alla i sällskapet egna kort som måste ha saldo,
|
||||
alla behöver biljett, och när biljetten är köpt ska sällskapet placeras i
|
||||
vagnen efter sina önskemål: fönster eller gång, vänster eller höger sida,
|
||||
eller bredvid en viss person. Missnöjda blir sura och drar ned poängen.
|
||||
|
||||

|
||||
|
||||
Ditt sällskap följer efter i en rad. Mätarna uppe till höger visar mat,
|
||||
dryck och toa för var och en; faller de får någon ett behov som saktar ned
|
||||
er alla tills ni hittar en kiosk eller en toalett. Minikartan i hörnet visar
|
||||
från början bara perrongerna, starten och spärrarna plus en avgångslista;
|
||||
resten fylls i allt eftersom ni går.
|
||||
|
||||

|
||||
|
||||
Fler bilder och en genomgång finns i
|
||||
[wikin](https://gitea.brasse-pc.eu/brasse/metro-puzzle-jp/wiki).
|
||||
Ladda ner zippen för Linux eller Windows från releasen
|
||||
[latest](https://gitea.brasse-pc.eu/brasse/metro-puzzle-jp/releases/tag/latest)
|
||||
(eller en versionerad `vX.Y.Z`), packa upp och kör `metro-puzzle-jp`;
|
||||
mappen `resources/` ska ligga bredvid. Första gången väljer du språk, och
|
||||
**Tutorial** i menyn går igenom grunderna. Hur allt fungerar – skyltar,
|
||||
kort och biljetter, sällskapet, tåg och byten, lägena, inställningarna –
|
||||
står i [wikin](https://gitea.brasse-pc.eu/brasse/metro-puzzle-jp/wiki).
|
||||
|
||||
## Bygga
|
||||
|
||||
Enda kravet är **Go ≥ 1.27**. Linux behöver dessutom en C-kompilator och
|
||||
X11/GL-headers för fönsterhanteringen (Ebitengine/GLFW). Windows-bygget är
|
||||
ren Go och kan korsbyggas från Linux.
|
||||
|
||||
### Linux
|
||||
Enda kravet är **Go ≥ 1.27**; Linux behöver dessutom gcc och X11/GL/ALSA-
|
||||
headers för fönstret (`sudo pacman -S --needed go gcc pkgconf mesa libx11
|
||||
libxrandr libxcursor libxinerama libxi libxxf86vm alsa-lib` på Arch/Garuda).
|
||||
Windows-bygget är ren Go och kan korsbyggas från Linux.
|
||||
|
||||
```bash
|
||||
# Arch / Garuda
|
||||
sudo pacman -S --needed go gcc pkgconf mesa libx11 libxrandr libxcursor libxinerama libxi libxxf86vm alsa-lib
|
||||
|
||||
# Debian / Ubuntu
|
||||
sudo apt install golang gcc libc6-dev pkg-config libgl1-mesa-dev libx11-dev libxrandr-dev libxcursor-dev libxinerama-dev libxi-dev libxxf86vm-dev libasound2-dev
|
||||
go run ./tools/build # debug → build/debug/
|
||||
go run ./tools/build -mode release -os linux # release → build/release/linux-amd64/
|
||||
go run ./tools/build -mode release -os windows # korsbygge → build/release/windows-amd64/
|
||||
go test ./...
|
||||
```
|
||||
|
||||
```bash
|
||||
git clone <remote>/metro-puzzle-jp.git && cd metro-puzzle-jp
|
||||
go run ./tools/build # debug → build/debug/
|
||||
go run ./tools/build -mode release -os linux # release → build/release/linux-amd64/
|
||||
go run ./tools/build -mode release -os windows # korsbygge → build/release/windows-amd64/
|
||||
./build/debug/metro-puzzle-jp # kör
|
||||
go test ./... # tester
|
||||
```
|
||||
|
||||
### Windows (bygga på Windows)
|
||||
|
||||
Installera Go från <https://go.dev/dl/>. Ingen C-kompilator behövs.
|
||||
|
||||
```powershell
|
||||
go run ./tools/build -mode release # → build\release\windows-amd64\metro-puzzle-jp.exe
|
||||
```
|
||||
|
||||
### VS Code
|
||||
|
||||
Öppna mappen med tillägget **Go** (`golang.go`, föreslås automatiskt).
|
||||
|
||||
| Vad | Hur |
|
||||
|-----|-----|
|
||||
| Bygga debug | `Ctrl+Shift+B` (tasken `build`) |
|
||||
| Debugga | `F5` – "Debug (dlv)". Tillägget erbjuder att installera delve första gången. |
|
||||
| Bygga release | Terminal › Run Task › `build: release (linux-amd64)` / `(windows-amd64)` / `(alla plattformar)` |
|
||||
| Köra tester | Terminal › Run Task › `test`, eller testknapparna i editorn |
|
||||
|
||||
### Resultatet av ett bygge
|
||||
|
||||
```
|
||||
build/
|
||||
├── debug/ debug-bygge för den här datorn
|
||||
│ ├── metro-puzzle-jp
|
||||
│ └── resources/
|
||||
└── release/
|
||||
├── linux-amd64/
|
||||
│ ├── metro-puzzle-jp en strippad binär
|
||||
│ └── resources/ sprites/ sound/ text/ data/
|
||||
└── windows-amd64/
|
||||
├── metro-puzzle-jp.exe
|
||||
└── resources/
|
||||
```
|
||||
|
||||
Spelet letar efter `resources/` bredvid binären, annars uppåt från
|
||||
arbetskatalogen (så `go run` och F5 fungerar), och `JP_RESOURCES=/sökväg`
|
||||
överstyr allt. Att distribuera spelet = zippa en plattformsmapp. Sparfilen
|
||||
ligger utanför spelmappen, i `~/.config/metro-puzzle-jp/` (Linux) eller
|
||||
`%AppData%\metro-puzzle-jp\` (Windows).
|
||||
|
||||
### Flaggor
|
||||
|
||||
```
|
||||
metro-puzzle-jp # meny
|
||||
metro-puzzle-jp -level 001-flygplatsen # hoppa direkt in i en bana
|
||||
metro-puzzle-jp -scale 3 # fönsterstorlek (320x180 × skala)
|
||||
metro-puzzle-jp -smoke 90 # öppna fönster, avsluta efter 90 bildrutor (smoke-test)
|
||||
metro-puzzle-jp -know 改札,番線 -points 3 # förkunskap och språkpoäng (tester/skärmdumpar)
|
||||
metro-puzzle-jp -seed 7 # fast seed för händelser, läsning och minispel (tester/skärmdumpar)
|
||||
metro-puzzle-jp -phone -card 0 # kortet i telefonen; kortets startsaldo (tester/skärmdumpar)
|
||||
metro-puzzle-jp -debuffs sore_feet,tired # debuffar från start (tester/skärmdumpar)
|
||||
metro-puzzle-jp -mute # inget ljud
|
||||
metro-puzzle-jp -save /sökväg/save.json # annan sparfil (standard ~/.config/metro-puzzle-jp/save.json)
|
||||
metro-puzzle-jp -nosave # läs sparfilen men skriv aldrig (tester/skärmdumpar)
|
||||
metro-puzzle-jp -mode signs # navigeringsläge från start: list eller signs (annars sparfilens; N på bankartan växlar)
|
||||
metro-puzzle-jp -ending # öppna slutscenen (hemresan) direkt (tester/skärmdumpar)
|
||||
metro-puzzle-jp -endless -seed 7 # starta oändligt läge direkt med en viss målföljd (tester/skärmdumpar)
|
||||
metro-puzzle-jp -script "move right 4; shot bild.png; quit" # skriptläge, se internal/game/script.go
|
||||
metro-puzzle-jp -version
|
||||
```
|
||||
|
||||
### Styrning
|
||||
|
||||
Pilar eller WASD går. Enter väljer, kliver på tåget, använder toaletten,
|
||||
handlar i kiosken, startar automaten eller kallar på personal vid en spärr
|
||||
som inte släpper ut en; i minispelen väljer pilarna och
|
||||
Enter bekräftar (håll Enter för att mata in sedlar). H frågar om hjälp vid en
|
||||
skylt, L öppnar ordlistan, T skillträdet, I garderoben, B butiken vid en kiosk, M
|
||||
döljer/visar minikartan, P på kartan lägger kortet i telefonen, N på kartan
|
||||
växlar navigeringsläge (lista/skyltar), 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
|
||||
med standardlayout fungerar: d-pad/vänster spak, A = välj, B = tillbaka,
|
||||
Y = hjälp, X = ordlista, Back/Select = karta.
|
||||
|
||||
### Verktyg
|
||||
|
||||
| Kommando | Gör |
|
||||
|----------|-----|
|
||||
| `go run ./tools/build …` | bygger (se ovan) |
|
||||
| `go run ./tools/genart` | genererar placeholder-grafiken `resources/sprites/tiles.png`, `player.png` och `plane.png` |
|
||||
| `go run ./tools/levelcheck [ord…]` | ritar varje bana (per våning) som PNG i `build/levelcheck/` och rapporterar lösbarhet, onåbara rutor, perronger utanför spärrar, tågens bedömning i linjenätet och resornas byten; avslutar med fel om något är trasigt |
|
||||
| `go run ./tools/gensound` | genererar alla ljud i `resources/sound/*.wav` (stationsmelodier, jinglar, effekter, sorl, menymusik), se `resources/sound/README.md` |
|
||||
| `go run ./tools/shots [ord…]` | tar dokumentationens skärmdumpar till `doc/img/` – **kör efter varje grafik-/UI-ändring och committa bilderna**. Med argument körs bara skott vars bana eller skript innehåller något av orden (t.ex. `go run ./tools/shots 005 ride.png`). På Linux körs spelet på en osynlig skärm (headless Wayfire + Xwayland) så att inget fönster syns eller tar fokus; `METRO_HEADLESS=0` använder skärmen |
|
||||
|
||||
## Beroenden
|
||||
|
||||
| | Linux | Windows |
|
||||
|---|---|---|
|
||||
| **Bygga** | Go ≥ 1.27, gcc, pkgconf, mesa (GL-headers), libx11/libxrandr/libxcursor/libxinerama/libxi/libxxf86vm, alsa-lib | Go ≥ 1.27 |
|
||||
| **Köra** | libc, samt ett skrivbord med OpenGL (mesa) och X11 eller Wayland (XWayland), ALSA/PipeWire för ljud – laddas dynamiskt vid start (`-mute` stänger av ljudet) | Windows 10+ med DirectX 11 eller OpenGL |
|
||||
| **Go-moduler** | `github.com/hajimehoshi/ebiten/v2` (spelbibliotek, inkl. `text/v2` för TTF-rendering) + indirekta beroenden (`purego`, `gomobile`, `go-text/typesetting`, `x/image`, `x/sys`, `x/sync`) | samma |
|
||||
| **Font** | PixelMplus 10/12 px (M+ FONT LICENSE), ligger i `resources/text/fonts/` | samma |
|
||||
|
||||
Release-binären för Linux är dynamiskt länkad bara mot libc; GL/X11 laddas
|
||||
vid körning, så den fungerar på alla vanliga distributioner.
|
||||
VS Code: `Ctrl+Shift+B` bygger, `F5` debuggar. Flaggor, verktyg, styrning,
|
||||
beroenden och katalogstruktur:
|
||||
[Bygga och verktyg](https://gitea.brasse-pc.eu/brasse/metro-puzzle-jp/wiki/Bygga-och-verktyg)
|
||||
i wikin.
|
||||
|
||||
## Dokumentation
|
||||
|
||||
- [doc/plan.md](doc/plan.md) – navet: mål, stack, milstolpar, öppna frågor
|
||||
- [Wikin](https://gitea.brasse-pc.eu/brasse/metro-puzzle-jp/wiki) – så fungerar spelet, bygga och verktyg
|
||||
- [doc/spelbeskrivning.md](doc/spelbeskrivning.md) – hela speldesignen
|
||||
- [doc/roadmap.md](doc/roadmap.md) – teknikval, katalogstruktur, milstolpar i ordning
|
||||
- [doc/mal-och-tester.md](doc/mal-och-tester.md) – projektmål, teststrategi, checklistor
|
||||
- `resources/*/README.md` – vad som ska ligga i respektive resursmapp
|
||||
|
||||
## Struktur i korthet
|
||||
|
||||
```
|
||||
cmd/metro-puzzle-jp/ programstart
|
||||
internal/game/ Ebitengine: scener, input, rendering, skriptläge (enda paketet som får känna till Ebitengine)
|
||||
internal/sim/ rutbaserad rörelse, kollision, kamera, banomgång med tåg/spärrar/hjälp (ren Go)
|
||||
internal/lang/ japanska: ordbok, kunskap, delvis översättning (ren Go)
|
||||
internal/minigame/ påfyllning, biljettköp, trängsel, tågresa – seedade minispel (ren Go)
|
||||
internal/progress/ XP, skillträd, items, banresultat, upplåsning (ren Go)
|
||||
internal/save/ sparfil (versionerad JSON)
|
||||
internal/level/ banformat, validering, framkomlighet, koppling till linjenätet
|
||||
internal/nav/ linjenätet: stationer, linjer, rätt/fel håll/för långt bort (ren Go)
|
||||
internal/endless/ oändligt läge: seedade mål på storyns stationer (ren Go)
|
||||
internal/res/ hittar och läser resources/
|
||||
internal/rng/ seedad slump
|
||||
tools/ build/ genart/ shots/
|
||||
resources/ sprites/ sound/ text/ data/ – kopieras bredvid binären
|
||||
doc/ dokumentation, doc/img/ = skärmdumpar
|
||||
.gitea/workflows/ CI: test + release-zippar (linux/windows)
|
||||
```
|
||||
|
||||
## Release
|
||||
|
||||
Varje push till master bygger zippar för Linux och Windows på Gitea och
|
||||
lägger dem på releasen
|
||||
[latest](https://gitea.brasse-pc.eu/brasse/metro-puzzle-jp/releases/tag/latest).
|
||||
En tagg `vX.Y.Z` ger en versionerad release. Packa upp och kör binären;
|
||||
`resources/` måste ligga bredvid.
|
||||
- [doc/roadmap.md](doc/roadmap.md) – teknikval och milstolpar
|
||||
- [doc/mal-och-tester.md](doc/mal-och-tester.md) – mål, teststrategi, checklistor
|
||||
- [doc/plan.md](doc/plan.md) – navet med öppna frågor
|
||||
- [CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 23 KiB |
BIN
doc/img/bus.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 23 KiB |
BIN
doc/img/cars.png
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
BIN
doc/img/lang.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 19 KiB |
BIN
doc/img/map-en.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 17 KiB |
BIN
doc/img/map.png
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 24 KiB |
BIN
doc/img/pause.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
BIN
doc/img/ride.png
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
BIN
doc/img/shop.png
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
BIN
doc/img/sign-en.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
BIN
doc/img/sign.png
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
BIN
doc/img/tips-sign.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
doc/img/tips.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
doc/img/title.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 21 KiB |
BIN
doc/img/tutorial.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
@@ -139,8 +139,13 @@ 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` | `TestSingleLineTextsFit`, `TestLevelNamesFitHUD` | **Textbredd (Björn 2026-10-25):** alla texter som ritas på en rad mäts med den riktiga fonten på svenska och engelska mot sitt utrymme (skärm, panel, pausmeny, inställningsrad, bankartans sammansatta rader); bannamn + resmål + mätare ryms på HUD:ens första rad för alla banor. `TestReportTextWidths -v` listar allt över skärmbredden |
|
||||
| `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` | `TestBusVehicleAndBoardText` | **M10b:** `vehicle: bus` ger ett fordon och "番" på tavlan, okänt fordon ger fel, tåg är standard |
|
||||
| `internal/nav` | `TestShippedBusLines` | **M10b:** flygbussen mot Shinjuku är rätt, mot Yokohama ingen trafik för Shinjuku |
|
||||
| `internal/sim` | `TestBusTerminalFlow` | **M10b:** biljett till Shinjuku, hållplats 1, bussen vinner |
|
||||
| `internal/level` | `TestToiletSides` | **M10d:** 005 och 008b har toaletter bara innanför, 007b på båda sidor, 007 och 008 bara utanför; innanför bara på banor med spärrar utan återvändo |
|
||||
| `internal/sim` | `TestJourneyWithTransfer`, `TestJourneyWrongTrainOnSecondLeg`, `TestChainLevelsLoad` | **M10:** bytet fortsätter omgången på nästa station med klocka, plånbok, sällskap och par-summa; vinst först på sista delbanan; fel håll på bytesstationen förlorar; delbanor laddas enskilt |
|
||||
| `internal/minigame` | `TestCarPickAlightingAndChoice`, `TestCrowdLevelNarrowsZone`, `TestRideCrowdLetPass` | Folk av först (för tidigt räknas), seedad trängsel med minst en lugn vagn, val; trängsel smalnar zonen; resan: kortare tid per station, passagerare som vill förbi, blockerade räknas |
|
||||
@@ -167,7 +172,8 @@ 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 |
|
||||
| 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 |
|
||||
| ~~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 tidtabellen, bedöms som tåg och ritas på väg; tavlan skiljer buss från tåg; busslinjerna i nätet; bussterminalen klaras – finns, se tabellen ovan; overload-tickern verifieras med skärmdump |
|
||||
| ~~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) |
|
||||
|
||||
@@ -176,13 +182,16 @@ att bygga på Linux som på Windows.
|
||||
En milstolpe är klar när:
|
||||
|
||||
1. Alla tester i raden ovan finns och är gröna.
|
||||
1. Ingen text går utanför skärmen på svenska eller engelska – nya enradstexter läggs till i `TestSingleLineTextsFit`, och långa texter radbryts med `wrapLine`.
|
||||
2. `go vet ./...` och `go test -race ./...` är gröna.
|
||||
3. Debug- och release-bygget går igenom för Linux och Windows.
|
||||
4. Roadmapen är uppdaterad med datum och eventuella avsteg.
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -563,7 +563,7 @@ på **brasse-linux01-runnern** (label `linux-amd64`), aldrig på Pi5:n.
|
||||
- **Kvar till M10b:** bussar och vägrutor, fler sidobanor, överstimulerande
|
||||
UI som eget svårighetsreglage.
|
||||
|
||||
### M10b – Bussar, vägar och fler sidospår
|
||||
### M10b – Bussar, vägar och fler sidospår ✓ (klar 2026-10-25)
|
||||
|
||||
- **Mål:** resten av M10-listan som inte rymdes: buss med vägrutor och
|
||||
hållplatser (egen fordonssprite, hållplatsskylt, tidtabell som tågen),
|
||||
@@ -572,6 +572,25 @@ på **brasse-linux01-runnern** (label `linux-amd64`), aldrig på Pi5:n.
|
||||
avgångstavlor) som kan slås på per bana.
|
||||
- **Teknik:** vägrutor i banformatet, `Vehicle`-abstraktion i sim som tåg
|
||||
och buss delar, återanvänd `nav` för busslinjer.
|
||||
- **Så blev det:** ett tåg kan vara en **buss** (`vehicle: "bus"` på tåget):
|
||||
samma tidtabell, faser, bedömning i nätet och ombordstigning, men ett
|
||||
enda fordon (gul buss-sprite) på en **väg** (asfalt med mittlinje) i
|
||||
stället för tre vagnar på räls, en **hållplatsskylt** på hållplatsen,
|
||||
"1番" i stället för "1番線" på tavlan, tuta när den svänger in och
|
||||
dörrsignal före avgång (ingen stationsmelodi). Nätet fick tre busslinjer
|
||||
från Narita (リムジンバス mot Shinjuku och Yokohama, 空港バス mot Tokyo).
|
||||
**Sidobanor:** 001b Flygbussterminalen (biljettbana med tre bussar,
|
||||
parent 001) och 003b Harajuku (Yamanote mot Shinjuku eller fel varv runt
|
||||
ringen, parent 003). **Överstimulerande UI** (`overload: true` på 005
|
||||
Ueno och 008 Tokyo): rullande reklamticker i blinkande färger under
|
||||
HUD:en och maximalt med affischer. Falska avgångstavlor fick vänta.
|
||||
- **Klar när:** fordon valideras och tavlan skiljer buss från tåg
|
||||
(`level.TestBusVehicleAndBoardText`), busslinjerna bedöms rätt
|
||||
(`nav.TestShippedBusLines`), bussterminalen går att klara
|
||||
(`sim.TestBusTerminalFlow`), busstutan finns (`game.TestAllSoundsDecode`). ✓
|
||||
- **Kvar till senare:** falska avgångstavlor, bussar i oändligt läge (går
|
||||
redan: 001b ligger i poolen), hållplatser mitt i en bana (vägrutor som
|
||||
golv).
|
||||
|
||||
### M10c – Vem betalar, och spärrar utan återvändo ✓ (klar 2026-10-24)
|
||||
|
||||
@@ -646,6 +665,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.
|
||||
|
||||
@@ -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).
|
||||
@@ -282,7 +296,7 @@ från skills i skill tree, och från unika skills man får av sidouppdrag.
|
||||
| "skyltar och terminaler" | Terminalen är biljettautomaten: på nätkopplade banor visar den linjetavlan (linjer vid stationen med grannstationer åt båda hållen), läst genom ens japanska som en skylt | Egna terminalrutor kan komma senare |
|
||||
| "flera stopp och byten" | En resa är en kedja av banfiler (`next` → delbana med `kind: "leg"`). Samma omgång fortsätter på nästa station med klocka, plånbok och sällskap; resmålet i HUD:en visas "via" bytesstationen. Vilken bytesstation som gäller bestämmer banan – spelaren väljer tåg, inte rutt | Fri ruttplanering (välj själv var man byter) är en möjlig utbyggnad |
|
||||
| "slutar när man ska tillbaka till flygplatsen" | Sista huvudbanan (008) går Tokyo → Nippori → Narita flygplats med Keisei. När den klaras första gången visas hemresan: planet lyfter, resan summeras, eftertexter | Slutet går att se igen via `-ending` |
|
||||
| Buss | Uppskjuten till M10b (vägrutor, hållplatser, egen sprite) | |
|
||||
| Buss | Ett tåg med `vehicle: "bus"`: samma tidtabell och bedömning, ett fordon på väg med hållplatsskylt; flygbussterminalen (001b) är en biljettbana med tre bussar | Vägrutor som golv och hållplatser mitt i banan kan komma senare |
|
||||
| "De ikoniska melodierna" | Egna syntetiserade stationsmelodier i klockklang (fyra stycken, en per station) – riktiga JR-melodier är upphovsrättsskyddade och spelas inte in | Byt filerna i `resources/sound/` om egna inspelningar finns |
|
||||
| "Oändligt läge … målen är slumpade" | Målen är storyns stationer med lottade resmål (samma labyrint, samma tåg, nytt resmål avgör vilket tåg som är rätt), seedat; sällskapet växer och tidsgränsen krymper per mål; omgången slutar vid första förlust och hamnar i en egen poänglista | Slumpade labyrinter vid körning kan komma senare |
|
||||
| "New Game+ … behåll alla dina skills" | Skills, XP och språkkunskap behålls; items, stjärnor, pengar och debuffar nollställs; sällskapet är en person större per NG+-varv | |
|
||||
|
||||
@@ -32,6 +32,7 @@ var tileOrder = []level.Tile{
|
||||
level.LaneLeft, level.LaneRight,
|
||||
level.Wall, level.Wall, level.Wall, // plats 13–15 = affischer (laddas separat som Posters)
|
||||
level.Kiosk,
|
||||
level.Wall, level.Wall, level.Wall, // plats 18–20 = buss, väg, hållplatsskylt (laddas separat)
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -39,6 +40,9 @@ const (
|
||||
railsIndex = 9
|
||||
posterIndex = 14
|
||||
posterCount = 3
|
||||
busIndex = 18
|
||||
roadIndex = 19
|
||||
stopIndex = 20
|
||||
)
|
||||
|
||||
// Assets är allt som laddats från resources/.
|
||||
@@ -50,6 +54,9 @@ type Assets struct {
|
||||
Rails *ebiten.Image // rälsruta (horisontell), roteras för vertikala spår
|
||||
Posters []*ebiten.Image // affischer för visuell röra (ritas över väggrutor)
|
||||
Plane *ebiten.Image // flygplanet i slutscenen
|
||||
Bus *ebiten.Image // bussen (ett fordon, inte vagnar)
|
||||
Road *ebiten.Image // vägruta (horisontell), roteras för vertikala
|
||||
StopSign *ebiten.Image // hållplatsskylt ovanpå busshållplatsen
|
||||
Icon image.Image // fönsterikonen (nil om den saknas)
|
||||
Player [4][3]*ebiten.Image // [riktning: ner, upp, vänster, höger][bildruta: stilla, gå1, gå2]
|
||||
Text map[string]string
|
||||
@@ -78,7 +85,7 @@ func LoadAssets() (*Assets, error) {
|
||||
return nil, fmt.Errorf("tiles.png är %d px bred, behöver minst %d", w, need)
|
||||
}
|
||||
for i, t := range tileOrder {
|
||||
if i == trainIndex || i == railsIndex || (i >= posterIndex && i < posterIndex+posterCount) {
|
||||
if i == trainIndex || i == railsIndex || (i >= posterIndex && i < posterIndex+posterCount) || i >= busIndex {
|
||||
continue
|
||||
}
|
||||
a.Tiles[t] = sub(i)
|
||||
@@ -88,6 +95,7 @@ func LoadAssets() (*Assets, error) {
|
||||
}
|
||||
a.Train = sub(trainIndex)
|
||||
a.Rails = sub(railsIndex)
|
||||
a.Bus, a.Road, a.StopSign = sub(busIndex), sub(roadIndex), sub(stopIndex)
|
||||
for i := 0; i < posterCount; i++ {
|
||||
a.Posters = append(a.Posters, sub(posterIndex+i))
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ const (
|
||||
SoundLose = "lose"
|
||||
SoundStaff = "staff"
|
||||
SoundPlane = "plane"
|
||||
SoundBusHorn = "bus-horn"
|
||||
)
|
||||
|
||||
// StationMelodies är avgångsmelodierna; varje station har sin (level.Melody
|
||||
@@ -57,7 +58,7 @@ var StationMelodies = []string{"melody-a", "melody-b", "melody-c", "melody-d"}
|
||||
// AllSounds är alla klipp som måste finnas i resources/sound.
|
||||
func AllSounds() []string {
|
||||
return append([]string{SoundApproach, SoundAnnounce, SoundDoors, SoundChatter, SoundMusicMap, SoundGateBeep, SoundGateDeny,
|
||||
SoundKiosk, SoundToilet, SoundLearn, SoundConfirm, SoundCancel, SoundTransfer, SoundWin, SoundLose, SoundStaff, SoundPlane},
|
||||
SoundKiosk, SoundToilet, SoundLearn, SoundConfirm, SoundCancel, SoundTransfer, SoundWin, SoundLose, SoundStaff, SoundPlane, SoundBusHorn},
|
||||
StationMelodies...)
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -36,11 +36,11 @@ func (s *LevelScene) drawNPCs(g *Game, screen *ebiten.Image, ox, oy float64) {
|
||||
func (s *LevelScene) buildPosters() {
|
||||
s.posters = map[level.Point]int{}
|
||||
l := s.run.Level
|
||||
if l.Clutter == 0 {
|
||||
if s.clutterLevel() == 0 {
|
||||
return
|
||||
}
|
||||
r := s.run.Seed.Derive("posters").New()
|
||||
chance := 0.12 * float64(l.Clutter)
|
||||
chance := 0.12 * float64(s.clutterLevel())
|
||||
for z := 0; z < l.NumFloors(); z++ {
|
||||
for y := 0; y < l.Height(); y++ {
|
||||
for x := 0; x < l.Width(); x++ {
|
||||
|
||||
@@ -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)
|
||||
@@ -55,6 +58,7 @@ type LevelScene struct {
|
||||
// Tåg och effekter (M7b).
|
||||
tracks []trackInfo
|
||||
rails map[level.Point]bool
|
||||
roads map[level.Point]bool // vägrutor för bussar (ritas med vägsprite)
|
||||
lastPhase map[level.Tile]level.Phase
|
||||
particles []particle
|
||||
rng *rand.Rand // bara för partiklar (ingen spellogik)
|
||||
@@ -185,6 +189,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 +205,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 {
|
||||
@@ -294,7 +300,7 @@ func (s *LevelScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
for y := y0; y < y1; y++ {
|
||||
for x := x0; x < x1; x++ {
|
||||
p := level.Point{X: x, Y: y, Z: z}
|
||||
if s.rails[p] {
|
||||
if s.rails[p] || s.roads[p] {
|
||||
s.drawRails(g, screen, p, ox, oy)
|
||||
continue
|
||||
}
|
||||
@@ -353,6 +359,8 @@ func (s *LevelScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
s.drawIndicator(screen, ox, oy)
|
||||
|
||||
s.drawHUD(g, screen)
|
||||
s.drawTicker(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
|
||||
@@ -408,13 +416,6 @@ func (s *LevelScene) drawHUD(g *Game, screen *ebiten.Image) {
|
||||
a := g.Assets
|
||||
vector.DrawFilledRect(screen, 0, 0, ScreenW, 24, color.RGBA{24, 20, 37, 200}, false)
|
||||
|
||||
dest := r.FinalDestination().Get("romaji")
|
||||
if !r.IsLastLeg() { // resa med byte: den här delbanan slutar vid bytesstationen
|
||||
dest += " " + a.T("hud.via") + " " + r.Level.Destination.Get("romaji")
|
||||
}
|
||||
if r.Know.ShowsDestinationJP() { // hint från nivå 1: delbanans mål på japanska, att matcha mot skyltarna
|
||||
dest += " " + r.Level.Destination.Get("ja")
|
||||
}
|
||||
name := r.Level.Name.Get(UILang)
|
||||
if r.Level.NumFloors() > 1 {
|
||||
name += " " + r.Level.FloorName(r.Player.Tile.Z)
|
||||
@@ -425,39 +426,72 @@ func (s *LevelScene) drawHUD(g *Game, screen *ebiten.Image) {
|
||||
if s.endless && g.Endless != nil {
|
||||
name = fmt.Sprintf("#%d %s", g.Endless.Step+1, name)
|
||||
}
|
||||
metersX := ScreenW - 4 - len(r.Party.Members)*(meterW+meterGap)
|
||||
name = a.fitText(name, metersX/2)
|
||||
a.Draw(screen, name, 4, 0, colText)
|
||||
destX := 4 + a.Width(name) + 12
|
||||
a.Draw(screen, a.T("hud.destination")+": "+dest, destX, 0, colKnown)
|
||||
// Japanska-nivån får plats till vänster om mätarna; annars kortas den.
|
||||
jpX := destX + a.Width(a.T("hud.destination")+": "+dest) + 12
|
||||
metersX := ScreenW - 4 - len(r.Party.Members)*(meterW+meterGap)
|
||||
jp := fmt.Sprintf("%s Lv%d·%d", a.T("hud.japanese"), r.Know.Level(), r.Know.Count())
|
||||
if jpX+a.Width(jp) > metersX-4 {
|
||||
jp = fmt.Sprintf("Lv%d", r.Know.Level())
|
||||
// Resmålet: slutmålet, "via" bytesstationen och japanska hinten – i den
|
||||
// ordningen stryks delar som inte får plats före mätarna.
|
||||
final := r.FinalDestination().Get("romaji")
|
||||
via, jp := "", ""
|
||||
if !r.IsLastLeg() { // resa med byte: den här delbanan slutar vid bytesstationen
|
||||
via = " " + a.T("hud.via") + " " + r.Level.Destination.Get("romaji")
|
||||
}
|
||||
if jpX+a.Width(jp) <= metersX-4 {
|
||||
a.Draw(screen, jp, jpX, 0, colDim)
|
||||
if r.Know.ShowsDestinationJP() { // hint från nivå 1: delbanans mål på japanska, att matcha mot skyltarna
|
||||
jp = " " + r.Level.Destination.Get("ja")
|
||||
}
|
||||
label := a.T("hud.destination") + ": "
|
||||
dest := label + final + via + jp
|
||||
for _, cand := range []string{label + final + via + jp, label + final + via, label + final + jp, label + final} {
|
||||
dest = cand
|
||||
if destX+a.Width(cand) <= metersX-4 {
|
||||
break
|
||||
}
|
||||
}
|
||||
dest = a.fitText(dest, metersX-4-destX)
|
||||
a.Draw(screen, dest, destX, 0, colKnown)
|
||||
// Japanska-nivån får plats till vänster om mätarna; annars kortas den.
|
||||
jpX := destX + a.Width(dest) + 12
|
||||
lv := fmt.Sprintf("%s Lv%d·%d", a.T("hud.japanese"), r.Know.Level(), r.Know.Count())
|
||||
if jpX+a.Width(lv) > metersX-4 {
|
||||
lv = fmt.Sprintf("Lv%d", r.Know.Level())
|
||||
}
|
||||
if jpX+a.Width(lv) <= metersX-4 {
|
||||
a.Draw(screen, lv, jpX, 0, colDim)
|
||||
}
|
||||
timeStr := fmt.Sprintf("%s %3.0fs", a.T("hud.time"), r.Time)
|
||||
if r.TimeLimit() > 0 {
|
||||
left := r.TimeLeft()
|
||||
timeStr = fmt.Sprintf("%s %d:%02d", a.T("hud.time_left"), int(left)/60, int(left)%60)
|
||||
}
|
||||
wallet := fmt.Sprintf("%s %s %d¥ %s %d¥", timeStr, a.T("hud.card"), r.Wallet.Card, a.T("hud.cash_short"), r.Wallet.Cash)
|
||||
// Plånboksraden: tid, kort, kontanter, sedan valfria delar (missade tåg,
|
||||
// biljett, telefon) som stryks bakifrån om de inte får plats före mätarna.
|
||||
must := fmt.Sprintf("%s %s %d¥ %s %d¥", timeStr, a.T("hud.card"), r.Wallet.Card, a.T("hud.cash_short"), r.Wallet.Cash)
|
||||
var optional []string
|
||||
if r.MaxMissed() > 0 {
|
||||
wallet += fmt.Sprintf(" %s %d/%d", a.T("hud.missed_short"), r.MissedTrains, r.MaxMissed())
|
||||
}
|
||||
if r.Wallet.Phone {
|
||||
wallet += fmt.Sprintf(" %s %.0f%%", a.T("hud.phone"), r.Wallet.Battery)
|
||||
optional = append(optional, fmt.Sprintf("%s %d/%d", a.T("hud.missed_short"), r.MissedTrains, r.MaxMissed()))
|
||||
}
|
||||
if r.Wallet.Ticket != "" {
|
||||
wallet += " " + a.T("hud.ticket") + " " + r.Wallet.Ticket
|
||||
optional = append(optional, a.T("hud.ticket")+" "+r.Wallet.Ticket)
|
||||
}
|
||||
if r.Wallet.Phone {
|
||||
optional = append(optional, fmt.Sprintf("%s %.0f%%", a.T("hud.phone"), r.Wallet.Battery))
|
||||
}
|
||||
wallet := must
|
||||
for n := len(optional); n >= 0; n-- {
|
||||
wallet = must
|
||||
for _, o := range optional[:n] {
|
||||
wallet += " " + o
|
||||
}
|
||||
if 4+a.Width(wallet) <= metersX-4 {
|
||||
break
|
||||
}
|
||||
}
|
||||
walletCol := colDim
|
||||
if r.TimeLimit() > 0 && r.TimeLeft() < 30 {
|
||||
walletCol = colBad
|
||||
}
|
||||
a.Draw(screen, wallet, 4, LineH, walletCol)
|
||||
a.Draw(screen, a.fitText(wallet, metersX-8), 4, LineH, walletCol)
|
||||
s.drawMeters(g, screen)
|
||||
|
||||
if r.Outcome != sim.Ongoing || r.Mini != nil || s.shop != nil {
|
||||
@@ -517,6 +551,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 +588,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 +633,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)...)
|
||||
@@ -617,9 +660,9 @@ func (s *LevelScene) infoLines(a *Assets) (lines []line, accent color.Color, bar
|
||||
case r.NoExitAt >= 0 && r.Time-r.NoExitAt < deniedShowSec:
|
||||
return a.wrapLine(plainLine(fmt.Sprintf(a.T("hud.no_exit"), sim.StaffSec), colBad), maxW), colBad, false
|
||||
case r.NeedsStaff():
|
||||
return []line{plainLine(fmt.Sprintf(a.T("hud.staff_prompt"), sim.StaffSec), colWarn)}, colWarn, false
|
||||
return a.wrapLine(plainLine(fmt.Sprintf(a.T("hud.staff_prompt"), sim.StaffSec), colWarn), maxW), colWarn, false
|
||||
case lvl.OneWayGates && r.StaffUnlocked:
|
||||
return []line{plainLine(a.T("hud.unlocked_prompt"), colGood)}, colGood, false
|
||||
return a.wrapLine(plainLine(a.T("hud.unlocked_prompt"), colGood), maxW), colGood, false
|
||||
}
|
||||
case level.Toilet:
|
||||
if r.Facility.Active {
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/buildinfo"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
||||
)
|
||||
|
||||
@@ -69,7 +68,7 @@ func (m *MapScene) nodes() ([]mapNode, map[string][]mapNode) {
|
||||
}
|
||||
mains = append(mains, mapNode{l, x, y})
|
||||
for k, s := range m.sides[l.ID] {
|
||||
sides[l.ID] = append(sides[l.ID], mapNode{s, x + 12 + k*18, y - 40})
|
||||
sides[l.ID] = append(sides[l.ID], mapNode{s, x + 12 + k*18, y - 28})
|
||||
}
|
||||
}
|
||||
return mains, sides
|
||||
@@ -91,7 +90,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
|
||||
@@ -265,7 +265,7 @@ func (m *MapScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
a.Draw(screen, a.T("hud.debuffs")+": "+strings.Join(parts, ", "), 8, y, colBad)
|
||||
}
|
||||
a.Draw(screen, a.T("map.hint"), 8, ScreenH-24, colDim)
|
||||
a.Draw(screen, fmt.Sprintf("%s (%s)", buildinfo.Version, buildinfo.Mode), 8, ScreenH-12, colDim)
|
||||
a.Draw(screen, a.T("map.hint2"), 8, ScreenH-12, colDim) // versionen står på titelskärmen
|
||||
}
|
||||
|
||||
// drawLine ritar en tjock linje mellan två punkter.
|
||||
|
||||
@@ -123,8 +123,10 @@ func (r *ResultScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
if len(run.LearnedDest) > 0 {
|
||||
msg += " " + a.T("hud.learned") + ": " + strings.Join(run.LearnedDest, " ") + " (" + a.T("result.destination") + ")"
|
||||
}
|
||||
a.Draw(screen, msg, 28, y, colKnown)
|
||||
y += LineH
|
||||
for _, l := range a.wrapLine(plainLine(msg, colKnown), ScreenW-56) {
|
||||
a.drawLines(screen, []line{l}, 28, y)
|
||||
y += LineH
|
||||
}
|
||||
}
|
||||
if o := r.from.outcome; run.Outcome == sim.Won {
|
||||
prog := fmt.Sprintf("+%d %s", o.XP, a.T("map.xp"))
|
||||
|
||||
211
internal/game/scene_title.go
Normal 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"), 16, y+2, 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
|
||||
}
|
||||
}
|
||||
92
internal/game/scene_tutorial.go
Normal 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)
|
||||
}
|
||||
@@ -135,3 +135,41 @@ func (a *Assets) drawLines(dst drawTarget, lines []line, x, y int) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fitText kortar en text med ".." tills den ryms i maxW pixlar.
|
||||
func (a *Assets) fitText(s string, maxW int) string {
|
||||
if a.Width(s) <= maxW {
|
||||
return s
|
||||
}
|
||||
r := []rune(s)
|
||||
for len(r) > 0 {
|
||||
r = r[:len(r)-1]
|
||||
if a.Width(string(r)+"..") <= maxW {
|
||||
return string(r) + ".."
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 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):]
|
||||
}
|
||||
}
|
||||
|
||||
16
internal/game/text_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
156
internal/game/textwidth_test.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2/text/v2"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/res"
|
||||
)
|
||||
|
||||
// loadTexts läser ett språks UI-texter.
|
||||
func loadTexts(t *testing.T, lang string) map[string]string {
|
||||
t.Helper()
|
||||
m := map[string]string{}
|
||||
if err := res.LoadJSON(&m, "text", lang+".json"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func uiFace(t *testing.T) *text.GoTextFace {
|
||||
t.Helper()
|
||||
face, err := loadFont("PixelMplus10-Regular.ttf", 10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return face
|
||||
}
|
||||
|
||||
// TestSingleLineTextsFit: texter som ritas på en rad utan radbrytning ska
|
||||
// rymmas i sitt utrymme på båda språken. Bredderna mäts med den riktiga
|
||||
// fonten. Texter som radbryts vid visning (tutorial, tips, premiss …) ingår
|
||||
// inte.
|
||||
func TestSingleLineTextsFit(t *testing.T) {
|
||||
face := uiFace(t)
|
||||
width := func(s string) int { return int(text.Advance(s, face)) }
|
||||
const (
|
||||
full = ScreenW - 16 // en rad över hela skärmen med marginal
|
||||
panel = ScreenW - 24 - 16 // minispelspanelens innerbredd
|
||||
resW = ScreenW - 56 // resultatpanelen
|
||||
pause = ScreenW - 96 // pausmenyn
|
||||
)
|
||||
type check struct {
|
||||
key string
|
||||
max int
|
||||
args []any
|
||||
}
|
||||
checks := []check{
|
||||
{"map.hint", full, nil}, {"map.hint2", full, nil}, {"menu.hint", full, nil}, {"settings.hint", full, nil},
|
||||
{"title.hint", full, nil}, {"tutorial.hint", full, nil}, {"ending.hint", full, nil},
|
||||
{"result.hint", resW, nil}, {"result.hint_endless", resW, nil}, {"result.hint_endless_over", resW, nil},
|
||||
{"pause.hint", pause, nil}, {"pause.quit_confirm", pause, nil}, {"pause.resume", pause, nil}, {"pause.leave", pause, nil}, {"pause.quit", pause, nil},
|
||||
{"title.quit_confirm", ScreenW - 32, nil}, {"title.tutorial", 110, nil}, {"menu.settings", 110, nil}, {"menu.quit", 110, nil}, {"menu.continue", 110, nil}, {"menu.start", 110, nil},
|
||||
{"items.hint", full, nil}, {"skills.hint", full, nil}, {"words.hint", full, nil}, {"shop.hint", panel, nil},
|
||||
{"mini.car.hint", panel, nil}, {"mini.car.pick", panel, nil}, {"mini.car.alighting", panel, nil}, {"mini.car.men", panel, nil},
|
||||
{"mini.crowd.title", panel - 60, nil}, {"mini.crowd.hint", panel, []any{2}},
|
||||
{"mini.ride.title", panel - 60, nil}, {"mini.ride.hint", panel, nil}, {"mini.ride.let_pass", panel, nil},
|
||||
{"mini.ticket.title", panel - 60, nil}, {"mini.ticket.hint", panel, []any{"Narita Airport", 5000}},
|
||||
{"mini.seat.hint", panel, []any{"Ville"}}, {"mini.seat.prefs", panel, nil},
|
||||
{"mini.topup.title", panel - 60, nil}, {"mini.topup.feed", panel, []any{5000}}, {"mini.topup.pick", panel, nil}, {"mini.topup.amount", panel, nil}, {"mini.topup.member", panel, nil}, {"mini.topup.wrong", panel - 60, nil},
|
||||
{"mini.payer.title", panel - 60, nil}, {"mini.payer.price", panel, []any{900}}, {"mini.payer.short", panel, nil}, {"mini.payer.hint", panel, nil},
|
||||
{"hud.kiosk_prompt", full - 60, []any{4, 800}}, {"hud.toilet_prompt", full, []any{4, 32.0}}, {"hud.machine_topup", full, []any{100000, 100000}}, {"hud.machine_ticket", full, []any{100000}},
|
||||
{"hud.help_prompt", full, []any{30.0}}, {"hud.train_near", full, nil}, {"hud.staff_busy", full, nil}, {"hud.toilet_busy", full, nil}, {"hud.kiosk_none", full - 60, nil}, {"hud.toilet_none", full, nil},
|
||||
{"settings.master", 118, nil}, {"settings.effects", 118, nil}, {"settings.music", 118, nil}, {"settings.lang", 118, nil}, {"settings.scale", 118, nil}, {"settings.fullscreen", 118, nil},
|
||||
{"settings.scale_auto", ScreenW - 130 - 8, nil},
|
||||
{"ending.summary", full, nil}, {"ending.story_done", full * 2, nil},
|
||||
}
|
||||
for _, lang := range []string{"sv", "en"} {
|
||||
m := loadTexts(t, lang)
|
||||
for _, c := range checks {
|
||||
s, ok := m[c.key]
|
||||
if !ok {
|
||||
t.Errorf("%s: nyckeln %s saknas", lang, c.key)
|
||||
continue
|
||||
}
|
||||
if len(c.args) > 0 {
|
||||
s = fmt.Sprintf(s, c.args...)
|
||||
}
|
||||
if w := width(s); w > c.max {
|
||||
t.Errorf("%s %s är %d px, får vara %d: %q", lang, c.key, w, c.max, s)
|
||||
}
|
||||
}
|
||||
// Bankartans sammansatta rader.
|
||||
line2 := fmt.Sprintf(m["menu.phone"], m["menu.on"]) + " " + fmt.Sprintf(m["menu.mode"], m["mode.signs"])
|
||||
if w := width(line2); w > full {
|
||||
t.Errorf("%s bankartans lägesrad är %d px: %q", lang, w, line2)
|
||||
}
|
||||
endless := m["map.endless"] + " (" + fmt.Sprintf(m["map.endless_best"], 99999, 99) + ") " + m["map.ngplus"] + " (" + fmt.Sprintf(m["map.ngplus_level"], 9) + ")"
|
||||
if w := width(endless); w > full {
|
||||
t.Errorf("%s bankartans oändligt-rad är %d px: %q", lang, w, endless)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLevelNamesFitHUD: bannamnet, resmålet och mätarna ska rymmas på
|
||||
// HUD:ens första rad även med fullt sällskap och byte, på båda språken.
|
||||
func TestLevelNamesFitHUD(t *testing.T) {
|
||||
face := uiFace(t)
|
||||
width := func(s string) int { return int(text.Advance(s, face)) }
|
||||
dir, err := res.Path("data", "levels")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
all, err := level.LoadAll(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, lang := range []string{"sv", "en"} {
|
||||
m := loadTexts(t, lang)
|
||||
for _, l := range all {
|
||||
name := l.Name.Get(lang)
|
||||
if l.NumFloors() > 1 {
|
||||
name += " " + l.FloorName(0)
|
||||
}
|
||||
chain := level.ChainOf(all, l.ID)
|
||||
if len(chain) > 1 {
|
||||
name += " 1/2"
|
||||
}
|
||||
dest := m["hud.destination"] + ": " + level.FinalOf(all, l).Destination.Get("romaji")
|
||||
if len(chain) > 1 {
|
||||
dest += " " + m["hud.via"] + " " + l.Destination.Get("romaji")
|
||||
}
|
||||
metersX := ScreenW - 4 - max(1, l.Party)*(meterW+meterGap)
|
||||
if w := 4 + width(name) + 12 + width(dest); w > metersX-4 {
|
||||
t.Errorf("%s %s: HUD-raden %q + %q är %d px, mätarna börjar vid %d", lang, l.ID, name, dest, w, metersX)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestReportTextWidths skriver (med -v) ut alla texter bredare än skärmen,
|
||||
// som hjälp vid textgranskning. Fäller inget: de flesta radbryts vid visning.
|
||||
func TestReportTextWidths(t *testing.T) {
|
||||
face := uiFace(t)
|
||||
for _, lang := range []string{"sv", "en"} {
|
||||
m := loadTexts(t, lang)
|
||||
var keys []string
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
for _, line := range strings.Split(m[k], "\n") {
|
||||
if w := int(text.Advance(line, face)); w > ScreenW-16 {
|
||||
t.Logf("WIDE %s %-28s %4d px %q", lang, k, w, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = filepath.Base
|
||||
}
|
||||
53
internal/game/ticker.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||
)
|
||||
|
||||
// Överstimulerande UI (level.Overload): en rullande reklamticker i
|
||||
// blinkande färger under HUD:en, och fler affischer på väggarna. Texten är
|
||||
// reklamfraser som inte betyder något för spelet – det är poängen.
|
||||
var adPhrases = []string{"大セール 半額!", "新発売 ラーメン", "免税 お土産", "駅弁 人気No.1", "本日限り 特価", "ポイント2倍", "新宿 ショッピング", "温泉 旅行 キャンペーン"}
|
||||
|
||||
const tickerSpeed = 30.0 // px/s
|
||||
|
||||
// drawTicker ritar reklamtickern (bara med Overload och när inget annat
|
||||
// ligger där).
|
||||
func (s *LevelScene) drawTicker(g *Game, screen *ebiten.Image) {
|
||||
r := s.run
|
||||
if !r.Level.Overload || r.Mini != nil || r.Outcome != 0 || s.tips.current != "" {
|
||||
return
|
||||
}
|
||||
a := g.Assets
|
||||
text := strings.Join(adPhrases, " ★ ") + " ★ "
|
||||
w := a.Width(text)
|
||||
if w == 0 {
|
||||
return
|
||||
}
|
||||
y := 26
|
||||
blink := math.Mod(r.Time, 1.0) < 0.5
|
||||
bg, fg := color.RGBA{120, 30, 60, 230}, color.RGBA{255, 230, 120, 255}
|
||||
if blink {
|
||||
bg, fg = color.RGBA{40, 60, 140, 230}, color.RGBA{255, 255, 255, 255}
|
||||
}
|
||||
vector.DrawFilledRect(screen, 0, float32(y), ScreenW, LineH+2, bg, false)
|
||||
x := ScreenW - int(math.Mod(r.Time*tickerSpeed, float64(w+ScreenW)))
|
||||
for _, sx := range []int{x, x + w} {
|
||||
if sx < ScreenW && sx+w > 0 {
|
||||
a.Draw(screen, text, sx, y+1, fg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// clutterLevel är den visuella röran: banans clutter, maximal med Overload.
|
||||
func (s *LevelScene) clutterLevel() int {
|
||||
if s.run.Level.Overload {
|
||||
return 3
|
||||
}
|
||||
return s.run.Level.Clutter
|
||||
}
|
||||
85
internal/game/tips.go
Normal 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)
|
||||
}
|
||||
@@ -31,15 +31,21 @@ type trackInfo struct {
|
||||
platform level.Point // perrongrutan
|
||||
track level.Point // rälsrutan bortom kanten
|
||||
horizontal bool // rälsens riktning
|
||||
bus bool // buss: väg och ett fordon i stället för räls och vagnar
|
||||
}
|
||||
|
||||
func (s *LevelScene) buildTracks() {
|
||||
s.tracks = nil
|
||||
s.rails = map[level.Point]bool{}
|
||||
s.roads = map[level.Point]bool{}
|
||||
for _, t := range level.PlatformTiles {
|
||||
for _, p := range s.run.Level.Find(t) {
|
||||
tt, horizontal := s.run.Level.TrackTile(p)
|
||||
s.tracks = append(s.tracks, trackInfo{tile: t, platform: p, track: tt, horizontal: horizontal})
|
||||
bus := false
|
||||
if tr := s.run.Level.TrainAt(t); tr != nil && tr.IsBus() {
|
||||
bus = true
|
||||
}
|
||||
s.tracks = append(s.tracks, trackInfo{tile: t, platform: p, track: tt, horizontal: horizontal, bus: bus})
|
||||
out := level.Point{X: tt.X - p.X, Y: tt.Y - p.Y} // riktningen bort från perrongen
|
||||
for k := -railsReach; k <= railsReach; k++ {
|
||||
c := tt
|
||||
@@ -51,7 +57,11 @@ func (s *LevelScene) buildTracks() {
|
||||
beyond := level.Point{X: c.X + out.X, Y: c.Y + out.Y, Z: c.Z}
|
||||
// Räls på väggrutor längs spåret, så länge det är vägg (eller kartkant) bortom dem.
|
||||
if s.run.Level.At(c) == level.Wall && s.run.Level.At(beyond) == level.Wall {
|
||||
s.rails[c] = true
|
||||
if bus {
|
||||
s.roads[c] = true
|
||||
} else {
|
||||
s.rails[c] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,7 +97,11 @@ func (s *LevelScene) drawRails(g *Game, screen *ebiten.Image, c level.Point, ox,
|
||||
op.GeoM.Translate(TileSize/2, TileSize/2)
|
||||
}
|
||||
op.GeoM.Translate(ox+float64(c.X*TileSize), oy+float64(c.Y*TileSize))
|
||||
screen.DrawImage(g.Assets.Rails, &op)
|
||||
img := g.Assets.Rails
|
||||
if s.roads[c] {
|
||||
img = g.Assets.Road
|
||||
}
|
||||
screen.DrawImage(img, &op)
|
||||
}
|
||||
|
||||
// drawTrains ritar tågen i rätt läge för sin fas.
|
||||
@@ -98,6 +112,11 @@ func (s *LevelScene) drawTrains(g *Game, screen *ebiten.Image, ox, oy float64) {
|
||||
continue // tågen på andra våningar syns inte härifrån
|
||||
}
|
||||
phase, prog := s.run.PlatformPhase(tr.tile)
|
||||
if tr.bus { // hållplatsskylten står på hållplatsen
|
||||
var op ebiten.DrawImageOptions
|
||||
op.GeoM.Translate(ox+float64(tr.platform.X*TileSize), oy+float64(tr.platform.Y*TileSize))
|
||||
screen.DrawImage(g.Assets.StopSign, &op)
|
||||
}
|
||||
// Strålkastare när tåget är på väg in: ett sken från det håll det kommer.
|
||||
if phase == level.Approaching {
|
||||
s.drawHeadlight(screen, tr, prog, ox, oy)
|
||||
@@ -106,8 +125,12 @@ func (s *LevelScene) drawTrains(g *Game, screen *ebiten.Image, ox, oy float64) {
|
||||
if !show {
|
||||
continue
|
||||
}
|
||||
for k := 0; k < trainCars; k++ {
|
||||
d := off + float64(k-trainCars/2)
|
||||
cars, sprite := trainCars, g.Assets.Train
|
||||
if tr.bus {
|
||||
cars, sprite = 1, g.Assets.Bus
|
||||
}
|
||||
for k := 0; k < cars; k++ {
|
||||
d := off + float64(k-cars/2)
|
||||
var op ebiten.DrawImageOptions
|
||||
x, y := float64(tr.track.X*TileSize), float64(tr.track.Y*TileSize)
|
||||
if tr.horizontal {
|
||||
@@ -119,7 +142,7 @@ func (s *LevelScene) drawTrains(g *Game, screen *ebiten.Image, ox, oy float64) {
|
||||
op.GeoM.Translate(TileSize/2, TileSize/2)
|
||||
}
|
||||
op.GeoM.Translate(ox+math.Round(x), oy+math.Round(y))
|
||||
screen.DrawImage(g.Assets.Train, &op)
|
||||
screen.DrawImage(sprite, &op)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,14 +185,19 @@ func (s *LevelScene) updateEffects(g *Game) {
|
||||
s.lastPhase[tr.tile] = phase
|
||||
dist := math.Hypot(float64(tr.platform.X*TileSize)-px, float64(tr.platform.Y*TileSize)-py) / TileSize
|
||||
mask := noiseMask(r.Level.Noise)
|
||||
switch phase {
|
||||
case level.Approaching: // ankomstjingeln: tåget är på väg
|
||||
switch {
|
||||
case tr.bus && phase == level.Arriving: // bussen tutar när den svänger in
|
||||
g.Audio.Play(SoundBusHorn, volumeAt(dist)*mask)
|
||||
case tr.bus && phase == level.Closing:
|
||||
g.Audio.Play(SoundDoors, volumeAt(dist)*mask)
|
||||
case tr.bus:
|
||||
case phase == level.Approaching: // ankomstjingeln: tåget är på väg
|
||||
g.Audio.Play(SoundApproach, volumeAt(dist)*mask)
|
||||
case level.Arriving: // ding-dong när det rullar in
|
||||
case phase == level.Arriving: // ding-dong när det rullar in
|
||||
g.Audio.Play(SoundAnnounce, volumeAt(dist)*mask)
|
||||
case level.Closing: // stationens avgångsmelodi innan dörrarna stängs
|
||||
case phase == level.Closing: // stationens avgångsmelodi innan dörrarna stängs
|
||||
g.Audio.Play(MelodyFor(r.Level), volumeAt(dist)*mask)
|
||||
case level.Leaving: // dörrsignalen när det går
|
||||
case phase == level.Leaving: // dörrsignalen när det går
|
||||
g.Audio.Play(SoundDoors, volumeAt(dist)*mask)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,6 +194,7 @@ type Train struct {
|
||||
Correct bool `json:"correct"` // tar spelaren till rätt resmål (räknas ut från nätet när banan har station/to)
|
||||
LineID string `json:"lineId"` // linjen i linjenätet (resources/data/network.json)
|
||||
Terminus string `json:"terminus"` // en station i färdriktningen (nätets id) – avgör riktningen
|
||||
Vehicle string `json:"vehicle"` // "" = tåg, "bus" = buss (väg i stället för räls, hållplats i stället för perrong)
|
||||
Crowded bool `json:"crowded"` // fullt tåg: minispel för att få plats (gäller alla vagnar)
|
||||
Cars int `json:"cars"` // antal vagnar (0 = DefaultCars)
|
||||
WomenOnlyCar int `json:"womenOnlyCar"` // 1-baserat vagnnummer för kvinnovagn, 0 = ingen
|
||||
@@ -269,6 +270,7 @@ type Level struct {
|
||||
SeatSelection bool `json:"seatSelection"` // biljettköpet fortsätter med sittplatsval efter sällskapets önskemål
|
||||
OneWayGates bool `json:"oneWayGates"` // spärren släpper inte ut en igen förrän man åkt; personal vid spärren låser upp (tar tid)
|
||||
Melody string `json:"melody"` // stationens avgångsmelodi: "a".."d" (tomt = vald efter id)
|
||||
Overload bool `json:"overload"` // överstimulerande UI: reklamticker, blinkande reklam, extra affischer
|
||||
FareYen int `json:"fareYen"` // vad spärren drar från kortet (0 = standard)
|
||||
Grid []string `json:"grid,omitempty"` // envåningsbana (kompatibilitet) – blir Floors[0]
|
||||
Floors []Storey `json:"floors,omitempty"` // flervåningsbana; alla våningar lika stora
|
||||
@@ -466,8 +468,14 @@ func (l *Level) Validate() error {
|
||||
if tr.Correct {
|
||||
correct++
|
||||
}
|
||||
if tr.Vehicle != "" && tr.Vehicle != "bus" {
|
||||
return fmt.Errorf("level %s: tåg %d har okänt fordon %q (tomt eller bus)", l.ID, i, tr.Vehicle)
|
||||
}
|
||||
if tr.Cars == 0 {
|
||||
tr.Cars = DefaultCars
|
||||
if tr.IsBus() {
|
||||
tr.Cars = 1
|
||||
}
|
||||
}
|
||||
if tr.Cars < 1 || tr.Cars > MaxCars {
|
||||
return fmt.Errorf("level %s: tåg %d har %d vagnar (1–%d)", l.ID, i, tr.Cars, MaxCars)
|
||||
@@ -720,11 +728,18 @@ func (l *Level) Unreachable() []Point {
|
||||
return out
|
||||
}
|
||||
|
||||
// BoardText är avgångstavlans text på japanska, t.ex. "2番線 成田エクスプレス 新宿方面".
|
||||
// BoardText är avgångstavlans text på japanska, t.ex. "2番線 成田エクスプレス 新宿方面"
|
||||
// (för bussar "1番 リムジンバス 新宿方面": hållplatsnummer i stället för spår).
|
||||
func (tr *Train) BoardText() string {
|
||||
if tr.IsBus() {
|
||||
return tr.Track + "番 " + tr.Line.Get("ja") + " " + tr.Destination.Get("ja") + "方面"
|
||||
}
|
||||
return tr.Track + "番線 " + tr.Line.Get("ja") + " " + tr.Destination.Get("ja") + "方面"
|
||||
}
|
||||
|
||||
// IsBus rapporterar om fordonet är en buss.
|
||||
func (tr *Train) IsBus() bool { return tr.Vehicle == "bus" }
|
||||
|
||||
// TrackTile är rutan "bortom perrongkanten" där rälsen går: första väggruta
|
||||
// nedanför, till höger, till vänster eller ovanför perrongen. horizontal
|
||||
// anger om rälsen (och tågets rörelse) går i sidled.
|
||||
|
||||
@@ -838,3 +838,24 @@ func TestToiletSides(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBusVehicleAndBoardText(t *testing.T) {
|
||||
src := `{"id":"bus","name":{"sv":"x"},"difficulty":1,"destination":{"ja":"新宿"},
|
||||
"grid":["#######","#S.GP.#","#######"],
|
||||
"trains":[{"platform":"P","track":"1","line":{"ja":"リムジンバス"},"destination":{"ja":"新宿"},"correct":true,"vehicle":"bus"}]}`
|
||||
l, err := Parse([]byte(src))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tr := &l.Trains[0]
|
||||
if !tr.IsBus() || tr.Cars != 1 || tr.BoardText() != "1番 リムジンバス 新宿方面" {
|
||||
t.Fatalf("buss: cars=%d board=%q", tr.Cars, tr.BoardText())
|
||||
}
|
||||
if _, err := Parse([]byte(strings.Replace(src, `"vehicle":"bus"`, `"vehicle":"tram"`, 1))); err == nil {
|
||||
t.Fatal("okänt fordon ska ge fel")
|
||||
}
|
||||
train, _ := Parse([]byte(strings.Replace(src, `,"vehicle":"bus"`, ``, 1)))
|
||||
if train.Trains[0].IsBus() || train.Trains[0].Cars != DefaultCars || train.Trains[0].BoardText() != "1番線 リムジンバス 新宿方面" {
|
||||
t.Fatal("utan vehicle är det ett tåg")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,3 +169,20 @@ func TestShippedNetwork(t *testing.T) {
|
||||
t.Errorf("Marunouchi via city = %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShippedBusLines(t *testing.T) {
|
||||
dir, err := res.Path("data")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
n, err := LoadFile(filepath.Join(dir, "network.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v := n.Judge("limousine-shinjuku", "narita-airport", "shinjuku", "shinjuku"); v != Direct {
|
||||
t.Errorf("flygbussen mot Shinjuku = %v", v)
|
||||
}
|
||||
if v := n.Judge("limousine-yokohama", "narita-airport", "yokohama", "shinjuku"); v != NoService {
|
||||
t.Errorf("flygbussen mot Yokohama för den som ska till Shinjuku = %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,3 +126,36 @@ func TestTerminalLines(t *testing.T) {
|
||||
t.Fatal("okopplad bana har ingen terminal")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBusTerminalFlow: flygbussterminalen är en biljettbana med bussar –
|
||||
// köp biljett till Shinjuku, gå till hållplats 1 och åk.
|
||||
func TestBusTerminalFlow(t *testing.T) {
|
||||
l := loadLevel(t, "001b-flygbussen")
|
||||
if !l.TicketRequired || !l.Trains[0].IsBus() || l.Trains[0].Cars != 1 {
|
||||
t.Fatal("001b ska vara en biljettbana med bussar")
|
||||
}
|
||||
r := NewRun(l, RunOptions{Dict: loadDict(t), Seed: rng.FromInt(3)})
|
||||
r.Party.nextEventAt = 1e9
|
||||
r.NPCs = nil
|
||||
walk(t, r, pathTo(r, l.Find(level.TicketMachine)[0]))
|
||||
r.Step(frame, None, true)
|
||||
tk, ok := r.Mini.(*minigame.Ticket)
|
||||
if !ok {
|
||||
t.Fatalf("ingen biljettautomat (kind=%v)", r.MiniKind)
|
||||
}
|
||||
for tk.Options[tk.Cursor].Dest != "新宿" {
|
||||
r.StepMini(frame, minigame.Input{Move: 1})
|
||||
}
|
||||
r.StepMini(frame, minigame.Input{Confirm: true})
|
||||
if r.Wallet.Ticket != "新宿" {
|
||||
t.Fatalf("biljett=%q", r.Wallet.Ticket)
|
||||
}
|
||||
tr := boardAt(t, r, level.Platform)
|
||||
if !tr.IsBus() || tr.Verdict != nav.Direct {
|
||||
t.Fatalf("hållplats 1: %v %v", tr.IsBus(), tr.Verdict)
|
||||
}
|
||||
ride(t, r)
|
||||
if r.Outcome != Won {
|
||||
t.Fatalf("outcome=%v (%s)", r.Outcome, r.LostReason)
|
||||
}
|
||||
}
|
||||
|
||||
154
resources/data/levels/001b-flygbussen.json
Normal file
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"destination": {
|
||||
"en": "Shinjuku",
|
||||
"ja": "新宿",
|
||||
"romaji": "Shinjuku",
|
||||
"sv": "Shinjuku"
|
||||
},
|
||||
"difficulty": 1,
|
||||
"fareYen": 0,
|
||||
"grid": [
|
||||
"#################################",
|
||||
"#S#.....#...#..i..#.....#.....#K#",
|
||||
"#.#.###.#.#.#.#.#.#.###.#.#.#.#.#",
|
||||
"#.#i..#.#.#...#.#...#...#.#.#...#",
|
||||
"#.#.#.#.#.#.###.#####.###.#.###.#",
|
||||
"#.#T#.#...#.....#.#...#.#.#.#.#.#",
|
||||
"#.###.###########.#.#.#.#.#.#.#.#",
|
||||
"#...#...#.........#.#.....#.#.#.#",
|
||||
"###.###.#.#########.#.#####.#.#.#",
|
||||
"#...#...#.#...#.....#.....#.#...#",
|
||||
"#.###.###.#.#.#.#########.#.#.###",
|
||||
"#.#...#.....#...#.....#...#.#...#",
|
||||
"#.#.###.#####.#.#.#.#.#####.#.#.#",
|
||||
"#..i#...#...#.#...#.#.#.....#.#.#",
|
||||
"###.#.###.###.#####.#.#.#####.#.#",
|
||||
"#...#...#...#.#.......#.#.....#.#",
|
||||
"#.#####.#.#.#.#.#######.#.#####.#",
|
||||
"#W#.....#.#.#.#...#...#.#.....#.#",
|
||||
"###.#####.#.#.###G#.#.#.#####.#G#",
|
||||
"#pG.........#...#Q#.#...#.....#P#",
|
||||
"#################################"
|
||||
],
|
||||
"id": "001b-flygbussen",
|
||||
"kind": "side",
|
||||
"name": {
|
||||
"en": "Airport buses",
|
||||
"ja": "空港バスのりば",
|
||||
"sv": "Flygbussarna"
|
||||
},
|
||||
"order": 1,
|
||||
"parent": "001-flygplatsen",
|
||||
"party": 2,
|
||||
"rewards": {
|
||||
"skillPoints": 1
|
||||
},
|
||||
"rideStops": 2,
|
||||
"signs": [
|
||||
{
|
||||
"text": {
|
||||
"en": "Bus terminal. Tickets at the machine. Stop 1 limousine bus for Shinjuku: south-east. Stop 2 limousine bus for Yokohama: south-west. Stop 3 airport bus for Tokyo: south.",
|
||||
"ja": "バスのりば 切符 → 券売機 1番 リムジンバス 新宿方面 → 南東 2番 リムジンバス 横浜方面 → 南西 3番 空港バス 東京方面 → 南",
|
||||
"sv": "Bussterminalen. Biljetter köps i automaten. Hållplats 1 flygbuss mot Shinjuku: sydost. Hållplats 2 flygbuss mot Yokohama: sydväst. Hållplats 3 flygplatsbuss mot Tokyo: söderut."
|
||||
},
|
||||
"x": 3,
|
||||
"y": 13,
|
||||
"z": 0
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"en": "<- Stop 2 limousine bus for Yokohama Stop 1 limousine bus for Shinjuku ->",
|
||||
"ja": "← 2番 リムジンバス 横浜方面 1番 リムジンバス 新宿方面 →",
|
||||
"sv": "<- Hållplats 2 flygbuss mot Yokohama Hållplats 1 flygbuss mot Shinjuku ->"
|
||||
},
|
||||
"x": 3,
|
||||
"y": 3,
|
||||
"z": 0
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"en": "Stop 1 limousine bus for Shinjuku: the gate in the south-east corner. Ticket required.",
|
||||
"ja": "1番 リムジンバス 新宿方面 → 南東口 改札 切符",
|
||||
"sv": "Hållplats 1 flygbuss mot Shinjuku: spärren i sydöstra hörnet. Biljett krävs."
|
||||
},
|
||||
"x": 15,
|
||||
"y": 1,
|
||||
"z": 0
|
||||
}
|
||||
],
|
||||
"station": "narita-airport",
|
||||
"ticketRequired": true,
|
||||
"timeLimitSec": 300,
|
||||
"to": "shinjuku",
|
||||
"trains": [
|
||||
{
|
||||
"cars": 1,
|
||||
"destination": {
|
||||
"en": "Shinjuku",
|
||||
"ja": "新宿",
|
||||
"romaji": "Shinjuku"
|
||||
},
|
||||
"line": {
|
||||
"en": "Airport Limousine",
|
||||
"ja": "リムジンバス",
|
||||
"romaji": "Limousine Bus"
|
||||
},
|
||||
"lineId": "limousine-shinjuku",
|
||||
"platform": "P",
|
||||
"schedule": {
|
||||
"dwellSec": 9,
|
||||
"intervalSec": 32,
|
||||
"offsetSec": 10
|
||||
},
|
||||
"terminus": "shinjuku",
|
||||
"track": "1",
|
||||
"vehicle": "bus"
|
||||
},
|
||||
{
|
||||
"cars": 1,
|
||||
"destination": {
|
||||
"en": "Yokohama",
|
||||
"ja": "横浜",
|
||||
"romaji": "Yokohama"
|
||||
},
|
||||
"line": {
|
||||
"en": "Airport Limousine",
|
||||
"ja": "リムジンバス",
|
||||
"romaji": "Limousine Bus"
|
||||
},
|
||||
"lineId": "limousine-yokohama",
|
||||
"platform": "p",
|
||||
"schedule": {
|
||||
"dwellSec": 8,
|
||||
"intervalSec": 32,
|
||||
"offsetSec": 20
|
||||
},
|
||||
"terminus": "yokohama",
|
||||
"track": "2",
|
||||
"vehicle": "bus"
|
||||
},
|
||||
{
|
||||
"cars": 1,
|
||||
"destination": {
|
||||
"en": "Tokyo",
|
||||
"ja": "東京",
|
||||
"romaji": "Tokyo"
|
||||
},
|
||||
"line": {
|
||||
"en": "Airport Bus",
|
||||
"ja": "空港バス",
|
||||
"romaji": "Kuko basu"
|
||||
},
|
||||
"lineId": "airport-bus-tokyo",
|
||||
"platform": "Q",
|
||||
"schedule": {
|
||||
"dwellSec": 8,
|
||||
"intervalSec": 28,
|
||||
"offsetSec": 4
|
||||
},
|
||||
"terminus": "tokyo",
|
||||
"track": "3",
|
||||
"vehicle": "bus"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -34,9 +34,9 @@
|
||||
"kind": "main",
|
||||
"maxMissedTrains": 2,
|
||||
"name": {
|
||||
"en": "Shinjuku Station",
|
||||
"en": "Shinjuku",
|
||||
"ja": "新宿駅",
|
||||
"sv": "Shinjuku station"
|
||||
"sv": "Shinjuku"
|
||||
},
|
||||
"order": 2,
|
||||
"party": 3,
|
||||
@@ -62,9 +62,9 @@
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"en": "\u003c- Track 8 Chuo Line (Tokyo) Track 14 Yamanote Line (Shibuya) -\u003e",
|
||||
"en": "<- Track 8 Chuo Line (Tokyo) Track 14 Yamanote Line (Shibuya) ->",
|
||||
"ja": "← 8番線 中央線 東京方面 14番線 山手線 渋谷方面 →",
|
||||
"sv": "\u003c- Spår 8 Chuo-linjen (Tokyo) Spår 14 Yamanote-linjen (Shibuya) -\u003e"
|
||||
"sv": "<- Spår 8 Chuo-linjen (Tokyo) Spår 14 Yamanote-linjen (Shibuya) ->"
|
||||
},
|
||||
"x": 11,
|
||||
"y": 15,
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
"kind": "main",
|
||||
"order": 3,
|
||||
"name": {
|
||||
"en": "Shibuya Station",
|
||||
"en": "Shibuya",
|
||||
"ja": "渋谷駅",
|
||||
"sv": "Shibuya station"
|
||||
"sv": "Shibuya"
|
||||
},
|
||||
"difficulty": 3,
|
||||
"party": 4,
|
||||
|
||||
131
resources/data/levels/003b-harajuku.json
Normal file
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"crowd": 1,
|
||||
"destination": {
|
||||
"en": "Shinjuku",
|
||||
"ja": "新宿",
|
||||
"romaji": "Shinjuku",
|
||||
"sv": "Shinjuku"
|
||||
},
|
||||
"difficulty": 2,
|
||||
"fareYen": 160,
|
||||
"grid": [
|
||||
"#################################",
|
||||
"#S..#W..............#.....#...#.#",
|
||||
"#.#.###.#########.#.#.###.#.#.#.#",
|
||||
"#.#.....#.........#.#.#K#...#...#",
|
||||
"#.#######.#.###.###.#.#.#######.#",
|
||||
"#...#.....#.#...#...#.#...#...#.#",
|
||||
"#.###.#####.#.###.###.#.###.#.#.#",
|
||||
"#...#.#.....#T#...#...#.#...#.#.#",
|
||||
"#.#.#.#.#######.###.###.#.###.#.#",
|
||||
"#...#.#...#.....#...#.....#...#.#",
|
||||
"###.#.###.#.#######.#.#####.###.#",
|
||||
"#..i#...#...#.......#.....#.#...#",
|
||||
"#.#.#.#.#####.#.#####.###.#.#.###",
|
||||
"#.#.#...#...#.#.#...#...#.#.#...#",
|
||||
"#.#.###.#.#.###.#.#.#.#.#.#.###.#",
|
||||
"#.#...#.#.#...#.#.#...#.#...#..i#",
|
||||
"#.#####.#.###.#.#.#####.#.###.#.#",
|
||||
"#.....#...#.#...#.#...#.#.#...#.#",
|
||||
"#G#.#.#####.#####.#.#.#.#.#.#####",
|
||||
"#p#.#......i........#.........GP#",
|
||||
"#################################"
|
||||
],
|
||||
"id": "003b-harajuku",
|
||||
"kind": "side",
|
||||
"name": {
|
||||
"en": "Harajuku",
|
||||
"ja": "原宿駅",
|
||||
"sv": "Harajuku"
|
||||
},
|
||||
"order": 3,
|
||||
"parent": "003-shibuya",
|
||||
"party": 2,
|
||||
"rewards": {
|
||||
"items": [
|
||||
"mask_tengu"
|
||||
]
|
||||
},
|
||||
"rideStops": 3,
|
||||
"signs": [
|
||||
{
|
||||
"text": {
|
||||
"en": "Harajuku Station. Track 1 Yamanote Line for Shinjuku: south-east. Track 2 Yamanote Line for Shibuya: south-west. Same line - the wrong way round the loop takes half an hour.",
|
||||
"ja": "原宿駅 1番線 山手線 新宿方面 → 南東 2番線 山手線 渋谷方面 → 南西",
|
||||
"sv": "Harajuku station. Spår 1 Yamanote-linjen mot Shinjuku: sydost. Spår 2 Yamanote-linjen mot Shibuya: sydväst. Samma linje - fel varv runt ringen tar en halvtimme."
|
||||
},
|
||||
"x": 3,
|
||||
"y": 11,
|
||||
"z": 0
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"en": "\u003c- Track 2 Yamanote Line for Shibuya Track 1 Yamanote Line for Shinjuku -\u003e",
|
||||
"ja": "← 2番線 山手線 渋谷方面 1番線 山手線 新宿方面 →",
|
||||
"sv": "\u003c- Spår 2 Yamanote-linjen mot Shibuya Spår 1 Yamanote-linjen mot Shinjuku -\u003e"
|
||||
},
|
||||
"x": 11,
|
||||
"y": 19,
|
||||
"z": 0
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"en": "Track 1 Yamanote Line for Shinjuku: the gate in the south-east corner.",
|
||||
"ja": "1番線 山手線 新宿方面 → 南東口 改札",
|
||||
"sv": "Spår 1 Yamanote-linjen mot Shinjuku: spärren i sydöstra hörnet."
|
||||
},
|
||||
"x": 31,
|
||||
"y": 15,
|
||||
"z": 0
|
||||
}
|
||||
],
|
||||
"station": "harajuku",
|
||||
"timeLimitSec": 240,
|
||||
"to": "shinjuku",
|
||||
"trains": [
|
||||
{
|
||||
"cars": 4,
|
||||
"destination": {
|
||||
"en": "Shinjuku",
|
||||
"ja": "新宿",
|
||||
"romaji": "Shinjuku"
|
||||
},
|
||||
"line": {
|
||||
"en": "Yamanote Line",
|
||||
"ja": "山手線",
|
||||
"romaji": "Yamanote-sen"
|
||||
},
|
||||
"lineId": "yamanote",
|
||||
"platform": "P",
|
||||
"schedule": {
|
||||
"dwellSec": 8,
|
||||
"intervalSec": 26,
|
||||
"offsetSec": 8
|
||||
},
|
||||
"terminus": "shinjuku",
|
||||
"track": "1"
|
||||
},
|
||||
{
|
||||
"cars": 4,
|
||||
"destination": {
|
||||
"en": "Shibuya",
|
||||
"ja": "渋谷",
|
||||
"romaji": "Shibuya"
|
||||
},
|
||||
"line": {
|
||||
"en": "Yamanote Line",
|
||||
"ja": "山手線",
|
||||
"romaji": "Yamanote-sen"
|
||||
},
|
||||
"lineId": "yamanote",
|
||||
"platform": "p",
|
||||
"schedule": {
|
||||
"dwellSec": 8,
|
||||
"intervalSec": 26,
|
||||
"offsetSec": 18
|
||||
},
|
||||
"terminus": "shibuya",
|
||||
"track": "2"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -65,9 +65,9 @@
|
||||
"kind": "main",
|
||||
"maxMissedTrains": 2,
|
||||
"name": {
|
||||
"en": "Tokyo Station",
|
||||
"en": "Tokyo",
|
||||
"ja": "東京駅",
|
||||
"sv": "Tokyo station"
|
||||
"sv": "Tokyo"
|
||||
},
|
||||
"order": 4,
|
||||
"party": 3,
|
||||
@@ -92,9 +92,9 @@
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"en": "\u003c- Track 7 Tokaido Line (Shinagawa) Track 4 Yamanote Line (Ueno) -\u003e",
|
||||
"en": "<- Track 7 Tokaido Line (Shinagawa) Track 4 Yamanote Line (Ueno) ->",
|
||||
"ja": "← 7番線 東海道線 品川方面 4番線 山手線 上野方面 →",
|
||||
"sv": "\u003c- Spår 7 Tokaido-linjen (Shinagawa) Spår 4 Yamanote-linjen (Ueno) -\u003e"
|
||||
"sv": "<- Spår 7 Tokaido-linjen (Shinagawa) Spår 4 Yamanote-linjen (Ueno) ->"
|
||||
},
|
||||
"x": 3,
|
||||
"y": 3,
|
||||
|
||||
@@ -11,23 +11,23 @@
|
||||
"fareYen": 200,
|
||||
"grid": [
|
||||
"#################################",
|
||||
"#S.\u003c..\u003c..#..\u003c..\u003c..\u003cK.\u003c..#..\u003c..###",
|
||||
"#..\u003e..\u003e..#..\u003e..\u003e..\u003e..\u003e..#.i\u003e..###",
|
||||
"#######\u003c\u003e#\u003c\u003e#\u003c\u003e####\u003c\u003e#\u003c\u003e####\u003c\u003e###",
|
||||
"#..\u003c..\u003cT.#..\u003c..\u003c..\u003c..#..\u003c..\u003c..###",
|
||||
"#.i\u003e..\u003e..#..\u003e..\u003e..\u003e..#..\u003e..\u003e..###",
|
||||
"#\u003c\u003e#############\u003c\u003e#######\u003c\u003e#\u003c\u003e###",
|
||||
"#..#..\u003c..\u003c..\u003c..\u003c..\u003c..\u003c..#..\u003c..###",
|
||||
"#..#..\u003e..\u003e..\u003e..\u003e..\u003e..\u003e..#..\u003e..###",
|
||||
"#\u003c\u003e####\u003c\u003e##########\u003c\u003e#\u003c\u003e#\u003c\u003e######",
|
||||
"#..\u003c..\u003c..\u003c..#..\u003c..\u003c..#..#..\u003c..###",
|
||||
"#..\u003e..\u003e..\u003e..#..\u003e..\u003e..#..#..\u003e..###",
|
||||
"####\u003c\u003e#\u003c\u003e#\u003c\u003e#\u003c\u003e####\u003c\u003e#######\u003c\u003e###",
|
||||
"#..\u003c..#..#..#..\u003c..#..\u003c..\u003c..\u003c..###",
|
||||
"#G.\u003e..#..#..#..\u003e..#..\u003e..\u003e..\u003e..###",
|
||||
"#\u003cG#######\u003c\u003e####\u003c\u003e###############",
|
||||
"#..#..\u003c..\u003c.i#..\u003c..\u003c..\u003c..\u003c..G..W##",
|
||||
"#p.#..\u003e..\u003e..#..\u003e..\u003e..\u003e..\u003e.G\u003e.P###",
|
||||
"#S.<..<..#..<..<..<K.<..#..<..###",
|
||||
"#..>..>..#..>..>..>..>..#.i>..###",
|
||||
"#######<>#<>#<>####<>#<>####<>###",
|
||||
"#..<..<T.#..<..<..<..#..<..<..###",
|
||||
"#.i>..>..#..>..>..>..#..>..>..###",
|
||||
"#<>#############<>#######<>#<>###",
|
||||
"#..#..<..<..<..<..<..<..#..<..###",
|
||||
"#..#..>..>..>..>..>..>..#..>..###",
|
||||
"#<>####<>##########<>#<>#<>######",
|
||||
"#..<..<..<..#..<..<..#..#..<..###",
|
||||
"#..>..>..>..#..>..>..#..#..>..###",
|
||||
"####<>#<>#<>#<>####<>#######<>###",
|
||||
"#..<..#..#..#..<..#..<..<..<..###",
|
||||
"#G.>..#..#..#..>..#..>..>..>..###",
|
||||
"#<G#######<>####<>###############",
|
||||
"#..#..<..<.i#..<..<..<..<..G..W##",
|
||||
"#p.#..>..>..#..>..>..>..>.G>.P###",
|
||||
"#################################",
|
||||
"#################################",
|
||||
"#################################"
|
||||
@@ -36,9 +36,9 @@
|
||||
"kind": "main",
|
||||
"maxMissedTrains": 2,
|
||||
"name": {
|
||||
"en": "Ueno Station",
|
||||
"en": "Ueno",
|
||||
"ja": "上野駅",
|
||||
"sv": "Ueno station"
|
||||
"sv": "Ueno"
|
||||
},
|
||||
"noise": 2,
|
||||
"oneWayGates": true,
|
||||
@@ -65,9 +65,9 @@
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"en": "\u003c- Track 9 Keihin-Tohoku Line (Omiya) Track 3 Yamanote Line (Akihabara) -\u003e",
|
||||
"en": "<- Track 9 Keihin-Tohoku Line (Omiya) Track 3 Yamanote Line (Akihabara) ->",
|
||||
"ja": "← 9番線 京浜東北線 大宮方面 3番線 山手線 秋葉原方面 →",
|
||||
"sv": "\u003c- Spår 9 Keihin-Tohoku-linjen (Omiya) Spår 3 Yamanote-linjen (Akihabara) -\u003e"
|
||||
"sv": "<- Spår 9 Keihin-Tohoku-linjen (Omiya) Spår 3 Yamanote-linjen (Akihabara) ->"
|
||||
},
|
||||
"x": 11,
|
||||
"y": 16,
|
||||
@@ -135,5 +135,6 @@
|
||||
"terminus": "omiya",
|
||||
"track": "9"
|
||||
}
|
||||
]
|
||||
],
|
||||
"overload": true
|
||||
}
|
||||
@@ -36,9 +36,9 @@
|
||||
"kind": "main",
|
||||
"maxMissedTrains": 2,
|
||||
"name": {
|
||||
"en": "Ikebukuro Station",
|
||||
"en": "Ikebukuro",
|
||||
"ja": "池袋駅",
|
||||
"sv": "Ikebukuro station"
|
||||
"sv": "Ikebukuro"
|
||||
},
|
||||
"noise": 1,
|
||||
"order": 6,
|
||||
@@ -62,9 +62,9 @@
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"en": "\u003c- Track 2 Saikyo Line for Omiya Track 1 Saikyo Line for Osaki -\u003e",
|
||||
"en": "<- Track 2 Saikyo Line for Omiya Track 1 Saikyo Line for Osaki ->",
|
||||
"ja": "← 2番線 埼京線 大宮方面 1番線 埼京線 大崎方面 →",
|
||||
"sv": "\u003c- Spår 2 Saikyo-linjen mot Omiya Spår 1 Saikyo-linjen mot Osaki -\u003e"
|
||||
"sv": "<- Spår 2 Saikyo-linjen mot Omiya Spår 1 Saikyo-linjen mot Osaki ->"
|
||||
},
|
||||
"x": 17,
|
||||
"y": 9,
|
||||
|
||||
@@ -37,9 +37,9 @@
|
||||
"kind": "main",
|
||||
"maxMissedTrains": 3,
|
||||
"name": {
|
||||
"en": "Akihabara Station",
|
||||
"en": "Akihabara",
|
||||
"ja": "秋葉原駅",
|
||||
"sv": "Akihabara station"
|
||||
"sv": "Akihabara"
|
||||
},
|
||||
"next": "007b-ueno",
|
||||
"noise": 1,
|
||||
@@ -65,9 +65,9 @@
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"en": "\u003c- Track 3 Hibiya Line for Naka-Meguro Track 4 Hibiya Line for Kita-Senju -\u003e",
|
||||
"en": "<- Track 3 Hibiya Line for Naka-Meguro Track 4 Hibiya Line for Kita-Senju ->",
|
||||
"ja": "← 3番線 日比谷線 中目黒方面 4番線 日比谷線 北千住方面 →",
|
||||
"sv": "\u003c- Spår 3 Hibiya-linjen mot Naka-Meguro Spår 4 Hibiya-linjen mot Kita-Senju -\u003e"
|
||||
"sv": "<- Spår 3 Hibiya-linjen mot Naka-Meguro Spår 4 Hibiya-linjen mot Kita-Senju ->"
|
||||
},
|
||||
"x": 9,
|
||||
"y": 11,
|
||||
|
||||
@@ -11,23 +11,23 @@
|
||||
"fareYen": 170,
|
||||
"grid": [
|
||||
"#################################",
|
||||
"#S.#.i\u003c..\u003c..\u003c..\u003c..\u003c..\u003c..\u003c..\u003c..###",
|
||||
"#..#..\u003e..\u003e..\u003e..\u003e..\u003e..\u003e..\u003e..\u003e..###",
|
||||
"#\u003c\u003e################\u003c\u003e#######\u003c\u003e###",
|
||||
"#..#..\u003c..\u003c..\u003c..\u003c..#..#..\u003c..#..###",
|
||||
"#..#..\u003e..\u003e..\u003e..\u003e..#..#..\u003e..#..###",
|
||||
"#\u003c\u003e#\u003c\u003e##########\u003c\u003e#\u003c\u003e#\u003c\u003e####\u003c\u003e###",
|
||||
"#..\u003c..#..\u003c..\u003c..#..#..#..\u003c..\u003c..###",
|
||||
"#..\u003e..#..\u003e..\u003e..#..#..#..\u003e..\u003e..###",
|
||||
"####\u003c\u003e#\u003c\u003e#\u003c\u003e#\u003c\u003e#\u003c\u003e#\u003c\u003e#\u003c\u003e#########",
|
||||
"#..\u003c..\u003ci.\u003cT.#..#..#..\u003c..#..\u003c..###",
|
||||
"#..\u003e..\u003e.K\u003e..#..#..#..\u003e..#..\u003e..###",
|
||||
"#\u003c\u003e#\u003c\u003e#######\u003c\u003e#\u003c\u003e#\u003c\u003e#\u003c\u003e####\u003c\u003e###",
|
||||
"#..#..\u003c..\u003c..#..#..\u003c.i#..\u003c..\u003c..###",
|
||||
"#..#..\u003e..\u003e..#..#..\u003e..#..\u003e..\u003e..###",
|
||||
"####\u003c\u003e####\u003c\u003e#\u003c\u003e#G\u003e##########\u003c\u003e###",
|
||||
"#.G\u003cW.\u003c..\u003c..#..#.G#..\u003c..\u003c..\u003c.G###",
|
||||
"#p.G..\u003e..\u003e..#..#Q.#..\u003e..\u003e..\u003eGP###",
|
||||
"#S.#.i<..<..<..<..<..<..<..<..###",
|
||||
"#..#..>..>..>..>..>..>..>..>..###",
|
||||
"#<>################<>#######<>###",
|
||||
"#..#..<..<..<..<..#..#..<..#..###",
|
||||
"#..#..>..>..>..>..#..#..>..#..###",
|
||||
"#<>#<>##########<>#<>#<>####<>###",
|
||||
"#..<..#..<..<..#..#..#..<..<..###",
|
||||
"#..>..#..>..>..#..#..#..>..>..###",
|
||||
"####<>#<>#<>#<>#<>#<>#<>#########",
|
||||
"#..<..<i.<T.#..#..#..<..#..<..###",
|
||||
"#..>..>.K>..#..#..#..>..#..>..###",
|
||||
"#<>#<>#######<>#<>#<>#<>####<>###",
|
||||
"#..#..<..<..#..#..<.i#..<..<..###",
|
||||
"#..#..>..>..#..#..>..#..>..>..###",
|
||||
"####<>####<>#<>#G>##########<>###",
|
||||
"#.G<W.<..<..#..#.G#..<..<..<.G###",
|
||||
"#p.G..>..>..#..#Q.#..>..>..>GP###",
|
||||
"##W##############################",
|
||||
"#################################",
|
||||
"#################################"
|
||||
@@ -36,9 +36,9 @@
|
||||
"individualCards": true,
|
||||
"kind": "leg",
|
||||
"name": {
|
||||
"en": "Ueno Station",
|
||||
"en": "Ueno",
|
||||
"ja": "上野駅",
|
||||
"sv": "Ueno station"
|
||||
"sv": "Ueno"
|
||||
},
|
||||
"noise": 2,
|
||||
"oneWayGates": true,
|
||||
@@ -57,9 +57,9 @@
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"en": "\u003c- Track 2 Ginza Line for Shibuya Track 1 Ginza Line for Asakusa -\u003e",
|
||||
"en": "<- Track 2 Ginza Line for Shibuya Track 1 Ginza Line for Asakusa ->",
|
||||
"ja": "← 2番線 銀座線 渋谷方面 1番線 銀座線 浅草方面 →",
|
||||
"sv": "\u003c- Spår 2 Ginza-linjen mot Shibuya Spår 1 Ginza-linjen mot Asakusa -\u003e"
|
||||
"sv": "<- Spår 2 Ginza-linjen mot Shibuya Spår 1 Ginza-linjen mot Asakusa ->"
|
||||
},
|
||||
"x": 20,
|
||||
"y": 13,
|
||||
|
||||
@@ -68,9 +68,9 @@
|
||||
"kind": "main",
|
||||
"maxMissedTrains": 3,
|
||||
"name": {
|
||||
"en": "Tokyo Station, going home",
|
||||
"en": "Tokyo",
|
||||
"ja": "東京駅",
|
||||
"sv": "Tokyo station, hemresan"
|
||||
"sv": "Tokyo"
|
||||
},
|
||||
"next": "008b-nippori",
|
||||
"noise": 2,
|
||||
@@ -100,9 +100,9 @@
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"en": "\u003c- Track 5 Yamanote Line for Shinagawa Track 4 Yamanote Line for Ueno -\u003e",
|
||||
"en": "<- Track 5 Yamanote Line for Shinagawa Track 4 Yamanote Line for Ueno ->",
|
||||
"ja": "← 5番線 山手線 品川方面 4番線 山手線 上野方面 →",
|
||||
"sv": "\u003c- Spår 5 Yamanote-linjen mot Shinagawa Spår 4 Yamanote-linjen mot Ueno -\u003e"
|
||||
"sv": "<- Spår 5 Yamanote-linjen mot Shinagawa Spår 4 Yamanote-linjen mot Ueno ->"
|
||||
},
|
||||
"x": 7,
|
||||
"y": 19,
|
||||
@@ -211,5 +211,6 @@
|
||||
"terminus": "takao",
|
||||
"track": "1"
|
||||
}
|
||||
]
|
||||
],
|
||||
"overload": true
|
||||
}
|
||||
@@ -36,9 +36,9 @@
|
||||
"individualCards": true,
|
||||
"kind": "leg",
|
||||
"name": {
|
||||
"en": "Nippori Station",
|
||||
"en": "Nippori",
|
||||
"ja": "日暮里駅",
|
||||
"sv": "Nippori station"
|
||||
"sv": "Nippori"
|
||||
},
|
||||
"noise": 1,
|
||||
"oneWayGates": true,
|
||||
@@ -58,9 +58,9 @@
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"en": "\u003c- Track 2 Keisei Line for Keisei-Ueno Track 1 Keisei Line for Narita Airport -\u003e",
|
||||
"en": "<- Track 2 Keisei Line for Keisei-Ueno Track 1 Keisei Line for Narita Airport ->",
|
||||
"ja": "← 2番線 京成本線 京成上野方面 1番線 京成本線 成田空港方面 →",
|
||||
"sv": "\u003c- Spår 2 Keisei-linjen mot Keisei-Ueno Spår 1 Keisei-linjen mot Narita flygplats -\u003e"
|
||||
"sv": "<- Spår 2 Keisei-linjen mot Keisei-Ueno Spår 1 Keisei-linjen mot Narita flygplats ->"
|
||||
},
|
||||
"x": 1,
|
||||
"y": 17,
|
||||
|
||||
@@ -1004,6 +1004,47 @@
|
||||
"yokohama",
|
||||
"ofuna"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "limousine-shinjuku",
|
||||
"name": {
|
||||
"ja": "リムジンバス",
|
||||
"romaji": "Limousine Bus",
|
||||
"en": "Airport Limousine (Shinjuku)"
|
||||
},
|
||||
"stations": [
|
||||
"narita-airport",
|
||||
"airport-terminal-2",
|
||||
"shinjuku"
|
||||
],
|
||||
"vehicle": "bus"
|
||||
},
|
||||
{
|
||||
"id": "limousine-yokohama",
|
||||
"name": {
|
||||
"ja": "リムジンバス",
|
||||
"romaji": "Limousine Bus",
|
||||
"en": "Airport Limousine (Yokohama)"
|
||||
},
|
||||
"stations": [
|
||||
"narita-airport",
|
||||
"airport-terminal-2",
|
||||
"yokohama"
|
||||
],
|
||||
"vehicle": "bus"
|
||||
},
|
||||
{
|
||||
"id": "airport-bus-tokyo",
|
||||
"name": {
|
||||
"ja": "空港バス",
|
||||
"romaji": "Kuko basu",
|
||||
"en": "Airport Bus (Tokyo)"
|
||||
},
|
||||
"stations": [
|
||||
"narita-airport",
|
||||
"tokyo"
|
||||
],
|
||||
"vehicle": "bus"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
resources/sound/bus-horn.wav
Normal file
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -5,7 +5,7 @@
|
||||
"menu.endless": "Endless mode",
|
||||
"menu.settings": "Settings",
|
||||
"menu.quit": "Quit",
|
||||
"hud.destination": "Destination",
|
||||
"hud.destination": "To",
|
||||
"hud.next_train": "Next train",
|
||||
"hud.card_balance": "Card balance",
|
||||
"meter.food": "Food",
|
||||
@@ -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",
|
||||
@@ -46,7 +46,7 @@
|
||||
"hud.progress": "Progress",
|
||||
"words.title": "Word list 言葉",
|
||||
"words.points": "Points",
|
||||
"words.hint": "Arrows: select Enter: learn the word for points Esc/L: back",
|
||||
"words.hint": "Arrows: select Enter: learn for points Esc: back",
|
||||
"words.already": "You already know that word.",
|
||||
"words.bought": "New word: %s = %s",
|
||||
"words.too_expensive": "Costs %d points - clear levels with good times for more.",
|
||||
@@ -89,7 +89,7 @@
|
||||
"mini.ticket.title": "Tickets",
|
||||
"mini.ticket.hint": "Buy a ticket to %s. Cash: %d¥",
|
||||
"mini.crowd.title": "Crowded train - squeeze in",
|
||||
"mini.crowd.hint": "Press Enter when the marker is in the green zone (%d tries left)",
|
||||
"mini.crowd.hint": "Enter when the marker is in the green zone (%d left)",
|
||||
"mini.ride.title": "On the train - next stop",
|
||||
"mini.ride.hint": "Enter: get off here (you are going to %s)",
|
||||
"hud.time_left": "Left",
|
||||
@@ -140,7 +140,7 @@
|
||||
"map.party": "Party",
|
||||
"map.resume": "⟳ resume (saved seed)",
|
||||
"map.locked": "Locked - clear the previous level first.",
|
||||
"map.hint": "Enter start N mode L words T skills I items P phone S set.",
|
||||
"map.hint": "Enter: start level N: mode L: words T: skills",
|
||||
"skills.title": "Skills 技",
|
||||
"skills.to_next": "XP to next",
|
||||
"skills.already": "Already unlocked.",
|
||||
@@ -185,7 +185,7 @@
|
||||
"mini.car.title": "Choose a car",
|
||||
"mini.car.alighting": "People are getting off - wait…",
|
||||
"mini.car.pick": "Pick a car with the arrows, board with Enter.",
|
||||
"mini.car.men": "There is a man in the party - not the women-only car (pink)!",
|
||||
"mini.car.men": "Man in the party - avoid the women-only car (pink)!",
|
||||
"mini.car.hint": "←/→: car Enter: board Esc: cancel",
|
||||
"car.crowd.low": "few people",
|
||||
"car.crowd.mid": "some people",
|
||||
@@ -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.",
|
||||
@@ -225,7 +225,7 @@
|
||||
"ending.hint": "Enter: continue Esc: level map",
|
||||
"ending.credits": "Metro Puzzle JP\n\nIdea and game design\nBjörn\n\nCode and pixels\nClaude (Anthropic)\n\nEngine\nEbitengine\n\nFont\nPixelMplus (M+ FONT LICENSE)\n\nThe trains, the signs and the chatter\nTokyo\n\nThanks for riding!\nまたね",
|
||||
"mini.payer.title": "Who pays?",
|
||||
"mini.payer.price": "To pay: %d¥ (food and drink for everyone who needs it)",
|
||||
"mini.payer.price": "To pay: %d¥ (for everyone who needs it)",
|
||||
"mini.payer.cash": "Cash",
|
||||
"mini.payer.card": "card",
|
||||
"mini.payer.short": "Not enough - pick someone else.",
|
||||
@@ -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,41 @@
|
||||
"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.",
|
||||
"map.hint2": "I: wardrobe P: phone S: settings Esc: title"
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
},
|
||||
"方面": {
|
||||
"kana": "ほうめん",
|
||||
"romaji": "hōmen",
|
||||
"romaji": "homen",
|
||||
"en": "for",
|
||||
"sv": "mot",
|
||||
"tier": 2
|
||||
@@ -79,14 +79,14 @@
|
||||
},
|
||||
"特急": {
|
||||
"kana": "とっきゅう",
|
||||
"romaji": "tokkyū",
|
||||
"romaji": "tokkyu",
|
||||
"en": "Limited express",
|
||||
"sv": "Expresståg",
|
||||
"tier": 3
|
||||
},
|
||||
"空港": {
|
||||
"kana": "くうこう",
|
||||
"romaji": "kūkō",
|
||||
"romaji": "kuko",
|
||||
"en": "Airport",
|
||||
"sv": "Flygplats",
|
||||
"tier": 2
|
||||
@@ -121,21 +121,21 @@
|
||||
},
|
||||
"終点": {
|
||||
"kana": "しゅうてん",
|
||||
"romaji": "shūten",
|
||||
"romaji": "shuten",
|
||||
"en": "Terminus",
|
||||
"sv": "Slutstation",
|
||||
"tier": 3
|
||||
},
|
||||
"自動券売機": {
|
||||
"kana": "じどうけんばいき",
|
||||
"romaji": "jidō kenbaiki",
|
||||
"romaji": "jido kenbaiki",
|
||||
"en": "Ticket machine",
|
||||
"sv": "Biljettautomat",
|
||||
"tier": 3
|
||||
},
|
||||
"チャージ": {
|
||||
"kana": "チャージ",
|
||||
"romaji": "chāji",
|
||||
"romaji": "chaji",
|
||||
"en": "Top up (IC card)",
|
||||
"sv": "Fyll på (kort)",
|
||||
"tier": 2
|
||||
@@ -158,7 +158,7 @@
|
||||
},
|
||||
"東京": {
|
||||
"kana": "とうきょう",
|
||||
"romaji": "Tōkyō",
|
||||
"romaji": "Tokyo",
|
||||
"en": "Tokyo",
|
||||
"sv": "Tokyo",
|
||||
"tier": 1,
|
||||
@@ -196,7 +196,7 @@
|
||||
},
|
||||
"中央線": {
|
||||
"kana": "ちゅうおうせん",
|
||||
"romaji": "Chūō-sen",
|
||||
"romaji": "Chuo-sen",
|
||||
"en": "Chuo Line",
|
||||
"sv": "Chuo-linjen",
|
||||
"tier": 2
|
||||
@@ -232,7 +232,7 @@
|
||||
},
|
||||
"南東": {
|
||||
"kana": "なんとう",
|
||||
"romaji": "nantō",
|
||||
"romaji": "nanto",
|
||||
"en": "south-east",
|
||||
"sv": "sydöst",
|
||||
"tier": 3
|
||||
@@ -246,7 +246,7 @@
|
||||
},
|
||||
"南東口": {
|
||||
"kana": "なんとうぐち",
|
||||
"romaji": "nantōguchi",
|
||||
"romaji": "nantoguchi",
|
||||
"en": "South-east exit",
|
||||
"sv": "Sydöstra utgången",
|
||||
"tier": 3
|
||||
@@ -281,14 +281,14 @@
|
||||
},
|
||||
"到着": {
|
||||
"kana": "とうちゃく",
|
||||
"romaji": "tōchaku",
|
||||
"romaji": "tochaku",
|
||||
"en": "Arrival",
|
||||
"sv": "Ankomst",
|
||||
"tier": 2
|
||||
},
|
||||
"注意": {
|
||||
"kana": "ちゅうい",
|
||||
"romaji": "chūi",
|
||||
"romaji": "chui",
|
||||
"en": "Caution",
|
||||
"sv": "Varning",
|
||||
"tier": 2
|
||||
@@ -316,14 +316,14 @@
|
||||
},
|
||||
"エレベーター": {
|
||||
"kana": "エレベーター",
|
||||
"romaji": "erebētā",
|
||||
"romaji": "erebeta",
|
||||
"en": "Elevator",
|
||||
"sv": "Hiss",
|
||||
"tier": 2
|
||||
},
|
||||
"エスカレーター": {
|
||||
"kana": "エスカレーター",
|
||||
"romaji": "esukarētā",
|
||||
"romaji": "esukareta",
|
||||
"en": "Escalator",
|
||||
"sv": "Rulltrappa",
|
||||
"tier": 2
|
||||
@@ -372,14 +372,14 @@
|
||||
},
|
||||
"普通": {
|
||||
"kana": "ふつう",
|
||||
"romaji": "futsū",
|
||||
"romaji": "futsu",
|
||||
"en": "Local",
|
||||
"sv": "Lokaltåg",
|
||||
"tier": 2
|
||||
},
|
||||
"急行": {
|
||||
"kana": "きゅうこう",
|
||||
"romaji": "kyūkō",
|
||||
"romaji": "kyuko",
|
||||
"en": "Express",
|
||||
"sv": "Expresståg",
|
||||
"tier": 2
|
||||
@@ -534,7 +534,7 @@
|
||||
},
|
||||
"歌舞伎町": {
|
||||
"kana": "かぶきちょう",
|
||||
"romaji": "Kabukichō",
|
||||
"romaji": "Kabukicho",
|
||||
"en": "Kabukicho",
|
||||
"sv": "Kabukicho",
|
||||
"tier": 3,
|
||||
@@ -542,14 +542,14 @@
|
||||
},
|
||||
"東京駅": {
|
||||
"kana": "とうきょうえき",
|
||||
"romaji": "Tōkyō-eki",
|
||||
"romaji": "Tokyo-eki",
|
||||
"en": "Tokyo Station",
|
||||
"sv": "Tokyo station",
|
||||
"tier": 3
|
||||
},
|
||||
"東海道線": {
|
||||
"kana": "とうかいどうせん",
|
||||
"romaji": "Tōkaidō-sen",
|
||||
"romaji": "Tokaido-sen",
|
||||
"en": "Tokaido Line",
|
||||
"sv": "Tokaido-linjen",
|
||||
"tier": 3
|
||||
@@ -598,14 +598,14 @@
|
||||
},
|
||||
"京浜東北線": {
|
||||
"kana": "けいひんとうほくせん",
|
||||
"romaji": "Keihin-Tōhoku-sen",
|
||||
"romaji": "Keihin-Tohoku-sen",
|
||||
"en": "Keihin-Tohoku Line",
|
||||
"sv": "Keihin-Tohoku-linjen",
|
||||
"tier": 3
|
||||
},
|
||||
"大宮": {
|
||||
"kana": "おおみや",
|
||||
"romaji": "Ōmiya",
|
||||
"romaji": "Omiya",
|
||||
"en": "Omiya",
|
||||
"sv": "Omiya",
|
||||
"tier": 3,
|
||||
@@ -613,14 +613,14 @@
|
||||
},
|
||||
"女性専用車": {
|
||||
"kana": "じょせいせんようしゃ",
|
||||
"romaji": "josei sen'yō-sha",
|
||||
"romaji": "josei sen'yo-sha",
|
||||
"en": "Women-only car",
|
||||
"sv": "Kvinnovagn",
|
||||
"tier": 2
|
||||
},
|
||||
"号車": {
|
||||
"kana": "ごうしゃ",
|
||||
"romaji": "gōsha",
|
||||
"romaji": "gosha",
|
||||
"en": "car no.",
|
||||
"sv": "vagn nr",
|
||||
"tier": 2
|
||||
@@ -641,7 +641,7 @@
|
||||
},
|
||||
"通行": {
|
||||
"kana": "つうこう",
|
||||
"romaji": "tsūkō",
|
||||
"romaji": "tsuko",
|
||||
"en": "passage / keep",
|
||||
"sv": "gång / håll",
|
||||
"tier": 3
|
||||
@@ -1175,5 +1175,26 @@
|
||||
"en": "transfer",
|
||||
"sv": "byte",
|
||||
"tier": 2
|
||||
},
|
||||
"リムジンバス": {
|
||||
"kana": "リムジンバス",
|
||||
"romaji": "rimujin basu",
|
||||
"en": "airport limousine bus",
|
||||
"sv": "flygbuss",
|
||||
"tier": 3
|
||||
},
|
||||
"番": {
|
||||
"kana": "ばん",
|
||||
"romaji": "ban",
|
||||
"en": "number",
|
||||
"sv": "nummer",
|
||||
"tier": 2
|
||||
},
|
||||
"バスのりば": {
|
||||
"kana": "バスのりば",
|
||||
"romaji": "basu noriba",
|
||||
"en": "bus stop",
|
||||
"sv": "busshållplats",
|
||||
"tier": 2
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
"menu.endless": "Oändligt läge",
|
||||
"menu.settings": "Inställningar",
|
||||
"menu.quit": "Avsluta",
|
||||
"hud.destination": "Resmål",
|
||||
"hud.destination": "Mål",
|
||||
"hud.next_train": "Nästa tåg",
|
||||
"hud.card_balance": "Kortsaldo",
|
||||
"meter.food": "Mat",
|
||||
@@ -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",
|
||||
@@ -46,7 +46,7 @@
|
||||
"hud.progress": "Framsteg",
|
||||
"words.title": "Ordlista 言葉",
|
||||
"words.points": "Poäng",
|
||||
"words.hint": "Pilar: välj Enter: lär dig ordet för poäng Esc/L: tillbaka",
|
||||
"words.hint": "Pilar: välj Enter: lär ordet för poäng Esc: tillbaka",
|
||||
"words.already": "Det ordet kan du redan.",
|
||||
"words.bought": "Nytt ord: %s = %s",
|
||||
"words.too_expensive": "Kostar %d poäng - klara banor med bra tid för fler.",
|
||||
@@ -89,7 +89,7 @@
|
||||
"mini.ticket.title": "Biljetter",
|
||||
"mini.ticket.hint": "Köp biljett till %s. Kontant: %d¥",
|
||||
"mini.crowd.title": "Fullt tåg - tränga sig på",
|
||||
"mini.crowd.hint": "Tryck Enter när markören är i det gröna fältet (%d försök kvar)",
|
||||
"mini.crowd.hint": "Enter när markören är i det gröna fältet (%d försök kvar)",
|
||||
"mini.ride.title": "På tåget - nästa station",
|
||||
"mini.ride.hint": "Enter: kliv av här (du ska till %s)",
|
||||
"hud.time_left": "Kvar",
|
||||
@@ -140,7 +140,7 @@
|
||||
"map.party": "Sällskap",
|
||||
"map.resume": "⟳ fortsätt (sparad seed)",
|
||||
"map.locked": "Låst - klara föregående bana först.",
|
||||
"map.hint": "Enter start N läge L ord T skills I items P tel S inst.",
|
||||
"map.hint": "Enter: starta banan N: läge L: ordlista T: skills",
|
||||
"skills.title": "Skills 技",
|
||||
"skills.to_next": "XP till nästa",
|
||||
"skills.already": "Redan upplåst.",
|
||||
@@ -185,7 +185,7 @@
|
||||
"mini.car.title": "Välj vagn",
|
||||
"mini.car.alighting": "Folk kliver av - vänta…",
|
||||
"mini.car.pick": "Välj vagn med pilarna, kliv på med Enter.",
|
||||
"mini.car.men": "Det finns en man i sällskapet - inte kvinnovagnen (rosa)!",
|
||||
"mini.car.men": "Man i sällskapet - undvik kvinnovagnen (rosa)!",
|
||||
"mini.car.hint": "←/→: vagn Enter: kliv på Esc: avbryt",
|
||||
"car.crowd.low": "lite folk",
|
||||
"car.crowd.mid": "en del folk",
|
||||
@@ -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.",
|
||||
@@ -225,7 +225,7 @@
|
||||
"ending.hint": "Enter: vidare Esc: bankartan",
|
||||
"ending.credits": "Metro Puzzle JP\n\nIdé och speldesign\nBjörn\n\nKod och pixlar\nClaude (Anthropic)\n\nMotor\nEbitengine\n\nTypsnitt\nPixelMplus (M+ FONT LICENSE)\n\nTågen, skyltarna och sorlet\nTokyo\n\nTack för resan!\nまたね",
|
||||
"mini.payer.title": "Vem betalar?",
|
||||
"mini.payer.price": "Att betala: %d¥ (mat och dryck åt alla som behöver)",
|
||||
"mini.payer.price": "Att betala: %d¥ (åt alla som behöver)",
|
||||
"mini.payer.cash": "Kontant",
|
||||
"mini.payer.card": "kort",
|
||||
"mini.payer.short": "Det räcker inte - välj någon annan.",
|
||||
@@ -235,15 +235,15 @@
|
||||
"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.",
|
||||
"result.endless_won": "Mål %d klart! Totalt %d poäng.",
|
||||
"result.endless_lost": "Oändligt läge slut efter %d mål: %d poäng (bästa %d).",
|
||||
"result.endless_best": "Ny bästa!",
|
||||
"result.hint_endless": "Enter: nästa mål L: ordlista Esc: avsluta omgången",
|
||||
"result.hint_endless": "Enter: nästa mål L: ord Esc: avsluta",
|
||||
"result.hint_endless_over": "Enter/Esc: bankartan L: ordlista",
|
||||
"settings.title": "Inställningar",
|
||||
"settings.master": "Huvudvolym",
|
||||
@@ -256,5 +256,41 @@
|
||||
"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 avslutar 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.",
|
||||
"map.hint2": "I: garderob P: telefon S: inställningar Esc: titel"
|
||||
}
|
||||
|
||||
@@ -213,6 +213,32 @@ var tiles = []func(canvas){
|
||||
c.rect(11, 5, 2, 2, green)
|
||||
c.rect(2, 10, 12, 3, metal) // disk
|
||||
},
|
||||
func(c canvas) { // buss (ritas på vägrutan bortom hållplatsen när bussen är inne)
|
||||
c.rect(0, 0, 16, 16, wallDark)
|
||||
c.rect(1, 2, 14, 11, color.RGBA{240, 200, 60, 255}) // gul kaross
|
||||
c.rect(1, 2, 14, 1, metalDark)
|
||||
c.rect(2, 4, 12, 4, screenC) // fönsterrad
|
||||
c.rect(7, 4, 1, 4, metalDark) // fönsterspröjs
|
||||
c.rect(1, 9, 14, 2, awningRed) // rand
|
||||
c.rect(11, 8, 2, 5, metalDark) // dörr
|
||||
c.rect(3, 13, 3, 2, boxDark) // hjul
|
||||
c.rect(10, 13, 3, 2, boxDark)
|
||||
},
|
||||
func(c canvas) { // väg (horisontell; roteras för vertikala): asfalt med mittlinje och kantsten
|
||||
c.rect(0, 0, 16, 16, color.RGBA{70, 72, 80, 255})
|
||||
c.rect(0, 0, 16, 1, wallMid)
|
||||
c.rect(0, 15, 16, 1, metal) // trottoarkant
|
||||
for x := 0; x < 16; x += 6 {
|
||||
c.rect(x, 7, 3, 1, color.RGBA{230, 220, 150, 255}) // streckad mittlinje
|
||||
}
|
||||
},
|
||||
func(c canvas) { // hållplatsskylt: stolpe med bussymbol (ritas över hållplatsrutan)
|
||||
c.rect(7, 4, 2, 12, metalDark)
|
||||
c.rect(3, 1, 10, 6, color.RGBA{240, 200, 60, 255})
|
||||
c.rect(4, 2, 8, 3, boxDark)
|
||||
c.rect(5, 3, 2, 1, white)
|
||||
c.rect(9, 3, 2, 1, white)
|
||||
},
|
||||
}
|
||||
|
||||
// player ritar en figur som tittar åt facing (0 ner, 1 upp, 2 vänster, 3 höger)
|
||||
|
||||
@@ -70,6 +70,7 @@ var (
|
||||
staffTone = []note{{a4, 0.18}, {0, 0.08}, {a4, 0.18}, {0, 0.12}, {e5, 0.3}}
|
||||
gateBeep = []note{{a6, 0.09}}
|
||||
gateDeny = []note{{e4, 0.12}, {0, 0.04}, {e4, 0.12}, {0, 0.04}, {e4, 0.14}}
|
||||
busHorn = []note{{a4, 0.22}, {f4, 0.3}}
|
||||
)
|
||||
|
||||
// render syntetiserar toner med sinus + överton, attack/release-envelope.
|
||||
@@ -264,6 +265,7 @@ func main() {
|
||||
"lose": render(loseSting, 0.01, 0.08, 0.2),
|
||||
"staff": square(staffTone),
|
||||
"plane": noiseSweep(3.0, 0.03, 0.4, func(t float64) float64 { return math.Sin(math.Pi * t / 3.0) }),
|
||||
"bus-horn": square(busHorn),
|
||||
"chatter": chatter(6),
|
||||
"music-map": mapMusic(),
|
||||
}
|
||||
|
||||
@@ -26,6 +26,19 @@ import (
|
||||
|
||||
// demoSave skriver en sparfil med lite progression så att kartan, skills
|
||||
// och garderoben har något att visa. Används bara av skärmdumparna.
|
||||
// demoSaveEN är samma demosparfil på engelska (för språkkontrollen).
|
||||
func demoSaveEN(path string) error {
|
||||
if err := demoSave(path); err != nil {
|
||||
return err
|
||||
}
|
||||
st, _, err := save.Load(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
st.Settings.Lang = "en"
|
||||
return save.Store(path, st)
|
||||
}
|
||||
|
||||
func demoSave(path string) error {
|
||||
st := save.New()
|
||||
st.Know.Learn("改札")
|
||||
@@ -55,7 +68,7 @@ type shot struct {
|
||||
// bilderna överlever ändringar i banorna.
|
||||
var shots []shot
|
||||
|
||||
var demo string
|
||||
var demo, demoEN, fresh string
|
||||
|
||||
func buildShots(levelsDir string) error {
|
||||
l, err := level.LoadFile(filepath.Join(levelsDir, "001-flygplatsen.json"))
|
||||
@@ -124,6 +137,18 @@ func buildShots(levelsDir string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l1b, err := level.LoadFile(filepath.Join(levelsDir, "001b-flygbussen.json"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
toMachine1b, err := moves(l1b, l1b.SpawnPoint(), l1b.Find(level.TicketMachine)[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
toStop1b, err := moves(l1b, l1b.Find(level.TicketMachine)[0], l1b.CorrectPlatform())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l7, err := level.LoadFile(filepath.Join(levelsDir, "007-akihabara.json"))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -146,7 +171,7 @@ func buildShots(levelsDir string) error {
|
||||
// delvisa översättningen syns; hjälpen lär sedan in nästa ord.
|
||||
known := []string{"-know", "改札,番線,方面", "-seed", "7"}
|
||||
shots = []shot{
|
||||
{"", "wait 10; shot $OUT/map.png; press l; wait 5; press down; press down; press down; wait 5; shot $OUT/words.png; press l; wait 3; press t; wait 5; press down; wait 3; shot $OUT/skills.png; press t; wait 3; press i; wait 5; shot $OUT/items.png; quit",
|
||||
{"", "wait 5; press enter; wait 10; shot $OUT/map.png; press l; wait 5; press down; press down; press down; wait 5; shot $OUT/words.png; press l; wait 3; press t; wait 5; press down; wait 3; shot $OUT/skills.png; press t; wait 3; press i; wait 5; shot $OUT/items.png; quit",
|
||||
[]string{"-know", "改札,出口,トイレ", "-points", "3", "-save", "$DEMO"}},
|
||||
{"001-flygplatsen", toKiosk + "; press b; wait 5; press down; wait 3; shot $OUT/shop.png; quit", []string{"-seed", "7", "-save", "$DEMO"}},
|
||||
{"001-flygplatsen", "move right 4; move down 2; wait 5; shot $OUT/level-001.png; quit", nil},
|
||||
@@ -162,13 +187,18 @@ func buildShots(levelsDir string) error {
|
||||
{"006-ikebukuro", "move right 2; wait 5; shot $OUT/nav-list.png; quit", []string{"-seed", "7", "-mode", "list"}},
|
||||
{"006-ikebukuro", toMachine6 + "; wait 5; shot $OUT/terminal.png; " + "quit", []string{"-seed", "7", "-mode", "signs", "-know", "山手線,番線,方面,新宿,池袋,埼京線"}},
|
||||
{"006-ikebukuro", toTrap6 + "; waittrain; wait 5; press enter; wait 100; pick calm; press enter; wait 60; shot $OUT/ride-real.png; waitover; wait 5; shot $OUT/too-far.png; quit", []string{"-seed", "7", "-mode", "signs"}},
|
||||
{"", "wait 10; press n; wait 5; shot $OUT/map-signs.png; quit", []string{"-save", "$DEMO"}},
|
||||
{"", "wait 5; press enter; wait 10; press n; wait 5; shot $OUT/map-signs.png; quit", []string{"-save", "$DEMO"}},
|
||||
{"007-akihabara", toPlatform7 + "; waittrain; wait 5; press enter; wait 100; pick calm; press enter; waitstop; press enter; wait 40; shot $OUT/transfer.png; quit",
|
||||
[]string{"-seed", "7", "-mode", "list", "-know", "上野,番線,方面,乗り換え"}},
|
||||
{"", "wait 150; shot $OUT/ending.png; press enter; wait 5; shot $OUT/ending-summary.png; press enter; wait 250; shot $OUT/credits.png; quit", []string{"-ending", "-save", "$DEMO"}},
|
||||
{"007-akihabara", toKiosk7 + "; urge Ville; press enter; wait 5; press right; wait 3; shot $OUT/payer.png; quit", []string{"-seed", "7"}},
|
||||
{"", "wait 10; shot $OUT/endless.png; quit", []string{"-endless", "-seed", "7", "-mode", "list", "-save", "$DEMO"}},
|
||||
{"", "wait 10; press s; wait 5; press down; wait 3; shot $OUT/settings.png; quit", []string{"-save", "$DEMO"}},
|
||||
{"", "wait 5; press enter; wait 10; shot $OUT/map-en.png; quit", []string{"-save", "$DEMOEN"}},
|
||||
{"001-flygplatsen", toSign + "; wait 5; shot $OUT/sign-en.png; quit", []string{"-seed", "7", "-save", "$DEMOEN", "-know", "改札,番線,方面"}},
|
||||
{"001b-flygbussen", toMachine1b + "; press enter; pick 新宿; press enter; wait 5; " + toStop1b + "; waittrain; wait 10; shot $OUT/bus.png; quit", []string{"-seed", "7", "-know", "新宿,方面"}},
|
||||
{"", "wait 5; press enter; 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 +282,10 @@ 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)
|
||||
demoEN = filepath.Join(root, "build", "demo-save-en.json")
|
||||
must(demoSaveEN(demoEN))
|
||||
must(buildShots(filepath.Join(root, "resources", "data", "levels")))
|
||||
|
||||
build := exec.Command("go", "run", "./tools/build", "-mode", "debug")
|
||||
@@ -280,7 +314,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.NewReplacer("$DEMOEN", demoEN, "$DEMO", demo, "$FRESH", fresh).Replace(a))
|
||||
}
|
||||
run := exec.Command(exe, args...)
|
||||
run.Dir, run.Stdout, run.Stderr = filepath.Dir(exe), os.Stdout, os.Stderr
|
||||
|
||||