Files
metro-puzzle-jp/internal/sim/npc.go
claude f762f9554e
All checks were successful
release / build-release (push) Successful in 1m34s
M9: navigeringslägen, linjenät och terminal
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>
2026-10-24 13:07:36 +02:00

145 lines
4.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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