M10c: vem betalar i kiosken och spärrar utan återvändo
All checks were successful
release / build-release (push) Successful in 2m7s

Björns tillägg 2026-10-24. Med egna kort och fler i sällskapet frågar
kiosken vem som betalar (minigame.Payer: kontant, spelarens kort eller en
följares kort; den som inte har råd nekas, Esc avbryter). Avgiften dras
nu när man passerar spärren in i det betalda området (level.InsideGates),
inte på spärrrutan. Banflaggan oneWayGates: incheckad släpps inte ut igen
förrän man åkt; Enter vid spärren kallar på personal (10 s) som låser upp
korten; ny inpassering kostar igen. På i 005, 007/007b och 008/008b.
Skriptkommandon urge och push. Docs, wiki och skärmdumpar uppdaterade.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 22:12:13 +02:00
parent 4ae6a1d24c
commit f04d6a88f9
26 changed files with 674 additions and 64 deletions

View File

@@ -47,6 +47,16 @@ type Run struct {
timeLimit int // resans tidsgräns (första delbanans), 0 = ingen
maxMissed int // resans gräns för missade tåg (första delbanans)
// Spärrar utan återvändo (level.OneWayGates): incheckad släpps inte ut
// förrän personalen låst upp korten.
inside map[level.Point]bool // rutorna innanför spärrarna
checkedIn bool
gateFromIn bool // spärrrutan man står på nåddes inifrån (på väg ut)
StaffUnlocked bool
Staff StaffState
StaffCalls int
NoExitAt float64 // när spärren senast vägrade släppa ut (< 0 = aldrig)
// Språk: ordboken, spelarens kunskap (delas mellan banor) och hjälpen.
Dict *lang.Dictionary
Know *lang.Knowledge
@@ -129,6 +139,7 @@ const (
const (
FlashWomenOnly = "hud.women_only"
FlashTransfer = "hud.transfer_done"
FlashStaff = "hud.staff_unlocked"
FlashEarly = "hud.early_board"
FlashBump = "hud.bump"
)
@@ -177,6 +188,7 @@ const (
MiniCrowd
MiniRide
MiniCarPick
MiniPayer
)
// Förlustorsaker (textnycklar).
@@ -208,6 +220,18 @@ type FacilityState struct {
Count int
}
// StaffState är ett pågående personalanrop vid spärren (tar tid, avbryts
// om man går).
type StaffState struct {
Active bool
At level.Point
Remaining float64
Total float64
}
// StaffSec är hur länge man väntar på personalen vid spärren.
const StaffSec = 10.0
// RunOptions styr en banomgång.
type RunOptions struct {
Speed float64 // rutor per sekund (0 = DefaultSpeed)
@@ -280,6 +304,8 @@ func NewRun(l *level.Level, o RunOptions) *Run {
timeLimit: l.TimeLimitSec,
maxMissed: l.MaxMissedTrains,
TransferAt: -1,
inside: l.InsideGates(),
NoExitAt: -1,
Party: NewParty(l.Party, l.SpawnPoint(), o.Seed.Derive("party")),
baseSpeed: o.Speed,
NoticeAt: -1,
@@ -375,6 +401,8 @@ func (r *Run) startLeg(l *level.Level) {
r.Mini, r.MiniKind, r.PendingTrain, r.CarChosen = nil, MiniNone, nil, nil
r.Help, r.Facility = HelpState{}, FacilityState{}
r.DeniedAt, r.BumpedAt, r.WrongLane = -1, -1, false
r.inside = l.InsideGates()
r.checkedIn, r.StaffUnlocked, r.gateFromIn, r.Staff = false, false, false, StaffState{}
r.parSec += parFor(l)
r.TransferAt = r.Time
r.flash(FlashTransfer)
@@ -418,6 +446,10 @@ func (r *Run) CanEnter(p level.Point) bool {
r.DeniedAt = r.Time
return false
}
if r.NeedsStaff() && r.inGate() && r.gateFromIn && t != level.Gate && !r.inside[p] {
r.NoExitAt = r.Time // incheckad: spärren släpper inte ut en
return false
}
for _, n := range r.NPCs {
if n.Occupies(p) {
r.BumpedAt = r.Time
@@ -427,6 +459,48 @@ func (r *Run) CanEnter(p level.Point) bool {
return true
}
// NeedsStaff rapporterar om man är incheckad i en spärr utan återvändo och
// behöver personalen för att komma ut.
func (r *Run) NeedsStaff() bool { return r.Level.OneWayGates && r.checkedIn && !r.StaffUnlocked }
// CheckedIn rapporterar om spelaren har checkat in i spärren (och inte åkt).
func (r *Run) CheckedIn() bool { return r.checkedIn }
// callStaff trycker på knappen vid spärren: efter StaffSec låser personalen
// upp korten så att man får gå ut.
func (r *Run) callStaff() {
if !r.NeedsStaff() || r.Staff.Active {
return
}
r.Staff = StaffState{Active: true, At: r.Player.Tile, Remaining: StaffSec, Total: StaffSec}
}
func (r *Run) stepStaff(dt float64) {
if !r.Staff.Active {
return
}
if r.Player.Moving() || r.Player.Tile != r.Staff.At {
r.Staff = StaffState{}
return
}
r.Staff.Remaining -= dt
if r.Staff.Remaining > 0 {
return
}
r.Staff = StaffState{}
r.StaffUnlocked = true
r.StaffCalls++
r.flash(FlashStaff)
}
// StaffProgress är 0..1 för ett pågående personalanrop.
func (r *Run) StaffProgress() float64 {
if !r.Staff.Active || r.Staff.Total <= 0 {
return 0
}
return 1 - r.Staff.Remaining/r.Staff.Total
}
func (r *Run) canPassGate() bool {
if r.opts.FreeGates {
return true // JR Pass: genväg genom alla spärrar
@@ -538,11 +612,21 @@ func (r *Run) Step(dt float64, want Dir, board bool) {
}
r.stepHelp(dt)
r.stepFacility(dt)
r.stepStaff(dt)
r.stepReading()
r.stepNPCs(dt)
r.checkLimits()
if r.Player.Tile != before && r.inGate() && !wasGate {
r.gateFromIn = r.inside[before] // på väg ut (eller bara på genomresa utifrån)
}
// Avgiften dras när man passerar spärren in i det betalda området, inte
// när man står på spärrrutan: spärrar som ligger i en gång kan passeras
// utifrån utan att det kostar.
if r.Player.Tile != before && r.inside[r.Player.Tile] && !r.inside[before] {
r.GatePasses++
if r.Level.OneWayGates {
r.checkedIn = true // incheckad: spärren släpper inte ut en förrän man åkt
}
if !r.Level.TicketRequired && !r.opts.FreeGates {
r.Wallet.PayCard(r.Level.FareYen)
if r.Level.IndividualCards {
@@ -558,8 +642,13 @@ func (r *Run) Step(dt float64, want Dir, board bool) {
}
}
}
if r.Level.OneWayGates && r.Player.Tile != before && wasGate && r.gateFromIn && !r.inside[r.Player.Tile] && !r.inGate() {
r.checkedIn, r.StaffUnlocked = false, false // ute igen (efter personalens upplåsning); ny inpassering kostar igen
}
if board && !r.Player.Moving() {
switch r.Level.At(r.Player.Tile) {
case level.Gate:
r.callStaff()
case level.Toilet:
r.useToilet()
case level.Kiosk:
@@ -791,6 +880,10 @@ func (r *Run) StepMini(dt float64, in minigame.Input) {
r.Party.Members[g.Member].Card += g.Amount
r.TopUps++
}
case *minigame.Payer:
if g.Success() && g.Chosen != nil {
r.purchaseAtKiosk(r.payFrom(*g.Chosen))
}
case *minigame.Ticket:
if !g.Success() || g.Chosen == nil {
break
@@ -882,10 +975,58 @@ func (r *Run) stepFacility(dt float64) {
r.Facility = FacilityState{}
}
// buyAtKiosk köper mat och dryck åt alla som behöver, betalat med kortet.
// Med liten seedad otur blir maten dålig: matförgiftning efter banan.
// buyAtKiosk köper mat och dryck åt alla som behöver. Med egna kort och
// fler i sällskapet frågar kiosken först vem som betalar (minigame.Payer),
// annars dras det från spelarens kort.
func (r *Run) buyAtKiosk() {
n := r.Party.BuyAtKiosk(r.Wallet.PayCard)
needs := r.Party.NeedsKiosk()
if len(needs) == 0 {
return
}
if r.Level.IndividualCards && r.Party.Size() > 1 {
r.Mini, r.MiniKind = minigame.NewPayer(KioskPriceYen*len(needs), r.payOptions()), MiniPayer
return
}
r.purchaseAtKiosk(r.Wallet.PayCard)
}
// payOptions är källorna man kan betala kiosken med: kontant, spelarens
// kort och varje följares kort.
func (r *Run) payOptions() []minigame.PayOption {
card := r.Wallet.Card
if !r.Wallet.CardUsable() {
card = 0
}
opts := []minigame.PayOption{{Cash: true, Yen: r.Wallet.Cash}, {Name: r.Party.Members[0].Name, Member: 0, Yen: card}}
for i, m := range r.Party.Followers() {
opts = append(opts, minigame.PayOption{Name: m.Name, Member: i + 1, Yen: m.Card})
}
return opts
}
// payFrom ger betalfunktionen för en vald källa.
func (r *Run) payFrom(o minigame.PayOption) func(int) bool {
switch {
case o.Cash:
return r.Wallet.PayCash
case o.Member == 0:
return r.Wallet.PayCard
}
m := r.Party.Members[o.Member]
return func(yen int) bool {
if m.Card < yen {
return false
}
m.Card -= yen
r.Wallet.Spent += yen
return true
}
}
// purchaseAtKiosk genomför köpet med en betalfunktion. Med liten seedad
// otur blir maten dålig: matförgiftning efter banan.
func (r *Run) purchaseAtKiosk(pay func(int) bool) {
n := r.Party.BuyAtKiosk(pay)
if n == 0 {
return
}