M8: progression, items, bankarta och sparfil
All checks were successful
release / build-release (push) Successful in 1m46s
All checks were successful
release / build-release (push) Successful in 1m46s
- internal/progress: katalog (skills.json 12 noder, items.json 25 items) med validering; XP/nivåer (150·n(n+1)/2) och skillpoäng; skillträd med krav och kostnad; items per plats med buffar, genvägar (JR Pass, kartor, powerbank) och färgton; banresultat och upplåsning (huvudbanor i ordning, sidospår efter föräldern); 33 %-regeln som test - internal/save: versionerad JSON med kunskap, plånbok, debuffar, progression och pågående bana + seed; atomisk skrivning; migrering; ~/.config/metro-puzzle-jp/save.json resp. %AppData% - sim: RunOptions får Extra/FogRadius/Landmarks/FreeGates/RevealAll/ InfiniteBattery; luck påverkar trängselzonen; Fog.RevealAll och extra landmärken - game: bankarta i spelbrädesstil ersätter menyn (⟳ återupptar med sparad seed), skillträd (T), garderob (I), butik i kiosken (B), XP/nivå/items/ upplåsningar på resultatet, spelaren tonas av skins; autospar vid banstart, efter banan och vid köp; flaggor -save/-nosave - level: kind/order/parent/rewards/shop; ny sidobana 002b Kabukicho som belönar skinnet Kimono; 001 belönar Salaryman-skinnet; butikssortiment på 002/003 - tools/shots: demo-sparfil (build/, aldrig den riktiga), bilder av karta, skills, garderob och butik; alla körningar med -nosave - docs/README/wiki uppdaterade Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -9,10 +9,11 @@ const FogRadius = 3
|
||||
// Fog håller reda på vilka rutor spelaren har sett i den här omgången.
|
||||
// Minikartan ritar bara sedda rutor plus riktmärken som alltid syns.
|
||||
type Fog struct {
|
||||
w, h int
|
||||
seen []bool
|
||||
Radius int
|
||||
count int
|
||||
w, h int
|
||||
seen []bool
|
||||
Radius int
|
||||
count int
|
||||
ExtraLandmarks bool // toaletter, kiosker och automater syns alltid (skillen Lokalkännedom)
|
||||
}
|
||||
|
||||
// NewFog skapar en tom dimma för banan.
|
||||
@@ -83,7 +84,25 @@ func Landmark(l *level.Level, p level.Point) bool {
|
||||
|
||||
// Visible rapporterar om rutan ska ritas på minikartan.
|
||||
func (f *Fog) Visible(l *level.Level, p level.Point) bool {
|
||||
return f.Seen(p) || Landmark(l, p)
|
||||
if f.Seen(p) || Landmark(l, p) {
|
||||
return true
|
||||
}
|
||||
if f.ExtraLandmarks {
|
||||
switch l.At(p.X, p.Y) {
|
||||
case level.Toilet, level.Kiosk, level.TicketMachine:
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RevealAll avslöjar hela banan (kartan köpt som item).
|
||||
func (f *Fog) RevealAll(l *level.Level) {
|
||||
for y := 0; y < l.Height(); y++ {
|
||||
for x := 0; x < l.Width(); x++ {
|
||||
f.mark(level.Point{X: x, Y: y})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Count är antalet sedda rutor (inkl. väggar).
|
||||
|
||||
@@ -84,6 +84,7 @@ type Run struct {
|
||||
spentStart int // plånbokens Spent vid banstart
|
||||
Score ScoreBreakdown
|
||||
NewDebuffs []Debuff // debuffar banan gav (appliceras på profilen efteråt)
|
||||
opts RunOptions
|
||||
}
|
||||
|
||||
// ScoreBreakdown är poängen för en vunnen bana.
|
||||
@@ -163,6 +164,14 @@ type RunOptions struct {
|
||||
Wallet *Wallet // nil = startkassa
|
||||
Debuffs []Debuff // aktiva debuffar från profilen
|
||||
Seed rng.Seed // omgångens seed (sparas med spelet)
|
||||
|
||||
// Från skills och items (progression):
|
||||
Extra Stats // multipliceras in i statsen (nollvärde = inga)
|
||||
FogRadius int // extra siktsteg
|
||||
Landmarks bool // toaletter/kiosker/automater alltid på minikartan
|
||||
FreeGates bool // spärrarna kostar inget och kräver inget
|
||||
RevealAll bool // hela banan känd från start
|
||||
InfiniteBattery bool // telefonen laddar inte ur
|
||||
}
|
||||
|
||||
// DefaultSpeed är spelarens grundfart i rutor per sekund.
|
||||
@@ -215,11 +224,21 @@ func NewRun(l *level.Level, o RunOptions) *Run {
|
||||
Party: NewParty(l.Party, l.SpawnPoint(), o.Seed.Derive("party")),
|
||||
baseSpeed: o.Speed,
|
||||
NoticeAt: -1,
|
||||
Fog: NewFog(l, FogRadius),
|
||||
Stats: CombinedStats(o.Debuffs),
|
||||
spentStart: o.Wallet.Spent,
|
||||
opts: o,
|
||||
}
|
||||
if o.Extra != (Stats{}) {
|
||||
for i := range r.Stats {
|
||||
r.Stats[i] *= o.Extra[i]
|
||||
}
|
||||
}
|
||||
r.Fog = NewFog(l, FogRadius+o.FogRadius)
|
||||
r.Fog.ExtraLandmarks = o.Landmarks
|
||||
r.Party.Rates = Rates{Thirst: r.Stats[StatThirst], Toilet: r.Stats[StatToilet]}
|
||||
if o.RevealAll {
|
||||
r.Fog.RevealAll(l)
|
||||
}
|
||||
r.Fog.Reveal(l, l.SpawnPoint())
|
||||
if r.parSec <= 0 {
|
||||
n := len(l.Path(l.SpawnPoint(), l.CorrectPlatform(), l.CanEnter))
|
||||
@@ -259,6 +278,9 @@ func (r *Run) CanEnter(p level.Point) bool {
|
||||
}
|
||||
|
||||
func (r *Run) canPassGate() bool {
|
||||
if r.opts.FreeGates {
|
||||
return true // JR Pass: genväg genom alla spärrar
|
||||
}
|
||||
if r.Level.TicketRequired {
|
||||
if r.Wallet.Ticket == "" {
|
||||
return false
|
||||
@@ -306,7 +328,9 @@ func (r *Run) Step(dt float64, want Dir, board bool) {
|
||||
return // minispel pågår: använd StepMini
|
||||
}
|
||||
r.Time += dt
|
||||
r.Wallet.Drain(dt)
|
||||
if !r.opts.InfiniteBattery {
|
||||
r.Wallet.Drain(dt)
|
||||
}
|
||||
before := r.Player.Tile
|
||||
wasGate := r.inGate()
|
||||
r.Player.Speed = r.baseSpeed * r.Party.SpeedFactor() * r.Stats[StatSpeed]
|
||||
@@ -332,7 +356,7 @@ func (r *Run) Step(dt float64, want Dir, board bool) {
|
||||
r.checkLimits()
|
||||
if r.Player.Tile != before && r.inGate() && !wasGate {
|
||||
r.GatePasses++
|
||||
if !r.Level.TicketRequired {
|
||||
if !r.Level.TicketRequired && !r.opts.FreeGates {
|
||||
r.Wallet.PayCard(r.Level.FareYen)
|
||||
if r.Level.IndividualCards {
|
||||
for _, m := range r.Party.Followers() {
|
||||
@@ -427,7 +451,7 @@ func (r *Run) tryBoard(tr *level.Train) {
|
||||
}
|
||||
r.PendingTrain = tr
|
||||
if tr.Crowded {
|
||||
r.Mini, r.MiniKind = minigame.NewCrowd(r.miniSeed(), CrowdTries), MiniCrowd
|
||||
r.Mini, r.MiniKind = minigame.NewCrowd(r.miniSeed(), CrowdTries, r.Stats[StatLuck]), MiniCrowd
|
||||
return
|
||||
}
|
||||
r.startRide()
|
||||
@@ -449,7 +473,9 @@ func (r *Run) StepMini(dt float64, in minigame.Input) {
|
||||
return
|
||||
}
|
||||
r.Time += dt
|
||||
r.Wallet.Drain(dt)
|
||||
if !r.opts.InfiniteBattery {
|
||||
r.Wallet.Drain(dt)
|
||||
}
|
||||
r.Party.Advance(dt, r.Time)
|
||||
r.Mini.Update(in, dt)
|
||||
if !r.Mini.Done() {
|
||||
|
||||
@@ -954,3 +954,50 @@ func TestNearTrackAndPhases(t *testing.T) {
|
||||
t.Fatal("tre rutor bort är utom räckhåll")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunOptionsFromProgression(t *testing.T) {
|
||||
l := loadLevel(t, "001-flygplatsen")
|
||||
extra := DefaultStats()
|
||||
extra[StatSpeed] = 1.2
|
||||
r := NewRun(l, RunOptions{Seed: rng.FromInt(1), Extra: extra, FogRadius: 2, Landmarks: true, FreeGates: true, RevealAll: true, InfiniteBattery: true})
|
||||
r.Party.nextEventAt = 1e9
|
||||
r.Wallet.Card, r.Wallet.Phone, r.Wallet.Battery = 0, true, 50
|
||||
r.Step(frame, Right, false)
|
||||
if math.Abs(r.Player.Speed-DefaultSpeed*1.2*PhoneSpeedFactor) > 1e-9 {
|
||||
t.Fatalf("extra fart: %.2f", r.Player.Speed)
|
||||
}
|
||||
if r.Wallet.Battery != 50 {
|
||||
t.Fatal("powerbank: batteriet laddar inte ur")
|
||||
}
|
||||
if r.Fog.Radius != FogRadius+2 || !r.Fog.ExtraLandmarks || r.Fog.Explored(l) != 1 {
|
||||
t.Fatalf("dimma: radius=%d landmarks=%v explored=%.2f", r.Fog.Radius, r.Fog.ExtraLandmarks, r.Fog.Explored(l))
|
||||
}
|
||||
gate := l.Find(level.Gate)[0]
|
||||
if !r.CanEnter(gate) {
|
||||
t.Fatal("JR Pass: spärren släpper igenom utan saldo")
|
||||
}
|
||||
for r.Player.Moving() {
|
||||
r.Step(frame, None, false)
|
||||
}
|
||||
path := l.Path(r.Player.Tile, gate, r.CanEnter)
|
||||
for i, p := range path {
|
||||
if l.At(p.X, p.Y) == level.Gate {
|
||||
path = path[:i+1]
|
||||
break
|
||||
}
|
||||
}
|
||||
walk(t, r, path)
|
||||
if r.Wallet.Spent != 0 || r.GatePasses != 1 {
|
||||
t.Fatalf("gratis spärr: spent=%d passes=%d", r.Wallet.Spent, r.GatePasses)
|
||||
}
|
||||
// Utan extras: standard.
|
||||
plain := NewRun(l, RunOptions{Seed: rng.FromInt(1)})
|
||||
if plain.Stats != DefaultStats() || plain.Fog.Radius != FogRadius || plain.Fog.ExtraLandmarks {
|
||||
t.Fatal("standardvärden utan progression")
|
||||
}
|
||||
// Lokalkännedom: kiosken syns på minikartan utan att vara sedd.
|
||||
k := l.Find(level.Kiosk)[0]
|
||||
if plain.Fog.Visible(l, k) || !r.Fog.Visible(l, k) {
|
||||
t.Fatal("landmärken från skillen")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user