M9b: folk, filer, reklam, sorl och vagnval
All checks were successful
release / build-release (push) Successful in 1m33s
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:
@@ -2,6 +2,7 @@ package sim
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/lang"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
||||
@@ -87,11 +88,39 @@ type Run struct {
|
||||
Score ScoreBreakdown
|
||||
NewDebuffs []Debuff // debuffar banan gav (appliceras på profilen efteråt)
|
||||
opts RunOptions
|
||||
|
||||
// Vagnar, folk och filer (M9b).
|
||||
CarChosen *minigame.Car // vagnen man klev på
|
||||
EarlyBoard int // försök att kliva på innan folk klivit av
|
||||
WomenOnlyViolation bool // en man i sällskapet på kvinnovagnen
|
||||
Blocked int // passagerare som inte släpptes förbi på resan
|
||||
NPCs []*NPC
|
||||
npcRand *rand.Rand
|
||||
platformPhase map[level.Tile]level.Phase
|
||||
BumpedAt float64 // när spelaren senast gick in i någon (< 0 = aldrig)
|
||||
WrongLane bool // går på fel sida i en fil just nu
|
||||
}
|
||||
|
||||
// Poäng och avdrag för vagnar.
|
||||
const (
|
||||
ScoreCarBonus = 50 // lite folk i vagnen
|
||||
PenaltyWomenOnly = 200 // grova minus
|
||||
PenaltyEarlyBoard = 20 // per gång man inte lät folk kliva av först
|
||||
PenaltyBlocked = 20 // per passagerare som inte släpptes förbi
|
||||
SweatyChance = 0.6 // chans att bli svettig i en full vagn
|
||||
WrongLaneFactor = 0.7 // fart på fel sida i en fil
|
||||
)
|
||||
|
||||
// Fler flash-nycklar.
|
||||
const (
|
||||
FlashWomenOnly = "hud.women_only"
|
||||
FlashEarly = "hud.early_board"
|
||||
FlashBump = "hud.bump"
|
||||
)
|
||||
|
||||
// ScoreBreakdown är poängen för en vunnen bana.
|
||||
type ScoreBreakdown struct {
|
||||
Time, Money, Meters, Explore, Penalty, Total int
|
||||
Time, Money, Meters, Explore, Car, Penalty, Total int
|
||||
}
|
||||
|
||||
// Poängformelns vikter.
|
||||
@@ -129,6 +158,7 @@ const (
|
||||
MiniTicket
|
||||
MiniCrowd
|
||||
MiniRide
|
||||
MiniCarPick
|
||||
)
|
||||
|
||||
// Förlustorsaker (textnycklar).
|
||||
@@ -209,26 +239,33 @@ func NewRun(l *level.Level, o RunOptions) *Run {
|
||||
o.Wallet = NewWallet()
|
||||
}
|
||||
r := &Run{
|
||||
Level: l,
|
||||
Player: NewMover(l.SpawnPoint(), o.Speed),
|
||||
Wallet: o.Wallet,
|
||||
FlashAt: -1,
|
||||
DeniedAt: -1,
|
||||
Dict: o.Dict,
|
||||
Know: o.Know,
|
||||
Revealed: map[level.Point]bool{},
|
||||
LearnedAt: -1,
|
||||
Seed: o.Seed,
|
||||
Visits: map[level.Point]int{},
|
||||
lastTile: l.SpawnPoint(),
|
||||
ProgressedAt: -1,
|
||||
parSec: float64(l.ParSec),
|
||||
Party: NewParty(l.Party, l.SpawnPoint(), o.Seed.Derive("party")),
|
||||
baseSpeed: o.Speed,
|
||||
NoticeAt: -1,
|
||||
Stats: CombinedStats(o.Debuffs),
|
||||
spentStart: o.Wallet.Spent,
|
||||
opts: o,
|
||||
Level: l,
|
||||
Player: NewMover(l.SpawnPoint(), o.Speed),
|
||||
Wallet: o.Wallet,
|
||||
FlashAt: -1,
|
||||
DeniedAt: -1,
|
||||
Dict: o.Dict,
|
||||
Know: o.Know,
|
||||
Revealed: map[level.Point]bool{},
|
||||
LearnedAt: -1,
|
||||
Seed: o.Seed,
|
||||
Visits: map[level.Point]int{},
|
||||
lastTile: l.SpawnPoint(),
|
||||
ProgressedAt: -1,
|
||||
parSec: float64(l.ParSec),
|
||||
Party: NewParty(l.Party, l.SpawnPoint(), o.Seed.Derive("party")),
|
||||
baseSpeed: o.Speed,
|
||||
NoticeAt: -1,
|
||||
Stats: CombinedStats(o.Debuffs),
|
||||
spentStart: o.Wallet.Spent,
|
||||
opts: o,
|
||||
platformPhase: map[level.Tile]level.Phase{},
|
||||
BumpedAt: -1,
|
||||
}
|
||||
r.npcRand = newNPCRand(o.Seed)
|
||||
r.NPCs = spawnNPCs(l, l.Crowd*NPCPerCrowd, r.npcRand)
|
||||
for _, t := range []level.Tile{level.Platform, level.WrongPlatform} {
|
||||
r.platformPhase[t], _ = r.PlatformPhase(t)
|
||||
}
|
||||
if o.Extra != (Stats{}) {
|
||||
for i := range r.Stats {
|
||||
@@ -276,6 +313,12 @@ func (r *Run) CanEnter(p level.Point) bool {
|
||||
r.DeniedAt = r.Time
|
||||
return false
|
||||
}
|
||||
for _, n := range r.NPCs {
|
||||
if n.Occupies(p) {
|
||||
r.BumpedAt = r.Time
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -302,6 +345,19 @@ func (r *Run) canPassGate() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// PathCanEnter är CanEnter utan hänsyn till folk som råkar stå i vägen –
|
||||
// för vägplanering (folk flyttar på sig; spärrar och väggar gör det inte).
|
||||
func (r *Run) PathCanEnter(p level.Point) bool {
|
||||
t := r.Level.At(p)
|
||||
if !t.Passable() {
|
||||
return false
|
||||
}
|
||||
if t == level.Gate && !r.inGate() && !r.canPassGate() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// MembersWithoutFare listar följare vars egna kort inte räcker (HUD).
|
||||
func (r *Run) MembersWithoutFare() []*Member {
|
||||
if !r.Level.IndividualCards {
|
||||
@@ -342,6 +398,17 @@ func (r *Run) Step(dt float64, want Dir, board bool) {
|
||||
if r.Time < r.RescanUntil {
|
||||
r.Player.Speed = 0 // står och skannar om
|
||||
}
|
||||
// Filer: på en markerad fil går man på rätt sida, annars möter man strömmen.
|
||||
r.WrongLane = false
|
||||
switch r.Level.At(r.Player.Tile) {
|
||||
case level.LaneLeft:
|
||||
r.WrongLane = want == Down || want == Right
|
||||
case level.LaneRight:
|
||||
r.WrongLane = want == Up || want == Left
|
||||
}
|
||||
if r.WrongLane {
|
||||
r.Player.Speed *= WrongLaneFactor
|
||||
}
|
||||
r.Steps += r.Player.Step(dt, want, r.CanEnter)
|
||||
if r.Player.Tile != before {
|
||||
// Trappa: hamnar man på en trappruta flyttas man till andra änden
|
||||
@@ -367,6 +434,7 @@ func (r *Run) Step(dt float64, want Dir, board bool) {
|
||||
r.stepHelp(dt)
|
||||
r.stepFacility(dt)
|
||||
r.stepReading()
|
||||
r.stepNPCs(dt)
|
||||
r.checkLimits()
|
||||
if r.Player.Tile != before && r.inGate() && !wasGate {
|
||||
r.GatePasses++
|
||||
@@ -464,20 +532,47 @@ func (r *Run) tryBoard(tr *level.Train) {
|
||||
return
|
||||
}
|
||||
r.PendingTrain = tr
|
||||
if tr.Crowded {
|
||||
r.Mini, r.MiniKind = minigame.NewCrowd(r.miniSeed(), CrowdTries, r.Stats[StatLuck]), MiniCrowd
|
||||
women := 0
|
||||
if tr.WomenOnlyActive(r.Time) {
|
||||
women = tr.WomenOnlyCar
|
||||
}
|
||||
r.Mini, r.MiniKind = minigame.NewCarPick(r.miniSeed(), tr.Cars, women, minigame.AlightSec), MiniCarPick
|
||||
}
|
||||
|
||||
// carPicked fortsätter ombordstigningen efter vagnvalet.
|
||||
func (r *Run) carPicked(c *minigame.Car, early int) {
|
||||
tr := r.PendingTrain
|
||||
r.CarChosen = c
|
||||
r.EarlyBoard += early
|
||||
if early > 0 {
|
||||
r.flash(FlashEarly)
|
||||
}
|
||||
if c.WomenOnly && r.Party.HasMen() {
|
||||
r.WomenOnlyViolation = true
|
||||
r.flash(FlashWomenOnly)
|
||||
}
|
||||
if tr.Crowded || c.Crowd == 2 {
|
||||
r.Mini, r.MiniKind = minigame.NewCrowd(r.miniSeed(), CrowdTries, r.Stats[StatLuck], c.Crowd), MiniCrowd
|
||||
return
|
||||
}
|
||||
r.startRide()
|
||||
}
|
||||
|
||||
// carCrowd är trängseln i den valda vagnen (0 om inget valts).
|
||||
func (r *Run) carCrowd() int {
|
||||
if r.CarChosen == nil {
|
||||
return 0
|
||||
}
|
||||
return r.CarChosen.Crowd
|
||||
}
|
||||
|
||||
func (r *Run) startRide() {
|
||||
tr := r.PendingTrain
|
||||
if r.Level.TicketRequired {
|
||||
r.Wallet.Ticket, r.Wallet.TicketFare, r.Wallet.TicketCount = "", 0, 0 // biljetterna är förbrukade
|
||||
}
|
||||
dest := r.Level.Destination.Get("ja")
|
||||
r.Mini, r.MiniKind = minigame.NewRide(r.miniSeed(), dest, r.stationPool(dest, 8), r.Level.RideStops, tr.Correct), MiniRide
|
||||
r.Mini, r.MiniKind = minigame.NewRide(r.miniSeed(), dest, r.stationPool(dest, 8), r.Level.RideStops, tr.Correct, r.carCrowd()), MiniRide
|
||||
}
|
||||
|
||||
// StepMini för ett pågående minispel framåt; klockan och sällskapet tickar
|
||||
@@ -491,6 +586,7 @@ func (r *Run) StepMini(dt float64, in minigame.Input) {
|
||||
r.Wallet.Drain(dt)
|
||||
}
|
||||
r.Party.Advance(dt, r.Time)
|
||||
r.stepNPCs(dt)
|
||||
r.Mini.Update(in, dt)
|
||||
if !r.Mini.Done() {
|
||||
r.checkLimits()
|
||||
@@ -531,6 +627,12 @@ func (r *Run) StepMini(dt float64, in minigame.Input) {
|
||||
}
|
||||
r.flash(FlashGrumpy)
|
||||
}
|
||||
case *minigame.CarPick:
|
||||
if g.Success() && g.Chosen != nil {
|
||||
r.carPicked(g.Chosen, g.Early)
|
||||
} else {
|
||||
r.PendingTrain = nil // avbröt på perrongen
|
||||
}
|
||||
case *minigame.Crowd:
|
||||
if g.Success() {
|
||||
r.startRide()
|
||||
@@ -542,6 +644,7 @@ func (r *Run) StepMini(dt float64, in minigame.Input) {
|
||||
case *minigame.Ride:
|
||||
tr := r.PendingTrain
|
||||
r.PendingTrain = nil
|
||||
r.Blocked += g.Blocked
|
||||
switch {
|
||||
case g.Success():
|
||||
r.finish(tr, true, "")
|
||||
@@ -628,8 +731,15 @@ func (r *Run) computeScore() {
|
||||
sc.Money = max(0, ScoreMoneyMax-(r.Wallet.Spent-r.spentStart)/10)
|
||||
sc.Meters = int(ScoreMetersMax * r.Party.AvgMeters())
|
||||
sc.Explore = int(ScoreExploreMax * r.Fog.Explored(r.Level))
|
||||
sc.Penalty = PenaltyMissed*r.MissedTrains + PenaltyRescan*r.Rescans + PenaltyGrumpy*r.Grumpy
|
||||
sc.Total = max(0, sc.Time+sc.Money+sc.Meters+sc.Explore-sc.Penalty)
|
||||
sc.Penalty = PenaltyMissed*r.MissedTrains + PenaltyRescan*r.Rescans + PenaltyGrumpy*r.Grumpy +
|
||||
PenaltyEarlyBoard*r.EarlyBoard + PenaltyBlocked*r.Blocked
|
||||
if r.WomenOnlyViolation {
|
||||
sc.Penalty += PenaltyWomenOnly
|
||||
}
|
||||
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)
|
||||
r.Score = sc
|
||||
}
|
||||
|
||||
@@ -651,9 +761,38 @@ func (r *Run) earnedDebuffs() []Debuff {
|
||||
if r.poisoned {
|
||||
out = append(out, Debuff{FoodPoisoning, FoodPoisonLevels})
|
||||
}
|
||||
if r.CarChosen != nil && r.CarChosen.Crowd == 2 && r.Seed.Derive("sweaty").New().Float64() < SweatyChance/r.Stats[StatLuck] {
|
||||
out = append(out, Debuff{Sweaty, 1})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// 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} {
|
||||
ph, _ := r.PlatformPhase(t)
|
||||
if r.platformPhase[t] != ph {
|
||||
r.platformPhase[t] = ph
|
||||
if tiles := r.Level.Find(t); ph == level.Dwelling && len(tiles) > 0 {
|
||||
// Avstigande per tåg (inte per perrongruta), fördelade längs perrongen.
|
||||
for i := 0; i < NPCAlightBase+r.Level.Crowd; i++ {
|
||||
p := tiles[i%len(tiles)]
|
||||
r.NPCs = append(r.NPCs, &NPC{Mover: NewMover(p, NPCSpeed), Kind: Exit, Target: r.Level.SpawnPoint(), Pause: 0.3 * float64(i/len(tiles))})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
kept := r.NPCs[:0]
|
||||
for _, n := range r.NPCs {
|
||||
n.step(r.Level, dt, r.npcRand)
|
||||
if !n.Done {
|
||||
kept = append(kept, n)
|
||||
}
|
||||
}
|
||||
r.NPCs = kept
|
||||
}
|
||||
|
||||
// FacilityProgress är 0..1 för ett pågående toalettbesök.
|
||||
func (r *Run) FacilityProgress() float64 {
|
||||
if !r.Facility.Active || r.Facility.Total <= 0 {
|
||||
|
||||
Reference in New Issue
Block a user