M11: oändligt läge och New Game+
All checks were successful
release / build-release (push) Successful in 1m34s
All checks were successful
release / build-release (push) Successful in 1m34s
internal/endless lottar mål ur en seed: en av storyns nätkopplade stationer (aldrig samma två gånger i rad) med ett resmål som minst ett tåg går rakt till men inte alla; banan kopieras, bedöms om mot nätet, sällskapet växer var tredje mål och tidsgränsen krymper. O på bankartan (eller -endless) startar en omgång; klarade mål ger poäng och XP, Enter ger nästa mål, förlust eller Esc avslutar och omgången hamnar i sparfilens poänglista (fem bästa). G två gånger startar New Game+: items, banresultat, pengar och debuffar nollställs, skills, XP och japanskan behålls, sällskapet är en person större per varv. Docs, wiki, tester och skärmdumpar uppdaterade. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -7,9 +7,11 @@ package game
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/endless"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/lang"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/nav"
|
||||
@@ -47,6 +49,7 @@ type Game struct {
|
||||
Levels []*level.Level // alla banor, sorterade på id
|
||||
Audio *Audio // nil = tyst
|
||||
Mode nav.Mode // navigeringsläge (= State.NavMode), växlas på bankartan
|
||||
Endless *EndlessRun // pågående omgång i oändligt läge (nil = ingen)
|
||||
|
||||
savePath string // "" = spara inte
|
||||
fixedSeed uint64 // >0: alla banor startar med denna seed (tester/skärmdumpar)
|
||||
@@ -72,6 +75,7 @@ type Options struct {
|
||||
SavePath string // sparfil ("" = standardplatsen)
|
||||
NoSave bool // läs sparfilen men skriv aldrig
|
||||
Ending bool // öppna slutscenen direkt (tester/skärmdumpar)
|
||||
Endless bool // starta oändligt läge direkt (tester/skärmdumpar)
|
||||
Mode string // navigeringsläge från start: "list" eller "signs" (tomt = sparfilens)
|
||||
}
|
||||
|
||||
@@ -141,6 +145,12 @@ func New(opt Options) (*Game, error) {
|
||||
switch {
|
||||
case opt.Ending:
|
||||
g.scene = NewEndingScene(g)
|
||||
case opt.Endless:
|
||||
s, err := g.startEndless(g.seed())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g.scene = s
|
||||
case opt.LevelID != "":
|
||||
s, err := g.startLevel(opt.LevelID)
|
||||
if err != nil {
|
||||
@@ -153,6 +163,77 @@ func New(opt Options) (*Game, error) {
|
||||
return g, nil
|
||||
}
|
||||
|
||||
// EndlessRun är en pågående omgång i oändligt läge.
|
||||
type EndlessRun struct {
|
||||
Seed rng.Seed
|
||||
Step int // 0-baserat: målet som spelas nu
|
||||
Score int // summan av klarade måls poäng
|
||||
Goals int // klarade mål
|
||||
}
|
||||
|
||||
// startEndless börjar en omgång i oändligt läge med första målet.
|
||||
func (g *Game) startEndless(seed rng.Seed) (*LevelScene, error) {
|
||||
g.Endless = &EndlessRun{Seed: seed}
|
||||
return g.endlessGoalScene()
|
||||
}
|
||||
|
||||
// endlessGoalScene bygger scenen för omgångens aktuella mål.
|
||||
func (g *Game) endlessGoalScene() (*LevelScene, error) {
|
||||
e := g.Endless
|
||||
goal, err := endless.Plan(g.Levels, e.Seed, e.Step)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := newLevelSceneFor(g, goal.Level, goal.Base, e.Seed.Derive(fmt.Sprintf("run:%d", e.Step)))
|
||||
s.endless = true
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// endlessEnded bokför ett avgjort mål: vinst ger poäng och nästa mål,
|
||||
// förlust avslutar omgången och lägger in den i poänglistan.
|
||||
func (g *Game) endlessEnded(run *sim.Run) LevelOutcome {
|
||||
var out LevelOutcome
|
||||
e := g.Endless
|
||||
if e == nil {
|
||||
return out
|
||||
}
|
||||
for _, d := range run.NewDebuffs {
|
||||
g.Profile.Add(d.Kind, d.LevelsLeft)
|
||||
}
|
||||
g.Profile.EndLevel()
|
||||
if run.Outcome == sim.Won {
|
||||
e.Score += run.Score.Total
|
||||
e.Goals++
|
||||
e.Step++
|
||||
out.XP = progress.XPFor(run.Score.Total, run.Stars)
|
||||
out.LevelUps = g.Progress.AddXP(out.XP)
|
||||
} else {
|
||||
out.EndlessBest = g.State.AddEndless(save.EndlessRecord{Score: e.Score, Goals: e.Goals, Seed: [2]uint64{e.Seed.A, e.Seed.B}, When: time.Now()})
|
||||
out.EndlessOver = true
|
||||
}
|
||||
g.Save()
|
||||
return out
|
||||
}
|
||||
|
||||
// endEndless avslutar en pågående omgång i förtid (Esc på resultatet).
|
||||
func (g *Game) endEndless() {
|
||||
if e := g.Endless; e != nil && e.Goals > 0 {
|
||||
g.State.AddEndless(save.EndlessRecord{Score: e.Score, Goals: e.Goals, Seed: [2]uint64{e.Seed.A, e.Seed.B}, When: time.Now()})
|
||||
g.Save()
|
||||
}
|
||||
g.Endless = nil
|
||||
}
|
||||
|
||||
// newGamePlus börjar om storyn med skills kvar: items, banresultat, pengar
|
||||
// och pågående omgång nollställs; sällskapet blir större på alla banor.
|
||||
func (g *Game) newGamePlus() {
|
||||
g.Progress.NewGamePlus()
|
||||
*g.Wallet = *sim.NewWallet()
|
||||
g.Profile.Debuffs = nil
|
||||
g.State.Current = nil
|
||||
g.Save()
|
||||
}
|
||||
|
||||
// chainAfter är resans fortsättning efter banan id (delbanorna via next).
|
||||
func (g *Game) chainAfter(id string) []*level.Level {
|
||||
if c := level.ChainOf(g.Levels, id); len(c) > 1 {
|
||||
@@ -264,6 +345,7 @@ func (g *Game) runOptions(id string, seed rng.Seed) sim.RunOptions {
|
||||
Dict: g.Assets.Dict, Know: g.Know, Wallet: g.Wallet, Debuffs: g.Profile.Debuffs, Seed: seed,
|
||||
Extra: eff.Stats, FogRadius: eff.FogRadius, Landmarks: eff.Landmarks, FreeGates: eff.FreeGates,
|
||||
RevealAll: eff.Reveal[id], InfiniteBattery: eff.Battery, Mode: g.Mode, Chain: g.chainAfter(id),
|
||||
ExtraParty: g.Progress.NGPlus,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,6 +393,9 @@ type LevelOutcome struct {
|
||||
NewPoints int
|
||||
Unlocked []string // banor som öppnades
|
||||
StoryCleared bool // sista huvudbanan klarades för första gången: visa slutet
|
||||
|
||||
EndlessOver bool // oändligt läge: omgången tog slut
|
||||
EndlessBest bool // … och blev ny bästa
|
||||
}
|
||||
|
||||
// levelEnded appliceras exakt en gång när en bana avgjorts: debuffar,
|
||||
|
||||
@@ -126,6 +126,20 @@ func shopJustPressed() bool { return virtual.pressed["b"] || inpututil.IsKeyJust
|
||||
// phoneJustPressed: P = kortet i telefonen av/på (meny).
|
||||
func phoneJustPressed() bool { return virtual.pressed["p"] || inpututil.IsKeyJustPressed(ebiten.KeyP) }
|
||||
|
||||
// endlessJustPressed: O startar oändligt läge (när storyn är klar).
|
||||
func endlessJustPressed() bool {
|
||||
return virtual.pressed["o"] || inpututil.IsKeyJustPressed(ebiten.KeyO)
|
||||
}
|
||||
|
||||
// ngPlusJustPressed: G (två gånger) startar New Game+.
|
||||
func ngPlusJustPressed() bool { return virtual.pressed["g"] || inpututil.IsKeyJustPressed(ebiten.KeyG) }
|
||||
|
||||
// miniMoveXYAny rapporterar om någon pil just trycktes (för att avbryta en bekräftelse).
|
||||
func miniMoveXYAny() bool {
|
||||
x, y := miniMoveXY()
|
||||
return x != 0 || y != 0
|
||||
}
|
||||
|
||||
// modeJustPressed: N växlar navigeringsläge (lista/skyltar) på bankartan.
|
||||
func modeJustPressed() bool { return virtual.pressed["n"] || inpututil.IsKeyJustPressed(ebiten.KeyN) }
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ type LevelScene struct {
|
||||
id string
|
||||
level *level.Level // aktuell delbana (byts när resan fortsätter på nästa station)
|
||||
run *sim.Run
|
||||
endless bool // målet är ett lottat mål i oändligt läge (inte storyn)
|
||||
cam *sim.Camera
|
||||
animT float64
|
||||
hideMap bool // M döljer/visar minikartan
|
||||
@@ -66,10 +67,25 @@ func NewLevelScene(g *Game, id string, seed rng.Seed) (*LevelScene, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newLevelSceneFor(g, lvl, id, seed), nil
|
||||
}
|
||||
|
||||
// newLevelSceneFor startar en redan laddad (eller lottad) bana.
|
||||
func newLevelSceneFor(g *Game, lvl *level.Level, id string, seed rng.Seed) *LevelScene {
|
||||
s := &LevelScene{id: id, run: sim.NewRun(lvl, g.runOptions(id, seed))}
|
||||
s.rng = rand.New(rand.NewPCG(1, 2))
|
||||
s.rebind(g)
|
||||
return s, nil
|
||||
return s
|
||||
}
|
||||
|
||||
// ended bokför banans utgång: storyn via levelEnded, oändligt läge via
|
||||
// endlessEnded.
|
||||
func (s *LevelScene) ended(g *Game) {
|
||||
if s.endless {
|
||||
s.outcome = g.endlessEnded(s.run)
|
||||
return
|
||||
}
|
||||
s.outcome = g.levelEnded(s.run, s.id)
|
||||
}
|
||||
|
||||
// rebind bygger om allt som hör till den aktuella delbanan: kamera, räls,
|
||||
@@ -404,6 +420,9 @@ func (s *LevelScene) drawHUD(g *Game, screen *ebiten.Image) {
|
||||
if r.Legs() > 1 {
|
||||
name += fmt.Sprintf(" %d/%d", r.LegNo(), r.Legs())
|
||||
}
|
||||
if s.endless && g.Endless != nil {
|
||||
name = fmt.Sprintf("#%d %s", g.Endless.Step+1, name)
|
||||
}
|
||||
a.Draw(screen, name, 4, 0, colText)
|
||||
destX := 4 + a.Width(name) + 12
|
||||
a.Draw(screen, a.T("hud.destination")+": "+dest, destX, 0, colKnown)
|
||||
|
||||
@@ -17,11 +17,12 @@ import (
|
||||
// linje från flygplatsen, sidobanorna som avstickare ovanför. Låsta banor
|
||||
// är grå, klarade visar stjärnor.
|
||||
type MapScene struct {
|
||||
mains []*level.Level
|
||||
sides map[string][]*level.Level // parent -> sidobanor
|
||||
sel int // vald huvudbana
|
||||
side int // 0 = huvudbanan, 1.. = sidobana nr
|
||||
msg string
|
||||
confirmNG bool // G tryckt en gång: nästa G startar New Game+
|
||||
mains []*level.Level
|
||||
sides map[string][]*level.Level // parent -> sidobanor
|
||||
sel int // vald huvudbana
|
||||
side int // 0 = huvudbanan, 1.. = sidobana nr
|
||||
msg string
|
||||
}
|
||||
|
||||
type mapNode struct {
|
||||
@@ -105,6 +106,26 @@ func (m *MapScene) Update(g *Game) error {
|
||||
g.Save()
|
||||
case modeJustPressed():
|
||||
g.toggleMode()
|
||||
case endlessJustPressed() && g.Progress.StoryCleared:
|
||||
s, err := g.startEndless(g.seed())
|
||||
if err != nil {
|
||||
m.msg = err.Error()
|
||||
return nil
|
||||
}
|
||||
g.SetScene(s)
|
||||
return nil
|
||||
case ngPlusJustPressed() && g.Progress.StoryCleared:
|
||||
if !m.confirmNG {
|
||||
m.confirmNG = true
|
||||
m.msg = g.Assets.T("map.ngplus_confirm")
|
||||
return nil
|
||||
}
|
||||
g.newGamePlus()
|
||||
g.SetScene(NewMapScene(g))
|
||||
return nil
|
||||
}
|
||||
if m.confirmNG && (confirmJustPressed() || miniMoveXYAny()) {
|
||||
m.confirmNG, m.msg = false, "" // något annat tryckt: ingen NG+
|
||||
}
|
||||
if n := len(m.mains); n > 0 {
|
||||
mx, my := miniMoveXY()
|
||||
@@ -207,7 +228,9 @@ func (m *MapScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
if !p.Unlocked(g.Levels, cur.ID) {
|
||||
a.Draw(screen, a.T("map.locked"), 150, 30, colBad)
|
||||
} else if m.msg != "" {
|
||||
a.Draw(screen, m.msg, 150, 30, colBad)
|
||||
for i, l := range a.wrapLine(plainLine(m.msg, colBad), ScreenW-158) {
|
||||
a.drawLines(screen, []line{l}, 150, 30+i*LineH)
|
||||
}
|
||||
}
|
||||
}
|
||||
onoff := a.T("menu.off")
|
||||
@@ -218,12 +241,25 @@ func (m *MapScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
a.T("hud.cash"), g.Wallet.Cash)
|
||||
a.Draw(screen, foot, 8, 30, colDim)
|
||||
a.Draw(screen, fmt.Sprintf(a.T("menu.phone"), onoff)+" "+fmt.Sprintf(a.T("menu.mode"), a.T("mode."+g.Mode.String())), 8, 42, colDim)
|
||||
y := 54
|
||||
if p.StoryCleared { // efter storyn: oändligt läge och New Game+
|
||||
line := a.T("map.endless")
|
||||
if b := g.State.BestEndless(); b != nil {
|
||||
line += fmt.Sprintf(" (%s)", fmt.Sprintf(a.T("map.endless_best"), b.Score, b.Goals))
|
||||
}
|
||||
line += " " + a.T("map.ngplus")
|
||||
if p.NGPlus > 0 {
|
||||
line += fmt.Sprintf(" (%s)", fmt.Sprintf(a.T("map.ngplus_level"), p.NGPlus))
|
||||
}
|
||||
a.Draw(screen, line, 8, y, colKnown)
|
||||
y += LineH
|
||||
}
|
||||
if len(g.Profile.Debuffs) > 0 {
|
||||
var parts []string
|
||||
for _, d := range g.Profile.Debuffs {
|
||||
parts = append(parts, fmt.Sprintf("%s (%d)", a.T("debuff."+string(d.Kind)), d.LevelsLeft))
|
||||
}
|
||||
a.Draw(screen, a.T("hud.debuffs")+": "+strings.Join(parts, ", "), 8, 54, colBad)
|
||||
a.Draw(screen, a.T("hud.debuffs")+": "+strings.Join(parts, ", "), 8, y, colBad)
|
||||
}
|
||||
a.Draw(screen, a.T("map.hint"), 8, ScreenH-24, colDim)
|
||||
a.Draw(screen, fmt.Sprintf("%s (%s)", buildinfo.Version, buildinfo.Mode), 8, ScreenH-12, colDim)
|
||||
|
||||
@@ -38,6 +38,17 @@ func (r *ResultScene) Update(g *Game) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// hint är tangentraden längst ner: oändligt läge har inget "spela om".
|
||||
func (r *ResultScene) hint(a *Assets) string {
|
||||
if r.from.endless {
|
||||
if r.from.outcome.EndlessOver {
|
||||
return a.T("result.hint_endless_over")
|
||||
}
|
||||
return a.T("result.hint_endless")
|
||||
}
|
||||
return a.T("result.hint")
|
||||
}
|
||||
|
||||
func (r *ResultScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
r.from.Draw(g, screen) // banan i bakgrunden
|
||||
run := r.from.run
|
||||
@@ -158,5 +169,5 @@ func (r *ResultScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
a.Draw(screen, fmt.Sprintf("%s Lv%d %d %s %s %d (%d %s)", a.T("hud.japanese"), run.Know.Level(), run.Know.Count(),
|
||||
a.T("hud.words"), a.T("words.points"), run.Know.Points, run.Know.Helps, a.T("result.helps")), 28, y, colDim)
|
||||
}
|
||||
a.Draw(screen, a.T("result.hint"), 28, 161, colDim)
|
||||
a.Draw(screen, r.hint(a), 28, 161, colDim)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user