M6: plånbok, biljetter, kortet i telefonen och seedade minispel
All checks were successful
release / build-release (push) Successful in 1m41s

- sim.Wallet: IC-kort, kontanter, pappersbiljett, telefon med batteri,
  spenderat; följer med mellan banor (Game.Wallet)
- internal/minigame: gemensamt Game-gränssnitt (Update/Done/Success), all
  slump från del-seeds: TopUp (hitta チャージ, belopp, mata sedlar),
  Ticket (taxetavla med resmål på japanska), Crowd (tidsfönster, två
  försök), Ride (stationerna passerar, kliv av vid resmålet)
- sim.Run: spärren kräver biljett på biljettbanor annars användbart kort;
  telefonen ger ×1,1 fart men 15 % seedad omskanning (frysning 1,5 s),
  dött batteri gör kortet oanvändbart; automaten startar påfyllning eller
  biljettköp; tåget kontrollerar biljetten; fulla tåg kräver trängsel;
  resan avgör vinst/förlust (fel station, åkte förbi, fel tåg); StepMini
  låter klockan och sällskapet ticka medan rörelsen pausar
- level: ticketRequired, rideStops, Train.Crowded; 002 är biljettbana med
  fullt tåg och fyra stopp; ordboken har stationer (kind) + sju nya namn
- game: vyer för minispelen, plånbok i HUD, P i menyn = kort i telefonen,
  förlustorsaker och spenderat på resultatet, flaggor -phone/-card,
  skriptkommandon hold/waitstop; inforuta/minikarta döljs under minispel
- plan: M7b (Björn) – tågvind, ankomstindikator nära rälsen,
  in-/utkörningsanimation för tåg och bussar – i spelbeskrivning, roadmap,
  testplan
- docs/README/wiki med tre nya bilder (automat, biljettavla, tågresa)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 09:50:12 +02:00
parent 3f56020777
commit 9fc7e94074
45 changed files with 1779 additions and 166 deletions

View File

