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() }