diff --git a/CHANGELOG.md b/CHANGELOG.md index 08a7d9e..9e992b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Oreleaset +- Bussar: flygbussterminalen (sidobana 001b) med tre bussar på väg, egen + sprite, hållplatsskylt och tuta; sidobanan 003b Harajuku; överstimulerande + UI med reklamticker på Ueno och Tokyo. - Titelskärm med premissen, språkval första gången, tutorial i fem sidor (skyltläsning med markering av resmålet), kontextuella tips i första banan, pausmeny på Esc, Avsluta i menyerna, kortare texter på bankartan. diff --git a/doc/img/bus.png b/doc/img/bus.png new file mode 100644 index 0000000..4f698fd Binary files /dev/null and b/doc/img/bus.png differ diff --git a/doc/img/cars-alight.png b/doc/img/cars-alight.png index fd958d0..36504cf 100644 Binary files a/doc/img/cars-alight.png and b/doc/img/cars-alight.png differ diff --git a/doc/img/cars.png b/doc/img/cars.png index 4ebc881..436ecc5 100644 Binary files a/doc/img/cars.png and b/doc/img/cars.png differ diff --git a/doc/img/crowd.png b/doc/img/crowd.png index 01764ed..179beeb 100644 Binary files a/doc/img/crowd.png and b/doc/img/crowd.png differ diff --git a/doc/img/items.png b/doc/img/items.png index b59784b..a219c8e 100644 Binary files a/doc/img/items.png and b/doc/img/items.png differ diff --git a/doc/img/map-signs.png b/doc/img/map-signs.png index 04aade5..42ac1be 100644 Binary files a/doc/img/map-signs.png and b/doc/img/map-signs.png differ diff --git a/doc/img/map.png b/doc/img/map.png index 2beb89b..58b0f38 100644 Binary files a/doc/img/map.png and b/doc/img/map.png differ diff --git a/doc/img/skills.png b/doc/img/skills.png index 32438e1..33ad79e 100644 Binary files a/doc/img/skills.png and b/doc/img/skills.png differ diff --git a/doc/img/words.png b/doc/img/words.png index 952d23d..f1a1468 100644 Binary files a/doc/img/words.png and b/doc/img/words.png differ diff --git a/doc/mal-och-tester.md b/doc/mal-och-tester.md index e59ab66..10fd222 100644 --- a/doc/mal-och-tester.md +++ b/doc/mal-och-tester.md @@ -142,6 +142,9 @@ att bygga på Linux som på Windows. | `internal/game` | `TestHighlightDestination` | **M13:** resmålets tecken får markeringsfärg, resten nedtonas, flera träffar, ingen träff | | `internal/game` | `TestAllSoundsDecode`, `TestMelodyForIsStableAndKnown` | **M12:** alla ljud i `AllSounds` finns, avkodas och är minst 50 ms; melodin per bana är stabil och känd; ljudlagret är nil-säkert | | `internal/save` | `TestSettingsDefaultsAndNormalize` | **M12:** standardinställningar, klampning av volymer/skala, okänt språk blir sv, saknade inställningar fylls i | +| `internal/level` | `TestBusVehicleAndBoardText` | **M10b:** `vehicle: bus` ger ett fordon och "番" på tavlan, okänt fordon ger fel, tåg är standard | +| `internal/nav` | `TestShippedBusLines` | **M10b:** flygbussen mot Shinjuku är rätt, mot Yokohama ingen trafik för Shinjuku | +| `internal/sim` | `TestBusTerminalFlow` | **M10b:** biljett till Shinjuku, hållplats 1, bussen vinner | | `internal/level` | `TestToiletSides` | **M10d:** 005 och 008b har toaletter bara innanför, 007b på båda sidor, 007 och 008 bara utanför; innanför bara på banor med spärrar utan återvändo | | `internal/sim` | `TestJourneyWithTransfer`, `TestJourneyWrongTrainOnSecondLeg`, `TestChainLevelsLoad` | **M10:** bytet fortsätter omgången på nästa station med klocka, plånbok, sällskap och par-summa; vinst först på sista delbanan; fel håll på bytesstationen förlorar; delbanor laddas enskilt | | `internal/minigame` | `TestCarPickAlightingAndChoice`, `TestCrowdLevelNarrowsZone`, `TestRideCrowdLetPass` | Folk av först (för tidigt räknas), seedad trängsel med minst en lugn vagn, val; trängsel smalnar zonen; resan: kortare tid per station, passagerare som vill förbi, blockerade räknas | @@ -169,7 +172,7 @@ att bygga på Linux som på Windows. | ~~M10c Vem betalar, spärrar utan återvändo~~ ✓ | Väljaren betalar från rätt källa och nekar den utan pengar; spärren håller kvar, personalen låser upp, ny inpassering kostar – finns, se tabellen ovan | | ~~M10d Toaletter innanför/utanför~~ ✓ | Toaletterna ligger på rätt sida av spärrarna per bana – finns, se tabellen ovan | | ~~M13 Titel, tutorial, trygghet~~ ✓ | Markeringen av resmålet i skylttext (`game.TestHighlightDestination`); titel/språkval/paus/tutorial/tips med skärmdumpar; karttexter ryms | -| M10b Bussar och vägar | Bussar följer tidtabell och vägrutor; hållplatser bedöms som perronger; överstimulerande UI-reglaget påverkar bara presentationen | +| ~~M10b Bussar och vägar~~ ✓ | Bussar följer tidtabellen, bedöms som tåg och ritas på väg; tavlan skiljer buss från tåg; busslinjerna i nätet; bussterminalen klaras – finns, se tabellen ovan; overload-tickern verifieras med skärmdump | | ~~M11 Oändligt läge & NG+~~ ✓ | Målsekvens deterministisk per seed, giltiga mål med fälla, NG+ behåller skills men nollställer items, poänglista – finns, se tabellen ovan | | ~~M12 Ljud & polish~~ ✓ | Alla ljudfiler som refereras finns och går att avkoda, stationsmelodi per bana, inställningar normaliseras – finns, se tabellen ovan; Windows-binär startar (manuellt, kvar) | diff --git a/doc/roadmap.md b/doc/roadmap.md index 51043aa..3d22674 100644 --- a/doc/roadmap.md +++ b/doc/roadmap.md @@ -563,7 +563,7 @@ på **brasse-linux01-runnern** (label `linux-amd64`), aldrig på Pi5:n. - **Kvar till M10b:** bussar och vägrutor, fler sidobanor, överstimulerande UI som eget svårighetsreglage. -### M10b – Bussar, vägar och fler sidospår +### M10b – Bussar, vägar och fler sidospår ✓ (klar 2026-10-25) - **Mål:** resten av M10-listan som inte rymdes: buss med vägrutor och hållplatser (egen fordonssprite, hållplatsskylt, tidtabell som tågen), @@ -572,6 +572,25 @@ på **brasse-linux01-runnern** (label `linux-amd64`), aldrig på Pi5:n. avgångstavlor) som kan slås på per bana. - **Teknik:** vägrutor i banformatet, `Vehicle`-abstraktion i sim som tåg och buss delar, återanvänd `nav` för busslinjer. +- **Så blev det:** ett tåg kan vara en **buss** (`vehicle: "bus"` på tåget): + samma tidtabell, faser, bedömning i nätet och ombordstigning, men ett + enda fordon (gul buss-sprite) på en **väg** (asfalt med mittlinje) i + stället för tre vagnar på räls, en **hållplatsskylt** på hållplatsen, + "1番" i stället för "1番線" på tavlan, tuta när den svänger in och + dörrsignal före avgång (ingen stationsmelodi). Nätet fick tre busslinjer + från Narita (リムジンバス mot Shinjuku och Yokohama, 空港バス mot Tokyo). + **Sidobanor:** 001b Flygbussterminalen (biljettbana med tre bussar, + parent 001) och 003b Harajuku (Yamanote mot Shinjuku eller fel varv runt + ringen, parent 003). **Överstimulerande UI** (`overload: true` på 005 + Ueno och 008 Tokyo): rullande reklamticker i blinkande färger under + HUD:en och maximalt med affischer. Falska avgångstavlor fick vänta. +- **Klar när:** fordon valideras och tavlan skiljer buss från tåg + (`level.TestBusVehicleAndBoardText`), busslinjerna bedöms rätt + (`nav.TestShippedBusLines`), bussterminalen går att klara + (`sim.TestBusTerminalFlow`), busstutan finns (`game.TestAllSoundsDecode`). ✓ +- **Kvar till senare:** falska avgångstavlor, bussar i oändligt läge (går + redan: 001b ligger i poolen), hållplatser mitt i en bana (vägrutor som + golv). ### M10c – Vem betalar, och spärrar utan återvändo ✓ (klar 2026-10-24) diff --git a/doc/spelbeskrivning.md b/doc/spelbeskrivning.md index 2b6bb2b..d786a28 100644 --- a/doc/spelbeskrivning.md +++ b/doc/spelbeskrivning.md @@ -296,7 +296,7 @@ från skills i skill tree, och från unika skills man får av sidouppdrag. | "skyltar och terminaler" | Terminalen är biljettautomaten: på nätkopplade banor visar den linjetavlan (linjer vid stationen med grannstationer åt båda hållen), läst genom ens japanska som en skylt | Egna terminalrutor kan komma senare | | "flera stopp och byten" | En resa är en kedja av banfiler (`next` → delbana med `kind: "leg"`). Samma omgång fortsätter på nästa station med klocka, plånbok och sällskap; resmålet i HUD:en visas "via" bytesstationen. Vilken bytesstation som gäller bestämmer banan – spelaren väljer tåg, inte rutt | Fri ruttplanering (välj själv var man byter) är en möjlig utbyggnad | | "slutar när man ska tillbaka till flygplatsen" | Sista huvudbanan (008) går Tokyo → Nippori → Narita flygplats med Keisei. När den klaras första gången visas hemresan: planet lyfter, resan summeras, eftertexter | Slutet går att se igen via `-ending` | -| Buss | Uppskjuten till M10b (vägrutor, hållplatser, egen sprite) | | +| Buss | Ett tåg med `vehicle: "bus"`: samma tidtabell och bedömning, ett fordon på väg med hållplatsskylt; flygbussterminalen (001b) är en biljettbana med tre bussar | Vägrutor som golv och hållplatser mitt i banan kan komma senare | | "De ikoniska melodierna" | Egna syntetiserade stationsmelodier i klockklang (fyra stycken, en per station) – riktiga JR-melodier är upphovsrättsskyddade och spelas inte in | Byt filerna i `resources/sound/` om egna inspelningar finns | | "Oändligt läge … målen är slumpade" | Målen är storyns stationer med lottade resmål (samma labyrint, samma tåg, nytt resmål avgör vilket tåg som är rätt), seedat; sällskapet växer och tidsgränsen krymper per mål; omgången slutar vid första förlust och hamnar i en egen poänglista | Slumpade labyrinter vid körning kan komma senare | | "New Game+ … behåll alla dina skills" | Skills, XP och språkkunskap behålls; items, stjärnor, pengar och debuffar nollställs; sällskapet är en person större per NG+-varv | | diff --git a/internal/game/assets.go b/internal/game/assets.go index cbe7d46..edcc3e1 100644 --- a/internal/game/assets.go +++ b/internal/game/assets.go @@ -32,6 +32,7 @@ var tileOrder = []level.Tile{ level.LaneLeft, level.LaneRight, level.Wall, level.Wall, level.Wall, // plats 13–15 = affischer (laddas separat som Posters) level.Kiosk, + level.Wall, level.Wall, level.Wall, // plats 18–20 = buss, väg, hållplatsskylt (laddas separat) } const ( @@ -39,6 +40,9 @@ const ( railsIndex = 9 posterIndex = 14 posterCount = 3 + busIndex = 18 + roadIndex = 19 + stopIndex = 20 ) // Assets är allt som laddats från resources/. @@ -50,6 +54,9 @@ type Assets struct { Rails *ebiten.Image // rälsruta (horisontell), roteras för vertikala spår Posters []*ebiten.Image // affischer för visuell röra (ritas över väggrutor) Plane *ebiten.Image // flygplanet i slutscenen + Bus *ebiten.Image // bussen (ett fordon, inte vagnar) + Road *ebiten.Image // vägruta (horisontell), roteras för vertikala + StopSign *ebiten.Image // hållplatsskylt ovanpå busshållplatsen Icon image.Image // fönsterikonen (nil om den saknas) Player [4][3]*ebiten.Image // [riktning: ner, upp, vänster, höger][bildruta: stilla, gå1, gå2] Text map[string]string @@ -78,7 +85,7 @@ func LoadAssets() (*Assets, error) { return nil, fmt.Errorf("tiles.png är %d px bred, behöver minst %d", w, need) } for i, t := range tileOrder { - if i == trainIndex || i == railsIndex || (i >= posterIndex && i < posterIndex+posterCount) { + if i == trainIndex || i == railsIndex || (i >= posterIndex && i < posterIndex+posterCount) || i >= busIndex { continue } a.Tiles[t] = sub(i) @@ -88,6 +95,7 @@ func LoadAssets() (*Assets, error) { } a.Train = sub(trainIndex) a.Rails = sub(railsIndex) + a.Bus, a.Road, a.StopSign = sub(busIndex), sub(roadIndex), sub(stopIndex) for i := 0; i < posterCount; i++ { a.Posters = append(a.Posters, sub(posterIndex+i)) } diff --git a/internal/game/audio.go b/internal/game/audio.go index 70eb38f..de84728 100644 --- a/internal/game/audio.go +++ b/internal/game/audio.go @@ -48,6 +48,7 @@ const ( SoundLose = "lose" SoundStaff = "staff" SoundPlane = "plane" + SoundBusHorn = "bus-horn" ) // StationMelodies är avgångsmelodierna; varje station har sin (level.Melody @@ -57,7 +58,7 @@ var StationMelodies = []string{"melody-a", "melody-b", "melody-c", "melody-d"} // AllSounds är alla klipp som måste finnas i resources/sound. func AllSounds() []string { return append([]string{SoundApproach, SoundAnnounce, SoundDoors, SoundChatter, SoundMusicMap, SoundGateBeep, SoundGateDeny, - SoundKiosk, SoundToilet, SoundLearn, SoundConfirm, SoundCancel, SoundTransfer, SoundWin, SoundLose, SoundStaff, SoundPlane}, + SoundKiosk, SoundToilet, SoundLearn, SoundConfirm, SoundCancel, SoundTransfer, SoundWin, SoundLose, SoundStaff, SoundPlane, SoundBusHorn}, StationMelodies...) } diff --git a/internal/game/people.go b/internal/game/people.go index 4b2caef..664d624 100644 --- a/internal/game/people.go +++ b/internal/game/people.go @@ -36,11 +36,11 @@ func (s *LevelScene) drawNPCs(g *Game, screen *ebiten.Image, ox, oy float64) { func (s *LevelScene) buildPosters() { s.posters = map[level.Point]int{} l := s.run.Level - if l.Clutter == 0 { + if s.clutterLevel() == 0 { return } r := s.run.Seed.Derive("posters").New() - chance := 0.12 * float64(l.Clutter) + chance := 0.12 * float64(s.clutterLevel()) for z := 0; z < l.NumFloors(); z++ { for y := 0; y < l.Height(); y++ { for x := 0; x < l.Width(); x++ { diff --git a/internal/game/scene_level.go b/internal/game/scene_level.go index 73c44db..465f528 100644 --- a/internal/game/scene_level.go +++ b/internal/game/scene_level.go @@ -58,6 +58,7 @@ type LevelScene struct { // Tåg och effekter (M7b). tracks []trackInfo rails map[level.Point]bool + roads map[level.Point]bool // vägrutor för bussar (ritas med vägsprite) lastPhase map[level.Tile]level.Phase particles []particle rng *rand.Rand // bara för partiklar (ingen spellogik) @@ -299,7 +300,7 @@ func (s *LevelScene) Draw(g *Game, screen *ebiten.Image) { for y := y0; y < y1; y++ { for x := x0; x < x1; x++ { p := level.Point{X: x, Y: y, Z: z} - if s.rails[p] { + if s.rails[p] || s.roads[p] { s.drawRails(g, screen, p, ox, oy) continue } @@ -358,6 +359,7 @@ func (s *LevelScene) Draw(g *Game, screen *ebiten.Image) { s.drawIndicator(screen, ox, oy) s.drawHUD(g, screen) + s.drawTicker(g, screen) s.drawTip(g, screen) if r := s.run; r.TransferAt >= 0 && r.Time-r.TransferAt < transferBannerSec && r.Mini == nil && r.Outcome == sim.Ongoing { txt := g.Assets.T("hud.transfer_done") diff --git a/internal/game/scene_map.go b/internal/game/scene_map.go index 4278426..b89ece8 100644 --- a/internal/game/scene_map.go +++ b/internal/game/scene_map.go @@ -69,7 +69,7 @@ func (m *MapScene) nodes() ([]mapNode, map[string][]mapNode) { } mains = append(mains, mapNode{l, x, y}) for k, s := range m.sides[l.ID] { - sides[l.ID] = append(sides[l.ID], mapNode{s, x + 12 + k*18, y - 40}) + sides[l.ID] = append(sides[l.ID], mapNode{s, x + 12 + k*18, y - 28}) } } return mains, sides diff --git a/internal/game/ticker.go b/internal/game/ticker.go new file mode 100644 index 0000000..e8f14a0 --- /dev/null +++ b/internal/game/ticker.go @@ -0,0 +1,53 @@ +package game + +import ( + "image/color" + "math" + "strings" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/vector" +) + +// Överstimulerande UI (level.Overload): en rullande reklamticker i +// blinkande färger under HUD:en, och fler affischer på väggarna. Texten är +// reklamfraser som inte betyder något för spelet – det är poängen. +var adPhrases = []string{"大セール 半額!", "新発売 ラーメン", "免税 お土産", "駅弁 人気No.1", "本日限り 特価", "ポイント2倍", "新宿 ショッピング", "温泉 旅行 キャンペーン"} + +const tickerSpeed = 30.0 // px/s + +// drawTicker ritar reklamtickern (bara med Overload och när inget annat +// ligger där). +func (s *LevelScene) drawTicker(g *Game, screen *ebiten.Image) { + r := s.run + if !r.Level.Overload || r.Mini != nil || r.Outcome != 0 || s.tips.current != "" { + return + } + a := g.Assets + text := strings.Join(adPhrases, " ★ ") + " ★ " + w := a.Width(text) + if w == 0 { + return + } + y := 26 + blink := math.Mod(r.Time, 1.0) < 0.5 + bg, fg := color.RGBA{120, 30, 60, 230}, color.RGBA{255, 230, 120, 255} + if blink { + bg, fg = color.RGBA{40, 60, 140, 230}, color.RGBA{255, 255, 255, 255} + } + vector.DrawFilledRect(screen, 0, float32(y), ScreenW, LineH+2, bg, false) + x := ScreenW - int(math.Mod(r.Time*tickerSpeed, float64(w+ScreenW))) + for _, sx := range []int{x, x + w} { + if sx < ScreenW && sx+w > 0 { + a.Draw(screen, text, sx, y+1, fg) + } + } +} + +// clutterLevel är den visuella röran: banans clutter, maximal med Overload. +func (s *LevelScene) clutterLevel() int { + if s.run.Level.Overload { + return 3 + } + return s.run.Level.Clutter +} diff --git a/internal/game/trains.go b/internal/game/trains.go index 44ea0f7..d47324e 100644 --- a/internal/game/trains.go +++ b/internal/game/trains.go @@ -31,15 +31,21 @@ type trackInfo struct { platform level.Point // perrongrutan track level.Point // rälsrutan bortom kanten horizontal bool // rälsens riktning + bus bool // buss: väg och ett fordon i stället för räls och vagnar } func (s *LevelScene) buildTracks() { s.tracks = nil s.rails = map[level.Point]bool{} + s.roads = map[level.Point]bool{} for _, t := range level.PlatformTiles { for _, p := range s.run.Level.Find(t) { tt, horizontal := s.run.Level.TrackTile(p) - s.tracks = append(s.tracks, trackInfo{tile: t, platform: p, track: tt, horizontal: horizontal}) + bus := false + if tr := s.run.Level.TrainAt(t); tr != nil && tr.IsBus() { + bus = true + } + s.tracks = append(s.tracks, trackInfo{tile: t, platform: p, track: tt, horizontal: horizontal, bus: bus}) out := level.Point{X: tt.X - p.X, Y: tt.Y - p.Y} // riktningen bort från perrongen for k := -railsReach; k <= railsReach; k++ { c := tt @@ -51,7 +57,11 @@ func (s *LevelScene) buildTracks() { beyond := level.Point{X: c.X + out.X, Y: c.Y + out.Y, Z: c.Z} // Räls på väggrutor längs spåret, så länge det är vägg (eller kartkant) bortom dem. if s.run.Level.At(c) == level.Wall && s.run.Level.At(beyond) == level.Wall { - s.rails[c] = true + if bus { + s.roads[c] = true + } else { + s.rails[c] = true + } } } } @@ -87,7 +97,11 @@ func (s *LevelScene) drawRails(g *Game, screen *ebiten.Image, c level.Point, ox, op.GeoM.Translate(TileSize/2, TileSize/2) } op.GeoM.Translate(ox+float64(c.X*TileSize), oy+float64(c.Y*TileSize)) - screen.DrawImage(g.Assets.Rails, &op) + img := g.Assets.Rails + if s.roads[c] { + img = g.Assets.Road + } + screen.DrawImage(img, &op) } // drawTrains ritar tågen i rätt läge för sin fas. @@ -98,6 +112,11 @@ func (s *LevelScene) drawTrains(g *Game, screen *ebiten.Image, ox, oy float64) { continue // tågen på andra våningar syns inte härifrån } phase, prog := s.run.PlatformPhase(tr.tile) + if tr.bus { // hållplatsskylten står på hållplatsen + var op ebiten.DrawImageOptions + op.GeoM.Translate(ox+float64(tr.platform.X*TileSize), oy+float64(tr.platform.Y*TileSize)) + screen.DrawImage(g.Assets.StopSign, &op) + } // Strålkastare när tåget är på väg in: ett sken från det håll det kommer. if phase == level.Approaching { s.drawHeadlight(screen, tr, prog, ox, oy) @@ -106,8 +125,12 @@ func (s *LevelScene) drawTrains(g *Game, screen *ebiten.Image, ox, oy float64) { if !show { continue } - for k := 0; k < trainCars; k++ { - d := off + float64(k-trainCars/2) + cars, sprite := trainCars, g.Assets.Train + if tr.bus { + cars, sprite = 1, g.Assets.Bus + } + for k := 0; k < cars; k++ { + d := off + float64(k-cars/2) var op ebiten.DrawImageOptions x, y := float64(tr.track.X*TileSize), float64(tr.track.Y*TileSize) if tr.horizontal { @@ -119,7 +142,7 @@ func (s *LevelScene) drawTrains(g *Game, screen *ebiten.Image, ox, oy float64) { op.GeoM.Translate(TileSize/2, TileSize/2) } op.GeoM.Translate(ox+math.Round(x), oy+math.Round(y)) - screen.DrawImage(g.Assets.Train, &op) + screen.DrawImage(sprite, &op) } } } @@ -162,14 +185,19 @@ func (s *LevelScene) updateEffects(g *Game) { s.lastPhase[tr.tile] = phase dist := math.Hypot(float64(tr.platform.X*TileSize)-px, float64(tr.platform.Y*TileSize)-py) / TileSize mask := noiseMask(r.Level.Noise) - switch phase { - case level.Approaching: // ankomstjingeln: tåget är på väg + switch { + case tr.bus && phase == level.Arriving: // bussen tutar när den svänger in + g.Audio.Play(SoundBusHorn, volumeAt(dist)*mask) + case tr.bus && phase == level.Closing: + g.Audio.Play(SoundDoors, volumeAt(dist)*mask) + case tr.bus: + case phase == level.Approaching: // ankomstjingeln: tåget är på väg g.Audio.Play(SoundApproach, volumeAt(dist)*mask) - case level.Arriving: // ding-dong när det rullar in + case phase == level.Arriving: // ding-dong när det rullar in g.Audio.Play(SoundAnnounce, volumeAt(dist)*mask) - case level.Closing: // stationens avgångsmelodi innan dörrarna stängs + case phase == level.Closing: // stationens avgångsmelodi innan dörrarna stängs g.Audio.Play(MelodyFor(r.Level), volumeAt(dist)*mask) - case level.Leaving: // dörrsignalen när det går + case phase == level.Leaving: // dörrsignalen när det går g.Audio.Play(SoundDoors, volumeAt(dist)*mask) } } diff --git a/internal/level/level.go b/internal/level/level.go index 8e46936..912e59a 100644 --- a/internal/level/level.go +++ b/internal/level/level.go @@ -194,6 +194,7 @@ type Train struct { Correct bool `json:"correct"` // tar spelaren till rätt resmål (räknas ut från nätet när banan har station/to) LineID string `json:"lineId"` // linjen i linjenätet (resources/data/network.json) Terminus string `json:"terminus"` // en station i färdriktningen (nätets id) – avgör riktningen + Vehicle string `json:"vehicle"` // "" = tåg, "bus" = buss (väg i stället för räls, hållplats i stället för perrong) Crowded bool `json:"crowded"` // fullt tåg: minispel för att få plats (gäller alla vagnar) Cars int `json:"cars"` // antal vagnar (0 = DefaultCars) WomenOnlyCar int `json:"womenOnlyCar"` // 1-baserat vagnnummer för kvinnovagn, 0 = ingen @@ -269,6 +270,7 @@ type Level struct { SeatSelection bool `json:"seatSelection"` // biljettköpet fortsätter med sittplatsval efter sällskapets önskemål OneWayGates bool `json:"oneWayGates"` // spärren släpper inte ut en igen förrän man åkt; personal vid spärren låser upp (tar tid) Melody string `json:"melody"` // stationens avgångsmelodi: "a".."d" (tomt = vald efter id) + Overload bool `json:"overload"` // överstimulerande UI: reklamticker, blinkande reklam, extra affischer FareYen int `json:"fareYen"` // vad spärren drar från kortet (0 = standard) Grid []string `json:"grid,omitempty"` // envåningsbana (kompatibilitet) – blir Floors[0] Floors []Storey `json:"floors,omitempty"` // flervåningsbana; alla våningar lika stora @@ -466,8 +468,14 @@ func (l *Level) Validate() error { if tr.Correct { correct++ } + if tr.Vehicle != "" && tr.Vehicle != "bus" { + return fmt.Errorf("level %s: tåg %d har okänt fordon %q (tomt eller bus)", l.ID, i, tr.Vehicle) + } if tr.Cars == 0 { tr.Cars = DefaultCars + if tr.IsBus() { + tr.Cars = 1 + } } if tr.Cars < 1 || tr.Cars > MaxCars { return fmt.Errorf("level %s: tåg %d har %d vagnar (1–%d)", l.ID, i, tr.Cars, MaxCars) @@ -720,11 +728,18 @@ func (l *Level) Unreachable() []Point { return out } -// BoardText är avgångstavlans text på japanska, t.ex. "2番線 成田エクスプレス 新宿方面". +// BoardText är avgångstavlans text på japanska, t.ex. "2番線 成田エクスプレス 新宿方面" +// (för bussar "1番 リムジンバス 新宿方面": hållplatsnummer i stället för spår). func (tr *Train) BoardText() string { + if tr.IsBus() { + return tr.Track + "番 " + tr.Line.Get("ja") + " " + tr.Destination.Get("ja") + "方面" + } return tr.Track + "番線 " + tr.Line.Get("ja") + " " + tr.Destination.Get("ja") + "方面" } +// IsBus rapporterar om fordonet är en buss. +func (tr *Train) IsBus() bool { return tr.Vehicle == "bus" } + // TrackTile är rutan "bortom perrongkanten" där rälsen går: första väggruta // nedanför, till höger, till vänster eller ovanför perrongen. horizontal // anger om rälsen (och tågets rörelse) går i sidled. diff --git a/internal/level/level_test.go b/internal/level/level_test.go index aa1db37..bc3b132 100644 --- a/internal/level/level_test.go +++ b/internal/level/level_test.go @@ -838,3 +838,24 @@ func TestToiletSides(t *testing.T) { } } } + +func TestBusVehicleAndBoardText(t *testing.T) { + src := `{"id":"bus","name":{"sv":"x"},"difficulty":1,"destination":{"ja":"新宿"}, + "grid":["#######","#S.GP.#","#######"], + "trains":[{"platform":"P","track":"1","line":{"ja":"リムジンバス"},"destination":{"ja":"新宿"},"correct":true,"vehicle":"bus"}]}` + l, err := Parse([]byte(src)) + if err != nil { + t.Fatal(err) + } + tr := &l.Trains[0] + if !tr.IsBus() || tr.Cars != 1 || tr.BoardText() != "1番 リムジンバス 新宿方面" { + t.Fatalf("buss: cars=%d board=%q", tr.Cars, tr.BoardText()) + } + if _, err := Parse([]byte(strings.Replace(src, `"vehicle":"bus"`, `"vehicle":"tram"`, 1))); err == nil { + t.Fatal("okänt fordon ska ge fel") + } + train, _ := Parse([]byte(strings.Replace(src, `,"vehicle":"bus"`, ``, 1))) + if train.Trains[0].IsBus() || train.Trains[0].Cars != DefaultCars || train.Trains[0].BoardText() != "1番線 リムジンバス 新宿方面" { + t.Fatal("utan vehicle är det ett tåg") + } +} diff --git a/internal/nav/nav_test.go b/internal/nav/nav_test.go index 4ac9152..dd2cb1c 100644 --- a/internal/nav/nav_test.go +++ b/internal/nav/nav_test.go @@ -169,3 +169,20 @@ func TestShippedNetwork(t *testing.T) { t.Errorf("Marunouchi via city = %v", v) } } + +func TestShippedBusLines(t *testing.T) { + dir, err := res.Path("data") + if err != nil { + t.Fatal(err) + } + n, err := LoadFile(filepath.Join(dir, "network.json")) + if err != nil { + t.Fatal(err) + } + if v := n.Judge("limousine-shinjuku", "narita-airport", "shinjuku", "shinjuku"); v != Direct { + t.Errorf("flygbussen mot Shinjuku = %v", v) + } + if v := n.Judge("limousine-yokohama", "narita-airport", "yokohama", "shinjuku"); v != NoService { + t.Errorf("flygbussen mot Yokohama för den som ska till Shinjuku = %v", v) + } +} diff --git a/internal/sim/nav_test.go b/internal/sim/nav_test.go index b8b1001..af433d2 100644 --- a/internal/sim/nav_test.go +++ b/internal/sim/nav_test.go @@ -126,3 +126,36 @@ func TestTerminalLines(t *testing.T) { t.Fatal("okopplad bana har ingen terminal") } } + +// TestBusTerminalFlow: flygbussterminalen är en biljettbana med bussar – +// köp biljett till Shinjuku, gå till hållplats 1 och åk. +func TestBusTerminalFlow(t *testing.T) { + l := loadLevel(t, "001b-flygbussen") + if !l.TicketRequired || !l.Trains[0].IsBus() || l.Trains[0].Cars != 1 { + t.Fatal("001b ska vara en biljettbana med bussar") + } + r := NewRun(l, RunOptions{Dict: loadDict(t), Seed: rng.FromInt(3)}) + r.Party.nextEventAt = 1e9 + r.NPCs = nil + walk(t, r, pathTo(r, l.Find(level.TicketMachine)[0])) + r.Step(frame, None, true) + tk, ok := r.Mini.(*minigame.Ticket) + if !ok { + t.Fatalf("ingen biljettautomat (kind=%v)", r.MiniKind) + } + for tk.Options[tk.Cursor].Dest != "新宿" { + r.StepMini(frame, minigame.Input{Move: 1}) + } + r.StepMini(frame, minigame.Input{Confirm: true}) + if r.Wallet.Ticket != "新宿" { + t.Fatalf("biljett=%q", r.Wallet.Ticket) + } + tr := boardAt(t, r, level.Platform) + if !tr.IsBus() || tr.Verdict != nav.Direct { + t.Fatalf("hållplats 1: %v %v", tr.IsBus(), tr.Verdict) + } + ride(t, r) + if r.Outcome != Won { + t.Fatalf("outcome=%v (%s)", r.Outcome, r.LostReason) + } +} diff --git a/resources/data/levels/001b-flygbussen.json b/resources/data/levels/001b-flygbussen.json new file mode 100644 index 0000000..f28152d --- /dev/null +++ b/resources/data/levels/001b-flygbussen.json @@ -0,0 +1,154 @@ +{ + "destination": { + "en": "Shinjuku", + "ja": "新宿", + "romaji": "Shinjuku", + "sv": "Shinjuku" + }, + "difficulty": 1, + "fareYen": 0, + "grid": [ + "#################################", + "#S#.....#...#..i..#.....#.....#K#", + "#.#.###.#.#.#.#.#.#.###.#.#.#.#.#", + "#.#i..#.#.#...#.#...#...#.#.#...#", + "#.#.#.#.#.#.###.#####.###.#.###.#", + "#.#T#.#...#.....#.#...#.#.#.#.#.#", + "#.###.###########.#.#.#.#.#.#.#.#", + "#...#...#.........#.#.....#.#.#.#", + "###.###.#.#########.#.#####.#.#.#", + "#...#...#.#...#.....#.....#.#...#", + "#.###.###.#.#.#.#########.#.#.###", + "#.#...#.....#...#.....#...#.#...#", + "#.#.###.#####.#.#.#.#.#####.#.#.#", + "#..i#...#...#.#...#.#.#.....#.#.#", + "###.#.###.###.#####.#.#.#####.#.#", + "#...#...#...#.#.......#.#.....#.#", + "#.#####.#.#.#.#.#######.#.#####.#", + "#W#.....#.#.#.#...#...#.#.....#.#", + "###.#####.#.#.###G#.#.#.#####.#G#", + "#pG.........#...#Q#.#...#.....#P#", + "#################################" + ], + "id": "001b-flygbussen", + "kind": "side", + "name": { + "en": "Airport bus terminal", + "ja": "空港バスのりば", + "sv": "Flygbussterminalen" + }, + "order": 1, + "parent": "001-flygplatsen", + "party": 2, + "rewards": { + "skillPoints": 1 + }, + "rideStops": 2, + "signs": [ + { + "text": { + "en": "Bus terminal. Tickets at the machine. Stop 1 limousine bus for Shinjuku: south-east. Stop 2 limousine bus for Yokohama: south-west. Stop 3 airport bus for Tokyo: south.", + "ja": "バスのりば 切符 → 券売機 1番 リムジンバス 新宿方面 → 南東 2番 リムジンバス 横浜方面 → 南西 3番 空港バス 東京方面 → 南", + "sv": "Bussterminalen. Biljetter köps i automaten. Hållplats 1 flygbuss mot Shinjuku: sydost. Hållplats 2 flygbuss mot Yokohama: sydväst. Hållplats 3 flygplatsbuss mot Tokyo: söderut." + }, + "x": 3, + "y": 13, + "z": 0 + }, + { + "text": { + "en": "\u003c- Stop 2 limousine bus for Yokohama Stop 1 limousine bus for Shinjuku -\u003e", + "ja": "← 2番 リムジンバス 横浜方面 1番 リムジンバス 新宿方面 →", + "sv": "\u003c- Hållplats 2 flygbuss mot Yokohama Hållplats 1 flygbuss mot Shinjuku -\u003e" + }, + "x": 3, + "y": 3, + "z": 0 + }, + { + "text": { + "en": "Stop 1 limousine bus for Shinjuku: the gate in the south-east corner. Ticket required.", + "ja": "1番 リムジンバス 新宿方面 → 南東口 改札 切符", + "sv": "Hållplats 1 flygbuss mot Shinjuku: spärren i sydöstra hörnet. Biljett krävs." + }, + "x": 15, + "y": 1, + "z": 0 + } + ], + "station": "narita-airport", + "ticketRequired": true, + "timeLimitSec": 300, + "to": "shinjuku", + "trains": [ + { + "cars": 1, + "destination": { + "en": "Shinjuku", + "ja": "新宿", + "romaji": "Shinjuku" + }, + "line": { + "en": "Airport Limousine", + "ja": "リムジンバス", + "romaji": "Limousine Bus" + }, + "lineId": "limousine-shinjuku", + "platform": "P", + "schedule": { + "dwellSec": 9, + "intervalSec": 32, + "offsetSec": 10 + }, + "terminus": "shinjuku", + "track": "1", + "vehicle": "bus" + }, + { + "cars": 1, + "destination": { + "en": "Yokohama", + "ja": "横浜", + "romaji": "Yokohama" + }, + "line": { + "en": "Airport Limousine", + "ja": "リムジンバス", + "romaji": "Limousine Bus" + }, + "lineId": "limousine-yokohama", + "platform": "p", + "schedule": { + "dwellSec": 8, + "intervalSec": 32, + "offsetSec": 20 + }, + "terminus": "yokohama", + "track": "2", + "vehicle": "bus" + }, + { + "cars": 1, + "destination": { + "en": "Tokyo", + "ja": "東京", + "romaji": "Tokyo" + }, + "line": { + "en": "Airport Bus", + "ja": "空港バス", + "romaji": "Kuko basu" + }, + "lineId": "airport-bus-tokyo", + "platform": "Q", + "schedule": { + "dwellSec": 8, + "intervalSec": 28, + "offsetSec": 4 + }, + "terminus": "tokyo", + "track": "3", + "vehicle": "bus" + } + ] +} \ No newline at end of file diff --git a/resources/data/levels/003b-harajuku.json b/resources/data/levels/003b-harajuku.json new file mode 100644 index 0000000..f326262 --- /dev/null +++ b/resources/data/levels/003b-harajuku.json @@ -0,0 +1,131 @@ +{ + "crowd": 1, + "destination": { + "en": "Shinjuku", + "ja": "新宿", + "romaji": "Shinjuku", + "sv": "Shinjuku" + }, + "difficulty": 2, + "fareYen": 160, + "grid": [ + "#################################", + "#S..#W..............#.....#...#.#", + "#.#.###.#########.#.#.###.#.#.#.#", + "#.#.....#.........#.#.#K#...#...#", + "#.#######.#.###.###.#.#.#######.#", + "#...#.....#.#...#...#.#...#...#.#", + "#.###.#####.#.###.###.#.###.#.#.#", + "#...#.#.....#T#...#...#.#...#.#.#", + "#.#.#.#.#######.###.###.#.###.#.#", + "#...#.#...#.....#...#.....#...#.#", + "###.#.###.#.#######.#.#####.###.#", + "#..i#...#...#.......#.....#.#...#", + "#.#.#.#.#####.#.#####.###.#.#.###", + "#.#.#...#...#.#.#...#...#.#.#...#", + "#.#.###.#.#.###.#.#.#.#.#.#.###.#", + "#.#...#.#.#...#.#.#...#.#...#..i#", + "#.#####.#.###.#.#.#####.#.###.#.#", + "#.....#...#.#...#.#...#.#.#...#.#", + "#G#.#.#####.#####.#.#.#.#.#.#####", + "#p#.#......i........#.........GP#", + "#################################" + ], + "id": "003b-harajuku", + "kind": "side", + "name": { + "en": "Harajuku", + "ja": "原宿駅", + "sv": "Harajuku" + }, + "order": 3, + "parent": "003-shibuya", + "party": 2, + "rewards": { + "items": [ + "mask_tengu" + ] + }, + "rideStops": 3, + "signs": [ + { + "text": { + "en": "Harajuku Station. Track 1 Yamanote Line for Shinjuku: south-east. Track 2 Yamanote Line for Shibuya: south-west. Same line - the wrong way round the loop takes half an hour.", + "ja": "原宿駅 1番線 山手線 新宿方面 → 南東 2番線 山手線 渋谷方面 → 南西", + "sv": "Harajuku station. Spår 1 Yamanote-linjen mot Shinjuku: sydost. Spår 2 Yamanote-linjen mot Shibuya: sydväst. Samma linje - fel varv runt ringen tar en halvtimme." + }, + "x": 3, + "y": 11, + "z": 0 + }, + { + "text": { + "en": "\u003c- Track 2 Yamanote Line for Shibuya Track 1 Yamanote Line for Shinjuku -\u003e", + "ja": "← 2番線 山手線 渋谷方面 1番線 山手線 新宿方面 →", + "sv": "\u003c- Spår 2 Yamanote-linjen mot Shibuya Spår 1 Yamanote-linjen mot Shinjuku -\u003e" + }, + "x": 11, + "y": 19, + "z": 0 + }, + { + "text": { + "en": "Track 1 Yamanote Line for Shinjuku: the gate in the south-east corner.", + "ja": "1番線 山手線 新宿方面 → 南東口 改札", + "sv": "Spår 1 Yamanote-linjen mot Shinjuku: spärren i sydöstra hörnet." + }, + "x": 31, + "y": 15, + "z": 0 + } + ], + "station": "harajuku", + "timeLimitSec": 240, + "to": "shinjuku", + "trains": [ + { + "cars": 4, + "destination": { + "en": "Shinjuku", + "ja": "新宿", + "romaji": "Shinjuku" + }, + "line": { + "en": "Yamanote Line", + "ja": "山手線", + "romaji": "Yamanote-sen" + }, + "lineId": "yamanote", + "platform": "P", + "schedule": { + "dwellSec": 8, + "intervalSec": 26, + "offsetSec": 8 + }, + "terminus": "shinjuku", + "track": "1" + }, + { + "cars": 4, + "destination": { + "en": "Shibuya", + "ja": "渋谷", + "romaji": "Shibuya" + }, + "line": { + "en": "Yamanote Line", + "ja": "山手線", + "romaji": "Yamanote-sen" + }, + "lineId": "yamanote", + "platform": "p", + "schedule": { + "dwellSec": 8, + "intervalSec": 26, + "offsetSec": 18 + }, + "terminus": "shibuya", + "track": "2" + } + ] +} \ No newline at end of file diff --git a/resources/data/levels/005-ueno.json b/resources/data/levels/005-ueno.json index acc279e..cb2b7ab 100644 --- a/resources/data/levels/005-ueno.json +++ b/resources/data/levels/005-ueno.json @@ -11,23 +11,23 @@ "fareYen": 200, "grid": [ "#################################", - "#S.\u003c..\u003c..#..\u003c..\u003c..\u003cK.\u003c..#..\u003c..###", - "#..\u003e..\u003e..#..\u003e..\u003e..\u003e..\u003e..#.i\u003e..###", - "#######\u003c\u003e#\u003c\u003e#\u003c\u003e####\u003c\u003e#\u003c\u003e####\u003c\u003e###", - "#..\u003c..\u003cT.#..\u003c..\u003c..\u003c..#..\u003c..\u003c..###", - "#.i\u003e..\u003e..#..\u003e..\u003e..\u003e..#..\u003e..\u003e..###", - "#\u003c\u003e#############\u003c\u003e#######\u003c\u003e#\u003c\u003e###", - "#..#..\u003c..\u003c..\u003c..\u003c..\u003c..\u003c..#..\u003c..###", - "#..#..\u003e..\u003e..\u003e..\u003e..\u003e..\u003e..#..\u003e..###", - "#\u003c\u003e####\u003c\u003e##########\u003c\u003e#\u003c\u003e#\u003c\u003e######", - "#..\u003c..\u003c..\u003c..#..\u003c..\u003c..#..#..\u003c..###", - "#..\u003e..\u003e..\u003e..#..\u003e..\u003e..#..#..\u003e..###", - "####\u003c\u003e#\u003c\u003e#\u003c\u003e#\u003c\u003e####\u003c\u003e#######\u003c\u003e###", - "#..\u003c..#..#..#..\u003c..#..\u003c..\u003c..\u003c..###", - "#G.\u003e..#..#..#..\u003e..#..\u003e..\u003e..\u003e..###", - "#\u003cG#######\u003c\u003e####\u003c\u003e###############", - "#..#..\u003c..\u003c.i#..\u003c..\u003c..\u003c..\u003c..G..W##", - "#p.#..\u003e..\u003e..#..\u003e..\u003e..\u003e..\u003e.G\u003e.P###", + "#S.<..<..#..<..<....>..#..>..>..>..>..#.i>..###", + "#######<>#<>#<>####<>#<>####<>###", + "#..<....>..#..>..>..>..#..>..>..###", + "#<>#############<>#######<>#<>###", + "#..#..<..<..<..<..<..<..#..<..###", + "#..#..>..>..>..>..>..>..#..>..###", + "#<>####<>##########<>#<>#<>######", + "#..<..<..<..#..<..<..#..#..<..###", + "#..>..>..>..#..>..>..#..#..>..###", + "####<>#<>#<>#<>####<>#######<>###", + "#..<..#..#..#..<..#..<..<..<..###", + "#G.>..#..#..#..>..#..>..>..>..###", + "#####<>###############", + "#..#..<..<.i#..<..<..<..<..G..W##", + "#p.#..>..>..#..>..>..>..>.G>.P###", "#################################", "#################################", "#################################" @@ -65,9 +65,9 @@ }, { "text": { - "en": "\u003c- Track 9 Keihin-Tohoku Line (Omiya) Track 3 Yamanote Line (Akihabara) -\u003e", + "en": "<- Track 9 Keihin-Tohoku Line (Omiya) Track 3 Yamanote Line (Akihabara) ->", "ja": "← 9番線 京浜東北線 大宮方面 3番線 山手線 秋葉原方面 →", - "sv": "\u003c- Spår 9 Keihin-Tohoku-linjen (Omiya) Spår 3 Yamanote-linjen (Akihabara) -\u003e" + "sv": "<- Spår 9 Keihin-Tohoku-linjen (Omiya) Spår 3 Yamanote-linjen (Akihabara) ->" }, "x": 11, "y": 16, @@ -135,5 +135,6 @@ "terminus": "omiya", "track": "9" } - ] -} \ No newline at end of file + ], + "overload": true +} diff --git a/resources/data/levels/008-tokyo-hemresa.json b/resources/data/levels/008-tokyo-hemresa.json index aee5a07..e9147e3 100644 --- a/resources/data/levels/008-tokyo-hemresa.json +++ b/resources/data/levels/008-tokyo-hemresa.json @@ -100,9 +100,9 @@ }, { "text": { - "en": "\u003c- Track 5 Yamanote Line for Shinagawa Track 4 Yamanote Line for Ueno -\u003e", + "en": "<- Track 5 Yamanote Line for Shinagawa Track 4 Yamanote Line for Ueno ->", "ja": "← 5番線 山手線 品川方面 4番線 山手線 上野方面 →", - "sv": "\u003c- Spår 5 Yamanote-linjen mot Shinagawa Spår 4 Yamanote-linjen mot Ueno -\u003e" + "sv": "<- Spår 5 Yamanote-linjen mot Shinagawa Spår 4 Yamanote-linjen mot Ueno ->" }, "x": 7, "y": 19, @@ -211,5 +211,6 @@ "terminus": "takao", "track": "1" } - ] -} \ No newline at end of file + ], + "overload": true +} diff --git a/resources/data/network.json b/resources/data/network.json index c6f0f53..efd5034 100644 --- a/resources/data/network.json +++ b/resources/data/network.json @@ -1004,6 +1004,47 @@ "yokohama", "ofuna" ] + }, + { + "id": "limousine-shinjuku", + "name": { + "ja": "リムジンバス", + "romaji": "Limousine Bus", + "en": "Airport Limousine (Shinjuku)" + }, + "stations": [ + "narita-airport", + "airport-terminal-2", + "shinjuku" + ], + "vehicle": "bus" + }, + { + "id": "limousine-yokohama", + "name": { + "ja": "リムジンバス", + "romaji": "Limousine Bus", + "en": "Airport Limousine (Yokohama)" + }, + "stations": [ + "narita-airport", + "airport-terminal-2", + "yokohama" + ], + "vehicle": "bus" + }, + { + "id": "airport-bus-tokyo", + "name": { + "ja": "空港バス", + "romaji": "Kuko basu", + "en": "Airport Bus (Tokyo)" + }, + "stations": [ + "narita-airport", + "tokyo" + ], + "vehicle": "bus" } ] } \ No newline at end of file diff --git a/resources/sound/bus-horn.wav b/resources/sound/bus-horn.wav new file mode 100644 index 0000000..0163db6 Binary files /dev/null and b/resources/sound/bus-horn.wav differ diff --git a/resources/sprites/tiles.png b/resources/sprites/tiles.png index f8b0c24..fe9868e 100644 Binary files a/resources/sprites/tiles.png and b/resources/sprites/tiles.png differ diff --git a/resources/text/jp-dictionary.json b/resources/text/jp-dictionary.json index 8897e38..8cbd3c6 100644 --- a/resources/text/jp-dictionary.json +++ b/resources/text/jp-dictionary.json @@ -51,7 +51,7 @@ }, "方面": { "kana": "ほうめん", - "romaji": "hōmen", + "romaji": "homen", "en": "for", "sv": "mot", "tier": 2 @@ -79,14 +79,14 @@ }, "特急": { "kana": "とっきゅう", - "romaji": "tokkyū", + "romaji": "tokkyu", "en": "Limited express", "sv": "Expresståg", "tier": 3 }, "空港": { "kana": "くうこう", - "romaji": "kūkō", + "romaji": "kuko", "en": "Airport", "sv": "Flygplats", "tier": 2 @@ -121,21 +121,21 @@ }, "終点": { "kana": "しゅうてん", - "romaji": "shūten", + "romaji": "shuten", "en": "Terminus", "sv": "Slutstation", "tier": 3 }, "自動券売機": { "kana": "じどうけんばいき", - "romaji": "jidō kenbaiki", + "romaji": "jido kenbaiki", "en": "Ticket machine", "sv": "Biljettautomat", "tier": 3 }, "チャージ": { "kana": "チャージ", - "romaji": "chāji", + "romaji": "chaji", "en": "Top up (IC card)", "sv": "Fyll på (kort)", "tier": 2 @@ -158,7 +158,7 @@ }, "東京": { "kana": "とうきょう", - "romaji": "Tōkyō", + "romaji": "Tokyo", "en": "Tokyo", "sv": "Tokyo", "tier": 1, @@ -196,7 +196,7 @@ }, "中央線": { "kana": "ちゅうおうせん", - "romaji": "Chūō-sen", + "romaji": "Chuo-sen", "en": "Chuo Line", "sv": "Chuo-linjen", "tier": 2 @@ -232,7 +232,7 @@ }, "南東": { "kana": "なんとう", - "romaji": "nantō", + "romaji": "nanto", "en": "south-east", "sv": "sydöst", "tier": 3 @@ -246,7 +246,7 @@ }, "南東口": { "kana": "なんとうぐち", - "romaji": "nantōguchi", + "romaji": "nantoguchi", "en": "South-east exit", "sv": "Sydöstra utgången", "tier": 3 @@ -281,14 +281,14 @@ }, "到着": { "kana": "とうちゃく", - "romaji": "tōchaku", + "romaji": "tochaku", "en": "Arrival", "sv": "Ankomst", "tier": 2 }, "注意": { "kana": "ちゅうい", - "romaji": "chūi", + "romaji": "chui", "en": "Caution", "sv": "Varning", "tier": 2 @@ -316,14 +316,14 @@ }, "エレベーター": { "kana": "エレベーター", - "romaji": "erebētā", + "romaji": "erebeta", "en": "Elevator", "sv": "Hiss", "tier": 2 }, "エスカレーター": { "kana": "エスカレーター", - "romaji": "esukarētā", + "romaji": "esukareta", "en": "Escalator", "sv": "Rulltrappa", "tier": 2 @@ -372,14 +372,14 @@ }, "普通": { "kana": "ふつう", - "romaji": "futsū", + "romaji": "futsu", "en": "Local", "sv": "Lokaltåg", "tier": 2 }, "急行": { "kana": "きゅうこう", - "romaji": "kyūkō", + "romaji": "kyuko", "en": "Express", "sv": "Expresståg", "tier": 2 @@ -534,7 +534,7 @@ }, "歌舞伎町": { "kana": "かぶきちょう", - "romaji": "Kabukichō", + "romaji": "Kabukicho", "en": "Kabukicho", "sv": "Kabukicho", "tier": 3, @@ -542,14 +542,14 @@ }, "東京駅": { "kana": "とうきょうえき", - "romaji": "Tōkyō-eki", + "romaji": "Tokyo-eki", "en": "Tokyo Station", "sv": "Tokyo station", "tier": 3 }, "東海道線": { "kana": "とうかいどうせん", - "romaji": "Tōkaidō-sen", + "romaji": "Tokaido-sen", "en": "Tokaido Line", "sv": "Tokaido-linjen", "tier": 3 @@ -598,14 +598,14 @@ }, "京浜東北線": { "kana": "けいひんとうほくせん", - "romaji": "Keihin-Tōhoku-sen", + "romaji": "Keihin-Tohoku-sen", "en": "Keihin-Tohoku Line", "sv": "Keihin-Tohoku-linjen", "tier": 3 }, "大宮": { "kana": "おおみや", - "romaji": "Ōmiya", + "romaji": "Omiya", "en": "Omiya", "sv": "Omiya", "tier": 3, @@ -613,14 +613,14 @@ }, "女性専用車": { "kana": "じょせいせんようしゃ", - "romaji": "josei sen'yō-sha", + "romaji": "josei sen'yo-sha", "en": "Women-only car", "sv": "Kvinnovagn", "tier": 2 }, "号車": { "kana": "ごうしゃ", - "romaji": "gōsha", + "romaji": "gosha", "en": "car no.", "sv": "vagn nr", "tier": 2 @@ -641,7 +641,7 @@ }, "通行": { "kana": "つうこう", - "romaji": "tsūkō", + "romaji": "tsuko", "en": "passage / keep", "sv": "gång / håll", "tier": 3 @@ -1175,5 +1175,26 @@ "en": "transfer", "sv": "byte", "tier": 2 + }, + "リムジンバス": { + "kana": "リムジンバス", + "romaji": "rimujin basu", + "en": "airport limousine bus", + "sv": "flygbuss", + "tier": 3 + }, + "番": { + "kana": "ばん", + "romaji": "ban", + "en": "number", + "sv": "nummer", + "tier": 2 + }, + "バスのりば": { + "kana": "バスのりば", + "romaji": "basu noriba", + "en": "bus stop", + "sv": "busshållplats", + "tier": 2 } } \ No newline at end of file diff --git a/tools/genart/main.go b/tools/genart/main.go index 6892186..95b7177 100644 --- a/tools/genart/main.go +++ b/tools/genart/main.go @@ -213,6 +213,32 @@ var tiles = []func(canvas){ c.rect(11, 5, 2, 2, green) c.rect(2, 10, 12, 3, metal) // disk }, + func(c canvas) { // buss (ritas på vägrutan bortom hållplatsen när bussen är inne) + c.rect(0, 0, 16, 16, wallDark) + c.rect(1, 2, 14, 11, color.RGBA{240, 200, 60, 255}) // gul kaross + c.rect(1, 2, 14, 1, metalDark) + c.rect(2, 4, 12, 4, screenC) // fönsterrad + c.rect(7, 4, 1, 4, metalDark) // fönsterspröjs + c.rect(1, 9, 14, 2, awningRed) // rand + c.rect(11, 8, 2, 5, metalDark) // dörr + c.rect(3, 13, 3, 2, boxDark) // hjul + c.rect(10, 13, 3, 2, boxDark) + }, + func(c canvas) { // väg (horisontell; roteras för vertikala): asfalt med mittlinje och kantsten + c.rect(0, 0, 16, 16, color.RGBA{70, 72, 80, 255}) + c.rect(0, 0, 16, 1, wallMid) + c.rect(0, 15, 16, 1, metal) // trottoarkant + for x := 0; x < 16; x += 6 { + c.rect(x, 7, 3, 1, color.RGBA{230, 220, 150, 255}) // streckad mittlinje + } + }, + func(c canvas) { // hållplatsskylt: stolpe med bussymbol (ritas över hållplatsrutan) + c.rect(7, 4, 2, 12, metalDark) + c.rect(3, 1, 10, 6, color.RGBA{240, 200, 60, 255}) + c.rect(4, 2, 8, 3, boxDark) + c.rect(5, 3, 2, 1, white) + c.rect(9, 3, 2, 1, white) + }, } // player ritar en figur som tittar åt facing (0 ner, 1 upp, 2 vänster, 3 höger) diff --git a/tools/gensound/main.go b/tools/gensound/main.go index 5579e93..9ead6f9 100644 --- a/tools/gensound/main.go +++ b/tools/gensound/main.go @@ -70,6 +70,7 @@ var ( staffTone = []note{{a4, 0.18}, {0, 0.08}, {a4, 0.18}, {0, 0.12}, {e5, 0.3}} gateBeep = []note{{a6, 0.09}} gateDeny = []note{{e4, 0.12}, {0, 0.04}, {e4, 0.12}, {0, 0.04}, {e4, 0.14}} + busHorn = []note{{a4, 0.22}, {f4, 0.3}} ) // render syntetiserar toner med sinus + överton, attack/release-envelope. @@ -264,6 +265,7 @@ func main() { "lose": render(loseSting, 0.01, 0.08, 0.2), "staff": square(staffTone), "plane": noiseSweep(3.0, 0.03, 0.4, func(t float64) float64 { return math.Sin(math.Pi * t / 3.0) }), + "bus-horn": square(busHorn), "chatter": chatter(6), "music-map": mapMusic(), } diff --git a/tools/shots/main.go b/tools/shots/main.go index a6fd27e..ff4b9e4 100644 --- a/tools/shots/main.go +++ b/tools/shots/main.go @@ -124,6 +124,18 @@ func buildShots(levelsDir string) error { if err != nil { return err } + l1b, err := level.LoadFile(filepath.Join(levelsDir, "001b-flygbussen.json")) + if err != nil { + return err + } + toMachine1b, err := moves(l1b, l1b.SpawnPoint(), l1b.Find(level.TicketMachine)[0]) + if err != nil { + return err + } + toStop1b, err := moves(l1b, l1b.Find(level.TicketMachine)[0], l1b.CorrectPlatform()) + if err != nil { + return err + } l7, err := level.LoadFile(filepath.Join(levelsDir, "007-akihabara.json")) if err != nil { return err @@ -146,7 +158,7 @@ func buildShots(levelsDir string) error { // delvisa översättningen syns; hjälpen lär sedan in nästa ord. known := []string{"-know", "改札,番線,方面", "-seed", "7"} shots = []shot{ - {"", "wait 10; shot $OUT/map.png; press l; wait 5; press down; press down; press down; wait 5; shot $OUT/words.png; press l; wait 3; press t; wait 5; press down; wait 3; shot $OUT/skills.png; press t; wait 3; press i; wait 5; shot $OUT/items.png; quit", + {"", "wait 5; press enter; wait 10; shot $OUT/map.png; press l; wait 5; press down; press down; press down; wait 5; shot $OUT/words.png; press l; wait 3; press t; wait 5; press down; wait 3; shot $OUT/skills.png; press t; wait 3; press i; wait 5; shot $OUT/items.png; quit", []string{"-know", "改札,出口,トイレ", "-points", "3", "-save", "$DEMO"}}, {"001-flygplatsen", toKiosk + "; press b; wait 5; press down; wait 3; shot $OUT/shop.png; quit", []string{"-seed", "7", "-save", "$DEMO"}}, {"001-flygplatsen", "move right 4; move down 2; wait 5; shot $OUT/level-001.png; quit", nil}, @@ -162,13 +174,14 @@ func buildShots(levelsDir string) error { {"006-ikebukuro", "move right 2; wait 5; shot $OUT/nav-list.png; quit", []string{"-seed", "7", "-mode", "list"}}, {"006-ikebukuro", toMachine6 + "; wait 5; shot $OUT/terminal.png; " + "quit", []string{"-seed", "7", "-mode", "signs", "-know", "山手線,番線,方面,新宿,池袋,埼京線"}}, {"006-ikebukuro", toTrap6 + "; waittrain; wait 5; press enter; wait 100; pick calm; press enter; wait 60; shot $OUT/ride-real.png; waitover; wait 5; shot $OUT/too-far.png; quit", []string{"-seed", "7", "-mode", "signs"}}, - {"", "wait 10; press n; wait 5; shot $OUT/map-signs.png; quit", []string{"-save", "$DEMO"}}, + {"", "wait 5; press enter; wait 10; press n; wait 5; shot $OUT/map-signs.png; quit", []string{"-save", "$DEMO"}}, {"007-akihabara", toPlatform7 + "; waittrain; wait 5; press enter; wait 100; pick calm; press enter; waitstop; press enter; wait 40; shot $OUT/transfer.png; quit", []string{"-seed", "7", "-mode", "list", "-know", "上野,番線,方面,乗り換え"}}, {"", "wait 150; shot $OUT/ending.png; press enter; wait 5; shot $OUT/ending-summary.png; press enter; wait 250; shot $OUT/credits.png; quit", []string{"-ending", "-save", "$DEMO"}}, {"007-akihabara", toKiosk7 + "; urge Ville; press enter; wait 5; press right; wait 3; shot $OUT/payer.png; quit", []string{"-seed", "7"}}, {"", "wait 10; shot $OUT/endless.png; quit", []string{"-endless", "-seed", "7", "-mode", "list", "-save", "$DEMO"}}, - {"", "wait 10; press s; wait 5; press down; wait 3; shot $OUT/settings.png; quit", []string{"-save", "$DEMO"}}, + {"001b-flygbussen", toMachine1b + "; press enter; pick 新宿; press enter; wait 5; " + toStop1b + "; waittrain; wait 10; shot $OUT/bus.png; quit", []string{"-seed", "7", "-know", "新宿,方面"}}, + {"", "wait 5; press enter; wait 10; press s; wait 5; press down; wait 3; shot $OUT/settings.png; quit", []string{"-save", "$DEMO"}}, {"", "wait 10; shot $OUT/lang.png; press enter; wait 30; shot $OUT/title.png; press down; press enter; wait 5; press right; wait 5; shot $OUT/tutorial.png; quit", []string{"-save", "$FRESH"}}, {"001-flygplatsen", "wait 40; shot $OUT/tips.png; " + toSign + "; wait 5; shot $OUT/tips-sign.png; press esc; wait 5; shot $OUT/pause.png; quit", []string{"-seed", "7", "-save", "$FRESH"}}, {"005-ueno", toPlatform5 + "; " + toGateBack + "; wait 5; push " + pushOut + " 30; wait 3; shot $OUT/no-exit.png; press enter; wait 150; shot $OUT/staff.png; quit", []string{"-seed", "7"}},