M9a: stationer i flera våningar med trappor
All checks were successful
release / build-release (push) Successful in 1m34s
- level: Point har Z; banor kan ha floors (lika stora rutnät med namn), gamla grid-banor läses som en våning; trappor U/D på samma x,y en våning upp/ner valideras parvis; Neighbors ger grannar över våningar så att Path, lösbarhet, spärregel och Unreachable fungerar; skyltar har z - sim: att kliva på en trappa flyttar spelaren till andra änden (FloorChanges); dimman är per våning och avslöjar trappans andra ände; tåg hörs/syns bara på egen våning - game: bara spelarens våning ritas (följare på andra våningar döljs), HUD och minikarta visar våningens namn, trappgrafik i genart och färg på minikartan - bana 004 Tokyo (svårighet 3, tre i sällskapet): start under mark på B1, trappa upp till 1F med perronger; generatorn gör våningar med trappor som flaskhalsar; ordboken utökad - testbotar och tools/shots förstår trappor; två våningsbilder - docs/README/wiki uppdaterade Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
17
README.md
@@ -10,14 +10,14 @@ 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+
|
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).
|
upp. Hela speldesignen finns i [doc/spelbeskrivning.md](doc/spelbeskrivning.md).
|
||||||
|
|
||||||
**Status:** M8 klar (2026-10-24) – fyra banor på en bankarta i
|
**Status:** M9a klar (2026-10-24) – fem banor på en bankarta i
|
||||||
spelbrädesstil, med sidospår. Japanska skyltar och ordinlärning,
|
spelbrädesstil, med sidospår. Japanska skyltar och ordinlärning,
|
||||||
lämmeltåget, minikarta, tågkort, biljetter och minispel, svårighet med
|
lämmeltåget, minikarta, tågkort, biljetter och minispel, svårighet med
|
||||||
tidsgräns, poäng och debuffar, tåg som rullar in till avgångsmelodin. Och nu
|
tidsgräns, poäng och debuffar, tåg som rullar in till avgångsmelodin,
|
||||||
progression: XP och nivåer, ett skillträd, items med buffar och genvägar som
|
progression med XP, skillträd, items och sparfil. Och nu stationer i flera
|
||||||
köps i kioskernas butiker eller vinns på sidospår, och en sparfil som minns
|
våningar: Tokyo station börjar under mark och trappan tar dig upp till
|
||||||
allt inklusive seeden på den bana du höll på med. Nästa är M9a: stationer i
|
perrongerna. Nästa är M9b: folk, reklam och ljudröra. Se
|
||||||
flera våningar. Se [doc/roadmap.md](doc/roadmap.md).
|
[doc/roadmap.md](doc/roadmap.md).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -51,6 +51,11 @@ 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å de svåraste banorna har alla i sällskapet egna kort som måste ha saldo,
|
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
|
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,
|
vagnen efter sina önskemål: fönster eller gång, vänster eller höger sida,
|
||||||
|
|||||||
BIN
doc/img/floors-1f.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
doc/img/floors-b1.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
doc/img/map.png
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
@@ -34,7 +34,7 @@ att bygga på Linux som på Windows.
|
|||||||
| Inkrementellt debug-bygge | < 10 s | 4,7 s |
|
| Inkrementellt debug-bygge | < 10 s | 4,7 s |
|
||||||
| Release-binär | < 20 MB | 10,2 MB (Linux), 10,8 MB (Windows) |
|
| Release-binär | < 20 MB | 10,2 MB (Linux), 10,8 MB (Windows) |
|
||||||
| Testsviten | `go test ./...` grön på < 30 s | ~0,1 s |
|
| Testsviten | `go test ./...` grön på < 30 s | ~0,1 s |
|
||||||
| Banor | 100 % passerar det automatiska lösbarhetstestet | 4 av 4 |
|
| Banor | 100 % passerar det automatiska lösbarhetstestet | 5 av 5 |
|
||||||
| Windows-korsbygge från Linux | fungerar utan extra verktyg | ✓ (CGO_ENABLED=0) |
|
| Windows-korsbygge från Linux | fungerar utan extra verktyg | ✓ (CGO_ENABLED=0) |
|
||||||
| Linux-binärens länkade bibliotek | bara libc | ✓ (GL/X11 laddas dynamiskt vid körning) |
|
| Linux-binärens länkade bibliotek | bara libc | ✓ (GL/X11 laddas dynamiskt vid körning) |
|
||||||
| Bildfrekvens | 60 fps vid 4× skala på integrerad grafik | ej mätt |
|
| Bildfrekvens | 60 fps vid 4× skala på integrerad grafik | ej mätt |
|
||||||
@@ -124,6 +124,8 @@ att bygga på Linux som på Windows.
|
|||||||
| `internal/save` | `TestRoundTrip`, `TestCorruptAndFutureVersions`, `TestDefaultPath` | Skriv → läs ger samma tillstånd inkl. seed; trasig/nyare fil ger fel; version 0 migreras |
|
| `internal/save` | `TestRoundTrip`, `TestCorruptAndFutureVersions`, `TestDefaultPath` | Skriv → läs ger samma tillstånd inkl. seed; trasig/nyare fil ger fel; version 0 migreras |
|
||||||
| `internal/sim` | `TestRunOptionsFromProgression` | Extra stats, sikt, landmärken, gratis spärrar, hela kartan, powerbank |
|
| `internal/sim` | `TestRunOptionsFromProgression` | Extra stats, sikt, landmärken, gratis spärrar, hela kartan, powerbank |
|
||||||
| `internal/minigame` | `TestCrowdLuckWidensZone` | Tur gör zonen bredare, otur smalare, klampat |
|
| `internal/minigame` | `TestCrowdLuckWidensZone` | Tur gör zonen bredare, otur smalare, klampat |
|
||||||
|
| `internal/level` | `TestFloorsStairsAndPaths`, `TestFloorsValidation` | **M9a:** flera våningar läses, trappor länkar, BFS-vägen byter våning bara via trappa, spärregeln och skyltar per våning; trappa utan motpart, olika storlekar, grid+floors och fel våning avvisas |
|
||||||
|
| `internal/sim` | `TestStairsMoveBetweenFloors` | Att kliva på trappan flyttar spelaren en våning, dimman lättar runt andra änden, perrongen hörs bara på egen våning, boten går hela vägen över våningar |
|
||||||
| `internal/archtest` | `TestOnlyPresentationPackagesImportEbitengine` | Beroenderegeln ovan |
|
| `internal/archtest` | `TestOnlyPresentationPackagesImportEbitengine` | Beroenderegeln ovan |
|
||||||
|
|
||||||
### Planerade tester per milstolpe
|
### Planerade tester per milstolpe
|
||||||
@@ -140,7 +142,7 @@ att bygga på Linux som på Windows.
|
|||||||
| ~~M7b Tåg som kommer och går~~ ✓ | Faser, ombordstigning, indikatoravstånd – finns, se tabellen ovan; ljud och animation verifieras manuellt/med skärmdumpar |
|
| ~~M7b Tåg som kommer och går~~ ✓ | Faser, ombordstigning, indikatoravstånd – finns, se tabellen ovan; ljud och animation verifieras manuellt/med skärmdumpar |
|
||||||
| M9b Folk, reklam, ljudröra | NPC:er följer seedade mål utan att fastna; krock saktar ned; fel sida i gång bromsar; bot klarar bana med folk; fler NPC:er = längre tid; clutter/noise läses per bana |
|
| M9b Folk, reklam, ljudröra | NPC:er följer seedade mål utan att fastna; krock saktar ned; fel sida i gång bromsar; bot klarar bana med folk; fler NPC:er = längre tid; clutter/noise läses per bana |
|
||||||
| ~~M8 Progression & sparfil~~ ✓ | Round-trip, skillkrav, upplåsningar, 33 %-regeln – finns, se tabellen ovan |
|
| ~~M8 Progression & sparfil~~ ✓ | Round-trip, skillkrav, upplåsningar, 33 %-regeln – finns, se tabellen ovan |
|
||||||
| M9a Flera våningar | Trappa byter våning för Mover och följare; `Path` över våningar; lösbarhetstest med trappor; dimma/minikarta per våning; gamla banor = en våning |
|
| ~~M9a Flera våningar~~ ✓ | Trappor, vägar över våningar, dimma per våning, gamla banor oförändrade – finns, se tabellen ovan |
|
||||||
| M9 Navigeringslägen | Listläge genererar rätt ordning av tåg; skyltläge: 1–4 giltiga vägar; fel-riktnings-förlust triggar vid rätt slutstation |
|
| M9 Navigeringslägen | Listläge genererar rätt ordning av tåg; skyltläge: 1–4 giltiga vägar; fel-riktnings-förlust triggar vid rätt slutstation |
|
||||||
| M10 Story-innehåll | Alla banor lösbara (redan automatiskt); flerbytesbanor har sammanhängande byten; sista banan slutar på flygplatsen |
|
| M10 Story-innehåll | Alla banor lösbara (redan automatiskt); flerbytesbanor har sammanhängande byten; sista banan slutar på flygplatsen |
|
||||||
| M11 Oändligt läge & NG+ | Målsekvens deterministisk per seed; NG+ behåller skills men nollställer items |
|
| M11 Oändligt läge & NG+ | Målsekvens deterministisk per seed; NG+ behåller skills men nollställer items |
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ brasse-linux01-runnern, inte Pi5:n.
|
|||||||
|
|
||||||
M0 skelett ✓ → M1 rörelse/kamera ✓ → M2 tåg/spärrar/vinst ✓ → M3 japanska/skyltar ✓
|
M0 skelett ✓ → M1 rörelse/kamera ✓ → M2 tåg/spärrar/vinst ✓ → M3 japanska/skyltar ✓
|
||||||
→ M4 lämmeltåget ✓ → M5 minikarta ✓ → M6 kort/biljetter/minispel ✓ → M7
|
→ M4 lämmeltåget ✓ → M5 minikarta ✓ → M6 kort/biljetter/minispel ✓ → M7
|
||||||
svårighet/poäng/debuffar ✓ → M7b tåg som kommer och går ✓ → M8 progression/sparfiler ✓ → M9a flera våningar → M9b folk/reklam/ljudröra → M9 navigeringslägen →
|
svårighet/poäng/debuffar ✓ → M7b tåg som kommer och går ✓ → M8 progression/sparfiler ✓ → M9a flera våningar ✓ → M9b folk/reklam/ljudröra → M9 navigeringslägen →
|
||||||
M10 story-innehåll → M11 oändligt läge/NG+ → M12 ljud/polish/release.
|
M10 story-innehåll → M11 oändligt läge/NG+ → M12 ljud/polish/release.
|
||||||
Innehåll per milstolpe: [roadmap.md › Milstolpar](roadmap.md#milstolpar).
|
Innehåll per milstolpe: [roadmap.md › Milstolpar](roadmap.md#milstolpar).
|
||||||
Vad som ska vara testat per milstolpe: [mal-och-tester.md](mal-och-tester.md).
|
Vad som ska vara testat per milstolpe: [mal-och-tester.md](mal-och-tester.md).
|
||||||
|
|||||||
@@ -401,22 +401,32 @@ på **brasse-linux01-runnern** (label `linux-amd64`), aldrig på Pi5:n.
|
|||||||
- **Klar när:** sparfil round-trip-test grön; "< 33 %" kontrolleras mot
|
- **Klar när:** sparfil round-trip-test grön; "< 33 %" kontrolleras mot
|
||||||
itemlistan; skillkrav, upplåsningar och belöningar testade. ✓
|
itemlistan; skillkrav, upplåsningar och belöningar testade. ✓
|
||||||
|
|
||||||
### M9a – Flera våningar (Björns tillägg 2026-10-24)
|
### M9a – Flera våningar ✓ (klar 2026-10-24)
|
||||||
|
|
||||||
- **Mål:** stationer i flera plan – ännu en svårighetsvariabel.
|
- **Mål:** stationer i flera plan – ännu en svårighetsvariabel.
|
||||||
- Banformatet får **flera rutnät** (`floors`), ett per våning, med
|
- **Banformat:** `floors: [{name, grid}, …]` med lika stora rutnät; gamla
|
||||||
**trappor** (och rulltrappor/hissar) som kopplar en ruta på en våning
|
banor med `grid` läses som en våning ("1F"). Rutkoordinaten `Point` fick
|
||||||
till en ruta på en annan. Start kan ligga på en trappa upp eller ner:
|
`Z`. **Trappor** är rutor: `U` (upp) på en våning kräver `D` (ner) på
|
||||||
under mark, ovan mark eller marknivå.
|
samma x,y en våning upp och tvärtom (valideras). Går man in på en trappa
|
||||||
- Kamera, dimma, minikarta (en flik per våning eller stapel) och BFS-vägar
|
hamnar man i andra änden i samma steg. Skyltar har `z`.
|
||||||
(`level.Path`, lösbarhetstestet, botarna) blir våningsmedvetna; skyltar
|
- **Logik:** `Neighbors` ger grannarna inkl. trappans andra ände, så
|
||||||
kan peka "↑ 2F / ↓ B1".
|
`Path`, lösbarhetstestet, spärregeln och `Unreachable` fungerar över
|
||||||
- **Teknik:** `level.Level.Floors []Grid` med `Stairs []Link`, `Point`
|
våningar. Dimman är per våning och avslöjar trappans andra ände. Man
|
||||||
får `Z`; `sim.Mover` byter våning vid trappa; `tools/genart` får
|
ser/hör bara tågen på sin egen våning.
|
||||||
trappruta; generatorn placerar trappor som flaskhalsar.
|
- **Spel:** bara spelarens våning ritas (följare på andra våningar syns
|
||||||
- **Klar när:** en bana med två våningar går att klara av boten headless,
|
inte förrän de kommer upp), HUD och minikartan visar våningens namn,
|
||||||
minikartan visar rätt våning, och alla gamla banor fungerar oförändrat
|
trappor har egen grafik och färg på minikartan.
|
||||||
(en våning).
|
- **Bana 004 Tokyo** (svårighet 3, tre i sällskapet): start under mark på
|
||||||
|
B1 (Marunouchi-linjen), trappan upp till 1F där perronger, spärrar,
|
||||||
|
automat, kiosk och toalett finns. Generatorn gör våningar: labyrint per
|
||||||
|
våning, trappor på gemensamma öppna rutor nära mitten, start på en våning
|
||||||
|
och perronger på en annan.
|
||||||
|
- **Teknik:** `level` (Storey, Point.Z, Neighbors, FloorAt), `sim.Run`
|
||||||
|
(trappbyte, FloorChanges), `sim.Fog` 3D, spelet ritar per våning;
|
||||||
|
testbotarna och `tools/shots` förstår trappor.
|
||||||
|
- **Klar när:** en bana med två våningar går att klara av boten headless
|
||||||
|
(`TestStairsMoveBetweenFloors`, `TestFloorsStairsAndPaths`), minikartan
|
||||||
|
visar rätt våning, och alla gamla banor fungerar oförändrat (en våning). ✓
|
||||||
|
|
||||||
### M9b – Folk, reklam och ljudröra (Björns tillägg 2026-10-24)
|
### M9b – Folk, reklam och ljudröra (Björns tillägg 2026-10-24)
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ var tileOrder = []level.Tile{
|
|||||||
level.Wall, // plats 7 = tågvagnen (laddas separat som Train)
|
level.Wall, // plats 7 = tågvagnen (laddas separat som Train)
|
||||||
level.Toilet,
|
level.Toilet,
|
||||||
level.Wall, // plats 9 = rälsen (laddas separat som Rails)
|
level.Wall, // plats 9 = rälsen (laddas separat som Rails)
|
||||||
|
level.StairsUp, level.StairsDown,
|
||||||
level.Kiosk,
|
level.Kiosk,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ var minimapColors = map[level.Tile]color.RGBA{
|
|||||||
level.Sign: {90, 140, 230, 255},
|
level.Sign: {90, 140, 230, 255},
|
||||||
level.Toilet: {235, 240, 250, 255},
|
level.Toilet: {235, 240, 250, 255},
|
||||||
level.Kiosk: {228, 59, 68, 255},
|
level.Kiosk: {228, 59, 68, 255},
|
||||||
|
level.StairsUp: {200, 150, 255, 255},
|
||||||
|
level.StairsDown: {150, 100, 220, 255},
|
||||||
}
|
}
|
||||||
|
|
||||||
// minimapScale väljer pixlar per ruta så att kartan ryms i ungefär en
|
// minimapScale väljer pixlar per ruta så att kartan ryms i ungefär en
|
||||||
@@ -80,8 +82,16 @@ func (s *LevelScene) drawMinimap(g *Game, screen *ebiten.Image, box image.Rectan
|
|||||||
}
|
}
|
||||||
sort.Slice(deps, func(i, j int) bool { return deps[i].track < deps[j].track })
|
sort.Slice(deps, func(i, j int) bool { return deps[i].track < deps[j].track })
|
||||||
|
|
||||||
|
z := r.Player.Tile.Z
|
||||||
|
floorLabel := ""
|
||||||
|
if l.NumFloors() > 1 {
|
||||||
|
floorLabel = l.FloorName(z)
|
||||||
|
}
|
||||||
w := max(mapW, 44) + 6
|
w := max(mapW, 44) + 6
|
||||||
h := mapH + 6 + len(deps)*LineH
|
h := mapH + 6 + len(deps)*LineH
|
||||||
|
if floorLabel != "" {
|
||||||
|
h += LineH
|
||||||
|
}
|
||||||
px, py := r.Player.WorldPos(TileSize)
|
px, py := r.Player.WorldPos(TileSize)
|
||||||
ox, oy := s.cam.Offset()
|
ox, oy := s.cam.Offset()
|
||||||
playerRect := image.Rect(int(px+ox), int(py+oy), int(px+ox)+TileSize, int(py+oy)+TileSize)
|
playerRect := image.Rect(int(px+ox), int(py+oy), int(px+ox)+TileSize, int(py+oy)+TileSize)
|
||||||
@@ -89,13 +99,17 @@ func (s *LevelScene) drawMinimap(g *Game, screen *ebiten.Image, box image.Rectan
|
|||||||
|
|
||||||
vector.DrawFilledRect(screen, float32(rc.Min.X), float32(rc.Min.Y), float32(w), float32(h), colPanel, false)
|
vector.DrawFilledRect(screen, float32(rc.Min.X), float32(rc.Min.Y), float32(w), float32(h), colPanel, false)
|
||||||
x0, y0 := rc.Min.X+3, rc.Min.Y+3
|
x0, y0 := rc.Min.X+3, rc.Min.Y+3
|
||||||
|
if floorLabel != "" {
|
||||||
|
a.Draw(screen, floorLabel, x0, y0, colKnown)
|
||||||
|
y0 += LineH
|
||||||
|
}
|
||||||
for y := 0; y < l.Height(); y++ {
|
for y := 0; y < l.Height(); y++ {
|
||||||
for x := 0; x < l.Width(); x++ {
|
for x := 0; x < l.Width(); x++ {
|
||||||
p := level.Point{X: x, Y: y}
|
p := level.Point{X: x, Y: y, Z: z}
|
||||||
if !r.Fog.Visible(l, p) {
|
if !r.Fog.Visible(l, p) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
t := l.At(x, y)
|
t := l.At(p)
|
||||||
c := minimapColors[t]
|
c := minimapColors[t]
|
||||||
if (t == level.Platform || t == level.WrongPlatform) && r.TrainVisible(t) {
|
if (t == level.Platform || t == level.WrongPlatform) && r.TrainVisible(t) {
|
||||||
c = color.RGBA{255, 255, 255, 255} // tåget är inne
|
c = color.RGBA{255, 255, 255, 255} // tåget är inne
|
||||||
@@ -106,6 +120,9 @@ func (s *LevelScene) drawMinimap(g *Game, screen *ebiten.Image, box image.Rectan
|
|||||||
// Sällskapet: följarna i sina toner, spelaren blinkande vit.
|
// Sällskapet: följarna i sina toner, spelaren blinkande vit.
|
||||||
for i := len(r.Party.Followers()); i >= 1; i-- {
|
for i := len(r.Party.Followers()); i >= 1; i-- {
|
||||||
from, _ := r.Party.FollowerTiles(i)
|
from, _ := r.Party.FollowerTiles(i)
|
||||||
|
if from.Z != z {
|
||||||
|
continue
|
||||||
|
}
|
||||||
tint := memberTints[i%len(memberTints)]
|
tint := memberTints[i%len(memberTints)]
|
||||||
vector.DrawFilledRect(screen, float32(x0+from.X*sc), float32(y0+from.Y*sc), float32(sc), float32(sc),
|
vector.DrawFilledRect(screen, float32(x0+from.X*sc), float32(y0+from.Y*sc), float32(sc), float32(sc),
|
||||||
color.RGBA{uint8(255 * tint[0]), uint8(255 * tint[1]), uint8(255 * tint[2]), 255}, false)
|
color.RGBA{uint8(255 * tint[0]), uint8(255 * tint[1]), uint8(255 * tint[2]), 255}, false)
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ func (s *LevelScene) Update(g *Game) error {
|
|||||||
g.SetScene(NewMapScene(g))
|
g.SetScene(NewMapScene(g))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if shopJustPressed() && !s.run.Player.Moving() && s.run.Level.At(s.run.Player.Tile.X, s.run.Player.Tile.Y) == level.Kiosk {
|
if shopJustPressed() && !s.run.Player.Moving() && s.run.Level.At(s.run.Player.Tile) == level.Kiosk {
|
||||||
s.openShop(g)
|
s.openShop(g)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -191,16 +191,18 @@ func (s *LevelScene) Draw(g *Game, screen *ebiten.Image) {
|
|||||||
y0 := max(0, int(s.cam.Y)/TileSize)
|
y0 := max(0, int(s.cam.Y)/TileSize)
|
||||||
x1 := min(lvl.Width(), int(s.cam.X+ScreenW)/TileSize+2)
|
x1 := min(lvl.Width(), int(s.cam.X+ScreenW)/TileSize+2)
|
||||||
y1 := min(lvl.Height(), int(s.cam.Y+ScreenH)/TileSize+2)
|
y1 := min(lvl.Height(), int(s.cam.Y+ScreenH)/TileSize+2)
|
||||||
|
z := s.run.Player.Tile.Z // bara spelarens våning ritas
|
||||||
var op ebiten.DrawImageOptions
|
var op ebiten.DrawImageOptions
|
||||||
for y := y0; y < y1; y++ {
|
for y := y0; y < y1; y++ {
|
||||||
for x := x0; x < x1; x++ {
|
for x := x0; x < x1; x++ {
|
||||||
if s.rails[level.Point{X: x, Y: y}] {
|
p := level.Point{X: x, Y: y, Z: z}
|
||||||
s.drawRails(g, screen, level.Point{X: x, Y: y}, ox, oy)
|
if s.rails[p] {
|
||||||
|
s.drawRails(g, screen, p, ox, oy)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
op.GeoM.Reset()
|
op.GeoM.Reset()
|
||||||
op.GeoM.Translate(ox+float64(x*TileSize), oy+float64(y*TileSize))
|
op.GeoM.Translate(ox+float64(x*TileSize), oy+float64(y*TileSize))
|
||||||
screen.DrawImage(g.Assets.Tiles[lvl.At(x, y)], &op)
|
screen.DrawImage(g.Assets.Tiles[lvl.At(p)], &op)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.drawTrains(g, screen, ox, oy)
|
s.drawTrains(g, screen, ox, oy)
|
||||||
@@ -211,6 +213,9 @@ func (s *LevelScene) Draw(g *Game, screen *ebiten.Image) {
|
|||||||
followers := s.run.Party.Followers()
|
followers := s.run.Party.Followers()
|
||||||
for i := len(followers); i >= 1; i-- {
|
for i := len(followers); i >= 1; i-- {
|
||||||
from, to := s.run.Party.FollowerTiles(i)
|
from, to := s.run.Party.FollowerTiles(i)
|
||||||
|
if from.Z != z && to.Z != z {
|
||||||
|
continue // på en annan våning
|
||||||
|
}
|
||||||
fx := float64(from.X*TileSize) + float64((to.X-from.X)*TileSize)*leader.Progress
|
fx := float64(from.X*TileSize) + float64((to.X-from.X)*TileSize)*leader.Progress
|
||||||
fy := float64(from.Y*TileSize) + float64((to.Y-from.Y)*TileSize)*leader.Progress
|
fy := float64(from.Y*TileSize) + float64((to.Y-from.Y)*TileSize)*leader.Progress
|
||||||
facing := sim.Down
|
facing := sim.Down
|
||||||
@@ -296,6 +301,9 @@ func (s *LevelScene) drawHUD(g *Game, screen *ebiten.Image) {
|
|||||||
dest += " " + r.Level.Destination.Get("ja")
|
dest += " " + r.Level.Destination.Get("ja")
|
||||||
}
|
}
|
||||||
name := r.Level.Name.Get(UILang)
|
name := r.Level.Name.Get(UILang)
|
||||||
|
if r.Level.NumFloors() > 1 {
|
||||||
|
name += " " + r.Level.FloorName(r.Player.Tile.Z)
|
||||||
|
}
|
||||||
a.Draw(screen, name, 4, 0, colText)
|
a.Draw(screen, name, 4, 0, colText)
|
||||||
destX := 4 + a.Width(name) + 12
|
destX := 4 + a.Width(name) + 12
|
||||||
a.Draw(screen, a.T("hud.destination")+": "+dest, destX, 0, colKnown)
|
a.Draw(screen, a.T("hud.destination")+": "+dest, destX, 0, colKnown)
|
||||||
@@ -411,7 +419,7 @@ func (s *LevelScene) infoLines(a *Assets) (lines []line, accent color.Color, bar
|
|||||||
lines = append(lines, a.wrapLine(plainLine(fmt.Sprintf("%s: Track %s %s for %s", a.T("hud.english"), tr.Track,
|
lines = append(lines, a.wrapLine(plainLine(fmt.Sprintf("%s: Track %s %s for %s", a.T("hud.english"), tr.Track,
|
||||||
tr.Line.Get("en"), tr.Destination.Get("en")), colEnglish), maxW)...)
|
tr.Line.Get("en"), tr.Destination.Get("en")), colEnglish), maxW)...)
|
||||||
}
|
}
|
||||||
phase, _ := r.PlatformPhase(lvl.At(tile.X, tile.Y))
|
phase, _ := r.PlatformPhase(lvl.At(tile))
|
||||||
switch {
|
switch {
|
||||||
case phase == level.Closing:
|
case phase == level.Closing:
|
||||||
accent = colWarn
|
accent = colWarn
|
||||||
@@ -461,7 +469,7 @@ func (s *LevelScene) infoLines(a *Assets) (lines []line, accent color.Color, bar
|
|||||||
return lines, accent, bar
|
return lines, accent, bar
|
||||||
}
|
}
|
||||||
|
|
||||||
switch lvl.At(tile.X, tile.Y) {
|
switch lvl.At(tile) {
|
||||||
case level.Toilet:
|
case level.Toilet:
|
||||||
if r.Facility.Active {
|
if r.Facility.Active {
|
||||||
return []line{plainLine(a.T("hud.toilet_busy"), colDim)}, colGood, true
|
return []line{plainLine(a.T("hud.toilet_busy"), colDim)}, colGood, true
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ func (s *LevelScene) buildTracks() {
|
|||||||
} else {
|
} else {
|
||||||
c.Y += k
|
c.Y += k
|
||||||
}
|
}
|
||||||
if s.run.Level.At(c.X, c.Y) == level.Wall && (c.X == 0 || c.Y == 0 || c.X == s.run.Level.Width()-1 || c.Y == s.run.Level.Height()-1) {
|
if s.run.Level.At(c) == level.Wall && (c.X == 0 || c.Y == 0 || c.X == s.run.Level.Width()-1 || c.Y == s.run.Level.Height()-1) {
|
||||||
s.rails[c] = true
|
s.rails[c] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,7 +74,7 @@ func (s *LevelScene) drawRails(g *Game, screen *ebiten.Image, c level.Point, ox,
|
|||||||
var op ebiten.DrawImageOptions
|
var op ebiten.DrawImageOptions
|
||||||
vertical := false
|
vertical := false
|
||||||
for _, tr := range s.tracks {
|
for _, tr := range s.tracks {
|
||||||
if !tr.horizontal && (c.X == tr.track.X) {
|
if !tr.horizontal && c.X == tr.track.X && c.Z == tr.track.Z {
|
||||||
vertical = true
|
vertical = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,7 +89,11 @@ func (s *LevelScene) drawRails(g *Game, screen *ebiten.Image, c level.Point, ox,
|
|||||||
|
|
||||||
// drawTrains ritar tågen i rätt läge för sin fas.
|
// drawTrains ritar tågen i rätt läge för sin fas.
|
||||||
func (s *LevelScene) drawTrains(g *Game, screen *ebiten.Image, ox, oy float64) {
|
func (s *LevelScene) drawTrains(g *Game, screen *ebiten.Image, ox, oy float64) {
|
||||||
|
z := s.run.Player.Tile.Z
|
||||||
for _, tr := range s.tracks {
|
for _, tr := range s.tracks {
|
||||||
|
if tr.platform.Z != z {
|
||||||
|
continue // tågen på andra våningar syns inte härifrån
|
||||||
|
}
|
||||||
phase, prog := s.run.PlatformPhase(tr.tile)
|
phase, prog := s.run.PlatformPhase(tr.tile)
|
||||||
// Strålkastare när tåget är på väg in: ett sken från det håll det kommer.
|
// Strålkastare när tåget är på väg in: ett sken från det håll det kommer.
|
||||||
if phase == level.Approaching {
|
if phase == level.Approaching {
|
||||||
@@ -148,6 +152,10 @@ func (s *LevelScene) updateEffects(g *Game) {
|
|||||||
for _, tr := range s.tracks {
|
for _, tr := range s.tracks {
|
||||||
phase, prog := r.PlatformPhase(tr.tile)
|
phase, prog := r.PlatformPhase(tr.tile)
|
||||||
if last, seen := s.lastPhase[tr.tile]; !seen || last != phase {
|
if last, seen := s.lastPhase[tr.tile]; !seen || last != phase {
|
||||||
|
if tr.platform.Z != r.Player.Tile.Z {
|
||||||
|
s.lastPhase[tr.tile] = phase
|
||||||
|
continue // hörs inte från en annan våning
|
||||||
|
}
|
||||||
s.lastPhase[tr.tile] = phase
|
s.lastPhase[tr.tile] = phase
|
||||||
dist := math.Hypot(float64(tr.platform.X*TileSize)-px, float64(tr.platform.Y*TileSize)-py) / TileSize
|
dist := math.Hypot(float64(tr.platform.X*TileSize)-px, float64(tr.platform.Y*TileSize)-py) / TileSize
|
||||||
switch phase {
|
switch phase {
|
||||||
@@ -159,7 +167,7 @@ func (s *LevelScene) updateEffects(g *Game) {
|
|||||||
}
|
}
|
||||||
// Vind längs perrongen när tåget rullar in eller ut (och strax före).
|
// Vind längs perrongen när tåget rullar in eller ut (och strax före).
|
||||||
windy := phase == level.Arriving || phase == level.Leaving || (phase == level.Approaching && prog > 0.7)
|
windy := phase == level.Arriving || phase == level.Leaving || (phase == level.Approaching && prog > 0.7)
|
||||||
if windy && len(s.particles) < maxParticles {
|
if windy && tr.platform.Z == r.Player.Tile.Z && len(s.particles) < maxParticles {
|
||||||
dir := 1.0
|
dir := 1.0
|
||||||
if phase == level.Arriving {
|
if phase == level.Arriving {
|
||||||
dir = 1.0 // luften trycks framför tåget: samma håll som tåget rör sig (in från minus-sidan)
|
dir = 1.0 // luften trycks framför tåget: samma håll som tåget rör sig (in från minus-sidan)
|
||||||
|
|||||||
@@ -31,13 +31,19 @@ const (
|
|||||||
Sign Tile = 'i' // skylt
|
Sign Tile = 'i' // skylt
|
||||||
Toilet Tile = 'W' // toalett (sällskapets toamätare)
|
Toilet Tile = 'W' // toalett (sällskapets toamätare)
|
||||||
Kiosk Tile = 'K' // kiosk/automat för mat och dryck
|
Kiosk Tile = 'K' // kiosk/automat för mat och dryck
|
||||||
|
StairsUp Tile = 'U' // trappa upp: leder till 'D' på samma x,y en våning upp
|
||||||
|
StairsDown Tile = 'D' // trappa ner: leder till 'U' på samma x,y en våning ner
|
||||||
)
|
)
|
||||||
|
|
||||||
var knownTiles = map[Tile]bool{
|
var knownTiles = map[Tile]bool{
|
||||||
Wall: true, Floor: true, Spawn: true, Platform: true, WrongPlatform: true,
|
Wall: true, Floor: true, Spawn: true, Platform: true, WrongPlatform: true,
|
||||||
Gate: true, TicketMachine: true, Sign: true, Toilet: true, Kiosk: true,
|
Gate: true, TicketMachine: true, Sign: true, Toilet: true, Kiosk: true,
|
||||||
|
StairsUp: true, StairsDown: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsStairs rapporterar om rutan är en trappa.
|
||||||
|
func (t Tile) IsStairs() bool { return t == StairsUp || t == StairsDown }
|
||||||
|
|
||||||
// Passable rapporterar om en ruta går att gå på (utan hänsyn till spärrar).
|
// Passable rapporterar om en ruta går att gå på (utan hänsyn till spärrar).
|
||||||
func (t Tile) Passable() bool { return t != Wall }
|
func (t Tile) Passable() bool { return t != Wall }
|
||||||
|
|
||||||
@@ -169,9 +175,17 @@ type Train struct {
|
|||||||
type SignText struct {
|
type SignText struct {
|
||||||
X int `json:"x"`
|
X int `json:"x"`
|
||||||
Y int `json:"y"`
|
Y int `json:"y"`
|
||||||
|
Z int `json:"z"` // våning (0 = första i floors)
|
||||||
Text Text `json:"text"`
|
Text Text `json:"text"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Storey är en våning: ett rutnät med namn ("B1", "1F", "2F").
|
||||||
|
type Storey struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Grid []string `json:"grid"`
|
||||||
|
Floors []Storey `json:"floors,omitempty"` // flervåningsbana; alla våningar lika stora
|
||||||
|
}
|
||||||
|
|
||||||
// Rewards är vad en klarad bana ger utöver poäng.
|
// Rewards är vad en klarad bana ger utöver poäng.
|
||||||
type Rewards struct {
|
type Rewards struct {
|
||||||
Items []string `json:"items"` // item-id:n (t.ex. skins) som delas ut första gången
|
Items []string `json:"items"` // item-id:n (t.ex. skins) som delas ut första gången
|
||||||
@@ -198,13 +212,21 @@ type Level struct {
|
|||||||
IndividualCards bool `json:"individualCards"` // alla i sällskapet har egna kort (och biljetter) som spärren kräver
|
IndividualCards bool `json:"individualCards"` // alla i sällskapet har egna kort (och biljetter) som spärren kräver
|
||||||
SeatSelection bool `json:"seatSelection"` // biljettköpet fortsätter med sittplatsval efter sällskapets önskemål
|
SeatSelection bool `json:"seatSelection"` // biljettköpet fortsätter med sittplatsval efter sällskapets önskemål
|
||||||
FareYen int `json:"fareYen"` // vad spärren drar från kortet (0 = standard)
|
FareYen int `json:"fareYen"` // vad spärren drar från kortet (0 = standard)
|
||||||
Grid []string `json:"grid"`
|
Grid []string `json:"grid,omitempty"` // envåningsbana (kompatibilitet) – blir Floors[0]
|
||||||
|
Floors []Storey `json:"floors,omitempty"` // flervåningsbana; alla våningar lika stora
|
||||||
Trains []Train `json:"trains"`
|
Trains []Train `json:"trains"`
|
||||||
Signs []SignText `json:"signs"`
|
Signs []SignText `json:"signs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Point är en rutkoordinat.
|
// Point är en rutkoordinat: x, y och våning z.
|
||||||
type Point struct{ X, Y int }
|
type Point struct{ X, Y, Z int }
|
||||||
|
|
||||||
|
// Up/Down ger rutan rakt ovanför/under (via trappa).
|
||||||
|
func (p Point) Up() Point { return Point{X: p.X, Y: p.Y, Z: p.Z + 1} }
|
||||||
|
func (p Point) Down() Point { return Point{X: p.X, Y: p.Y, Z: p.Z - 1} }
|
||||||
|
|
||||||
|
// dirs är de fyra grannriktningarna på samma våning.
|
||||||
|
var dirs = [...]Point{{X: 1}, {X: -1}, {Y: 1}, {Y: -1}}
|
||||||
|
|
||||||
// Parse avkodar och validerar en bana.
|
// Parse avkodar och validerar en bana.
|
||||||
func Parse(data []byte) (*Level, error) {
|
func Parse(data []byte) (*Level, error) {
|
||||||
@@ -218,36 +240,77 @@ func Parse(data []byte) (*Level, error) {
|
|||||||
return &l, nil
|
return &l, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Width och Height i rutor.
|
// Width och Height i rutor (alla våningar är lika stora).
|
||||||
func (l *Level) Width() int {
|
func (l *Level) Width() int {
|
||||||
if len(l.Grid) == 0 {
|
if len(l.Floors) == 0 || len(l.Floors[0].Grid) == 0 {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
return len(l.Grid[0])
|
return len(l.Floors[0].Grid[0])
|
||||||
|
}
|
||||||
|
func (l *Level) Height() int {
|
||||||
|
if len(l.Floors) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return len(l.Floors[0].Grid)
|
||||||
}
|
}
|
||||||
func (l *Level) Height() int { return len(l.Grid) }
|
|
||||||
|
|
||||||
// At returnerar rutan på (x, y); utanför kartan räknas som vägg.
|
// NumFloors är antalet våningar.
|
||||||
func (l *Level) At(x, y int) Tile {
|
func (l *Level) NumFloors() int { return len(l.Floors) }
|
||||||
if y < 0 || y >= len(l.Grid) || x < 0 || x >= len(l.Grid[y]) {
|
|
||||||
|
// FloorName är våningens namn ("1F" om det saknas).
|
||||||
|
func (l *Level) FloorName(z int) string {
|
||||||
|
if z < 0 || z >= len(l.Floors) || l.Floors[z].Name == "" {
|
||||||
|
return "1F"
|
||||||
|
}
|
||||||
|
return l.Floors[z].Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// At returnerar rutan på p; utanför kartan (eller våningarna) räknas som vägg.
|
||||||
|
func (l *Level) At(p Point) Tile { return l.FloorAt(p.Z, p.X, p.Y) }
|
||||||
|
|
||||||
|
// FloorAt returnerar rutan (x, y) på våning z; utanför kartan räknas som vägg.
|
||||||
|
func (l *Level) FloorAt(z, x, y int) Tile {
|
||||||
|
if z < 0 || z >= len(l.Floors) {
|
||||||
return Wall
|
return Wall
|
||||||
}
|
}
|
||||||
return Tile(l.Grid[y][x])
|
g := l.Floors[z].Grid
|
||||||
|
if y < 0 || y >= len(g) || x < 0 || x >= len(g[y]) {
|
||||||
|
return Wall
|
||||||
|
}
|
||||||
|
return Tile(g[y][x])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find returnerar alla rutor av en viss typ.
|
// Find returnerar alla rutor av en viss typ på alla våningar.
|
||||||
func (l *Level) Find(t Tile) []Point {
|
func (l *Level) Find(t Tile) []Point {
|
||||||
var pts []Point
|
var pts []Point
|
||||||
for y, row := range l.Grid {
|
for z, f := range l.Floors {
|
||||||
|
for y, row := range f.Grid {
|
||||||
for x := 0; x < len(row); x++ {
|
for x := 0; x < len(row); x++ {
|
||||||
if Tile(row[x]) == t {
|
if Tile(row[x]) == t {
|
||||||
pts = append(pts, Point{x, y})
|
pts = append(pts, Point{X: x, Y: y, Z: z})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return pts
|
return pts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Neighbors är rutorna man kan gå till från p: de fyra grannarna på samma
|
||||||
|
// våning samt trappans andra ände. Passerbarhet avgörs av anroparen.
|
||||||
|
func (l *Level) Neighbors(p Point) []Point {
|
||||||
|
out := make([]Point, 0, 5)
|
||||||
|
for _, d := range dirs {
|
||||||
|
out = append(out, Point{X: p.X + d.X, Y: p.Y + d.Y, Z: p.Z})
|
||||||
|
}
|
||||||
|
switch l.At(p) {
|
||||||
|
case StairsUp:
|
||||||
|
out = append(out, p.Up())
|
||||||
|
case StairsDown:
|
||||||
|
out = append(out, p.Down())
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// SpawnPoint returnerar startrutan (valideringen garanterar att den finns).
|
// SpawnPoint returnerar startrutan (valideringen garanterar att den finns).
|
||||||
func (l *Level) SpawnPoint() Point { return l.Find(Spawn)[0] }
|
func (l *Level) SpawnPoint() Point { return l.Find(Spawn)[0] }
|
||||||
|
|
||||||
@@ -274,25 +337,47 @@ func (l *Level) Validate() error {
|
|||||||
if l.Order < 0 {
|
if l.Order < 0 {
|
||||||
return fmt.Errorf("level %s: negativ order", l.ID)
|
return fmt.Errorf("level %s: negativ order", l.ID)
|
||||||
}
|
}
|
||||||
|
if len(l.Floors) == 0 {
|
||||||
if len(l.Grid) == 0 {
|
if len(l.Grid) == 0 {
|
||||||
return fmt.Errorf("level %s: tomt rutnät", l.ID)
|
return fmt.Errorf("level %s: tomt rutnät", l.ID)
|
||||||
}
|
}
|
||||||
w := len(l.Grid[0])
|
l.Floors = []Storey{{Name: "1F", Grid: l.Grid}}
|
||||||
|
} else if len(l.Grid) > 0 {
|
||||||
|
return fmt.Errorf("level %s: ange antingen grid eller floors, inte båda", l.ID)
|
||||||
|
}
|
||||||
|
l.Grid = l.Floors[0].Grid // kompatibilitet: Grid = första våningen
|
||||||
|
if len(l.Floors[0].Grid) == 0 {
|
||||||
|
return fmt.Errorf("level %s: tom första våning", l.ID)
|
||||||
|
}
|
||||||
|
w, h := len(l.Floors[0].Grid[0]), len(l.Floors[0].Grid)
|
||||||
spawns, platforms := 0, 0
|
spawns, platforms := 0, 0
|
||||||
for y, row := range l.Grid {
|
for z, f := range l.Floors {
|
||||||
|
if len(f.Grid) != h {
|
||||||
|
return fmt.Errorf("level %s: våning %d har %d rader, förväntade %d", l.ID, z, len(f.Grid), h)
|
||||||
|
}
|
||||||
|
for y, row := range f.Grid {
|
||||||
if len(row) != w {
|
if len(row) != w {
|
||||||
return fmt.Errorf("level %s: rad %d har längd %d, förväntade %d", l.ID, y, len(row), w)
|
return fmt.Errorf("level %s: våning %d rad %d har längd %d, förväntade %d", l.ID, z, y, len(row), w)
|
||||||
}
|
}
|
||||||
for x := 0; x < len(row); x++ {
|
for x := 0; x < len(row); x++ {
|
||||||
t := Tile(row[x])
|
t := Tile(row[x])
|
||||||
if !knownTiles[t] {
|
if !knownTiles[t] {
|
||||||
return fmt.Errorf("level %s: okänt tecken %q på (%d,%d)", l.ID, row[x], x, y)
|
return fmt.Errorf("level %s: okänt tecken %q på (%d,%d,%d)", l.ID, row[x], x, y, z)
|
||||||
}
|
}
|
||||||
switch t {
|
switch t {
|
||||||
case Spawn:
|
case Spawn:
|
||||||
spawns++
|
spawns++
|
||||||
case Platform, WrongPlatform:
|
case Platform, WrongPlatform:
|
||||||
platforms++
|
platforms++
|
||||||
|
case StairsUp:
|
||||||
|
if l.FloorAt(z+1, x, y) != StairsDown {
|
||||||
|
return fmt.Errorf("level %s: trappa upp på (%d,%d,%d) saknar trappa ner ovanför", l.ID, x, y, z)
|
||||||
|
}
|
||||||
|
case StairsDown:
|
||||||
|
if l.FloorAt(z-1, x, y) != StairsUp {
|
||||||
|
return fmt.Errorf("level %s: trappa ner på (%d,%d,%d) saknar trappa upp under", l.ID, x, y, z)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,7 +396,7 @@ func (l *Level) Validate() error {
|
|||||||
if len(tr.Platform) != 1 || (Tile(tr.Platform[0]) != Platform && Tile(tr.Platform[0]) != WrongPlatform) {
|
if len(tr.Platform) != 1 || (Tile(tr.Platform[0]) != Platform && Tile(tr.Platform[0]) != WrongPlatform) {
|
||||||
return fmt.Errorf("level %s: tåg %d har ogiltig perrong %q", l.ID, i, tr.Platform)
|
return fmt.Errorf("level %s: tåg %d har ogiltig perrong %q", l.ID, i, tr.Platform)
|
||||||
}
|
}
|
||||||
if !strings.Contains(strings.Join(l.Grid, ""), tr.Platform) {
|
if len(l.Find(Tile(tr.Platform[0]))) == 0 {
|
||||||
return fmt.Errorf("level %s: tåg %d står vid perrong %q som inte finns i rutnätet", l.ID, i, tr.Platform)
|
return fmt.Errorf("level %s: tåg %d står vid perrong %q som inte finns i rutnätet", l.ID, i, tr.Platform)
|
||||||
}
|
}
|
||||||
if tr.Correct {
|
if tr.Correct {
|
||||||
@@ -364,9 +449,9 @@ func (l *Level) Validate() error {
|
|||||||
// Skyltar: varje post måste stå på en 'i'-ruta och varje 'i'-ruta ha text.
|
// Skyltar: varje post måste stå på en 'i'-ruta och varje 'i'-ruta ha text.
|
||||||
seen := map[Point]bool{}
|
seen := map[Point]bool{}
|
||||||
for i, sg := range l.Signs {
|
for i, sg := range l.Signs {
|
||||||
p := Point{sg.X, sg.Y}
|
p := Point{X: sg.X, Y: sg.Y, Z: sg.Z}
|
||||||
if l.At(p.X, p.Y) != Sign {
|
if l.At(p) != Sign {
|
||||||
return fmt.Errorf("level %s: skylt %d på (%d,%d) står inte på en skyltruta", l.ID, i, p.X, p.Y)
|
return fmt.Errorf("level %s: skylt %d på (%d,%d,%d) står inte på en skyltruta", l.ID, i, p.X, p.Y, p.Z)
|
||||||
}
|
}
|
||||||
if seen[p] {
|
if seen[p] {
|
||||||
return fmt.Errorf("level %s: två skyltar på (%d,%d)", l.ID, p.X, p.Y)
|
return fmt.Errorf("level %s: två skyltar på (%d,%d)", l.ID, p.X, p.Y)
|
||||||
@@ -378,7 +463,7 @@ func (l *Level) Validate() error {
|
|||||||
}
|
}
|
||||||
for _, p := range l.Find(Sign) {
|
for _, p := range l.Find(Sign) {
|
||||||
if !seen[p] {
|
if !seen[p] {
|
||||||
return fmt.Errorf("level %s: skyltrutan (%d,%d) saknar text i \"signs\"", l.ID, p.X, p.Y)
|
return fmt.Errorf("level %s: skyltrutan (%d,%d,%d) saknar text i \"signs\"", l.ID, p.X, p.Y, p.Z)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -387,7 +472,7 @@ func (l *Level) Validate() error {
|
|||||||
// SignAt returnerar skylten på en ruta, eller nil.
|
// SignAt returnerar skylten på en ruta, eller nil.
|
||||||
func (l *Level) SignAt(p Point) *SignText {
|
func (l *Level) SignAt(p Point) *SignText {
|
||||||
for i := range l.Signs {
|
for i := range l.Signs {
|
||||||
if l.Signs[i].X == p.X && l.Signs[i].Y == p.Y {
|
if l.Signs[i].X == p.X && l.Signs[i].Y == p.Y && l.Signs[i].Z == p.Z {
|
||||||
return &l.Signs[i]
|
return &l.Signs[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -413,8 +498,7 @@ func (l *Level) Path(from, to Point, canEnter func(Point) bool) []Point {
|
|||||||
}
|
}
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
for _, d := range [...]Point{{1, 0}, {-1, 0}, {0, 1}, {0, -1}} {
|
for _, n := range l.Neighbors(p) {
|
||||||
n := Point{p.X + d.X, p.Y + d.Y}
|
|
||||||
if _, ok := prev[n]; ok || !canEnter(n) {
|
if _, ok := prev[n]; ok || !canEnter(n) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -434,9 +518,8 @@ func (l *Level) Reachable() map[Point]bool {
|
|||||||
for len(queue) > 0 {
|
for len(queue) > 0 {
|
||||||
p := queue[0]
|
p := queue[0]
|
||||||
queue = queue[1:]
|
queue = queue[1:]
|
||||||
for _, d := range [...]Point{{1, 0}, {-1, 0}, {0, 1}, {0, -1}} {
|
for _, n := range l.Neighbors(p) {
|
||||||
n := Point{p.X + d.X, p.Y + d.Y}
|
if seen[n] || !l.At(n).Passable() {
|
||||||
if seen[n] || !l.At(n.X, n.Y).Passable() {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seen[n] = true
|
seen[n] = true
|
||||||
@@ -472,9 +555,8 @@ func (l *Level) UngatedPlatforms() []Point {
|
|||||||
for len(queue) > 0 {
|
for len(queue) > 0 {
|
||||||
p := queue[0]
|
p := queue[0]
|
||||||
queue = queue[1:]
|
queue = queue[1:]
|
||||||
for _, d := range [...]Point{{1, 0}, {-1, 0}, {0, 1}, {0, -1}} {
|
for _, n := range l.Neighbors(p) {
|
||||||
n := Point{p.X + d.X, p.Y + d.Y}
|
t := l.At(n)
|
||||||
t := l.At(n.X, n.Y)
|
|
||||||
if seen[n] || !t.Passable() || t == Gate {
|
if seen[n] || !t.Passable() || t == Gate {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -518,8 +600,8 @@ func (tr *Train) BoardText() string {
|
|||||||
// anger om rälsen (och tågets rörelse) går i sidled.
|
// anger om rälsen (och tågets rörelse) går i sidled.
|
||||||
func (l *Level) TrackTile(p Point) (tile Point, horizontal bool) {
|
func (l *Level) TrackTile(p Point) (tile Point, horizontal bool) {
|
||||||
for _, d := range [...]Point{{X: 0, Y: 1}, {X: 1, Y: 0}, {X: -1, Y: 0}, {X: 0, Y: -1}} {
|
for _, d := range [...]Point{{X: 0, Y: 1}, {X: 1, Y: 0}, {X: -1, Y: 0}, {X: 0, Y: -1}} {
|
||||||
n := Point{X: p.X + d.X, Y: p.Y + d.Y}
|
n := Point{X: p.X + d.X, Y: p.Y + d.Y, Z: p.Z}
|
||||||
if l.At(n.X, n.Y) == Wall {
|
if l.At(n) == Wall {
|
||||||
return n, d.Y != 0
|
return n, d.Y != 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -540,7 +622,7 @@ func (l *Level) CorrectPlatform() Point {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CanEnter rapporterar om rutan går att gå in på (för sim.Mover).
|
// CanEnter rapporterar om rutan går att gå in på (för sim.Mover).
|
||||||
func (l *Level) CanEnter(p Point) bool { return l.At(p.X, p.Y).Passable() }
|
func (l *Level) CanEnter(p Point) bool { return l.At(p).Passable() }
|
||||||
|
|
||||||
// TrainAt returnerar tåget vid en perrongruta, eller nil.
|
// TrainAt returnerar tåget vid en perrongruta, eller nil.
|
||||||
func (l *Level) TrainAt(t Tile) *Train {
|
func (l *Level) TrainAt(t Tile) *Train {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ func TestParseAndSolve(t *testing.T) {
|
|||||||
if l.Width() != 7 || l.Height() != 5 {
|
if l.Width() != 7 || l.Height() != 5 {
|
||||||
t.Fatalf("storlek %dx%d", l.Width(), l.Height())
|
t.Fatalf("storlek %dx%d", l.Width(), l.Height())
|
||||||
}
|
}
|
||||||
if l.SpawnPoint() != (Point{1, 1}) {
|
if l.SpawnPoint() != (Point{X: 1, Y: 1}) {
|
||||||
t.Fatalf("spawn %v", l.SpawnPoint())
|
t.Fatalf("spawn %v", l.SpawnPoint())
|
||||||
}
|
}
|
||||||
if !l.Solvable() {
|
if !l.Solvable() {
|
||||||
@@ -56,14 +56,14 @@ func TestUnsolvableWhenWalledOff(t *testing.T) {
|
|||||||
if l.Solvable() {
|
if l.Solvable() {
|
||||||
t.Fatal("banan ska INTE vara lösbar när perrongen är inmurad")
|
t.Fatal("banan ska INTE vara lösbar när perrongen är inmurad")
|
||||||
}
|
}
|
||||||
if u := l.Unreachable(); len(u) != 1 || u[0] != (Point{5, 1}) {
|
if u := l.Unreachable(); len(u) != 1 || u[0] != (Point{X: 5, Y: 1}) {
|
||||||
t.Fatalf("Unreachable = %v, vill ha [(5,1)]", u)
|
t.Fatalf("Unreachable = %v, vill ha [(5,1)]", u)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOutsideGridIsWall(t *testing.T) {
|
func TestOutsideGridIsWall(t *testing.T) {
|
||||||
l := mustParse(t, sample)
|
l := mustParse(t, sample)
|
||||||
if l.At(-1, 0).Passable() || l.At(0, 99).Passable() {
|
if l.FloorAt(0, -1, 0).Passable() || l.FloorAt(0, 0, 99).Passable() || l.At(Point{Z: 5}).Passable() {
|
||||||
t.Fatal("utanför kartan ska räknas som vägg")
|
t.Fatal("utanför kartan ska räknas som vägg")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -227,19 +227,19 @@ func TestSignAt(t *testing.T) {
|
|||||||
src := strings.Replace(strings.Replace(sample, `"#S..#P#"`, `"#Si.#P#"`, 1),
|
src := strings.Replace(strings.Replace(sample, `"#S..#P#"`, `"#Si.#P#"`, 1),
|
||||||
`"trains"`, `"signs": [{"x": 2, "y": 1, "text": {"sv": "Hej"}}], "trains"`, 1)
|
`"trains"`, `"signs": [{"x": 2, "y": 1, "text": {"sv": "Hej"}}], "trains"`, 1)
|
||||||
l := mustParse(t, src)
|
l := mustParse(t, src)
|
||||||
if sg := l.SignAt(Point{2, 1}); sg == nil || sg.Text.Get("sv") != "Hej" {
|
if sg := l.SignAt(Point{X: 2, Y: 1}); sg == nil || sg.Text.Get("sv") != "Hej" {
|
||||||
t.Fatalf("SignAt = %v", sg)
|
t.Fatalf("SignAt = %v", sg)
|
||||||
}
|
}
|
||||||
if l.SignAt(Point{1, 1}) != nil {
|
if l.SignAt(Point{X: 1, Y: 1}) != nil {
|
||||||
t.Fatal("ingen skylt på startrutan")
|
t.Fatal("ingen skylt på startrutan")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPath(t *testing.T) {
|
func TestPath(t *testing.T) {
|
||||||
l := mustParse(t, sample)
|
l := mustParse(t, sample)
|
||||||
path := l.Path(l.SpawnPoint(), Point{5, 1}, l.CanEnter)
|
path := l.Path(l.SpawnPoint(), Point{X: 5, Y: 1}, l.CanEnter)
|
||||||
// S(1,1) -> (1,2) -> (1,3) -> (2,3) -> (3,3) -> (4,3) -> (5,3) -> (5,2) -> (5,1)
|
// S(1,1) -> (1,2) -> (1,3) -> (2,3) -> (3,3) -> (4,3) -> (5,3) -> (5,2) -> (5,1)
|
||||||
if len(path) != 9 || path[0] != (Point{1, 1}) || path[len(path)-1] != (Point{5, 1}) {
|
if len(path) != 9 || path[0] != (Point{X: 1, Y: 1}) || path[len(path)-1] != (Point{X: 5, Y: 1}) {
|
||||||
t.Fatalf("path = %v", path)
|
t.Fatalf("path = %v", path)
|
||||||
}
|
}
|
||||||
for i := 1; i < len(path); i++ {
|
for i := 1; i < len(path); i++ {
|
||||||
@@ -248,7 +248,7 @@ func TestPath(t *testing.T) {
|
|||||||
t.Fatalf("steg %d är inte en grannruta: %v -> %v", i, path[i-1], path[i])
|
t.Fatalf("steg %d är inte en grannruta: %v -> %v", i, path[i-1], path[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if l.Path(l.SpawnPoint(), Point{0, 0}, l.CanEnter) != nil {
|
if l.Path(l.SpawnPoint(), Point{X: 0, Y: 0}, l.CanEnter) != nil {
|
||||||
t.Fatal("väg in i en vägg ska vara nil")
|
t.Fatal("väg in i en vägg ska vara nil")
|
||||||
}
|
}
|
||||||
if p := l.Path(l.SpawnPoint(), l.SpawnPoint(), l.CanEnter); len(p) != 1 {
|
if p := l.Path(l.SpawnPoint(), l.SpawnPoint(), l.CanEnter); len(p) != 1 {
|
||||||
@@ -259,7 +259,7 @@ func TestPath(t *testing.T) {
|
|||||||
func TestUngatedPlatforms(t *testing.T) {
|
func TestUngatedPlatforms(t *testing.T) {
|
||||||
// Perrong utan spärr framför -> rapporteras.
|
// Perrong utan spärr framför -> rapporteras.
|
||||||
l := mustParse(t, sample)
|
l := mustParse(t, sample)
|
||||||
if u := l.UngatedPlatforms(); len(u) != 1 || u[0] != (Point{5, 1}) {
|
if u := l.UngatedPlatforms(); len(u) != 1 || u[0] != (Point{X: 5, Y: 1}) {
|
||||||
t.Fatalf("UngatedPlatforms = %v, vill ha [(5,1)]", u)
|
t.Fatalf("UngatedPlatforms = %v, vill ha [(5,1)]", u)
|
||||||
}
|
}
|
||||||
// Spärr i enda passagen -> inget rapporteras, men banan är fortfarande lösbar.
|
// Spärr i enda passagen -> inget rapporteras, men banan är fortfarande lösbar.
|
||||||
@@ -280,14 +280,14 @@ func TestPartyAndParDefaults(t *testing.T) {
|
|||||||
if _, err := Parse([]byte(strings.Replace(sample, `"difficulty": 1`, `"difficulty": 1, "party": 9`, 1))); err == nil {
|
if _, err := Parse([]byte(strings.Replace(sample, `"difficulty": 1`, `"difficulty": 1, "party": 9`, 1))); err == nil {
|
||||||
t.Fatal("för stort sällskap ska ge fel")
|
t.Fatal("för stort sällskap ska ge fel")
|
||||||
}
|
}
|
||||||
if l.CorrectPlatform() != (Point{5, 1}) {
|
if l.CorrectPlatform() != (Point{X: 5, Y: 1}) {
|
||||||
t.Fatalf("CorrectPlatform = %v", l.CorrectPlatform())
|
t.Fatalf("CorrectPlatform = %v", l.CorrectPlatform())
|
||||||
}
|
}
|
||||||
if got := l.Trains[0].BoardText(); got != "番線 A B方面" {
|
if got := l.Trains[0].BoardText(); got != "番線 A B方面" {
|
||||||
t.Fatalf("BoardText = %q", got)
|
t.Fatalf("BoardText = %q", got)
|
||||||
}
|
}
|
||||||
l2 := mustParse(t, strings.Replace(sample, `"#S..#P#"`, `"#SW.KP#"`, 1))
|
l2 := mustParse(t, strings.Replace(sample, `"#S..#P#"`, `"#SW.KP#"`, 1))
|
||||||
if l2.At(2, 1) != Toilet || l2.At(4, 1) != Kiosk || !l2.Solvable() {
|
if l2.FloorAt(0, 2, 1) != Toilet || l2.FloorAt(0, 4, 1) != Kiosk || !l2.Solvable() {
|
||||||
t.Fatal("W och K ska vara passerbara rutor")
|
t.Fatal("W och K ska vara passerbara rutor")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -378,12 +378,12 @@ func TestScheduleValidationForPhases(t *testing.T) {
|
|||||||
|
|
||||||
func TestTrackTile(t *testing.T) {
|
func TestTrackTile(t *testing.T) {
|
||||||
l := mustParse(t, sample)
|
l := mustParse(t, sample)
|
||||||
tile, horizontal := l.TrackTile(Point{5, 1}) // P uppe till höger: vägg ovanför? nej, (5,0) är vägg -> men ordningen är ner, höger, vänster, upp
|
tile, horizontal := l.TrackTile(Point{X: 5, Y: 1}) // ordningen är ner, höger, vänster, upp
|
||||||
if tile != (Point{5, 2}) && tile != (Point{6, 1}) && tile != (Point{5, 0}) {
|
if tile != (Point{X: 5, Y: 2}) && tile != (Point{X: 6, Y: 1}) && tile != (Point{X: 5, Y: 0}) {
|
||||||
t.Fatalf("TrackTile = %v", tile)
|
t.Fatalf("TrackTile = %v", tile)
|
||||||
}
|
}
|
||||||
// (5,2) är golv, (6,1) är vägg -> höger: vertikal räls
|
// (5,2) är golv, (6,1) är vägg -> höger: vertikal räls
|
||||||
if tile == (Point{6, 1}) && horizontal {
|
if tile == (Point{X: 6, Y: 1}) && horizontal {
|
||||||
t.Fatal("räls till höger om perrongen är vertikal")
|
t.Fatal("räls till höger om perrongen är vertikal")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -430,3 +430,88 @@ func TestKindOrderParentAndLoadAll(t *testing.T) {
|
|||||||
t.Fatal("det ska finnas minst en sidobana")
|
t.Fatal("det ska finnas minst en sidobana")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const floorsSample = `{
|
||||||
|
"id": "two-floors", "name": {"sv": "Två våningar"}, "difficulty": 2, "destination": {"sv": "x"},
|
||||||
|
"floors": [
|
||||||
|
{"name": "B1", "grid": [
|
||||||
|
"#######",
|
||||||
|
"#S..U.#",
|
||||||
|
"#.###.#",
|
||||||
|
"#.....#",
|
||||||
|
"#######"
|
||||||
|
]},
|
||||||
|
{"name": "1F", "grid": [
|
||||||
|
"#######",
|
||||||
|
"#...D.#",
|
||||||
|
"#.#####",
|
||||||
|
"#G..i.#",
|
||||||
|
"#P#####"
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
"signs": [{"x": 4, "y": 3, "z": 1, "text": {"sv": "Upp", "en": "Up"}}],
|
||||||
|
"trains": [{"platform": "P", "track": "1", "line": {"en": "A"}, "destination": {"en": "B"}, "correct": true}]
|
||||||
|
}`
|
||||||
|
|
||||||
|
func TestFloorsStairsAndPaths(t *testing.T) {
|
||||||
|
l := mustParse(t, floorsSample)
|
||||||
|
if l.NumFloors() != 2 || l.FloorName(0) != "B1" || l.FloorName(1) != "1F" || l.FloorName(9) != "1F" {
|
||||||
|
t.Fatalf("våningar: %d %s %s", l.NumFloors(), l.FloorName(0), l.FloorName(1))
|
||||||
|
}
|
||||||
|
if l.Width() != 7 || l.Height() != 5 || len(l.Grid) != 5 {
|
||||||
|
t.Fatal("storlek/kompatibilitet")
|
||||||
|
}
|
||||||
|
if l.SpawnPoint() != (Point{X: 1, Y: 1, Z: 0}) || l.CorrectPlatform() != (Point{X: 1, Y: 4, Z: 1}) {
|
||||||
|
t.Fatalf("spawn=%v platform=%v", l.SpawnPoint(), l.CorrectPlatform())
|
||||||
|
}
|
||||||
|
if l.At(Point{X: 4, Y: 1}) != StairsUp || l.At(Point{X: 4, Y: 1, Z: 1}) != StairsDown || !StairsUp.IsStairs() || !StairsUp.Passable() {
|
||||||
|
t.Fatal("trappor")
|
||||||
|
}
|
||||||
|
up := Point{X: 4, Y: 1}
|
||||||
|
n := l.Neighbors(up)
|
||||||
|
if len(n) != 5 || n[4] != up.Up() {
|
||||||
|
t.Fatalf("grannar vid trappa: %v", n)
|
||||||
|
}
|
||||||
|
if !l.Solvable() {
|
||||||
|
t.Fatal("banan ska vara lösbar via trappan")
|
||||||
|
}
|
||||||
|
path := l.Path(l.SpawnPoint(), l.CorrectPlatform(), l.CanEnter)
|
||||||
|
if path == nil || path[0].Z != 0 || path[len(path)-1].Z != 1 {
|
||||||
|
t.Fatalf("vägen ska byta våning: %v", path)
|
||||||
|
}
|
||||||
|
climbed := false
|
||||||
|
for i := 1; i < len(path); i++ {
|
||||||
|
if path[i].Z != path[i-1].Z {
|
||||||
|
climbed = true
|
||||||
|
if path[i].X != path[i-1].X || path[i].Y != path[i-1].Y || !l.At(path[i-1]).IsStairs() {
|
||||||
|
t.Fatalf("våningsbyte utan trappa: %v -> %v", path[i-1], path[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !climbed {
|
||||||
|
t.Fatal("vägen använde ingen trappa")
|
||||||
|
}
|
||||||
|
if u := l.UngatedPlatforms(); len(u) != 0 {
|
||||||
|
t.Fatalf("perrongen ligger innanför spärren: %v", u)
|
||||||
|
}
|
||||||
|
if sg := l.SignAt(Point{X: 4, Y: 3, Z: 1}); sg == nil || l.SignAt(Point{X: 4, Y: 3}) != nil {
|
||||||
|
t.Fatal("skylten hör till våning 1")
|
||||||
|
}
|
||||||
|
if u := l.Unreachable(); len(u) != 0 {
|
||||||
|
t.Fatalf("onåbart: %v", u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFloorsValidation(t *testing.T) {
|
||||||
|
bad := map[string]string{
|
||||||
|
"trappa utan motpart": strings.Replace(floorsSample, `"#...D.#"`, `"#.....#"`, 1),
|
||||||
|
"olika storlek": strings.Replace(floorsSample, `"#P#####"`, `"#P######"`, 1),
|
||||||
|
"både grid och floors": strings.Replace(floorsSample, `"floors"`, `"grid": ["###","#S#","###"], "floors"`, 1),
|
||||||
|
"skylt på fel våning": strings.Replace(floorsSample, `"z": 1`, `"z": 0`, 1),
|
||||||
|
}
|
||||||
|
for name, src := range bad {
|
||||||
|
if _, err := Parse([]byte(src)); err == nil {
|
||||||
|
t.Errorf("%s: förväntade fel", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,23 +9,24 @@ const FogRadius = 3
|
|||||||
// Fog håller reda på vilka rutor spelaren har sett i den här omgången.
|
// Fog håller reda på vilka rutor spelaren har sett i den här omgången.
|
||||||
// Minikartan ritar bara sedda rutor plus riktmärken som alltid syns.
|
// Minikartan ritar bara sedda rutor plus riktmärken som alltid syns.
|
||||||
type Fog struct {
|
type Fog struct {
|
||||||
w, h int
|
w, h, floors int
|
||||||
seen []bool
|
seen []bool
|
||||||
Radius int
|
Radius int
|
||||||
count int
|
count int
|
||||||
ExtraLandmarks bool // toaletter, kiosker och automater syns alltid (skillen Lokalkännedom)
|
ExtraLandmarks bool // toaletter, kiosker och automater syns alltid (skillen Lokalkännedom)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFog skapar en tom dimma för banan.
|
// NewFog skapar en tom dimma för banan (alla våningar).
|
||||||
func NewFog(l *level.Level, radius int) *Fog {
|
func NewFog(l *level.Level, radius int) *Fog {
|
||||||
return &Fog{w: l.Width(), h: l.Height(), seen: make([]bool, l.Width()*l.Height()), Radius: radius}
|
n := max(1, l.NumFloors())
|
||||||
|
return &Fog{w: l.Width(), h: l.Height(), floors: n, seen: make([]bool, l.Width()*l.Height()*n), Radius: radius}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fog) idx(p level.Point) (int, bool) {
|
func (f *Fog) idx(p level.Point) (int, bool) {
|
||||||
if p.X < 0 || p.Y < 0 || p.X >= f.w || p.Y >= f.h {
|
if p.X < 0 || p.Y < 0 || p.Z < 0 || p.X >= f.w || p.Y >= f.h || p.Z >= f.floors {
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
return p.Y*f.w + p.X, true
|
return (p.Z*f.h+p.Y)*f.w + p.X, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seen rapporterar om rutan har setts.
|
// Seen rapporterar om rutan har setts.
|
||||||
@@ -41,8 +42,9 @@ func (f *Fog) mark(p level.Point) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reveal avslöjar allt inom Radius steg från from via passerbara rutor,
|
// Reveal avslöjar allt inom Radius steg från from via passerbara rutor på
|
||||||
// samt väggarna som gränsar till dem (så att gångarnas form syns).
|
// samma våning, samt väggarna som gränsar till dem (så att gångarnas form
|
||||||
|
// syns). Trappans andra ände avslöjas också.
|
||||||
func (f *Fog) Reveal(l *level.Level, from level.Point) {
|
func (f *Fog) Reveal(l *level.Level, from level.Point) {
|
||||||
dist := map[level.Point]int{from: 0}
|
dist := map[level.Point]int{from: 0}
|
||||||
queue := []level.Point{from}
|
queue := []level.Point{from}
|
||||||
@@ -50,10 +52,16 @@ func (f *Fog) Reveal(l *level.Level, from level.Point) {
|
|||||||
p := queue[0]
|
p := queue[0]
|
||||||
queue = queue[1:]
|
queue = queue[1:]
|
||||||
f.mark(p)
|
f.mark(p)
|
||||||
|
switch l.At(p) {
|
||||||
|
case level.StairsUp:
|
||||||
|
f.mark(p.Up())
|
||||||
|
case level.StairsDown:
|
||||||
|
f.mark(p.Down())
|
||||||
|
}
|
||||||
for dx := -1; dx <= 1; dx++ {
|
for dx := -1; dx <= 1; dx++ {
|
||||||
for dy := -1; dy <= 1; dy++ {
|
for dy := -1; dy <= 1; dy++ {
|
||||||
n := level.Point{X: p.X + dx, Y: p.Y + dy}
|
n := level.Point{X: p.X + dx, Y: p.Y + dy, Z: p.Z}
|
||||||
if !l.At(n.X, n.Y).Passable() {
|
if !l.At(n).Passable() {
|
||||||
f.mark(n) // gränsande vägg
|
f.mark(n) // gränsande vägg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,8 +70,8 @@ func (f *Fog) Reveal(l *level.Level, from level.Point) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, d := range [...]level.Point{{X: 1}, {X: -1}, {Y: 1}, {Y: -1}} {
|
for _, d := range [...]level.Point{{X: 1}, {X: -1}, {Y: 1}, {Y: -1}} {
|
||||||
n := level.Point{X: p.X + d.X, Y: p.Y + d.Y}
|
n := level.Point{X: p.X + d.X, Y: p.Y + d.Y, Z: p.Z}
|
||||||
if _, ok := dist[n]; ok || !l.At(n.X, n.Y).Passable() {
|
if _, ok := dist[n]; ok || !l.At(n).Passable() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
dist[n] = dist[p] + 1
|
dist[n] = dist[p] + 1
|
||||||
@@ -75,8 +83,8 @@ func (f *Fog) Reveal(l *level.Level, from level.Point) {
|
|||||||
// Landmark är rutor som alltid syns på minikartan: perrongerna (avgångarna),
|
// Landmark är rutor som alltid syns på minikartan: perrongerna (avgångarna),
|
||||||
// starten och spärrarna – det man orienterar sig efter.
|
// starten och spärrarna – det man orienterar sig efter.
|
||||||
func Landmark(l *level.Level, p level.Point) bool {
|
func Landmark(l *level.Level, p level.Point) bool {
|
||||||
switch l.At(p.X, p.Y) {
|
switch l.At(p) {
|
||||||
case level.Platform, level.WrongPlatform, level.Spawn, level.Gate:
|
case level.Platform, level.WrongPlatform, level.Spawn, level.Gate, level.StairsUp, level.StairsDown:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -88,7 +96,7 @@ func (f *Fog) Visible(l *level.Level, p level.Point) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if f.ExtraLandmarks {
|
if f.ExtraLandmarks {
|
||||||
switch l.At(p.X, p.Y) {
|
switch l.At(p) {
|
||||||
case level.Toilet, level.Kiosk, level.TicketMachine:
|
case level.Toilet, level.Kiosk, level.TicketMachine:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -98,9 +106,11 @@ func (f *Fog) Visible(l *level.Level, p level.Point) bool {
|
|||||||
|
|
||||||
// RevealAll avslöjar hela banan (kartan köpt som item).
|
// RevealAll avslöjar hela banan (kartan köpt som item).
|
||||||
func (f *Fog) RevealAll(l *level.Level) {
|
func (f *Fog) RevealAll(l *level.Level) {
|
||||||
|
for z := 0; z < l.NumFloors(); z++ {
|
||||||
for y := 0; y < l.Height(); y++ {
|
for y := 0; y < l.Height(); y++ {
|
||||||
for x := 0; x < l.Width(); x++ {
|
for x := 0; x < l.Width(); x++ {
|
||||||
f.mark(level.Point{X: x, Y: y})
|
f.mark(level.Point{X: x, Y: y, Z: z})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,17 +121,20 @@ func (f *Fog) Count() int { return f.count }
|
|||||||
// Explored är andelen passerbara rutor som setts, 0..1.
|
// Explored är andelen passerbara rutor som setts, 0..1.
|
||||||
func (f *Fog) Explored(l *level.Level) float64 {
|
func (f *Fog) Explored(l *level.Level) float64 {
|
||||||
total, seen := 0, 0
|
total, seen := 0, 0
|
||||||
|
for z := 0; z < l.NumFloors(); z++ {
|
||||||
for y := 0; y < l.Height(); y++ {
|
for y := 0; y < l.Height(); y++ {
|
||||||
for x := 0; x < l.Width(); x++ {
|
for x := 0; x < l.Width(); x++ {
|
||||||
if !l.At(x, y).Passable() {
|
p := level.Point{X: x, Y: y, Z: z}
|
||||||
|
if !l.At(p).Passable() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
total++
|
total++
|
||||||
if f.Seen(level.Point{X: x, Y: y}) {
|
if f.Seen(p) {
|
||||||
seen++
|
seen++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if total == 0 {
|
if total == 0 {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func (m *Mover) Step(dt float64, want Dir, canEnter func(level.Point) bool) int
|
|||||||
}
|
}
|
||||||
m.Facing = want
|
m.Facing = want
|
||||||
dx, dy := want.Delta()
|
dx, dy := want.Delta()
|
||||||
next := level.Point{X: m.Tile.X + dx, Y: m.Tile.Y + dy}
|
next := level.Point{X: m.Tile.X + dx, Y: m.Tile.Y + dy, Z: m.Tile.Z}
|
||||||
if !canEnter(next) {
|
if !canEnter(next) {
|
||||||
return arrived
|
return arrived
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ type Run struct {
|
|||||||
Progressed []string // ord som fick framsteg vid senaste läsningen, "ord n/3"
|
Progressed []string // ord som fick framsteg vid senaste läsningen, "ord n/3"
|
||||||
ProgressedAt float64 // < 0 = aldrig
|
ProgressedAt float64 // < 0 = aldrig
|
||||||
|
|
||||||
|
FloorChanges int // antal trappbyten (statistik)
|
||||||
|
|
||||||
// Resultat när banan vunnits.
|
// Resultat när banan vunnits.
|
||||||
Stars int // 1–3 efter tid mot par
|
Stars int // 1–3 efter tid mot par
|
||||||
PointsEarned int // språkpoäng
|
PointsEarned int // språkpoäng
|
||||||
@@ -266,7 +268,7 @@ func (r *Run) StarsFor(t float64) int {
|
|||||||
// kräver biljett (på biljettbanor) eller ett användbart kort med saldo (att
|
// kräver biljett (på biljettbanor) eller ett användbart kort med saldo (att
|
||||||
// gå mellan två spärrutor är gratis).
|
// gå mellan två spärrutor är gratis).
|
||||||
func (r *Run) CanEnter(p level.Point) bool {
|
func (r *Run) CanEnter(p level.Point) bool {
|
||||||
t := r.Level.At(p.X, p.Y)
|
t := r.Level.At(p)
|
||||||
if !t.Passable() {
|
if !t.Passable() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -315,7 +317,7 @@ func (r *Run) MembersWithoutFare() []*Member {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Run) inGate() bool {
|
func (r *Run) inGate() bool {
|
||||||
return r.Level.At(r.Player.Tile.X, r.Player.Tile.Y) == level.Gate
|
return r.Level.At(r.Player.Tile) == level.Gate
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step för banan framåt dt sekunder. want är hållen riktning, board=true
|
// Step för banan framåt dt sekunder. want är hållen riktning, board=true
|
||||||
@@ -342,6 +344,18 @@ func (r *Run) Step(dt float64, want Dir, board bool) {
|
|||||||
}
|
}
|
||||||
r.Steps += r.Player.Step(dt, want, r.CanEnter)
|
r.Steps += r.Player.Step(dt, want, r.CanEnter)
|
||||||
if r.Player.Tile != before {
|
if r.Player.Tile != before {
|
||||||
|
// Trappa: hamnar man på en trappruta flyttas man till andra änden
|
||||||
|
// (samma x, y, en våning upp eller ner) och fortsätter därifrån.
|
||||||
|
switch r.Level.At(r.Player.Tile) {
|
||||||
|
case level.StairsUp:
|
||||||
|
r.Player.Tile = r.Player.Tile.Up()
|
||||||
|
r.Player.Target = r.Player.Tile
|
||||||
|
r.FloorChanges++
|
||||||
|
case level.StairsDown:
|
||||||
|
r.Player.Tile = r.Player.Tile.Down()
|
||||||
|
r.Player.Target = r.Player.Tile
|
||||||
|
r.FloorChanges++
|
||||||
|
}
|
||||||
r.Party.OnLeaderArrived(r.Player.Tile)
|
r.Party.OnLeaderArrived(r.Player.Tile)
|
||||||
r.Fog.Reveal(r.Level, r.Player.Tile)
|
r.Fog.Reveal(r.Level, r.Player.Tile)
|
||||||
}
|
}
|
||||||
@@ -372,7 +386,7 @@ func (r *Run) Step(dt float64, want Dir, board bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if board && !r.Player.Moving() {
|
if board && !r.Player.Moving() {
|
||||||
switch r.Level.At(r.Player.Tile.X, r.Player.Tile.Y) {
|
switch r.Level.At(r.Player.Tile) {
|
||||||
case level.Toilet:
|
case level.Toilet:
|
||||||
r.useToilet()
|
r.useToilet()
|
||||||
case level.Kiosk:
|
case level.Kiosk:
|
||||||
@@ -652,7 +666,7 @@ func (r *Run) FacilityProgress() float64 {
|
|||||||
// spelaren inte står på en perrong), om det är inne, och sekunder kvar
|
// spelaren inte står på en perrong), om det är inne, och sekunder kvar
|
||||||
// till avgång respektive ankomst.
|
// till avgång respektive ankomst.
|
||||||
func (r *Run) PlatformHere() (tr *level.Train, atPlatform bool, seconds float64) {
|
func (r *Run) PlatformHere() (tr *level.Train, atPlatform bool, seconds float64) {
|
||||||
t := r.Level.At(r.Player.Tile.X, r.Player.Tile.Y)
|
t := r.Level.At(r.Player.Tile)
|
||||||
if t != level.Platform && t != level.WrongPlatform {
|
if t != level.Platform && t != level.WrongPlatform {
|
||||||
return nil, false, 0
|
return nil, false, 0
|
||||||
}
|
}
|
||||||
@@ -679,6 +693,9 @@ func (r *Run) NearTrack(maxDist int) (tr *level.Train, platform level.Point, pha
|
|||||||
best := maxDist + 1
|
best := maxDist + 1
|
||||||
for _, t := range []level.Tile{level.Platform, level.WrongPlatform} {
|
for _, t := range []level.Tile{level.Platform, level.WrongPlatform} {
|
||||||
for _, p := range r.Level.Find(t) {
|
for _, p := range r.Level.Find(t) {
|
||||||
|
if p.Z != r.Player.Tile.Z {
|
||||||
|
continue // man ser och hör bara tågen på sin egen våning
|
||||||
|
}
|
||||||
d := max(abs(p.X-r.Player.Tile.X), abs(p.Y-r.Player.Tile.Y))
|
d := max(abs(p.X-r.Player.Tile.X), abs(p.Y-r.Player.Tile.Y))
|
||||||
if d < best {
|
if d < best {
|
||||||
if cand := r.Level.TrainAt(t); cand != nil {
|
if cand := r.Level.TrainAt(t); cand != nil {
|
||||||
|
|||||||
@@ -85,21 +85,29 @@ func dirTo(from, to level.Point) Dir {
|
|||||||
// walk låter en bot följa en väg ruta för ruta, en bildruta i taget. Tangenten
|
// walk låter en bot följa en väg ruta för ruta, en bildruta i taget. Tangenten
|
||||||
// släpps så fort steget mot nästa ruta påbörjats, så att boten stannar exakt
|
// släpps så fort steget mot nästa ruta påbörjats, så att boten stannar exakt
|
||||||
// där även när farten inte är bildruteexakt (telefon- eller sällskapsfaktor).
|
// där även när farten inte är bildruteexakt (telefon- eller sällskapsfaktor).
|
||||||
|
// Trappor: att kliva på en trappruta flyttar en till andra änden i samma
|
||||||
|
// steg, så vägens nästa punkt (motparten) är redan nådd.
|
||||||
func walk(t *testing.T, r *Run, path []level.Point) {
|
func walk(t *testing.T, r *Run, path []level.Point) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
for _, next := range path[1:] {
|
for idx := 1; idx < len(path); idx++ {
|
||||||
d := dirTo(r.Player.Tile, next)
|
next := path[idx]
|
||||||
for i := 0; i < 600; i++ {
|
cur := r.Player.Tile
|
||||||
if r.Player.Tile == next && !r.Player.Moving() {
|
if next.Z != cur.Z && next.X == cur.X && next.Y == cur.Y {
|
||||||
break
|
continue // våningsbytet skedde när vi klev på trappan
|
||||||
}
|
}
|
||||||
if r.Player.Target == next || r.Player.Tile == next {
|
d := dirTo(cur, next)
|
||||||
|
arrived := func() bool {
|
||||||
|
p := r.Player.Tile
|
||||||
|
return !r.Player.Moving() && (p == next || (r.Level.At(next).IsStairs() && p.X == next.X && p.Y == next.Y))
|
||||||
|
}
|
||||||
|
for i := 0; i < 600 && !arrived(); i++ {
|
||||||
|
if r.Player.Target == next || r.Player.Tile == next || (r.Player.Tile.X == next.X && r.Player.Tile.Y == next.Y) {
|
||||||
r.Step(frame, None, false)
|
r.Step(frame, None, false)
|
||||||
} else {
|
} else {
|
||||||
r.Step(frame, d, false)
|
r.Step(frame, d, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if r.Player.Tile != next || r.Player.Moving() {
|
if !arrived() {
|
||||||
t.Fatalf("boten fastnade på %v på väg mot %v", r.Player.Tile, next)
|
t.Fatalf("boten fastnade på %v på väg mot %v", r.Player.Tile, next)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,7 +211,7 @@ func TestGateNeedsCardBalance(t *testing.T) {
|
|||||||
r.Player.Tile, r.Player.Target = gate, gate
|
r.Player.Tile, r.Player.Target = gate, gate
|
||||||
r.Wallet.Card = 0
|
r.Wallet.Card = 0
|
||||||
other := level.Point{X: gate.X + 1, Y: gate.Y}
|
other := level.Point{X: gate.X + 1, Y: gate.Y}
|
||||||
if l.At(other.X, other.Y) == level.Gate && !r.CanEnter(other) {
|
if l.At(other) == level.Gate && !r.CanEnter(other) {
|
||||||
t.Fatal("att gå mellan två spärrutor ska inte kosta")
|
t.Fatal("att gå mellan två spärrutor ska inte kosta")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -635,7 +643,7 @@ func TestPhoneSpeedRescanAndBattery(t *testing.T) {
|
|||||||
gate := l.Find(level.Gate)[0]
|
gate := l.Find(level.Gate)[0]
|
||||||
path := l.Path(r.Player.Tile, gate, r.CanEnter)
|
path := l.Path(r.Player.Tile, gate, r.CanEnter)
|
||||||
for i, p := range path { // stanna på den första spärrutan – där sker skanningen
|
for i, p := range path { // stanna på den första spärrutan – där sker skanningen
|
||||||
if l.At(p.X, p.Y) == level.Gate {
|
if l.At(p) == level.Gate {
|
||||||
path = path[:i+1]
|
path = path[:i+1]
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -810,7 +818,7 @@ func TestIndividualCardsAtGateAndMachine(t *testing.T) {
|
|||||||
before := r.Wallet.Spent
|
before := r.Wallet.Spent
|
||||||
path := l1.Path(r.Player.Tile, gate, r.CanEnter)
|
path := l1.Path(r.Player.Tile, gate, r.CanEnter)
|
||||||
for i, p := range path {
|
for i, p := range path {
|
||||||
if l1.At(p.X, p.Y) == level.Gate {
|
if l1.At(p) == level.Gate {
|
||||||
path = path[:i+1]
|
path = path[:i+1]
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -981,7 +989,7 @@ func TestRunOptionsFromProgression(t *testing.T) {
|
|||||||
}
|
}
|
||||||
path := l.Path(r.Player.Tile, gate, r.CanEnter)
|
path := l.Path(r.Player.Tile, gate, r.CanEnter)
|
||||||
for i, p := range path {
|
for i, p := range path {
|
||||||
if l.At(p.X, p.Y) == level.Gate {
|
if l.At(p) == level.Gate {
|
||||||
path = path[:i+1]
|
path = path[:i+1]
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -1001,3 +1009,58 @@ func TestRunOptionsFromProgression(t *testing.T) {
|
|||||||
t.Fatal("landmärken från skillen")
|
t.Fatal("landmärken från skillen")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stairsLevel = `{
|
||||||
|
"id": "stairs", "name": {"sv": "Trappor"}, "difficulty": 1, "destination": {"sv": "x", "ja": "新宿"},
|
||||||
|
"floors": [
|
||||||
|
{"name": "B1", "grid": ["#######", "#S..U.#", "#######"]},
|
||||||
|
{"name": "1F", "grid": ["#######", "#.P.D.#", "#######"]}
|
||||||
|
],
|
||||||
|
"trains": [{"platform": "P", "track": "1", "line": {"en": "A"}, "destination": {"en": "B", "ja": "新宿"}, "correct": true}]
|
||||||
|
}`
|
||||||
|
|
||||||
|
func TestStairsMoveBetweenFloors(t *testing.T) {
|
||||||
|
l, err := level.Parse([]byte(stairsLevel))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
r := NewRun(l, RunOptions{Seed: rng.FromInt(1)})
|
||||||
|
if r.Player.Tile.Z != 0 || r.Fog.Seen(level.Point{X: 2, Y: 1, Z: 1}) {
|
||||||
|
t.Fatal("startar på B1; 1F är inte sedd än (utom trappans ände)")
|
||||||
|
}
|
||||||
|
walk(t, r, []level.Point{pt(1, 1), pt(2, 1), pt(3, 1)})
|
||||||
|
if !r.Fog.Seen(level.Point{X: 4, Y: 1, Z: 1}) {
|
||||||
|
t.Fatal("nära trappan avslöjas dess andra ände")
|
||||||
|
}
|
||||||
|
// Gå in på trappan: hamnar på 1F i trappan ner.
|
||||||
|
for i := 0; i < 120 && r.Player.Tile.Z == 0; i++ {
|
||||||
|
r.Step(frame, Right, false)
|
||||||
|
}
|
||||||
|
if r.Player.Tile != (level.Point{X: 4, Y: 1, Z: 1}) || r.Player.Moving() || r.FloorChanges != 1 {
|
||||||
|
t.Fatalf("efter trappan: %v moving=%v changes=%d", r.Player.Tile, r.Player.Moving(), r.FloorChanges)
|
||||||
|
}
|
||||||
|
if _, _, _, _, _, ok := r.NearTrack(2); !ok {
|
||||||
|
t.Fatal("på 1F är perrongen nära")
|
||||||
|
}
|
||||||
|
// Dimman på 1F är nu avslöjad runt trappan.
|
||||||
|
if !r.Fog.Seen(level.Point{X: 2, Y: 1, Z: 1}) {
|
||||||
|
t.Fatal("dimman på 1F ska lätta runt trappan")
|
||||||
|
}
|
||||||
|
// Tillbaka ner: kliv av trappan och gå på den igen.
|
||||||
|
walk(t, r, []level.Point{r.Player.Tile, {X: 3, Y: 1, Z: 1}})
|
||||||
|
walk(t, r, []level.Point{{X: 3, Y: 1, Z: 1}, {X: 4, Y: 1, Z: 1}})
|
||||||
|
if r.Player.Tile != (level.Point{X: 4, Y: 1, Z: 0}) || r.FloorChanges != 2 {
|
||||||
|
t.Fatalf("tillbaka till B1: %v changes=%d", r.Player.Tile, r.FloorChanges)
|
||||||
|
}
|
||||||
|
// Hela vägen till perrongen med BFS över våningar. Man står i trappans
|
||||||
|
// nedre ände, så kliv av först – trappan tar en när man kliver PÅ den.
|
||||||
|
walk(t, r, []level.Point{r.Player.Tile, {X: 3, Y: 1, Z: 0}})
|
||||||
|
path := l.Path(r.Player.Tile, l.CorrectPlatform(), r.CanEnter)
|
||||||
|
if path == nil {
|
||||||
|
t.Fatal("ingen väg")
|
||||||
|
}
|
||||||
|
walk(t, r, path)
|
||||||
|
if r.Player.Tile != l.CorrectPlatform() {
|
||||||
|
t.Fatalf("boten nådde inte perrongen: %v", r.Player.Tile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
159
resources/data/levels/004-tokyo.json
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
{
|
||||||
|
"destination": {
|
||||||
|
"en": "Ueno",
|
||||||
|
"ja": "上野",
|
||||||
|
"romaji": "Ueno",
|
||||||
|
"sv": "Ueno"
|
||||||
|
},
|
||||||
|
"difficulty": 3,
|
||||||
|
"fareYen": 200,
|
||||||
|
"floors": [
|
||||||
|
{
|
||||||
|
"grid": [
|
||||||
|
"#################################",
|
||||||
|
"#S..#.......#.............#.....#",
|
||||||
|
"#.#.#.#####.###.#########.#####.#",
|
||||||
|
"#.#.#.#...#...#.#.....#...#...#.#",
|
||||||
|
"#.#.#.#.#.###.#.#.###.#.###.#.#.#",
|
||||||
|
"#.#...#.#...#.#.....#.#.....#...#",
|
||||||
|
"#.#####.#####.#######.#.#########",
|
||||||
|
"#.......#...#.#.....#.#.#.......#",
|
||||||
|
"#.#.#####.#.#.#.###.#.#.#.#####.#",
|
||||||
|
"#.#.#...#.#.#.#.#...#.#...#...#.#",
|
||||||
|
"#.#.#.#.#.#.#.#.#.###.###.#.#.#.#",
|
||||||
|
"#...#.#...#.#...#...#...#.#.#.#.#",
|
||||||
|
"###.#.#####.#######.###.#.#.#.#.#",
|
||||||
|
"#...#.....#.......#...#...#.#...#",
|
||||||
|
"#.#.#####.#.#########.#####.###.#",
|
||||||
|
"#.#.#...#.#.........#...#...#.#.#",
|
||||||
|
"#.###.#.#.#######.#.###.#.###.#.#",
|
||||||
|
"#.#...#...#.....#.#.......#...#.#",
|
||||||
|
"#.#.#########.#.#.#########.###.#",
|
||||||
|
"#.....U.......#.#...............#",
|
||||||
|
"#################################"
|
||||||
|
],
|
||||||
|
"name": "B1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"grid": [
|
||||||
|
"#################################",
|
||||||
|
"#.......#.............#...#.....#",
|
||||||
|
"#.#####.#.#########.#.#.#.#.#.#.#",
|
||||||
|
"#..i..#.#.....#...#.#.#.#.#.#.#.#",
|
||||||
|
"###.#.#.#######.#.#.#.#.#.###.###",
|
||||||
|
"#...#.#.......#.#...#.#.#...#...#",
|
||||||
|
"#.###.#######.#.#####.#.#.#.###.#",
|
||||||
|
"#.........#T#.#.#K..#...#.#...#.#",
|
||||||
|
"#.#######.#.#.#.###.#####.###.#.#",
|
||||||
|
"#...#...#...#.#...#.........#..i#",
|
||||||
|
"#.#.#.#.###.#.###.#.###########.#",
|
||||||
|
"#W#...#...#.#.#...#.......#...#.#",
|
||||||
|
"#########.#.#.#.###.#####.#.#.#.#",
|
||||||
|
"#........i#.#...#.#.#...#...#...#",
|
||||||
|
"#.#######.#######.#.#.#.#########",
|
||||||
|
"#...#.....#.........#.#...#.....#",
|
||||||
|
"###.#.#.###.###########.#.#.###.#",
|
||||||
|
"#...#.#...#.....#.......#.#...#.#",
|
||||||
|
"#G###.###.#####.#.#.#####.###.#G#",
|
||||||
|
"#pG...D.#...........#.........#P#",
|
||||||
|
"#################################"
|
||||||
|
],
|
||||||
|
"name": "1F"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"id": "004-tokyo",
|
||||||
|
"kind": "main",
|
||||||
|
"maxMissedTrains": 2,
|
||||||
|
"name": {
|
||||||
|
"en": "Tokyo Station",
|
||||||
|
"ja": "東京駅",
|
||||||
|
"sv": "Tokyo station"
|
||||||
|
},
|
||||||
|
"order": 4,
|
||||||
|
"party": 3,
|
||||||
|
"rideStops": 4,
|
||||||
|
"shop": [
|
||||||
|
"beanie",
|
||||||
|
"mask_fox",
|
||||||
|
"hiking_boots",
|
||||||
|
"omamori",
|
||||||
|
"conductor_cap"
|
||||||
|
],
|
||||||
|
"signs": [
|
||||||
|
{
|
||||||
|
"text": {
|
||||||
|
"en": "Tokyo Station, floor 1F. You arrived on the Marunouchi Line underground (B1). Track 4 Yamanote Line for Ueno: south-east. Track 7 Tokaido Line for Shinagawa: south-west.",
|
||||||
|
"ja": "東京駅 1階 地下 丸ノ内線 ↓ 4番線 山手線 上野方面 → 南東 7番線 東海道線 品川方面 → 南西",
|
||||||
|
"sv": "Tokyo station, våning 1F. Du kom med Marunouchi-linjen under mark (B1). Spår 4 Yamanote-linjen mot Ueno: sydost. Spår 7 Tokaido-linjen mot Shinagawa: sydväst."
|
||||||
|
},
|
||||||
|
"x": 9,
|
||||||
|
"y": 13,
|
||||||
|
"z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": {
|
||||||
|
"en": "\u003c- Track 7 Tokaido Line (Shinagawa) Track 4 Yamanote Line (Ueno) -\u003e",
|
||||||
|
"ja": "← 7番線 東海道線 品川方面 4番線 山手線 上野方面 →",
|
||||||
|
"sv": "\u003c- Spår 7 Tokaido-linjen (Shinagawa) Spår 4 Yamanote-linjen (Ueno) -\u003e"
|
||||||
|
},
|
||||||
|
"x": 3,
|
||||||
|
"y": 3,
|
||||||
|
"z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": {
|
||||||
|
"en": "Track 4 Yamanote Line for Ueno: the gate in the south-east corner. The stairs down lead back to B1.",
|
||||||
|
"ja": "4番線 山手線 上野方面 → 南東口 改札 ↓ 地下",
|
||||||
|
"sv": "Spår 4 Yamanote-linjen mot Ueno: spärren i sydöstra hörnet. Trappan ner leder tillbaka till B1."
|
||||||
|
},
|
||||||
|
"x": 31,
|
||||||
|
"y": 9,
|
||||||
|
"z": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ticketRequired": false,
|
||||||
|
"timeLimitSec": 420,
|
||||||
|
"trains": [
|
||||||
|
{
|
||||||
|
"correct": true,
|
||||||
|
"crowded": true,
|
||||||
|
"destination": {
|
||||||
|
"en": "Ueno",
|
||||||
|
"ja": "上野",
|
||||||
|
"romaji": "Ueno"
|
||||||
|
},
|
||||||
|
"line": {
|
||||||
|
"en": "Yamanote Line",
|
||||||
|
"ja": "山手線",
|
||||||
|
"romaji": "Yamanote-sen"
|
||||||
|
},
|
||||||
|
"platform": "P",
|
||||||
|
"schedule": {
|
||||||
|
"dwellSec": 8,
|
||||||
|
"intervalSec": 24,
|
||||||
|
"offsetSec": 5
|
||||||
|
},
|
||||||
|
"track": "4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"correct": false,
|
||||||
|
"destination": {
|
||||||
|
"en": "Shinagawa",
|
||||||
|
"ja": "品川",
|
||||||
|
"romaji": "Shinagawa"
|
||||||
|
},
|
||||||
|
"line": {
|
||||||
|
"en": "Tokaido Line",
|
||||||
|
"ja": "東海道線",
|
||||||
|
"romaji": "Tokaido-sen"
|
||||||
|
},
|
||||||
|
"platform": "p",
|
||||||
|
"schedule": {
|
||||||
|
"dwellSec": 8,
|
||||||
|
"intervalSec": 28,
|
||||||
|
"offsetSec": 11
|
||||||
|
},
|
||||||
|
"track": "7"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 950 B After Width: | Height: | Size: 1.1 KiB |
@@ -539,5 +539,54 @@
|
|||||||
"sv": "Kabukicho",
|
"sv": "Kabukicho",
|
||||||
"tier": 3,
|
"tier": 3,
|
||||||
"kind": "station"
|
"kind": "station"
|
||||||
|
},
|
||||||
|
"東京駅": {
|
||||||
|
"kana": "とうきょうえき",
|
||||||
|
"romaji": "Tōkyō-eki",
|
||||||
|
"en": "Tokyo Station",
|
||||||
|
"sv": "Tokyo station",
|
||||||
|
"tier": 3
|
||||||
|
},
|
||||||
|
"東海道線": {
|
||||||
|
"kana": "とうかいどうせん",
|
||||||
|
"romaji": "Tōkaidō-sen",
|
||||||
|
"en": "Tokaido Line",
|
||||||
|
"sv": "Tokaido-linjen",
|
||||||
|
"tier": 3
|
||||||
|
},
|
||||||
|
"丸ノ内線": {
|
||||||
|
"kana": "まるのうちせん",
|
||||||
|
"romaji": "Marunouchi-sen",
|
||||||
|
"en": "Marunouchi Line",
|
||||||
|
"sv": "Marunouchi-linjen",
|
||||||
|
"tier": 3
|
||||||
|
},
|
||||||
|
"階": {
|
||||||
|
"kana": "かい",
|
||||||
|
"romaji": "kai",
|
||||||
|
"en": "floor",
|
||||||
|
"sv": "våning",
|
||||||
|
"tier": 2
|
||||||
|
},
|
||||||
|
"地下": {
|
||||||
|
"kana": "ちか",
|
||||||
|
"romaji": "chika",
|
||||||
|
"en": "underground",
|
||||||
|
"sv": "under mark",
|
||||||
|
"tier": 2
|
||||||
|
},
|
||||||
|
"上": {
|
||||||
|
"kana": "うえ",
|
||||||
|
"romaji": "ue",
|
||||||
|
"en": "up",
|
||||||
|
"sv": "upp",
|
||||||
|
"tier": 1
|
||||||
|
},
|
||||||
|
"下": {
|
||||||
|
"kana": "した",
|
||||||
|
"romaji": "shita",
|
||||||
|
"en": "down",
|
||||||
|
"sv": "ner",
|
||||||
|
"tier": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
// go run ./tools/genart
|
// go run ./tools/genart
|
||||||
//
|
//
|
||||||
// Skriver resources/sprites/tiles.png (en remsa 16×16-rutor i ordningen
|
// Skriver resources/sprites/tiles.png (en remsa 16×16-rutor i ordningen
|
||||||
// vägg, golv, start, perrong, spärr, automat, skylt, tågvagn, toalett, kiosk, räls) och
|
// vägg, golv, start, perrong, spärr, automat, skylt, tågvagn, toalett, kiosk, räls,
|
||||||
|
// trappa upp, trappa ner) och
|
||||||
// resources/sprites/player.png (3 bildrutor × 4 riktningar: ner, upp,
|
// resources/sprites/player.png (3 bildrutor × 4 riktningar: ner, upp,
|
||||||
// vänster, höger; kolumn 0 = stilla, 1–2 = gå). Ritas med enkla rektanglar
|
// vänster, höger; kolumn 0 = stilla, 1–2 = gå). Ritas med enkla rektanglar
|
||||||
// så att allt är deterministiskt och lätt att justera.
|
// så att allt är deterministiskt och lätt att justera.
|
||||||
@@ -142,6 +143,26 @@ var tiles = []func(canvas){
|
|||||||
c.rect(0, 10, 16, 1, metal)
|
c.rect(0, 10, 16, 1, metal)
|
||||||
c.rect(0, 11, 16, 1, metalDark)
|
c.rect(0, 11, 16, 1, metalDark)
|
||||||
},
|
},
|
||||||
|
func(c canvas) { // trappa upp: steg som blir ljusare uppåt + pil
|
||||||
|
floor(c)
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
shade := uint8(90 + i*25)
|
||||||
|
c.rect(2, 13-i*3, 12, 3, color.RGBA{shade, shade + 10, shade + 30, 255})
|
||||||
|
}
|
||||||
|
c.rect(7, 1, 2, 4, white) // pil upp
|
||||||
|
c.rect(6, 2, 4, 1, white)
|
||||||
|
c.rect(5, 3, 6, 1, white)
|
||||||
|
},
|
||||||
|
func(c canvas) { // trappa ner: steg som blir mörkare neråt + pil
|
||||||
|
floor(c)
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
shade := uint8(190 - i*25)
|
||||||
|
c.rect(2, 1+i*3, 12, 3, color.RGBA{shade, shade + 10, shade + 30, 255})
|
||||||
|
}
|
||||||
|
c.rect(7, 11, 2, 4, boxDark) // pil ner
|
||||||
|
c.rect(6, 13, 4, 1, boxDark)
|
||||||
|
c.rect(5, 12, 6, 1, boxDark)
|
||||||
|
},
|
||||||
func(c canvas) { // kiosk: randig markis och disk
|
func(c canvas) { // kiosk: randig markis och disk
|
||||||
floor(c)
|
floor(c)
|
||||||
c.rect(1, 1, 14, 3, awningRed)
|
c.rect(1, 1, 14, 3, awningRed)
|
||||||
|
|||||||
@@ -94,6 +94,14 @@ func buildShots(levelsDir string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
l4, err := level.LoadFile(filepath.Join(levelsDir, "004-tokyo.json"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
toStairs, err := moves(l4, l4.SpawnPoint(), l4.Find(level.StairsUp)[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
// Skyltbilderna tas med några kända ord så att markeringen och den
|
// Skyltbilderna tas med några kända ord så att markeringen och den
|
||||||
// delvisa översättningen syns; hjälpen lär sedan in nästa ord.
|
// delvisa översättningen syns; hjälpen lär sedan in nästa ord.
|
||||||
known := []string{"-know", "改札,番線,方面", "-seed", "7"}
|
known := []string{"-know", "改札,番線,方面", "-seed", "7"}
|
||||||
@@ -109,6 +117,7 @@ func buildShots(levelsDir string) error {
|
|||||||
{"002-shinjuku", toMachine2 + "; press enter; wait 5; shot $OUT/ticket.png; quit", []string{"-seed", "7", "-know", "渋谷"}},
|
{"002-shinjuku", toMachine2 + "; press enter; wait 5; shot $OUT/ticket.png; quit", []string{"-seed", "7", "-know", "渋谷"}},
|
||||||
{"003-shibuya", toMachine3 + "; press enter; pick 池袋; press enter; wait 5; press enter; press right; wait 5; shot $OUT/seats.png; quit",
|
{"003-shibuya", toMachine3 + "; press enter; pick 池袋; press enter; wait 5; press enter; press right; wait 5; shot $OUT/seats.png; quit",
|
||||||
[]string{"-seed", "7", "-know", "池袋", "-debuffs", "sore_feet"}},
|
[]string{"-seed", "7", "-know", "池袋", "-debuffs", "sore_feet"}},
|
||||||
|
{"004-tokyo", "wait 10; shot $OUT/floors-b1.png; " + toStairs + "; wait 20; shot $OUT/floors-1f.png; quit", []string{"-seed", "7"}},
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -127,6 +136,9 @@ func moves(l *level.Level, from, to level.Point) (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i := 1; i < len(path); i++ {
|
for i := 1; i < len(path); i++ {
|
||||||
|
if path[i].Z != path[i-1].Z {
|
||||||
|
continue // trappan flyttar en till andra våningen i samma steg
|
||||||
|
}
|
||||||
d := dirName(path[i-1], path[i])
|
d := dirName(path[i-1], path[i])
|
||||||
if d != dir {
|
if d != dir {
|
||||||
flush()
|
flush()
|
||||||
|
|||||||