M9b: folk, filer, reklam, sorl och vagnval
All checks were successful
release / build-release (push) Successful in 1m33s

Folk på stationen (NPC:er som vandrar, kliver av tåg och går mot
utgången, blockerar spelaren men inte varandra), vänster-/högerfiler
som bromsar på fel sida, bredare gångar, reklamaffischer (clutter),
japanskt sorl i loop (noise), vagnval med kvinnovagn (stort avdrag med
man i sällskapet, tidsstyrd), kömarkeringar på perrongen, trängsel per
vagn (bonus för tom vagn, svettig-debuff och svårare resa i full vagn,
släpp förbi passagerare) och att folk kliver av innan man får gå på.
Ny bana 005 Ueno. Skärmdumpsverktyget kan filtrera på bana/skript.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 12:33:44 +02:00
parent 8fca14e42d
commit de61568e44
49 changed files with 1568 additions and 136 deletions

144
internal/sim/npc.go Normal file
View File

@@ -0,0 +1,144 @@
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 []level.Tile{level.Kiosk, level.Toilet, level.Sign, level.TicketMachine, level.Platform, level.WrongPlatform, level.Spawn} {
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() }