M5: minikarta med dimma, riktmärken och avgångslista
All checks were successful
release / build-release (push) Successful in 1m40s
- sim.Fog: avslöjar tre gångsteg från spelaren plus gränsande väggar (ser längs gången, inte genom väggen); riktmärken (perronger, start, spärrar) syns alltid; Explored ger utforskad andel; nollställs per omgång - game/minimap: 1–3 px per ruta i det hörn som varken täcker inforutan eller spelaren, perrong vit när tåget är inne, spelaren blinkar, följarna i sina toner, avgångslista per spår (● inne / ○ kommer); M döljer/visar - resultatskärmen visar utforskad andel och antal försörjda; layout justerad - docs/README/wiki uppdaterade Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
18
README.md
@@ -10,11 +10,12 @@ 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:** M4 klar (2026-10-23) – skyltarna är på japanska och du lär dig
|
**Status:** M5 klar (2026-10-24) – skyltarna är på japanska och du lär dig
|
||||||
ord genom att läsa, fråga om hjälp, klara banor med bra tid eller lägga
|
ord genom att läsa, fråga om hjälp, klara banor med bra tid eller lägga
|
||||||
språkpoäng i ordlistan. Lämmeltåget följer efter dig: polarna blir hungriga,
|
språkpoäng i ordlistan. Lämmeltåget följer efter dig med sina behov, och
|
||||||
törstiga och måste på toa, oftare ju fler ni är, och du löser det vid kiosker
|
minikartan visar bara avgångar och riktmärken tills du utforskat stationen.
|
||||||
och toaletter. Nästa är M5: minikartan. Se [doc/roadmap.md](doc/roadmap.md).
|
Nästa är M6: tågkort, biljetter, telefon och minispel. Se
|
||||||
|
[doc/roadmap.md](doc/roadmap.md).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -31,7 +32,9 @@ Klarar du banan under par får du stjärnor, språkpoäng och resmålets namn.
|
|||||||
|
|
||||||
Ditt sällskap följer efter i en rad. Mätarna uppe till höger visar mat,
|
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
|
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.
|
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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -119,8 +122,9 @@ metro-puzzle-jp -version
|
|||||||
|
|
||||||
Pilar eller WASD går. Enter väljer, kliver på tåget, använder toaletten
|
Pilar eller WASD går. Enter väljer, kliver på tåget, använder toaletten
|
||||||
eller handlar i kiosken. H frågar om hjälp vid en skylt, L öppnar
|
eller handlar i kiosken. H frågar om hjälp vid en skylt, L öppnar
|
||||||
ordlistan, Esc går tillbaka, R spelar om. Gamepad med standardlayout
|
ordlistan, M döljer/visar minikartan, Esc går tillbaka, R spelar om. Gamepad
|
||||||
fungerar: d-pad/vänster spak, A = välj, B = tillbaka, Y = hjälp, X = ordlista.
|
med standardlayout fungerar: d-pad/vänster spak, A = välj, B = tillbaka,
|
||||||
|
Y = hjälp, X = ordlista, Back/Select = karta.
|
||||||
|
|
||||||
### Verktyg
|
### Verktyg
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 18 KiB |
BIN
doc/img/menu.png
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 25 KiB |
BIN
doc/img/sign.png
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 25 KiB |
@@ -106,6 +106,7 @@ att bygga på Linux som på Windows.
|
|||||||
| `internal/sim` | `TestMoreMembersMoreEvents`, `TestEventsAreSeeded` | **M4:** fyra i sällskapet ger minst 3× så många händelser som en; samma seed = samma logg |
|
| `internal/sim` | `TestMoreMembersMoreEvents`, `TestEventsAreSeeded` | **M4:** fyra i sällskapet ger minst 3× så många händelser som en; samma seed = samma logg |
|
||||||
| `internal/sim` | `TestUrgesSlowAndFacilitiesRestore`, `TestFollowersTrailLeader`, `TestPartySizeIsClamped` | Fartstraff, toalett/kiosk återställer och kostar, distraktion går över, följarna står på ledarens spår |
|
| `internal/sim` | `TestUrgesSlowAndFacilitiesRestore`, `TestFollowersTrailLeader`, `TestPartySizeIsClamped` | Fartstraff, toalett/kiosk återställer och kostar, distraktion går över, följarna står på ledarens spår |
|
||||||
| `internal/sim` | `TestToiletAndKioskInRun`, `TestPartySlowsLeader` | Enter på W startar ett besök som tar tid och avbryts om man går; K drar från kortet; sällskapet saktar ledaren |
|
| `internal/sim` | `TestToiletAndKioskInRun`, `TestPartySlowsLeader` | Enter på W startar ett besök som tar tid och avbryts om man går; K drar från kortet; sällskapet saktar ledaren |
|
||||||
|
| `internal/sim` | `TestFogRevealsAlongCorridorsNotThroughWalls`, `TestLandmarksAlwaysVisible`, `TestFogGrowsWithMovementAndResetsPerRun` | **M5:** dimman lättar längs gångar men inte genom väggar; perronger/start/spärrar syns alltid; dimman växer när man går och nollställs per omgång |
|
||||||
| `internal/archtest` | `TestOnlyPresentationPackagesImportEbitengine` | Beroenderegeln ovan |
|
| `internal/archtest` | `TestOnlyPresentationPackagesImportEbitengine` | Beroenderegeln ovan |
|
||||||
|
|
||||||
### Planerade tester per milstolpe
|
### Planerade tester per milstolpe
|
||||||
@@ -116,7 +117,7 @@ att bygga på Linux som på Windows.
|
|||||||
| ~~M2 Stationen~~ ✓ | Tidtabell, spärr med/utan saldo, vinst/förlust, bot klarar bana 001 headless – finns, se tabellen ovan |
|
| ~~M2 Stationen~~ ✓ | Tidtabell, spärr med/utan saldo, vinst/förlust, bot klarar bana 001 headless – finns, se tabellen ovan |
|
||||||
| ~~M3 Japanska & skyltar~~ ✓ | Delvis översättning, engelska-andel, ordbokens fullständighet och täckning, hjälptid – finns, se tabellen ovan |
|
| ~~M3 Japanska & skyltar~~ ✓ | Delvis översättning, engelska-andel, ordbokens fullständighet och täckning, hjälptid – finns, se tabellen ovan |
|
||||||
| ~~M4 Lämmeltåget~~ ✓ | Mätare, händelsefrekvens efter gruppstorlek, seedade händelser – finns, se tabellen ovan |
|
| ~~M4 Lämmeltåget~~ ✓ | Mätare, händelsefrekvens efter gruppstorlek, seedade händelser – finns, se tabellen ovan |
|
||||||
| M5 Minikarta | Upptäckta rutor markeras, återställs per bana; avgångar syns från start |
|
| ~~M5 Minikarta~~ ✓ | Upptäckta rutor, nollställning per bana, avgångar från start – finns, se tabellen ovan |
|
||||||
| M6 Kort/biljetter/minispel | Saldo dras i spärr; tomt kort → automatkrav; biljettminispel: rätt/fel biljett; telefon-skanning misslyckas med given procent per seed; batteri tar bara slut över tröskeltid |
|
| M6 Kort/biljetter/minispel | Saldo dras i spärr; tomt kort → automatkrav; biljettminispel: rätt/fel biljett; telefon-skanning misslyckas med given procent per seed; batteri tar bara slut över tröskeltid |
|
||||||
| M7 Svårighet/poäng/debuffs | Poängformeln (tid, pengar, mätare, läge); tidsgräns och maxantal missade tåg; varje debuff ändrar rätt stat; temporära debuffar räknar ned 1–4 banor |
|
| M7 Svårighet/poäng/debuffs | Poängformeln (tid, pengar, mätare, läge); tidsgräns och maxantal missade tåg; varje debuff ändrar rätt stat; temporära debuffar räknar ned 1–4 banor |
|
||||||
| M8 Progression & sparfil | Sparfil round-trip (skriv → läs ger samma tillstånd inkl. seed); skill tree-krav; upplåsningar efter rätt bana; "< 33 %"-regeln kontrolleras mot itemlistan |
|
| M8 Progression & sparfil | Sparfil round-trip (skriv → läs ger samma tillstånd inkl. seed); skill tree-krav; upplåsningar efter rätt bana; "< 33 %"-regeln kontrolleras mot itemlistan |
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ brasse-linux01-runnern, inte Pi5:n.
|
|||||||
## Milstolpar
|
## Milstolpar
|
||||||
|
|
||||||
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 → M8 progression/sparfiler → M9 navigeringslägen →
|
svårighet/poäng/debuffar → M8 progression/sparfiler → 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).
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ metro-puzzle-jp/
|
|||||||
│ ├── res/ hittar och läser resources/ (ren Go)
|
│ ├── res/ hittar och läser resources/ (ren Go)
|
||||||
│ ├── rng/ seedad slump (ren Go)
|
│ ├── rng/ seedad slump (ren Go)
|
||||||
│ ├── level/ banformat, validering, BFS (ren Go)
|
│ ├── level/ banformat, validering, BFS (ren Go)
|
||||||
│ ├── sim/ rutbaserad rörelse, kollision, kamera, banomgång (Run: klocka, kort, spärrar, tåg, hjälp) – ren Go
|
│ ├── sim/ rutbaserad rörelse, kollision, kamera, banomgång (Run), sällskap (Party), dimma (Fog) – ren Go
|
||||||
│ ├── lang/ ordbok, tokenisering, kunskap, delvis översättning, engelska-andel, hjälptid – ren Go
|
│ ├── lang/ ordbok, tokenisering, kunskap, delvis översättning, engelska-andel, hjälptid – ren Go
|
||||||
│ ├── game/ Ebitengine-loop, scener, input, rendering, skriptläge ← får använda Ebitengine
|
│ ├── game/ Ebitengine-loop, scener, input, rendering, skriptläge ← får använda Ebitengine
|
||||||
│ ├── archtest/ beroenderegler
|
│ ├── archtest/ beroenderegler
|
||||||
@@ -233,12 +233,26 @@ på **brasse-linux01-runnern** (label `linux-amd64`), aldrig på Pi5:n.
|
|||||||
- **Skjutet till M7:** mätarnas värde vid målgång påverkar poängen; debuffar
|
- **Skjutet till M7:** mätarnas värde vid målgång påverkar poängen; debuffar
|
||||||
vid tomma mätare.
|
vid tomma mätare.
|
||||||
|
|
||||||
### M5 – Minikarta och utforskning
|
### M5 – Minikarta och utforskning ✓ (klar 2026-10-24)
|
||||||
|
|
||||||
- **Mål:** orientering utan att avslöja labyrinten.
|
- **Mål:** orientering utan att avslöja labyrinten.
|
||||||
- Minikarta som visar avgångar och riktmärken från start och fylls i när
|
- **Dimma** (`sim.Fog`): när spelaren når en ruta avslöjas allt inom tre
|
||||||
man utforskar. Nollställs per bana och omgång.
|
*steg* via passerbara rutor plus de väggar som gränsar till dem – man ser
|
||||||
- **Teknik:** `internal/sim/fog` (ren Go), rendering i `internal/game`.
|
längs gången, inte genom väggen. Nollställs per omgång (ny `Run`).
|
||||||
|
- **Riktmärken** syns alltid: perrongerna (avgångarna), starten och
|
||||||
|
spärrarna. Allt annat (kiosker, toaletter, skyltar, automater, gångar)
|
||||||
|
ritas först när det setts.
|
||||||
|
- **Minikartan** ritas i ett hörn som varken täcker inforutan eller
|
||||||
|
spelaren, 1–3 px per ruta beroende på banans storlek. Under kartan står
|
||||||
|
en avgångslista per spår: `2 ●6s` = tåget inne, avgår om 6 s;
|
||||||
|
`1 ○14s` = kommer om 14 s. Perrong med tåg inne lyser vit. Spelaren
|
||||||
|
blinkar vit, följarna i sina färgtoner. **M** döljer/visar kartan.
|
||||||
|
- Resultatskärmen visar **utforskad andel** av banan (används av poängen i
|
||||||
|
M7) samt antal försörjda i sällskapet.
|
||||||
|
- **Teknik:** `sim.Fog` (ren Go, 3 tester), `internal/game/minimap.go`.
|
||||||
|
- **Klar när:** upptäckta rutor markeras och nollställs per bana, avgångar
|
||||||
|
syns från start – `TestFogRevealsAlongCorridorsNotThroughWalls`,
|
||||||
|
`TestLandmarksAlwaysVisible`, `TestFogGrowsWithMovementAndResetsPerRun`. ✓
|
||||||
|
|
||||||
### M6 – Kort, biljetter, telefon och minispel
|
### M6 – Kort, biljetter, telefon och minispel
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,11 @@ func wordsJustPressed() bool {
|
|||||||
return virtual.pressed["l"] || inpututil.IsKeyJustPressed(ebiten.KeyL) || gamepadJustPressed(ebiten.StandardGamepadButtonLeftRight)
|
return virtual.pressed["l"] || inpututil.IsKeyJustPressed(ebiten.KeyL) || gamepadJustPressed(ebiten.StandardGamepadButtonLeftRight)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mapJustPressed: M eller gamepadens vänstra mittknapp (Back/Select) = minikartan av/på.
|
||||||
|
func mapJustPressed() bool {
|
||||||
|
return virtual.pressed["m"] || inpututil.IsKeyJustPressed(ebiten.KeyM) || gamepadJustPressed(ebiten.StandardGamepadButtonCenterLeft)
|
||||||
|
}
|
||||||
|
|
||||||
// menuMove returnerar -1/+1 när användaren just tryckte upp/ner i en meny.
|
// menuMove returnerar -1/+1 när användaren just tryckte upp/ner i en meny.
|
||||||
func menuMove() int {
|
func menuMove() int {
|
||||||
switch {
|
switch {
|
||||||
|
|||||||
126
internal/game/minimap.go
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||||
|
|
||||||
|
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
||||||
|
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/sim"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Minikartan: bara det spelaren sett plus riktmärkena (perronger, start,
|
||||||
|
// spärrar), och en liten avgångslista per spår. Den lägger sig i det hörn
|
||||||
|
// som varken täcker inforutan eller spelaren.
|
||||||
|
|
||||||
|
var minimapColors = map[level.Tile]color.RGBA{
|
||||||
|
level.Wall: {58, 68, 102, 255},
|
||||||
|
level.Floor: {139, 155, 180, 255},
|
||||||
|
level.Spawn: {99, 199, 77, 255},
|
||||||
|
level.Platform: {254, 174, 52, 255},
|
||||||
|
level.WrongPlatform: {254, 174, 52, 255},
|
||||||
|
level.Gate: {44, 232, 245, 255},
|
||||||
|
level.TicketMachine: {247, 118, 34, 255},
|
||||||
|
level.Sign: {90, 140, 230, 255},
|
||||||
|
level.Toilet: {235, 240, 250, 255},
|
||||||
|
level.Kiosk: {228, 59, 68, 255},
|
||||||
|
}
|
||||||
|
|
||||||
|
// minimapScale väljer pixlar per ruta så att kartan ryms i ungefär en
|
||||||
|
// tredjedel av skärmen.
|
||||||
|
func minimapScale(l *level.Level) int {
|
||||||
|
sc := min(3, (ScreenW/3)/max(1, l.Width()), (ScreenH/3)/max(1, l.Height()))
|
||||||
|
return max(1, sc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// minimapRect väljer hörn: nedre höger som standard, annars det första hörnet
|
||||||
|
// som inte krockar med inforutan (full bredd vid box) eller spelaren.
|
||||||
|
func minimapRect(w, h int, box image.Rectangle, hasBox bool, player image.Rectangle) image.Rectangle {
|
||||||
|
const m = 4
|
||||||
|
candidates := []image.Rectangle{
|
||||||
|
image.Rect(ScreenW-m-w, ScreenH-m-h, ScreenW-m, ScreenH-m), // nedre höger
|
||||||
|
image.Rect(m, ScreenH-m-h, m+w, ScreenH-m), // nedre vänster
|
||||||
|
image.Rect(ScreenW-m-w, 26, ScreenW-m, 26+h), // övre höger (under HUD:en)
|
||||||
|
image.Rect(m, 26, m+w, 26+h), // övre vänster
|
||||||
|
}
|
||||||
|
for _, c := range candidates {
|
||||||
|
if hasBox && c.Overlaps(box) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c.Overlaps(player.Inset(-8)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return candidates[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LevelScene) drawMinimap(g *Game, screen *ebiten.Image, box image.Rectangle, hasBox bool) {
|
||||||
|
a := g.Assets
|
||||||
|
r := s.run
|
||||||
|
l := r.Level
|
||||||
|
sc := minimapScale(l)
|
||||||
|
mapW, mapH := l.Width()*sc, l.Height()*sc
|
||||||
|
|
||||||
|
// Avgångslistan: ett spår per rad.
|
||||||
|
type dep struct {
|
||||||
|
track string
|
||||||
|
at bool
|
||||||
|
secs float64
|
||||||
|
}
|
||||||
|
var deps []dep
|
||||||
|
for _, tr := range l.Trains {
|
||||||
|
at, secs := tr.Schedule.Status(r.Time)
|
||||||
|
deps = append(deps, dep{tr.Track, at, secs})
|
||||||
|
}
|
||||||
|
sort.Slice(deps, func(i, j int) bool { return deps[i].track < deps[j].track })
|
||||||
|
|
||||||
|
w := max(mapW, 44) + 6
|
||||||
|
h := mapH + 6 + len(deps)*LineH
|
||||||
|
px, py := r.Player.WorldPos(TileSize)
|
||||||
|
ox, oy := s.cam.Offset()
|
||||||
|
playerRect := image.Rect(int(px+ox), int(py+oy), int(px+ox)+TileSize, int(py+oy)+TileSize)
|
||||||
|
rc := minimapRect(w, h, box, hasBox, playerRect)
|
||||||
|
|
||||||
|
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
|
||||||
|
for y := 0; y < l.Height(); y++ {
|
||||||
|
for x := 0; x < l.Width(); x++ {
|
||||||
|
p := level.Point{X: x, Y: y}
|
||||||
|
if !r.Fog.Visible(l, p) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t := l.At(x, y)
|
||||||
|
c := minimapColors[t]
|
||||||
|
if (t == level.Platform || t == level.WrongPlatform) && r.TrainVisible(t) {
|
||||||
|
c = color.RGBA{255, 255, 255, 255} // tåget är inne
|
||||||
|
}
|
||||||
|
vector.DrawFilledRect(screen, float32(x0+x*sc), float32(y0+y*sc), float32(sc), float32(sc), c, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Sällskapet: följarna i sina toner, spelaren blinkande vit.
|
||||||
|
for i := len(r.Party.Followers()); i >= 1; i-- {
|
||||||
|
from, _ := r.Party.FollowerTiles(i)
|
||||||
|
tint := memberTints[i%len(memberTints)]
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if math.Mod(r.Time, 0.6) < 0.4 {
|
||||||
|
pt := r.Player.Tile
|
||||||
|
vector.DrawFilledRect(screen, float32(x0+pt.X*sc), float32(y0+pt.Y*sc), float32(sc), float32(sc), colText, false)
|
||||||
|
}
|
||||||
|
// Avgångar: "2 ●6s" = inne, avgår om 6 s; "1 ○14s" = kommer om 14 s.
|
||||||
|
for i, d := range deps {
|
||||||
|
mark, col := "○", colDim
|
||||||
|
if d.at {
|
||||||
|
mark, col = "●", colGood
|
||||||
|
}
|
||||||
|
a.Draw(screen, fmt.Sprintf("%s %s%.0fs", d.track, mark, math.Ceil(d.secs)), x0, y0+mapH+2+i*LineH, col)
|
||||||
|
}
|
||||||
|
_ = sim.FogRadius
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package game
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"math"
|
"math"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -30,6 +31,7 @@ type LevelScene struct {
|
|||||||
run *sim.Run
|
run *sim.Run
|
||||||
cam *sim.Camera
|
cam *sim.Camera
|
||||||
animT float64
|
animT float64
|
||||||
|
hideMap bool // M döljer/visar minikartan
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLevelScene laddar banan och ställer spelaren på startrutan. know är
|
// NewLevelScene laddar banan och ställer spelaren på startrutan. know är
|
||||||
@@ -77,6 +79,9 @@ func (s *LevelScene) Update(g *Game) error {
|
|||||||
if helpJustPressed() {
|
if helpJustPressed() {
|
||||||
s.run.AskHelp()
|
s.run.AskHelp()
|
||||||
}
|
}
|
||||||
|
if mapJustPressed() {
|
||||||
|
s.hideMap = !s.hideMap
|
||||||
|
}
|
||||||
s.run.Step(dt, dirInput(), confirmJustPressed())
|
s.run.Step(dt, dirInput(), confirmJustPressed())
|
||||||
if s.run.Player.Moving() {
|
if s.run.Player.Moving() {
|
||||||
s.animT += dt
|
s.animT += dt
|
||||||
@@ -219,7 +224,7 @@ func (s *LevelScene) drawHUD(g *Game, screen *ebiten.Image) {
|
|||||||
s.drawMeters(g, screen)
|
s.drawMeters(g, screen)
|
||||||
|
|
||||||
if r.Outcome != sim.Ongoing {
|
if r.Outcome != sim.Ongoing {
|
||||||
return
|
return // resultatpanelen tar över; ingen inforuta eller minikarta
|
||||||
}
|
}
|
||||||
lines, accent, bar := s.infoLines(a)
|
lines, accent, bar := s.infoLines(a)
|
||||||
if entry := a.Dict.Lookup(r.Learned); entry != nil && r.Time-r.LearnedAt < learnedShowSec {
|
if entry := a.Dict.Lookup(r.Learned); entry != nil && r.Time-r.LearnedAt < learnedShowSec {
|
||||||
@@ -236,20 +241,30 @@ func (s *LevelScene) drawHUD(g *Game, screen *ebiten.Image) {
|
|||||||
}
|
}
|
||||||
lines = append(lines, a.wrapLine(plainLine(strings.Join(parts, " "), colWarn), ScreenW-8)...)
|
lines = append(lines, a.wrapLine(plainLine(strings.Join(parts, " "), colWarn), ScreenW-8)...)
|
||||||
}
|
}
|
||||||
if len(lines) == 0 {
|
|
||||||
return
|
// Inforutans läge: nederkant, men flyttas upp under HUD:en om spelaren
|
||||||
}
|
// (eller tåget under hen) annars skulle hamna bakom den.
|
||||||
|
var box image.Rectangle
|
||||||
|
hasBox := len(lines) > 0
|
||||||
|
if hasBox {
|
||||||
h := 8 + LineH*len(lines)
|
h := 8 + LineH*len(lines)
|
||||||
if bar {
|
if bar {
|
||||||
h += 8
|
h += 8
|
||||||
}
|
}
|
||||||
// Rutan ligger i nederkant, men flyttas upp under HUD:en om spelaren
|
|
||||||
// (eller tåget under hen) annars skulle hamna bakom den.
|
|
||||||
_, py := r.Player.WorldPos(TileSize)
|
_, py := r.Player.WorldPos(TileSize)
|
||||||
y := ScreenH - h
|
y := ScreenH - h
|
||||||
if int(py-s.cam.Y)+2*TileSize > y {
|
if int(py-s.cam.Y)+2*TileSize > y {
|
||||||
y = 24
|
y = 24
|
||||||
}
|
}
|
||||||
|
box = image.Rect(0, y, ScreenW, y+h)
|
||||||
|
}
|
||||||
|
if !s.hideMap {
|
||||||
|
s.drawMinimap(g, screen, box, hasBox)
|
||||||
|
}
|
||||||
|
if !hasBox {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
y, h := box.Min.Y, box.Dy()
|
||||||
vector.DrawFilledRect(screen, 0, float32(y), ScreenW, float32(h), colPanel, false)
|
vector.DrawFilledRect(screen, 0, float32(y), ScreenW, float32(h), colPanel, false)
|
||||||
vector.DrawFilledRect(screen, 0, float32(y), ScreenW, 2, accent, false)
|
vector.DrawFilledRect(screen, 0, float32(y), ScreenW, 2, accent, false)
|
||||||
a.drawLines(screen, lines, 4, y+4)
|
a.drawLines(screen, lines, 4, y+4)
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func (r *ResultScene) Draw(g *Game, screen *ebiten.Image) {
|
|||||||
r.from.Draw(g, screen) // banan i bakgrunden
|
r.from.Draw(g, screen) // banan i bakgrunden
|
||||||
run := r.from.run
|
run := r.from.run
|
||||||
a := g.Assets
|
a := g.Assets
|
||||||
vector.DrawFilledRect(screen, 20, 36, ScreenW-40, 112, colPanel, false)
|
vector.DrawFilledRect(screen, 20, 36, ScreenW-40, 120, colPanel, false)
|
||||||
|
|
||||||
accent, headline := colBad, a.T("level.wrong_train")
|
accent, headline := colBad, a.T("level.wrong_train")
|
||||||
if run.Outcome == sim.Won {
|
if run.Outcome == sim.Won {
|
||||||
@@ -71,8 +71,11 @@ func (r *ResultScene) Draw(g *Game, screen *ebiten.Image) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
y += 4
|
y += 4
|
||||||
a.Draw(screen, fmt.Sprintf("%s %.1fs (%s %.0fs) %s %d %s %d %s %d¥", a.T("hud.time"), run.Time, a.T("result.par"), run.ParSec(),
|
a.Draw(screen, fmt.Sprintf("%s %.1fs (%s %.0fs) %s %d %s %.0f %%", a.T("hud.time"), run.Time, a.T("result.par"), run.ParSec(),
|
||||||
a.T("hud.steps"), run.Steps, a.T("result.gates"), run.GatePasses, a.T("result.card"), run.Card.Balance), 28, y, colText)
|
a.T("hud.steps"), run.Steps, a.T("result.explored"), 100*run.Fog.Explored(run.Level)), 28, y, colText)
|
||||||
|
y += LineH
|
||||||
|
a.Draw(screen, fmt.Sprintf("%s %d %s %d¥ %s %d", a.T("result.gates"), run.GatePasses, a.T("result.card"), run.Card.Balance,
|
||||||
|
a.T("result.served"), run.Served), 28, y, colText)
|
||||||
y += LineH
|
y += LineH
|
||||||
if run.Outcome == sim.Won {
|
if run.Outcome == sim.Won {
|
||||||
msg := fmt.Sprintf("+%d %s", run.PointsEarned, a.T("words.points"))
|
msg := fmt.Sprintf("+%d %s", run.PointsEarned, a.T("words.points"))
|
||||||
@@ -84,5 +87,5 @@ func (r *ResultScene) Draw(g *Game, screen *ebiten.Image) {
|
|||||||
}
|
}
|
||||||
a.Draw(screen, fmt.Sprintf("%s Lv%d %d %s %s %d (%d %s)", a.T("hud.japanese"), run.Know.Level(), run.Know.Count(),
|
a.Draw(screen, fmt.Sprintf("%s Lv%d %d %s %s %d (%d %s)", a.T("hud.japanese"), run.Know.Level(), run.Know.Count(),
|
||||||
a.T("hud.words"), a.T("words.points"), run.Know.Points, run.Know.Helps, a.T("result.helps")), 28, y, colDim)
|
a.T("hud.words"), a.T("words.points"), run.Know.Points, run.Know.Helps, a.T("result.helps")), 28, y, colDim)
|
||||||
a.Draw(screen, a.T("result.hint"), 28, 130, colDim)
|
a.Draw(screen, a.T("result.hint"), 28, 141, colDim)
|
||||||
}
|
}
|
||||||
|
|||||||
110
internal/sim/fog.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package sim
|
||||||
|
|
||||||
|
import "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
||||||
|
|
||||||
|
// FogRadius är hur många steg från spelaren som avslöjas (gångavstånd, så
|
||||||
|
// väggar skymmer – man ser längs gången, inte genom väggen).
|
||||||
|
const FogRadius = 3
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
type Fog struct {
|
||||||
|
w, h int
|
||||||
|
seen []bool
|
||||||
|
Radius int
|
||||||
|
count int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFog skapar en tom dimma för banan.
|
||||||
|
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}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fog) idx(p level.Point) (int, bool) {
|
||||||
|
if p.X < 0 || p.Y < 0 || p.X >= f.w || p.Y >= f.h {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return p.Y*f.w + p.X, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seen rapporterar om rutan har setts.
|
||||||
|
func (f *Fog) Seen(p level.Point) bool {
|
||||||
|
i, ok := f.idx(p)
|
||||||
|
return ok && f.seen[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fog) mark(p level.Point) {
|
||||||
|
if i, ok := f.idx(p); ok && !f.seen[i] {
|
||||||
|
f.seen[i] = true
|
||||||
|
f.count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reveal avslöjar allt inom Radius steg från from via passerbara rutor,
|
||||||
|
// samt väggarna som gränsar till dem (så att gångarnas form syns).
|
||||||
|
func (f *Fog) Reveal(l *level.Level, from level.Point) {
|
||||||
|
dist := map[level.Point]int{from: 0}
|
||||||
|
queue := []level.Point{from}
|
||||||
|
for len(queue) > 0 {
|
||||||
|
p := queue[0]
|
||||||
|
queue = queue[1:]
|
||||||
|
f.mark(p)
|
||||||
|
for dx := -1; dx <= 1; dx++ {
|
||||||
|
for dy := -1; dy <= 1; dy++ {
|
||||||
|
n := level.Point{X: p.X + dx, Y: p.Y + dy}
|
||||||
|
if !l.At(n.X, n.Y).Passable() {
|
||||||
|
f.mark(n) // gränsande vägg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if dist[p] >= f.Radius {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
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}
|
||||||
|
if _, ok := dist[n]; ok || !l.At(n.X, n.Y).Passable() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dist[n] = dist[p] + 1
|
||||||
|
queue = append(queue, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Landmark är rutor som alltid syns på minikartan: perrongerna (avgångarna),
|
||||||
|
// starten och spärrarna – det man orienterar sig efter.
|
||||||
|
func Landmark(l *level.Level, p level.Point) bool {
|
||||||
|
switch l.At(p.X, p.Y) {
|
||||||
|
case level.Platform, level.WrongPlatform, level.Spawn, level.Gate:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visible rapporterar om rutan ska ritas på minikartan.
|
||||||
|
func (f *Fog) Visible(l *level.Level, p level.Point) bool {
|
||||||
|
return f.Seen(p) || Landmark(l, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count är antalet sedda rutor (inkl. väggar).
|
||||||
|
func (f *Fog) Count() int { return f.count }
|
||||||
|
|
||||||
|
// Explored är andelen passerbara rutor som setts, 0..1.
|
||||||
|
func (f *Fog) Explored(l *level.Level) float64 {
|
||||||
|
total, seen := 0, 0
|
||||||
|
for y := 0; y < l.Height(); y++ {
|
||||||
|
for x := 0; x < l.Width(); x++ {
|
||||||
|
if !l.At(x, y).Passable() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
total++
|
||||||
|
if f.Seen(level.Point{X: x, Y: y}) {
|
||||||
|
seen++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if total == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return float64(seen) / float64(total)
|
||||||
|
}
|
||||||
92
internal/sim/fog_test.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package sim
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
||||||
|
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/rng"
|
||||||
|
)
|
||||||
|
|
||||||
|
const fogSample = `{
|
||||||
|
"id": "fog", "name": {"sv": "Fog"}, "difficulty": 1, "destination": {"sv": "x"},
|
||||||
|
"grid": [
|
||||||
|
"#########",
|
||||||
|
"#S......#",
|
||||||
|
"#.#####.#",
|
||||||
|
"#.#...#.#",
|
||||||
|
"#.#.#.#.#",
|
||||||
|
"#.#.#P#.#",
|
||||||
|
"#.#.###.#",
|
||||||
|
"#.......#",
|
||||||
|
"#########"
|
||||||
|
],
|
||||||
|
"trains": [{"platform": "P", "track": "1", "line": {"en": "A"}, "destination": {"en": "B"}, "correct": true}]
|
||||||
|
}`
|
||||||
|
|
||||||
|
func TestFogRevealsAlongCorridorsNotThroughWalls(t *testing.T) {
|
||||||
|
l, err := level.Parse([]byte(fogSample))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f := NewFog(l, 3)
|
||||||
|
f.Reveal(l, pt(1, 1))
|
||||||
|
for _, p := range []level.Point{pt(1, 1), pt(4, 1), pt(1, 4), pt(2, 1)} {
|
||||||
|
if !f.Seen(p) {
|
||||||
|
t.Errorf("%v inom 3 steg ska vara sedd", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// (2,2) är vägg bredvid gången -> sedd; (3,3) är golv men 2 steg bort fågelvägen
|
||||||
|
// och mycket längre gångvägen -> inte sedd.
|
||||||
|
if !f.Seen(pt(2, 2)) {
|
||||||
|
t.Error("väggen vid gången ska synas")
|
||||||
|
}
|
||||||
|
if f.Seen(pt(3, 3)) || f.Seen(pt(5, 1)) || f.Seen(pt(1, 5)) {
|
||||||
|
t.Errorf("för långt bort ska inte vara sett: (3,3)=%v (5,1)=%v (1,5)=%v", f.Seen(pt(3, 3)), f.Seen(pt(5, 1)), f.Seen(pt(1, 5)))
|
||||||
|
}
|
||||||
|
if f.Seen(pt(-1, 0)) || f.Seen(pt(99, 99)) {
|
||||||
|
t.Error("utanför kartan är aldrig sett")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLandmarksAlwaysVisible(t *testing.T) {
|
||||||
|
l, _ := level.Parse([]byte(fogSample))
|
||||||
|
f := NewFog(l, FogRadius)
|
||||||
|
if f.Count() != 0 {
|
||||||
|
t.Fatal("tom dimma från start")
|
||||||
|
}
|
||||||
|
if !f.Visible(l, pt(5, 5)) || !f.Visible(l, pt(1, 1)) {
|
||||||
|
t.Fatal("perrong och start ska alltid synas")
|
||||||
|
}
|
||||||
|
if f.Visible(l, pt(7, 7)) {
|
||||||
|
t.Fatal("vanligt golv långt bort ska vara dolt")
|
||||||
|
}
|
||||||
|
if Landmark(l, pt(7, 7)) || !Landmark(l, pt(5, 5)) {
|
||||||
|
t.Fatal("Landmark")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFogGrowsWithMovementAndResetsPerRun(t *testing.T) {
|
||||||
|
l := loadLevel(t, "001-flygplatsen")
|
||||||
|
r := NewRun(l, RunOptions{Seed: rng.FromInt(1)})
|
||||||
|
start := r.Fog.Count()
|
||||||
|
if start == 0 || !r.Fog.Seen(l.SpawnPoint()) {
|
||||||
|
t.Fatal("starten ska vara avslöjad direkt")
|
||||||
|
}
|
||||||
|
e0 := r.Fog.Explored(l)
|
||||||
|
walk(t, r, l.Path(r.Player.Tile, l.Find(level.Sign)[1], r.CanEnter))
|
||||||
|
if r.Fog.Count() <= start || r.Fog.Explored(l) <= e0 {
|
||||||
|
t.Fatalf("dimman ska lätta när man går: %d -> %d", start, r.Fog.Count())
|
||||||
|
}
|
||||||
|
if e := r.Fog.Explored(l); e <= 0 || e > 1 {
|
||||||
|
t.Fatalf("Explored=%.2f", e)
|
||||||
|
}
|
||||||
|
fresh := NewRun(l, RunOptions{Seed: rng.FromInt(1)})
|
||||||
|
if fresh.Fog.Count() != start {
|
||||||
|
t.Fatal("ny omgång = ny dimma")
|
||||||
|
}
|
||||||
|
// Gates är riktmärken men ändå inte "sedda" förrän man är där.
|
||||||
|
gate := l.Find(level.Gate)[0]
|
||||||
|
if fresh.Fog.Seen(gate) || !fresh.Fog.Visible(l, gate) {
|
||||||
|
t.Fatal("spärren syns som riktmärke utan att vara sedd")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -70,6 +70,9 @@ type Run struct {
|
|||||||
LearnedDest []string // resmålets ord som lärdes in tack vare bra resultat
|
LearnedDest []string // resmålets ord som lärdes in tack vare bra resultat
|
||||||
parSec float64
|
parSec float64
|
||||||
|
|
||||||
|
// Minikartans dimma: vad spelaren sett i den här omgången.
|
||||||
|
Fog *Fog
|
||||||
|
|
||||||
// Sällskapet (lämmeltåget) och pågående toalettbesök.
|
// Sällskapet (lämmeltåget) och pågående toalettbesök.
|
||||||
Party *Party
|
Party *Party
|
||||||
Facility FacilityState
|
Facility FacilityState
|
||||||
@@ -142,7 +145,9 @@ func NewRun(l *level.Level, o RunOptions) *Run {
|
|||||||
Party: NewParty(l.Party, l.SpawnPoint(), o.Seed.Derive("party")),
|
Party: NewParty(l.Party, l.SpawnPoint(), o.Seed.Derive("party")),
|
||||||
baseSpeed: o.Speed,
|
baseSpeed: o.Speed,
|
||||||
NoticeAt: -1,
|
NoticeAt: -1,
|
||||||
|
Fog: NewFog(l, FogRadius),
|
||||||
}
|
}
|
||||||
|
r.Fog.Reveal(l, l.SpawnPoint())
|
||||||
if r.parSec <= 0 {
|
if r.parSec <= 0 {
|
||||||
n := len(l.Path(l.SpawnPoint(), l.CorrectPlatform(), l.CanEnter))
|
n := len(l.Path(l.SpawnPoint(), l.CorrectPlatform(), l.CanEnter))
|
||||||
r.parSec = float64(max(0, n-1))*ParSecPerTile + ParSlackSec
|
r.parSec = float64(max(0, n-1))*ParSecPerTile + ParSlackSec
|
||||||
@@ -196,6 +201,7 @@ 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 {
|
||||||
r.Party.OnLeaderArrived(r.Player.Tile)
|
r.Party.OnLeaderArrived(r.Player.Tile)
|
||||||
|
r.Fog.Reveal(r.Level, r.Player.Tile)
|
||||||
}
|
}
|
||||||
logged := len(r.Party.Log)
|
logged := len(r.Party.Log)
|
||||||
r.Party.Advance(dt, r.Time)
|
r.Party.Advance(dt, r.Time)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"menu.hint": "Arrows/WASD: select Enter: start L: word list Esc: quit",
|
"menu.hint": "Arrows/WASD: select Enter: start L: word list Esc: quit",
|
||||||
"hud.time": "Time",
|
"hud.time": "Time",
|
||||||
"hud.steps": "Steps",
|
"hud.steps": "Steps",
|
||||||
"hud.controls": "Esc: menu",
|
"hud.controls": "Esc: menu M: map",
|
||||||
"level.wrong_train": "Wrong train!",
|
"level.wrong_train": "Wrong train!",
|
||||||
"result.hint": "R: replay L: word list Enter/Esc: menu",
|
"result.hint": "R: replay L: word list Enter/Esc: menu",
|
||||||
"hud.card": "Card",
|
"hud.card": "Card",
|
||||||
@@ -58,5 +58,7 @@
|
|||||||
"hud.toilet_busy": "In the toilet…",
|
"hud.toilet_busy": "In the toilet…",
|
||||||
"hud.toilet_none": "Nobody needs the toilet right now.",
|
"hud.toilet_none": "Nobody needs the toilet right now.",
|
||||||
"hud.kiosk_prompt": "Enter: buy food/drink for %d person(s), %d¥",
|
"hud.kiosk_prompt": "Enter: buy food/drink for %d person(s), %d¥",
|
||||||
"hud.kiosk_none": "Nobody is hungry or thirsty right now."
|
"hud.kiosk_none": "Nobody is hungry or thirsty right now.",
|
||||||
|
"result.explored": "Explored",
|
||||||
|
"result.served": "served"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"menu.hint": "Pilar/WASD: välj Enter: starta L: ordlista Esc: avsluta",
|
"menu.hint": "Pilar/WASD: välj Enter: starta L: ordlista Esc: avsluta",
|
||||||
"hud.time": "Tid",
|
"hud.time": "Tid",
|
||||||
"hud.steps": "Steg",
|
"hud.steps": "Steg",
|
||||||
"hud.controls": "Esc: meny",
|
"hud.controls": "Esc: meny M: karta",
|
||||||
"level.wrong_train": "Fel tåg!",
|
"level.wrong_train": "Fel tåg!",
|
||||||
"result.hint": "R: spela om L: ordlista Enter/Esc: meny",
|
"result.hint": "R: spela om L: ordlista Enter/Esc: meny",
|
||||||
"hud.card": "Kort",
|
"hud.card": "Kort",
|
||||||
@@ -58,5 +58,7 @@
|
|||||||
"hud.toilet_busy": "På toaletten…",
|
"hud.toilet_busy": "På toaletten…",
|
||||||
"hud.toilet_none": "Ingen behöver toa just nu.",
|
"hud.toilet_none": "Ingen behöver toa just nu.",
|
||||||
"hud.kiosk_prompt": "Enter: köp mat/dryck åt %d person(er), %d¥",
|
"hud.kiosk_prompt": "Enter: köp mat/dryck åt %d person(er), %d¥",
|
||||||
"hud.kiosk_none": "Ingen är hungrig eller törstig just nu."
|
"hud.kiosk_none": "Ingen är hungrig eller törstig just nu.",
|
||||||
|
"result.explored": "Utforskat",
|
||||||
|
"result.served": "försörjda"
|
||||||
}
|
}
|
||||||
|
|||||||