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>
145 lines
4.1 KiB
Go
145 lines
4.1 KiB
Go
package sim
|
||
|
||
import (
|
||
"math/rand/v2"
|
||
|
||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/rng"
|
||
)
|
||
|
||
// NPC är en passagerare som rör sig i stationen: vandrar mellan platser,
|
||
// eller har klivit av ett tåg och är på väg till utgången. NPC:er tar plats
|
||
// i rutnätet – spelaren kan inte gå in i dem – men går själva igenom
|
||
// varandra och spelaren, så att ingen kan låsa fast någon annan.
|
||
type NPC struct {
|
||
Mover *Mover
|
||
Target level.Point
|
||
Kind NPCKind
|
||
Pause float64 // sekunder kvar att stå still
|
||
path []level.Point
|
||
Done bool // på väg ut och framme: tas bort
|
||
}
|
||
|
||
// NPCKind är vad passageraren håller på med.
|
||
type NPCKind int
|
||
|
||
// Slag av passagerare.
|
||
const (
|
||
Wander NPCKind = iota // går mellan kiosker, toaletter, skyltar och perronger
|
||
Exit // kom med tåget, går mot utgången (startrutan)
|
||
)
|
||
|
||
// Folk: antal per trängselnivå och fart.
|
||
const (
|
||
NPCPerCrowd = 2 // vandrare per crowd-steg vid banstart
|
||
NPCAlightBase = 1 // extra avstigande per tåg (+ crowd)
|
||
NPCSpeed = 3.5 // rutor/s
|
||
NPCPauseMinSec = 1.0
|
||
NPCPauseMaxSec = 3.0
|
||
)
|
||
|
||
// npcTargets är rutor som folk vill till.
|
||
func npcTargets(l *level.Level) []level.Point {
|
||
var out []level.Point
|
||
for _, t := range append([]level.Tile{level.Kiosk, level.Toilet, level.Sign, level.TicketMachine, level.Spawn}, level.PlatformTiles...) {
|
||
out = append(out, l.Find(t)...)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// spawnNPCs skapar banans vandrare på slumpade golvrutor (seedat).
|
||
func spawnNPCs(l *level.Level, n int, r *rand.Rand) []*NPC {
|
||
var floorTiles []level.Point
|
||
for _, t := range []level.Tile{level.Floor, level.LaneLeft, level.LaneRight} {
|
||
floorTiles = append(floorTiles, l.Find(t)...)
|
||
}
|
||
targets := npcTargets(l)
|
||
var out []*NPC
|
||
for i := 0; i < n && len(floorTiles) > 0 && len(targets) > 0; i++ {
|
||
start := floorTiles[r.IntN(len(floorTiles))]
|
||
npc := &NPC{Mover: NewMover(start, NPCSpeed), Kind: Wander, Target: targets[r.IntN(len(targets))]}
|
||
out = append(out, npc)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// step för en NPC framåt: följer sin väg, pausar vid målet och väljer nytt.
|
||
func (n *NPC) step(l *level.Level, dt float64, r *rand.Rand) {
|
||
if n.Done {
|
||
return
|
||
}
|
||
if n.Pause > 0 {
|
||
n.Pause -= dt
|
||
return
|
||
}
|
||
if !n.Mover.Moving() && n.Mover.Tile == n.Target {
|
||
n.arrived(l, r)
|
||
return
|
||
}
|
||
if len(n.path) < 2 || n.path[0] != n.Mover.Tile {
|
||
n.path = l.Path(n.Mover.Tile, n.Target, l.CanEnter)
|
||
}
|
||
if len(n.path) < 2 {
|
||
n.arrived(l, r) // framme eller oåtkomligt mål: välj nytt
|
||
return
|
||
}
|
||
next := n.path[1]
|
||
if n.Mover.Tile == next || (next.Z != n.Mover.Tile.Z) {
|
||
n.path = n.path[1:]
|
||
return
|
||
}
|
||
want := Right
|
||
switch {
|
||
case next.X < n.Mover.Tile.X:
|
||
want = Left
|
||
case next.Y > n.Mover.Tile.Y:
|
||
want = Down
|
||
case next.Y < n.Mover.Tile.Y:
|
||
want = Up
|
||
}
|
||
if n.Mover.Moving() {
|
||
want = None
|
||
if n.Mover.Target == next {
|
||
want = n.Mover.Facing
|
||
}
|
||
}
|
||
before := n.Mover.Tile
|
||
// Bara nästa ruta på vägen får påbörjas: annars fortsätter resten av
|
||
// bildrutans rörelse rakt fram förbi hörn och mål, och NPC:n pendlar
|
||
// kring rutor den aldrig stannar exakt på.
|
||
onlyNext := func(p level.Point) bool { return p == next && l.CanEnter(p) }
|
||
n.Mover.Step(dt, want, onlyNext)
|
||
if n.Mover.Tile != before {
|
||
switch l.At(n.Mover.Tile) { // trappor flyttar även folk
|
||
case level.StairsUp:
|
||
n.Mover.Tile = n.Mover.Tile.Up()
|
||
n.Mover.Target = n.Mover.Tile
|
||
case level.StairsDown:
|
||
n.Mover.Tile = n.Mover.Tile.Down()
|
||
n.Mover.Target = n.Mover.Tile
|
||
}
|
||
n.path = nil
|
||
}
|
||
}
|
||
|
||
func (n *NPC) arrived(l *level.Level, r *rand.Rand) {
|
||
if n.Kind == Exit {
|
||
n.Done = true
|
||
return
|
||
}
|
||
targets := npcTargets(l)
|
||
if len(targets) > 0 {
|
||
n.Target = targets[r.IntN(len(targets))]
|
||
}
|
||
n.Pause = NPCPauseMinSec + (NPCPauseMaxSec-NPCPauseMinSec)*r.Float64()
|
||
n.path = nil
|
||
}
|
||
|
||
// Occupies rapporterar om NPC:n står på eller är på väg in i rutan.
|
||
func (n *NPC) Occupies(p level.Point) bool {
|
||
return !n.Done && (n.Mover.Tile == p || (n.Mover.Moving() && n.Mover.Target == p))
|
||
}
|
||
|
||
// newNPCRand ger slumpen för folket.
|
||
func newNPCRand(seed rng.Seed) *rand.Rand { return seed.Derive("npc").New() }
|