@@ -9,6 +9,7 @@ import (
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/lang"
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/rng"
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/sim"
)
// Intern upplösning. Allt ritas i denna storlek och skalas sedan upp med
@@ -30,6 +31,7 @@ type Scene interface {
type Game struct {
Assets *Assets
Know *lang.Knowledge // spelarens japanska – följer med mellan banor (sparas i M8)
Wallet *sim.Wallet // pengar, kort, telefon, biljett – följer med mellan banor
fixedSeed uint64 // >0: alla banor startar med denna seed (tester/skärmdumpar)
scene Scene
next Scene
@@ -46,6 +48,8 @@ type Options struct {
KnownWords []string // ord spelaren kan från start (för tester/skärmdumpar)
Points int // språkpoäng från start (för tester/skärmdumpar)
Seed uint64 // >0: fast seed för banorna (för tester/skärmdumpar)
Phone bool // kortet i telefonen från start
CardYen int // >=0: kortets startsaldo (för tester/skärmdumpar), -1 = standard
}
// New laddar resurser och bygger spelet.
@@ -60,13 +64,18 @@ func New(opt Options) (*Game, error) {
}
g.Know.Points = opt.Points
g.fixedSeed = opt.Seed
g.Wallet = sim.NewWallet()
g.Wallet.Phone = opt.Phone
if opt.CardYen >= 0 {
g.Wallet.Card = opt.CardYen
}
if opt.Script != "" {
if g.script, err = parseScript(opt.Script); err != nil {
return nil, err
}
}
if opt.LevelID != "" {
s, err := NewLevelScene(a, opt.LevelID, g.Know, g.seed())
s, err := NewLevelScene(a, opt.LevelID, g.Know, g.Wallet, g.seed())
if err != nil {
return nil, err
}

View File

@@ -81,6 +81,40 @@ func wordsJustPressed() bool {
return virtual.pressed["l"] || inpututil.IsKeyJustPressed(ebiten.KeyL) || gamepadJustPressed(ebiten.StandardGamepadButtonLeftRight)
}
// miniMove: -1/+1 vid just-tryck på pil/WASD (upp/vänster = -1, ner/höger = +1).
func miniMove() int {
switch {
case virtual.pressed["up"] || virtual.pressed["left"] || inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) || inpututil.IsKeyJustPressed(ebiten.KeyArrowLeft) ||
inpututil.IsKeyJustPressed(ebiten.KeyW) || inpututil.IsKeyJustPressed(ebiten.KeyA) ||
gamepadJustPressed(ebiten.StandardGamepadButtonLeftTop) || gamepadJustPressed(ebiten.StandardGamepadButtonLeftLeft):
return -1
case virtual.pressed["down"] || virtual.pressed["right"] || inpututil.IsKeyJustPressed(ebiten.KeyArrowDown) || inpututil.IsKeyJustPressed(ebiten.KeyArrowRight) ||
inpututil.IsKeyJustPressed(ebiten.KeyS) || inpututil.IsKeyJustPressed(ebiten.KeyD) ||
gamepadJustPressed(ebiten.StandardGamepadButtonLeftBottom) || gamepadJustPressed(ebiten.StandardGamepadButtonLeftRight):
return 1
}
return 0
}
// confirmHeld: Enter/mellanslag eller A hålls nere (sedelinmatning).
func confirmHeld() bool {
if virtual.pressed["enter"] || virtual.holdEnter {
return true
}
if ebiten.IsKeyPressed(ebiten.KeyEnter) || ebiten.IsKeyPressed(ebiten.KeySpace) {
return true
}
for _, id := range ebiten.AppendGamepadIDs(nil) {
if ebiten.IsStandardGamepadLayoutAvailable(id) && ebiten.IsStandardGamepadButtonPressed(id, ebiten.StandardGamepadButtonRightBottom) {
return true
}
}
return false
}
// phoneJustPressed: P = kortet i telefonen av/på (meny).
func phoneJustPressed() bool { return virtual.pressed["p"] || inpututil.IsKeyJustPressed(ebiten.KeyP) }
// mapJustPressed: M eller gamepadens vänstra mittknapp (Back/Select) = minikartan av/på.
func mapJustPressed() bool {
return virtual.pressed["m"] || inpututil.IsKeyJustPressed(ebiten.KeyM) || gamepadJustPressed(ebiten.StandardGamepadButtonCenterLeft)

View File

@@ -0,0 +1,156 @@
package game
import (
"fmt"
"image"
"image/color"
"strings"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/vector"
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/lang"
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/minigame"
)
// Minispelen ritas som en panel över banan. Logiken bor i internal/minigame.
func (s *LevelScene) drawMinigame(g *Game, screen *ebiten.Image) {
a := g.Assets
r := s.run
panel := image.Rect(12, 28, ScreenW-12, ScreenH-10)
vector.DrawFilledRect(screen, float32(panel.Min.X), float32(panel.Min.Y), float32(panel.Dx()), float32(panel.Dy()), colPanel, false)
vector.DrawFilledRect(screen, float32(panel.Min.X), float32(panel.Min.Y), float32(panel.Dx()), 3, colKnown, false)
switch m := r.Mini.(type) {
case *minigame.TopUp:
s.drawTopUp(a, screen, m, panel)
case *minigame.Ticket:
s.drawTicket(a, screen, m, panel)
case *minigame.Crowd:
s.drawCrowd(a, screen, m, panel)
case *minigame.Ride:
s.drawRide(a, screen, m, panel)
}
}
// jp ritar japansk text sedd genom spelarens kunskap (kända ord i gult).
func (s *LevelScene) jp(a *Assets, screen *ebiten.Image, text string, x, y int) {
rd := lang.Render(a.Dict, s.run.Know, text, UILang)
a.drawLines(screen, []line{shownLine(rd.Original, colText, false)}, x, y)
}
func (s *LevelScene) drawTopUp(a *Assets, screen *ebiten.Image, m *minigame.TopUp, p image.Rectangle) {
x, y := p.Min.X+8, p.Min.Y+8
a.DrawBig(screen, "チャージ", x, y, colKnown)
a.Draw(screen, a.T("mini.topup.title"), x+60, y+2, colText)
y += 20
switch m.Stage {
case minigame.TopUpPickButton:
a.Draw(screen, a.T("mini.topup.pick"), x, y, colDim)
y += LineH + 4
for i, b := range m.Buttons {
bx, by := x+(i%2)*140, y+(i/2)*26
col := colBar
if i == m.Cursor {
col = color.RGBA{90, 105, 136, 255}
}
vector.DrawFilledRect(screen, float32(bx), float32(by), 132, 22, col, false)
s.jp(a, screen, b, bx+6, by+5)
}
if m.Penalty > 0 {
a.Draw(screen, "ピッ! "+a.T("mini.topup.wrong"), x, y+56, colBad)
}
case minigame.TopUpPickAmount:
a.Draw(screen, a.T("mini.topup.amount"), x, y, colDim)
y += LineH + 4
for i, amt := range m.Amounts {
col := colText
if i == m.Cursor {
vector.DrawFilledRect(screen, float32(x-2), float32(y+i*LineH-1), 120, LineH, colBar, false)
col = colKnown
}
a.Draw(screen, fmt.Sprintf("%d円 (%d¥)", amt, amt), x, y+i*LineH, col)
}
case minigame.TopUpFeed:
a.Draw(screen, fmt.Sprintf(a.T("mini.topup.feed"), m.Amount), x, y, colText)
y += LineH + 8
vector.DrawFilledRect(screen, float32(x), float32(y), float32(p.Dx()-16), 8, colBar, false)
vector.DrawFilledRect(screen, float32(x), float32(y), float32(float64(p.Dx()-16)*m.Fill), 8, colGood, false)
}
a.Draw(screen, a.T("mini.cancel"), x, p.Max.Y-16, colDim)
}
func (s *LevelScene) drawTicket(a *Assets, screen *ebiten.Image, m *minigame.Ticket, p image.Rectangle) {
x, y := p.Min.X+8, p.Min.Y+8
a.DrawBig(screen, "きっぷ", x, y, colKnown)
a.Draw(screen, a.T("mini.ticket.title"), x+48, y+2, colText)
y += 20
a.Draw(screen, fmt.Sprintf(a.T("mini.ticket.hint"), s.run.Level.Destination.Get("romaji"), m.Cash), x, y, colDim)
y += LineH + 4
for i, o := range m.Options {
ry := y + i*LineH
col := colText
if i == m.Cursor {
vector.DrawFilledRect(screen, float32(x-2), float32(ry-1), float32(p.Dx()-12), LineH, colBar, false)
}
if o.Fare > m.Cash {
col = colDim
}
s.jp(a, screen, o.Dest, x, ry)
fare := fmt.Sprintf("%d円", o.Fare)
a.Draw(screen, fare, p.Max.X-10-a.Width(fare), ry, col)
}
a.Draw(screen, a.T("mini.cancel"), x, p.Max.Y-16, colDim)
}
func (s *LevelScene) drawCrowd(a *Assets, screen *ebiten.Image, m *minigame.Crowd, p image.Rectangle) {
x, y := p.Min.X+8, p.Min.Y+8
a.DrawBig(screen, "満員", x, y, colBad)
a.Draw(screen, a.T("mini.crowd.title"), x+36, y+2, colText)
y += 24
a.Draw(screen, fmt.Sprintf(a.T("mini.crowd.hint"), m.MaxTries-m.Attempts), x, y, colDim)
y += LineH + 10
w := float32(p.Dx() - 16)
vector.DrawFilledRect(screen, float32(x), float32(y), w, 14, colBar, false)
vector.DrawFilledRect(screen, float32(x)+w*float32(m.ZoneLo), float32(y), w*float32(m.ZoneHi-m.ZoneLo), 14, colGood, false)
vector.DrawFilledRect(screen, float32(x)+w*float32(m.Pos)-1, float32(y)-3, 3, 20, colText, false)
}
func (s *LevelScene) drawRide(a *Assets, screen *ebiten.Image, m *minigame.Ride, p image.Rectangle) {
x, y := p.Min.X+8, p.Min.Y+8
a.DrawBig(screen, "次は", x, y, colDim)
a.Draw(screen, a.T("mini.ride.title"), x+36, y+2, colText)
y += 22
cur := m.Current()
a.DrawBig(screen, cur, x, y, colText)
rd := lang.Render(a.Dict, s.run.Know, cur, UILang)
a.drawLines(screen, []line{shownLine(rd.Partial, colText, false)}, x+a.Width(cur)*12/10+12, y+2)
lvl := s.run.Level
if lang.ShowEnglish(lvl.ID, m.Index, 77, lvl.Difficulty) {
if e := a.Dict.Lookup(cur); e != nil {
a.Draw(screen, e.Romaji, x, y+16, colEnglish)
}
}
y += 34
// Linjen: en prick per station, den aktuella större.
for i := range m.Stops {
cx := x + 6 + i*22
col := colDim
if i == m.Index {
col = colText
}
if i < m.Index {
col = colBar
}
vector.DrawFilledRect(screen, float32(cx), float32(y+3), 14, 2, colBar, false)
vector.DrawFilledRect(screen, float32(cx-3), float32(y), 6, 8, col, false)
}
y += 14
vector.DrawFilledRect(screen, float32(x), float32(y), float32(p.Dx()-16), 3, colBar, false)
vector.DrawFilledRect(screen, float32(x), float32(y), float32(float64(p.Dx()-16)*(1-m.Timer/m.StopSec)), 3, colKnown, false)
hint := a.T("mini.ride.hint")
if dest := lvl.Destination.Get("romaji"); dest != "" {
hint = strings.Replace(hint, "%s", dest, 1)
}
a.Draw(screen, hint, x, p.Max.Y-16, colGood)
}

View File

@@ -13,6 +13,7 @@ import (
"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/minigame"
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/rng"
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/sim"
)
@@ -22,6 +23,7 @@ const (
deniedShowSec = 2.0
learnedShowSec = 3.0
progressedShowSec = 2.5
flashShowSec = 2.5
)
// LevelScene är själva spelandet av en bana. All spellogik ligger i sim.Run;
@@ -36,14 +38,14 @@ type LevelScene struct {
// NewLevelScene laddar banan och ställer spelaren på startrutan. know är
// spelarens japanska, som delas mellan banor; seed styr omgångens slump.
func NewLevelScene(a *Assets, id string, know *lang.Knowledge, seed rng.Seed) (*LevelScene, error) {
func NewLevelScene(a *Assets, id string, know *lang.Knowledge, wallet *sim.Wallet, seed rng.Seed) (*LevelScene, error) {
lvl, err := level.LoadFile(filepath.Join(a.LevelsDir, id+".json"))
if err != nil {
return nil, err
}
s := &LevelScene{
id: id,
run: sim.NewRun(lvl, sim.RunOptions{Dict: a.Dict, Know: know, Seed: seed}),
run: sim.NewRun(lvl, sim.RunOptions{Dict: a.Dict, Know: know, Wallet: wallet, Seed: seed}),
cam: sim.NewCamera(ScreenW, ScreenH, float64(lvl.Width()*TileSize), float64(lvl.Height()*TileSize)),
}
s.followPlayer()
@@ -62,12 +64,27 @@ func (s *LevelScene) TrainHere() bool {
return tr != nil && at
}
// RideAtDest rapporterar om tågresan står vid resmålet (skriptläget). Utan
// pågående resa: true, så att skriptet inte hänger.
func (s *LevelScene) RideAtDest() bool {
g, ok := s.run.Mini.(*minigame.Ride)
return !ok || g.Index == g.Dest
}
func (s *LevelScene) followPlayer() {
x, y := s.run.Player.WorldPos(TileSize)
s.cam.Follow(x+TileSize/2, y+TileSize/2)
}
func (s *LevelScene) Update(g *Game) error {
if s.run.Mini != nil {
// Minispel: all input går dit; Esc avbryter minispelet, inte banan.
s.run.StepMini(dt, minigame.Input{Move: miniMove(), Confirm: confirmJustPressed(), Hold: confirmHeld(), Cancel: backJustPressed()})
if s.run.Outcome != sim.Ongoing {
g.SetScene(NewResultScene(s))
}
return nil
}
if backJustPressed() {
m, err := NewMenuScene(g.Assets)
if err != nil {
@@ -172,6 +189,9 @@ func (s *LevelScene) Draw(g *Game, screen *ebiten.Image) {
s.drawUrge(g, screen, s.run.Party.Members[0], int(ox+math.Round(px)), int(oy+math.Round(py)))
s.drawHUD(g, screen)
if s.run.Mini != nil {
s.drawMinigame(g, screen)
}
}
// memberTints färgar följarnas sprites tills de har egna.
@@ -217,14 +237,24 @@ func (s *LevelScene) drawHUD(g *Game, screen *ebiten.Image) {
if r.Know.ShowsDestinationJP() { // hint från nivå 1: resmålet på japanska, att matcha mot skyltarna
dest += " " + r.Level.Destination.Get("ja")
}
a.Draw(screen, r.Level.Name.Get(UILang), 4, 0, colText)
a.Draw(screen, a.T("hud.destination")+": "+dest, 4+a.Width(r.Level.Name.Get(UILang))+12, 0, colKnown)
a.Draw(screen, fmt.Sprintf("%s %3.0fs %s %d %s %d¥ %s Lv%d·%d", a.T("hud.time"), r.Time,
a.T("hud.steps"), r.Steps, a.T("hud.card"), r.Card.Balance, a.T("hud.japanese"), r.Know.Level(), r.Know.Count()), 4, LineH, colDim)
name := r.Level.Name.Get(UILang)
a.Draw(screen, name, 4, 0, colText)
destX := 4 + a.Width(name) + 12
a.Draw(screen, a.T("hud.destination")+": "+dest, destX, 0, colKnown)
a.Draw(screen, fmt.Sprintf("%s Lv%d·%d", a.T("hud.japanese"), r.Know.Level(), r.Know.Count()),
destX+a.Width(a.T("hud.destination")+": "+dest)+12, 0, colDim)
wallet := fmt.Sprintf("%s %3.0fs %s %d¥ %s %d¥", a.T("hud.time"), r.Time, a.T("hud.card"), r.Wallet.Card, a.T("hud.cash"), r.Wallet.Cash)
if r.Wallet.Phone {
wallet += fmt.Sprintf(" %s %.0f%%", a.T("hud.phone"), r.Wallet.Battery)
}
if r.Wallet.Ticket != "" {
wallet += " " + a.T("hud.ticket") + " " + r.Wallet.Ticket
}
a.Draw(screen, wallet, 4, LineH, colDim)
s.drawMeters(g, screen)
if r.Outcome != sim.Ongoing {
return // resultatpanelen tar över; ingen inforuta eller minikarta
if r.Outcome != sim.Ongoing || r.Mini != nil {
return // resultatpanelen eller minispelet tar över; ingen inforuta eller minikarta
}
lines, accent, bar := s.infoLines(a)
if entry := a.Dict.Lookup(r.Learned); entry != nil && r.Time-r.LearnedAt < learnedShowSec {
@@ -241,6 +271,13 @@ func (s *LevelScene) drawHUD(g *Game, screen *ebiten.Image) {
}
lines = append(lines, a.wrapLine(plainLine(strings.Join(parts, " "), colWarn), ScreenW-8)...)
}
if r.Flashing(flashShowSec) {
col := colBad
if r.FlashKey == sim.FlashRescan {
col = colWarn
}
lines = append(lines, a.wrapLine(plainLine(a.T(r.FlashKey), col), ScreenW-8)...)
}
// Inforutans läge: nederkant, men flyttas upp under HUD:en om spelaren
// (eller tåget under hen) annars skulle hamna bakom den.
@@ -344,9 +381,21 @@ func (s *LevelScene) infoLines(a *Assets) (lines []line, accent color.Color, bar
return []line{plainLine(fmt.Sprintf(a.T("hud.kiosk_prompt"), n, sim.KioskPriceYen*n), colGood)}, colGood, false
}
return []line{plainLine(a.T("hud.kiosk_none"), colDim)}, accent, false
case level.TicketMachine:
if lvl.TicketRequired {
return []line{plainLine(fmt.Sprintf(a.T("hud.machine_ticket"), r.Wallet.Cash), colGood)}, colGood, false
}
return []line{plainLine(fmt.Sprintf(a.T("hud.machine_topup"), r.Wallet.Card, r.Wallet.Cash), colGood)}, colGood, false
}
if r.DeniedAt >= 0 && r.Time-r.DeniedAt < deniedShowSec {
return []line{plainLine(a.T("hud.gate_denied"), colBad)}, colBad, false
key := "hud.gate_denied"
switch {
case lvl.TicketRequired:
key = sim.FlashNoTicket
case r.Wallet.Phone && r.Wallet.Battery <= 0:
key = "hud.phone_dead"
}
return []line{plainLine(a.T(key), colBad)}, colBad, false
}
return nil, accent, false
}

View File

@@ -58,10 +58,13 @@ func (m *MenuScene) Update(g *Game) error {
g.SetScene(NewWordsScene(g.Assets, m))
return nil
}
if phoneJustPressed() {
g.Wallet.Phone = !g.Wallet.Phone
}
if n := len(m.entries); n > 0 {
m.sel = (m.sel + menuMove() + n) % n
if confirmJustPressed() {
s, err := NewLevelScene(g.Assets, m.entries[m.sel].id, g.Know, g.seed())
s, err := NewLevelScene(g.Assets, m.entries[m.sel].id, g.Know, g.Wallet, g.seed())
if err != nil {
m.err = err
return nil
@@ -91,8 +94,12 @@ func (m *MenuScene) Draw(g *Game, screen *ebiten.Image) {
if m.err != nil {
a.Draw(screen, "FEL: "+m.err.Error(), 8, ScreenH-40, colBad)
}
a.Draw(screen, fmt.Sprintf("%s Lv%d %d %s %s %d", a.T("hud.japanese"), g.Know.Level(), g.Know.Count(), a.T("hud.words"),
a.T("words.points"), g.Know.Points), 8, ScreenH-38, colDim)
onoff := a.T("menu.off")
if g.Wallet.Phone {
onoff = a.T("menu.on")
}
a.Draw(screen, fmt.Sprintf("%s Lv%d %d %s %s %d %s %d¥ %s %d¥ %s", a.T("hud.japanese"), g.Know.Level(), g.Know.Count(), a.T("hud.words"),
a.T("words.points"), g.Know.Points, a.T("hud.card"), g.Wallet.Card, a.T("hud.cash"), g.Wallet.Cash, fmt.Sprintf(a.T("menu.phone"), onoff)), 8, ScreenH-38, colDim)
a.Draw(screen, a.T("menu.hint"), 8, ScreenH-26, colDim)
a.Draw(screen, fmt.Sprintf("%s (%s)", buildinfo.Version, buildinfo.Mode), 8, ScreenH-13, colDim)
}

View File

@@ -23,7 +23,7 @@ func (r *ResultScene) Update(g *Game) error {
case wordsJustPressed():
g.SetScene(NewWordsScene(g.Assets, r))
case inpututil_R():
s, err := NewLevelScene(g.Assets, r.from.id, g.Know, g.seed())
s, err := NewLevelScene(g.Assets, r.from.id, g.Know, g.Wallet, g.seed())
if err != nil {
return err
}
@@ -44,7 +44,7 @@ func (r *ResultScene) Draw(g *Game, screen *ebiten.Image) {
a := g.Assets
vector.DrawFilledRect(screen, 20, 36, ScreenW-40, 120, colPanel, false)
accent, headline := colBad, a.T("level.wrong_train")
accent, headline := colBad, a.T("level.lost")
if run.Outcome == sim.Won {
accent, headline = colGood, a.T("level.cleared")
}
@@ -63,8 +63,11 @@ func (r *ResultScene) Draw(g *Game, screen *ebiten.Image) {
a.Draw(screen, fmt.Sprintf("%s %s %s → %s", a.T("hud.track"), tr.Track, tr.Line.Get("romaji"), tr.Destination.Get("romaji")), 28, y, colDim)
y += LineH
if run.Outcome == sim.Lost {
for _, l := range a.wrapLine(plainLine(fmt.Sprintf(a.T("result.wrong_detail"), tr.Destination.Get("romaji"),
run.Level.Destination.Get("romaji")), colText), ScreenW-56) {
reason := a.T(run.LostReason)
if run.LostReason == sim.LostWrongTrain {
reason = fmt.Sprintf(a.T("result.wrong_detail"), tr.Destination.Get("romaji"), run.Level.Destination.Get("romaji"))
}
for _, l := range a.wrapLine(plainLine(reason, colText), ScreenW-56) {
a.drawLines(screen, []line{l}, 28, y)
y += LineH
}
@@ -74,8 +77,8 @@ func (r *ResultScene) Draw(g *Game, screen *ebiten.Image) {
a.Draw(screen, fmt.Sprintf("%s %.1fs (%s %.0fs) %s %d %s %.0f %%", a.T("hud.time"), run.Time, a.T("result.par"), run.ParSec(),
a.T("hud.steps"), run.Steps, a.T("result.explored"), 100*run.Fog.Explored(run.Level)), 28, y, colText)
y += LineH
a.Draw(screen, fmt.Sprintf("%s %d %s %d¥ %s %d", a.T("result.gates"), run.GatePasses, a.T("result.card"), run.Card.Balance,
a.T("result.served"), run.Served), 28, y, colText)
a.Draw(screen, fmt.Sprintf("%s %d %s %d¥ %s %d¥ %s %d %s %d", a.T("result.gates"), run.GatePasses, a.T("result.card"), run.Wallet.Card,
a.T("result.spent"), run.Wallet.Spent, a.T("result.missed"), run.MissedTrains, a.T("result.served"), run.Served), 28, y, colText)
y += LineH
if run.Outcome == sim.Won {
msg := fmt.Sprintf("+%d %s", run.PointsEarned, a.T("words.points"))

View File

@@ -19,7 +19,9 @@ import (
// Stegen skiljs med ';':
//
// wait N vänta N bildrutor
// hold N håll Enter nere N bildrutor (sedelinmatning)
// waittrain vänta tills tåget vid perrongen spelaren står på är inne
// waitstop vänta tills tågresan står vid resmålet
// press KEY tryck enter | esc | r | h | l | up | down en bildruta
// move DIR N gå N rutor åt up | down | left | right (stannar exakt där)
// shot PATH spara aktuell bild som PNG (intern upplösning × shotScale)
@@ -51,8 +53,9 @@ type script struct {
// virtual är den simulerade inputen som input.go läser före riktig input.
var virtual struct {
held sim.Dir
pressed map[string]bool
held sim.Dir
pressed map[string]bool
holdEnter bool
}
func parseScript(src string) (*script, error) {
@@ -64,9 +67,9 @@ func parseScript(src string) (*script, error) {
}
st := scriptStep{cmd: f[0]}
switch f[0] {
case "wait":
case "wait", "hold":
if len(f) != 2 {
return nil, fmt.Errorf("script: 'wait N'")
return nil, fmt.Errorf("script: '%s N'", f[0])
}
st.n, _ = strconv.Atoi(f[1])
case "press":
@@ -88,7 +91,7 @@ func parseScript(src string) (*script, error) {
return nil, fmt.Errorf("script: 'shot PATH'")
}
st.arg = f[1]
case "quit", "waittrain":
case "quit", "waittrain", "waitstop":
default:
return nil, fmt.Errorf("script: okänt kommando %q", f[0])
}
@@ -118,13 +121,17 @@ type playerTiler interface {
}
// trainWaiter implementeras av scener där man kan vänta på ett tåg.
type trainWaiter interface{ TrainHere() bool }
type trainWaiter interface {
TrainHere() bool
RideAtDest() bool
}
// tick körs i början av varje Update. frame är senast ritade bild.
// Returnerar ebiten.Termination när skriptet vill avsluta.
func (s *script) tick(g *Game, frame *ebiten.Image) error {
virtual.held = sim.None
virtual.pressed = map[string]bool{}
virtual.holdEnter = false
if s.i >= len(s.steps) {
return nil
}
@@ -135,6 +142,12 @@ func (s *script) tick(g *Game, frame *ebiten.Image) error {
if s.counter >= st.n {
s.advance()
}
case "hold":
virtual.holdEnter = true
s.counter++
if s.counter >= st.n {
s.advance()
}
case "press":
virtual.pressed[st.arg] = true
s.advance()
@@ -174,6 +187,10 @@ func (s *script) tick(g *Game, frame *ebiten.Image) error {
if tw, ok := g.scene.(trainWaiter); !ok || tw.TrainHere() {
s.advance()
}
case "waitstop":
if tw, ok := g.scene.(trainWaiter); !ok || tw.RideAtDest() {
s.advance()
}
case "shot":
if err := saveShot(frame, st.arg); err != nil {
return err