M7: svårighet, tid, poäng, debuffar, egna kort och sittplatsval
All checks were successful
release / build-release (push) Successful in 1m37s

- sim/stats.go: Stats (fart, törst, fokus, toalett, tur), debuffar som
  multiplikatorer, Profile som följer med mellan banor och räknar ned
- sim.Run: tidsgräns och maxantal missade tåg förlorar banan; poängformel
  (tid mot par, pengar, sällskapets mätare, utforskat, avdrag); debuffar
  tjänas av många steg, lång tid, tom matmätare och seedad otur vid
  kiosken; stats påverkar fart, mätartakter, läsning och hjälptid
- egna kort (individualCards): Member.Card, spärren kräver allas saldo och
  drar från alla, automaten frågar vems kort, biljetter köps åt alla
- sittplatsval (seatSelection): Ticket får ett sittplatssteg med seedade
  önskemål (fönster/gång/sida/bredvid); missnöjda ger mätar- och poängavdrag
- level: maxMissedTrains, individualCards, seatSelection; bana 003 Shibuya
  (svårighet 3, fyra i sällskapet) och 002 med tidsgräns och sittplatser;
  ordboken utökad; generatorn är spec-driven
- game: tid kvar/missade i HUD, debuffar i meny/HUD/resultat, poäng och
  förlustorsaker på resultatet, vyer för personval och vagn, skript-pick,
  flaggan -debuffs; HUD och resultat komprimerade för fyra i sällskapet
- plan (Björn 2026-10-24): flera våningar med trappor som M9a, egna kort
  och sittplatsval som svårighetsinställningar i M7 – i spelbeskrivning,
  roadmap, testplan
- docs/README/wiki med sittplatsbilden

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 10:18:17 +02:00
parent 9fc7e94074
commit b2995d4ca9
44 changed files with 1567 additions and 177 deletions

121
internal/sim/stats.go Normal file
View File

@@ -0,0 +1,121 @@
package sim
// Stats är spelarens multiplikatorer: 1,0 = normalt. Debuffar (och i M8
// items/skills) ändrar dem.
type Stats [5]float64
// Statindex.
const (
StatSpeed = iota // gångfart
StatThirst // hur fort dryckesmätaren sjunker (>1 = fortare)
StatFocus // chans att lära av skyltar, hjälptid (>1 = bättre)
StatToilet // hur fort toamätaren sjunker (>1 = fortare)
StatLuck // omskanning, trängsel (>1 = bättre; items i M8)
)
// DefaultStats är allt på 1,0.
func DefaultStats() Stats { return Stats{1, 1, 1, 1, 1} }
// DebuffKind är en typ av debuff (textnyckel: "debuff.<kind>").
type DebuffKind string
// Debuffar. De fyra första uppstår av spelandet (M7), resten delas ut av
// sidouppdrag (M10) och varar 1–4 banor.
const (
SoreFeet DebuffKind = "sore_feet" // många steg
Tired DebuffKind = "tired" // lång bana
Hangry DebuffKind = "hangry" // tom matmätare
FoodPoisoning DebuffKind = "food_poisoning" // otur vid kiosken
DOMS DebuffKind = "doms" // träningsvärk
Blisters DebuffKind = "blisters" // skoskav
HeavyBag DebuffKind = "heavy_bag" // köpt något tungt/stort
Stuffed DebuffKind = "stuffed" // proppmätt
Sweaty DebuffKind = "sweaty" // svettig och varm
)
// DebuffEffects är vad varje debuff gör med statsen.
var DebuffEffects = map[DebuffKind]Stats{
SoreFeet: {0.85, 1, 1, 1, 1},
Tired: {0.9, 1, 0.8, 1, 1},
Hangry: {1, 1.2, 0.7, 1, 1},
FoodPoisoning: {1, 1, 1, 2.0, 1},
DOMS: {0.9, 1, 1, 1, 1},
Blisters: {0.8, 1, 1, 1, 1},
HeavyBag: {0.9, 1.2, 1, 1, 1},
Stuffed: {0.9, 1, 1, 1.3, 1},
Sweaty: {1, 1.5, 0.9, 1, 1},
}
// Utlösare för de spelgenererade debuffarna.
const (
SoreFeetSteps = 150
TiredSec = 300.0
FoodPoisonChance = 0.05
FoodPoisonLevels = 2
)
// Debuff är en aktiv debuff med återstående antal banor.
type Debuff struct {
Kind DebuffKind `json:"kind"`
LevelsLeft int `json:"levelsLeft"`
}
// CombinedStats multiplicerar ihop effekterna av alla debuffar.
func CombinedStats(debuffs []Debuff) Stats {
st := DefaultStats()
for _, d := range debuffs {
if e, ok := DebuffEffects[d.Kind]; ok {
for i := range st {
st[i] *= e[i]
}
}
}
return st
}
// Profile är det som följer spelaren mellan banor och inte hör till
// kunskap eller plånbok: aktiva debuffar (items och skills i M8).
type Profile struct {
Debuffs []Debuff `json:"debuffs"`
}
// Add lägger till en debuff; finns den redan förlängs den till det längsta.
func (p *Profile) Add(kind DebuffKind, levels int) {
for i := range p.Debuffs {
if p.Debuffs[i].Kind == kind {
p.Debuffs[i].LevelsLeft = max(p.Debuffs[i].LevelsLeft, levels)
return
}
}
p.Debuffs = append(p.Debuffs, Debuff{Kind: kind, LevelsLeft: levels})
}
// Has rapporterar om debuffen är aktiv.
func (p *Profile) Has(kind DebuffKind) bool {
for _, d := range p.Debuffs {
if d.Kind == kind {
return true
}
}
return false
}
// EndLevel räknar ned alla debuffar en bana och tar bort de som gått ut.
// Returnerar de som försvann.
func (p *Profile) EndLevel() []DebuffKind {
var expired []DebuffKind
kept := p.Debuffs[:0]
for _, d := range p.Debuffs {
d.LevelsLeft--
if d.LevelsLeft <= 0 {
expired = append(expired, d.Kind)
continue
}
kept = append(kept, d)
}
p.Debuffs = kept
return expired
}
// Stats är de sammanlagda multiplikatorerna just nu.
func (p *Profile) Stats() Stats { return CombinedStats(p.Debuffs) }