M9: navigeringslägen, linjenät och terminal
All checks were successful
release / build-release (push) Successful in 1m34s

internal/nav med resources/data/network.json (76 stationer, 11 linjer,
Yamanote som ringlinje). Tåg bedöms från lineId + terminus mot banans
station/to: rätt, fel håll, för långt bort (fler än 12 stationer) eller
ingen trafik; correct räknas ut för nätkopplade banor, 1-4 rätta tåg.
Tågresan visar linjens riktiga stationer. Nya förlustorsaker "Fel håll"
och "För långt bort". Listläge (nästa tåg nere till vänster) och
skyltläge (+50 % poäng) växlas med N på bankartan och sparas; -mode.
Terminalen: biljettautomaten visar linjetavlan på nätbanor. Perronger
P p Q q R r. Bana 006 Ikebukuro med fyra perronger. Banorna 001-005
kopplade till nätet. Skript: waitover och pick calm; poängraden
radbryts; skärmdumpar uppdaterade.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 13:07:36 +02:00
parent de61568e44
commit f762f9554e
53 changed files with 3041 additions and 122 deletions

View File

@@ -7,6 +7,7 @@ import (
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/lang"
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/minigame"
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/nav"
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/rng"
)
@@ -32,6 +33,7 @@ type Run struct {
DeniedAt float64 // när spärren senast nekade (för HUD); < 0 = aldrig
Outcome Outcome
Boarded *level.Train
Mode nav.Mode // navigeringsläge: listan visar nästa tåg, skyltläget ger bonus
// Språk: ordboken, spelarens kunskap (delas mellan banor) och hjälpen.
Dict *lang.Dictionary
@@ -120,9 +122,12 @@ const (
// ScoreBreakdown är poängen för en vunnen bana.
type ScoreBreakdown struct {
Time, Money, Meters, Explore, Car, Penalty, Total int
Time, Money, Meters, Explore, Car, Bonus, Penalty, Total int
}
// ScoreSignsBonusPct är skyltlägets bonus i procent av grundpoängen.
const ScoreSignsBonusPct = 50
// Poängformelns vikter.
const (
ScoreTimeMax = 500 // par eller snabbare
@@ -163,9 +168,11 @@ const (
// Förlustorsaker (textnycklar).
const (
LostWrongTrain = "result.lost_wrong_train"
LostWrongStop = "result.lost_wrong_stop"
LostMissedStop = "result.lost_missed_stop"
LostWrongTrain = "result.lost_wrong_train"
LostWrongDirection = "result.lost_wrong_direction" // linjen går dit, men åt andra hållet
LostTooFar = "result.lost_too_far" // fel varv runt ringlinjen / lång omväg
LostWrongStop = "result.lost_wrong_stop"
LostMissedStop = "result.lost_missed_stop"
)
// Flash-nycklar.
@@ -204,6 +211,8 @@ type RunOptions struct {
FreeGates bool // spärrarna kostar inget och kräver inget
RevealAll bool // hela banan känd från start
InfiniteBattery bool // telefonen laddar inte ur
Mode nav.Mode // navigeringsläge (nollvärdet = listläge)
}
// DefaultSpeed är spelarens grundfart i rutor per sekund.
@@ -264,7 +273,7 @@ func NewRun(l *level.Level, o RunOptions) *Run {
}
r.npcRand = newNPCRand(o.Seed)
r.NPCs = spawnNPCs(l, l.Crowd*NPCPerCrowd, r.npcRand)
for _, t := range []level.Tile{level.Platform, level.WrongPlatform} {
for _, t := range level.PlatformTiles {
r.platformPhase[t], _ = r.PlatformPhase(t)
}
if o.Extra != (Stats{}) {
@@ -283,6 +292,7 @@ func NewRun(l *level.Level, o RunOptions) *Run {
n := len(l.Path(l.SpawnPoint(), l.CorrectPlatform(), l.CanEnter))
r.parSec = float64(max(0, n-1))*ParSecPerTile + ParSlackSec
}
r.Mode = o.Mode
return r
}
@@ -572,9 +582,87 @@ func (r *Run) startRide() {
r.Wallet.Ticket, r.Wallet.TicketFare, r.Wallet.TicketCount = "", 0, 0 // biljetterna är förbrukade
}
dest := r.Level.Destination.Get("ja")
if stops, di := r.networkStops(tr); stops != nil {
r.Mini, r.MiniKind = minigame.NewRideStops(r.miniSeed(), stops, di, r.carCrowd()), MiniRide
return
}
r.Mini, r.MiniKind = minigame.NewRide(r.miniSeed(), dest, r.stationPool(dest, 8), r.Level.RideStops, tr.Correct, r.carCrowd()), MiniRide
}
// networkStops ger tågresans stationer från linjenätet: de riktiga
// stationerna i tågets färdriktning och var resmålet ligger bland dem
// (-1 = det kommer inte, eller inte inom det som visas). nil utan nät.
func (r *Run) networkStops(tr *level.Train) ([]string, int) {
l := r.Level
if l.Net == nil {
return nil, -1
}
ids := l.Net.Stops(tr.LineID, l.Station, tr.Terminus, nav.MaxStops+2)
di := -1
for i, id := range ids {
if id == l.To {
di = i
break
}
}
count := l.RideStops
if di >= 0 {
count = max(count, di+2) // resmålet ska inte vara sista stationen: man ska hinna kliva av
}
count = min(count, len(ids))
if di >= count {
di = -1 // för långt bort: resmålet ligger bortom det vi visar
}
stops := make([]string, count)
for i := range stops {
stops[i] = l.Net.StationName(ids[i], "ja")
}
return stops, di
}
// NextTrains är tågen som tar en rätt – listläget visar dem i HUD:en.
func (r *Run) NextTrains() []*level.Train {
var out []*level.Train
for i := range r.Level.Trains {
if r.Level.Trains[i].Correct {
out = append(out, &r.Level.Trains[i])
}
}
return out
}
// TerminalStops är hur många stationer åt varje håll terminalen visar.
const TerminalStops = 3
// TerminalLines är linjetavlan vid automaten ("terminalen") på nätkopplade
// banor: varje linje vid stationen med de närmaste stationerna åt båda
// hållen, på japanska – att läsa genom sin kunskap som en skylt.
func (r *Run) TerminalLines() []string {
l := r.Level
if l.Net == nil {
return nil
}
var out []string
for _, ln := range l.Net.LinesAt(l.Station) {
s := ln.Name["ja"] + ": "
if back := l.Net.StopsDir(ln.ID, l.Station, -1, TerminalStops); len(back) > 0 {
for i := len(back) - 1; i >= 0; i-- {
s += l.Net.StationName(back[i], "ja") + " "
}
s += "← "
}
s += l.Net.StationName(l.Station, "ja")
if fwd := l.Net.StopsDir(ln.ID, l.Station, 1, TerminalStops); len(fwd) > 0 {
s += " →"
for _, id := range fwd {
s += " " + l.Net.StationName(id, "ja")
}
}
out = append(out, s)
}
return out
}
// StepMini för ett pågående minispel framåt; klockan och sällskapet tickar
// under tiden men spelaren står still.
func (r *Run) StepMini(dt float64, in minigame.Input) {
@@ -649,7 +737,14 @@ func (r *Run) StepMini(dt float64, in minigame.Input) {
case g.Success():
r.finish(tr, true, "")
case !tr.Correct:
r.finish(tr, false, LostWrongTrain)
reason := LostWrongTrain
switch tr.Verdict {
case nav.WrongDirection:
reason = LostWrongDirection
case nav.TooFar:
reason = LostTooFar
}
r.finish(tr, false, reason)
case g.GotOff >= 0:
r.finish(tr, false, LostWrongStop)
default:
@@ -739,7 +834,11 @@ func (r *Run) computeScore() {
if r.CarChosen != nil && r.CarChosen.Crowd == 0 {
sc.Car = ScoreCarBonus
}
sc.Total = max(0, sc.Time+sc.Money+sc.Meters+sc.Explore+sc.Car-sc.Penalty)
base := sc.Time + sc.Money + sc.Meters + sc.Explore + sc.Car
if r.Mode == nav.Signs {
sc.Bonus = base * ScoreSignsBonusPct / 100
}
sc.Total = max(0, base+sc.Bonus-sc.Penalty)
r.Score = sc
}
@@ -770,7 +869,7 @@ func (r *Run) earnedDebuffs() []Debuff {
// stepNPCs flyttar folket, låter avstigande kliva av när dörrarna öppnas och
// tar bort dem som nått utgången.
func (r *Run) stepNPCs(dt float64) {
for _, t := range []level.Tile{level.Platform, level.WrongPlatform} {
for _, t := range level.PlatformTiles {
ph, _ := r.PlatformPhase(t)
if r.platformPhase[t] != ph {
r.platformPhase[t] = ph
@@ -806,7 +905,7 @@ func (r *Run) FacilityProgress() float64 {
// till avgång respektive ankomst.
func (r *Run) PlatformHere() (tr *level.Train, atPlatform bool, seconds float64) {
t := r.Level.At(r.Player.Tile)
if t != level.Platform && t != level.WrongPlatform {
if !t.IsPlatform() {
return nil, false, 0
}
tr = r.Level.TrainAt(t)
@@ -830,7 +929,7 @@ func (r *Run) PlatformPhase(t level.Tile) (level.Phase, float64) {
// spelaren – det är där man ser och hör tåget komma.
func (r *Run) NearTrack(maxDist int) (tr *level.Train, platform level.Point, phase level.Phase, progress float64, dist int, ok bool) {
best := maxDist + 1
for _, t := range []level.Tile{level.Platform, level.WrongPlatform} {
for _, t := range level.PlatformTiles {
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