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

View File

@@ -32,6 +32,7 @@ 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
Profile *sim.Profile // aktiva debuffar – följer med mellan banor
fixedSeed uint64 // >0: alla banor startar med denna seed (tester/skärmdumpar)
scene Scene
next Scene
@@ -49,6 +50,7 @@ type Options struct {
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
Debuffs []string // debuffar från start (för tester/skärmdumpar)
CardYen int // >=0: kortets startsaldo (för tester/skärmdumpar), -1 = standard
}
@@ -65,6 +67,10 @@ func New(opt Options) (*Game, error) {
g.Know.Points = opt.Points
g.fixedSeed = opt.Seed
g.Wallet = sim.NewWallet()
g.Profile = &sim.Profile{}
for _, d := range opt.Debuffs {
g.Profile.Add(sim.DebuffKind(d), 1)
}
g.Wallet.Phone = opt.Phone
if opt.CardYen >= 0 {
g.Wallet.Card = opt.CardYen
@@ -75,7 +81,7 @@ func New(opt Options) (*Game, error) {
}
}
if opt.LevelID != "" {
s, err := NewLevelScene(a, opt.LevelID, g.Know, g.Wallet, g.seed())
s, err := NewLevelScene(a, opt.LevelID, g, g.seed())
if err != nil {
return nil, err
}
@@ -127,3 +133,12 @@ func (g *Game) seed() rng.Seed {
}
return rng.NewSeed()
}
// levelEnded appliceras exakt en gång när en bana avgjorts: banans nya
// debuffar läggs på profilen och alla räknas ned en bana.
func (g *Game) levelEnded(run *sim.Run) {
for _, d := range run.NewDebuffs {
g.Profile.Add(d.Kind, d.LevelsLeft)
}
g.Profile.EndLevel()
}

View File

@@ -81,19 +81,21 @@ 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 {
// miniMoveXY: just-tryck på pil/WASD/d-pad som (x, y) i -1/0/+1.
func miniMoveXY() (x, y 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
case virtual.pressed["left"] || inpututil.IsKeyJustPressed(ebiten.KeyArrowLeft) || inpututil.IsKeyJustPressed(ebiten.KeyA) || gamepadJustPressed(ebiten.StandardGamepadButtonLeftLeft):
x = -1
case virtual.pressed["right"] || inpututil.IsKeyJustPressed(ebiten.KeyArrowRight) || inpututil.IsKeyJustPressed(ebiten.KeyD) || gamepadJustPressed(ebiten.StandardGamepadButtonLeftRight):
x = 1
}
return 0
switch {
case virtual.pressed["up"] || inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) || inpututil.IsKeyJustPressed(ebiten.KeyW) || gamepadJustPressed(ebiten.StandardGamepadButtonLeftTop):
y = -1
case virtual.pressed["down"] || inpututil.IsKeyJustPressed(ebiten.KeyArrowDown) || inpututil.IsKeyJustPressed(ebiten.KeyS) || gamepadJustPressed(ebiten.StandardGamepadButtonLeftBottom):
y = 1
}
return x, y
}
// confirmHeld: Enter/mellanslag eller A hålls nere (sedelinmatning).

View File

@@ -45,6 +45,21 @@ func (s *LevelScene) drawTopUp(a *Assets, screen *ebiten.Image, m *minigame.TopU
a.Draw(screen, a.T("mini.topup.title"), x+60, y+2, colText)
y += 20
switch m.Stage {
case minigame.TopUpPickMember:
a.Draw(screen, a.T("mini.topup.member"), x, y, colDim)
y += LineH + 4
for i, name := range m.Members {
col := colText
if i == m.Cursor {
vector.DrawFilledRect(screen, float32(x-2), float32(y+i*LineH-1), 200, LineH, colBar, false)
col = colKnown
}
bal := s.run.Wallet.Card
if i > 0 {
bal = s.run.Party.Members[i].Card
}
a.Draw(screen, fmt.Sprintf("%-8s %5d¥", name, bal), x, y+i*LineH, col)
}
case minigame.TopUpPickButton:
a.Draw(screen, a.T("mini.topup.pick"), x, y, colDim)
y += LineH + 4
@@ -85,6 +100,10 @@ func (s *LevelScene) drawTicket(a *Assets, screen *ebiten.Image, m *minigame.Tic
a.DrawBig(screen, "きっぷ", x, y, colKnown)
a.Draw(screen, a.T("mini.ticket.title"), x+48, y+2, colText)
y += 20
if m.Stage == minigame.TicketPickSeat {
s.drawSeats(a, screen, m, p, x, y)
return
}
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 {
@@ -154,3 +173,64 @@ func (s *LevelScene) drawRide(a *Assets, screen *ebiten.Image, m *minigame.Ride,
}
a.Draw(screen, hint, x, p.Max.Y-16, colGood)
}
// prefText beskriver ett önskemål.
func prefText(a *Assets, m *minigame.Ticket, p minigame.SeatPref) string {
switch p.Kind {
case minigame.PrefWindow:
return a.T("seat.window")
case minigame.PrefAisle:
return a.T("seat.aisle")
case minigame.PrefLeft:
return a.T("seat.left")
case minigame.PrefRight:
return a.T("seat.right")
default:
return fmt.Sprintf(a.T("seat.with"), m.Members[p.With])
}
}
// drawSeats ritar vagnen: tre rader, fönster V | gång | fönster H, och
// sällskapets önskemål till höger.
func (s *LevelScene) drawSeats(a *Assets, screen *ebiten.Image, m *minigame.Ticket, p image.Rectangle, x, y int) {
a.Draw(screen, fmt.Sprintf(a.T("mini.seat.hint"), m.Members[m.Current]), x, y, colDim)
y += LineH + 4
const sw, sh, gap = 22, 18, 10
a.Draw(screen, a.T("seat.front"), x+2, y-2, colDim)
y += 10
for row := 0; row < minigame.SeatRows; row++ {
for col := 0; col < minigame.SeatCols; col++ {
sx := x + col*(sw+2)
if col >= 2 {
sx += gap
}
sy := y + row*(sh+3)
seat := minigame.Seat{Row: row, Col: col}
bg := colBar
if m.SeatTaken(seat) {
bg = color.RGBA{90, 105, 136, 255}
}
vector.DrawFilledRect(screen, float32(sx), float32(sy), sw, sh, bg, false)
if seat == m.Seat {
vector.DrawFilledRect(screen, float32(sx), float32(sy), sw, 2, colKnown, false)
vector.DrawFilledRect(screen, float32(sx), float32(sy+sh-2), sw, 2, colKnown, false)
}
for i, as := range m.Assigned {
if as == seat {
a.Draw(screen, string([]rune(m.Members[i])[:1]), sx+8, sy+3, colText)
}
}
}
}
// Önskemål.
px := x + 4*(sw+2) + gap + 14
a.Draw(screen, a.T("mini.seat.prefs"), px, y-10, colDim)
for i, pr := range m.Prefs {
col := colText
if pr.Member == m.Current {
col = colKnown
}
a.Draw(screen, m.Members[pr.Member]+": "+prefText(a, m, pr), px, y+i*LineH, col)
}
a.Draw(screen, a.T("mini.cancel"), x, p.Max.Y-16, colDim)
}

View File

@@ -36,16 +36,16 @@ type LevelScene struct {
hideMap bool // M döljer/visar minikartan
}
// 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, wallet *sim.Wallet, seed rng.Seed) (*LevelScene, error) {
// NewLevelScene laddar banan och ställer spelaren på startrutan. Kunskap,
// plånbok och debuffar tas från Game (delas mellan banor); seed styr slumpen.
func NewLevelScene(a *Assets, id string, g *Game, 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, Wallet: wallet, Seed: seed}),
run: sim.NewRun(lvl, sim.RunOptions{Dict: a.Dict, Know: g.Know, Wallet: g.Wallet, Debuffs: g.Profile.Debuffs, Seed: seed}),
cam: sim.NewCamera(ScreenW, ScreenH, float64(lvl.Width()*TileSize), float64(lvl.Height()*TileSize)),
}
s.followPlayer()
@@ -79,8 +79,10 @@ func (s *LevelScene) followPlayer() {
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()})
mx, my := miniMoveXY()
s.run.StepMini(dt, minigame.Input{Move: my, MoveX: mx, Confirm: confirmJustPressed(), Hold: confirmHeld(), Cancel: backJustPressed()})
if s.run.Outcome != sim.Ongoing {
g.levelEnded(s.run)
g.SetScene(NewResultScene(s))
}
return nil
@@ -107,11 +109,43 @@ func (s *LevelScene) Update(g *Game) error {
}
s.followPlayer()
if s.run.Outcome != sim.Ongoing {
g.levelEnded(s.run)
g.SetScene(NewResultScene(s))
}
return nil
}
// MiniPick flyttar markören i det pågående minispelet ett steg mot
// alternativet med texten text; true när det är valt (skriptläget).
func (s *LevelScene) MiniPick(text string) bool {
switch m := s.run.Mini.(type) {
case *minigame.Ticket:
if m.Stage == minigame.TicketPickDest {
if m.Options[m.Cursor].Dest == text {
return true
}
s.run.StepMini(0, minigame.Input{Move: 1})
return false
}
case *minigame.TopUp:
switch m.Stage {
case minigame.TopUpPickMember:
if m.Members[m.Cursor] == text {
return true
}
s.run.StepMini(0, minigame.Input{Move: 1})
return false
case minigame.TopUpPickButton:
if m.Buttons[m.Cursor] == text {
return true
}
s.run.StepMini(0, minigame.Input{MoveX: 1})
return false
}
}
return true
}
// trainTile är rutan "bortom perrongkanten" där tågvagnen ritas: första
// väggruta nedanför, till höger, till vänster eller ovanför perrongen.
func (s *LevelScene) trainTile(p level.Point) level.Point {
@@ -243,14 +277,26 @@ func (s *LevelScene) drawHUD(g *Game, screen *ebiten.Image) {
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)
timeStr := fmt.Sprintf("%s %3.0fs", a.T("hud.time"), r.Time)
if r.Level.TimeLimitSec > 0 {
left := r.TimeLeft()
timeStr = fmt.Sprintf("%s %d:%02d", a.T("hud.time_left"), int(left)/60, int(left)%60)
}
wallet := fmt.Sprintf("%s %s %d¥ %s %d¥", timeStr, a.T("hud.card"), r.Wallet.Card, a.T("hud.cash_short"), r.Wallet.Cash)
if r.Level.MaxMissedTrains > 0 {
wallet += fmt.Sprintf(" %s %d/%d", a.T("hud.missed_short"), r.MissedTrains, r.Level.MaxMissedTrains)
}
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)
walletCol := colDim
if r.Level.TimeLimitSec > 0 && r.TimeLeft() < 30 {
walletCol = colBad
}
a.Draw(screen, wallet, 4, LineH, walletCol)
s.drawMeters(g, screen)
if r.Outcome != sim.Ongoing || r.Mini != nil {
@@ -278,6 +324,13 @@ func (s *LevelScene) drawHUD(g *Game, screen *ebiten.Image) {
}
lines = append(lines, a.wrapLine(plainLine(a.T(r.FlashKey), col), ScreenW-8)...)
}
if debuffs := g.Profile.Debuffs; len(debuffs) > 0 && r.Time < 6 {
var parts []string
for _, d := range debuffs {
parts = append(parts, fmt.Sprintf("%s (%d)", a.T("debuff."+string(d.Kind)), d.LevelsLeft))
}
lines = append(lines, a.wrapLine(plainLine(a.T("hud.debuffs")+": "+strings.Join(parts, ", "), colBad), ScreenW-8)...)
}
// Inforutans läge: nederkant, men flyttas upp under HUD:en om spelaren
// (eller tåget under hen) annars skulle hamna bakom den.
@@ -390,10 +443,18 @@ func (s *LevelScene) infoLines(a *Assets) (lines []line, accent color.Color, bar
if r.DeniedAt >= 0 && r.Time-r.DeniedAt < deniedShowSec {
key := "hud.gate_denied"
switch {
case lvl.TicketRequired:
case lvl.TicketRequired && r.Wallet.Ticket == "":
key = sim.FlashNoTicket
case lvl.TicketRequired:
key = "hud.need_all_tickets"
case r.Wallet.Phone && r.Wallet.Battery <= 0:
key = "hud.phone_dead"
case len(r.MembersWithoutFare()) > 0:
var names []string
for _, m := range r.MembersWithoutFare() {
names = append(names, m.Name)
}
return []line{plainLine(fmt.Sprintf(a.T(sim.FlashNoCards), strings.Join(names, ", ")), colBad)}, colBad, false
}
return []line{plainLine(a.T(key), colBad)}, colBad, false
}
@@ -403,11 +464,11 @@ func (s *LevelScene) infoLines(a *Assets) (lines []line, accent color.Color, bar
// drawMeters ritar sällskapets mat/dryck/toa-mätare uppe till höger.
func (s *LevelScene) drawMeters(g *Game, screen *ebiten.Image) {
members := s.run.Party.Members
const w, gap = 14, 4
const w, gap = 11, 3
x0 := ScreenW - 4 - len(members)*(w+gap)
for i, m := range members {
x := float32(x0 + i*(w+gap))
g.Assets.Draw(screen, string([]rune(m.Name)[:1]), int(x)+3, 0, colDim)
g.Assets.Draw(screen, string([]rune(m.Name)[:1]), int(x)+2, 0, colDim)
for j, v := range []struct {
val float64
col color.Color

View File

@@ -64,7 +64,7 @@ func (m *MenuScene) Update(g *Game) error {
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.Wallet, g.seed())
s, err := NewLevelScene(g.Assets, m.entries[m.sel].id, g, g.seed())
if err != nil {
m.err = err
return nil
@@ -100,6 +100,13 @@ func (m *MenuScene) Draw(g *Game, screen *ebiten.Image) {
}
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)
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, ScreenH-50, colBad)
}
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.Wallet, g.seed())
s, err := NewLevelScene(g.Assets, r.from.id, g, g.seed())
if err != nil {
return err
}
@@ -42,26 +42,28 @@ func (r *ResultScene) Draw(g *Game, screen *ebiten.Image) {
r.from.Draw(g, screen) // banan i bakgrunden
run := r.from.run
a := g.Assets
vector.DrawFilledRect(screen, 20, 36, ScreenW-40, 120, colPanel, false)
vector.DrawFilledRect(screen, 20, 30, ScreenW-40, 138, colPanel, false)
accent, headline := colBad, a.T("level.lost")
if run.Outcome == sim.Won {
accent, headline = colGood, a.T("level.cleared")
}
vector.DrawFilledRect(screen, 20, 36, ScreenW-40, 3, accent, false)
a.DrawBig(screen, headline, 28, 44, accent)
vector.DrawFilledRect(screen, 20, 30, ScreenW-40, 3, accent, false)
a.DrawBig(screen, headline, 28, 36, accent)
if run.Outcome == sim.Won {
stars := strings.Repeat("★", run.Stars) + strings.Repeat("☆", 3-run.Stars)
a.DrawBig(screen, stars, ScreenW-28-a.Width(stars)-8, 44, colWarn)
a.DrawBig(screen, stars, ScreenW-28-a.Width(stars)-8, 36, colWarn)
}
y := 62
y := 54
if run.Boarded == nil && run.Outcome == sim.Lost {
a.Draw(screen, a.T(run.LostReason), 28, y, colText)
y += LineH
}
if tr := run.Boarded; tr != nil {
a.Draw(screen, fmt.Sprintf("%s%s %s %s%s", tr.Track, a.T("hud.track_suffix_ja"), tr.Line.Get("ja"),
tr.Destination.Get("ja"), a.T("hud.for_ja")), 28, y, colKnown)
y += LineH
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 {
reason := a.T(run.LostReason)
if run.LostReason == sim.LostWrongTrain {
@@ -77,18 +79,32 @@ 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¥ %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)
a.Draw(screen, fmt.Sprintf("%s %d %s %d¥ %s %d¥ %s %d", a.T("result.gates"), run.GatePasses, a.T("hud.card"), run.Wallet.Card,
a.T("result.spent"), run.Wallet.Spent, a.T("result.missed_short"), run.MissedTrains), 28, y, colText)
y += LineH
if run.Outcome == sim.Won {
msg := fmt.Sprintf("+%d %s", run.PointsEarned, a.T("words.points"))
sc := run.Score
a.Draw(screen, fmt.Sprintf("%s %d", a.T("result.score"), sc.Total), 28, y, colText)
y += LineH
a.Draw(screen, fmt.Sprintf("%s %d %s %d %s %d %s %d %s %d", strings.ToLower(a.T("hud.time")), sc.Time, a.T("result.money"), sc.Money,
a.T("result.meters"), sc.Meters, a.T("result.map"), sc.Explore, a.T("result.penalty"), sc.Penalty), 28, y, colDim)
y += LineH
msg := fmt.Sprintf("+%d %s", run.PointsEarned, a.T("result.lang_points"))
if len(run.LearnedDest) > 0 {
msg += " " + a.T("hud.learned") + ": " + strings.Join(run.LearnedDest, " ") + " (" + a.T("result.destination") + ")"
}
a.Draw(screen, msg, 28, y, colKnown)
y += LineH
}
if len(run.NewDebuffs) > 0 {
var parts []string
for _, d := range run.NewDebuffs {
parts = append(parts, fmt.Sprintf("%s (%d)", a.T("debuff."+string(d.Kind)), d.LevelsLeft))
}
a.Draw(screen, a.T("result.new_debuffs")+": "+strings.Join(parts, ", "), 28, y, colBad)
y += LineH
}
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, 141, colDim)
a.Draw(screen, a.T("result.hint"), 28, 155, colDim)
}

View File

@@ -22,6 +22,7 @@ import (
// 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
// pick TEXT flytta markören i minispelet tills alternativet TEXT är valt
// 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)
@@ -86,9 +87,9 @@ func parseScript(src string) (*script, error) {
return nil, fmt.Errorf("script: okänd riktning %q", f[1])
}
st.n, _ = strconv.Atoi(f[2])
case "shot":
case "shot", "pick":
if len(f) != 2 {
return nil, fmt.Errorf("script: 'shot PATH'")
return nil, fmt.Errorf("script: '%s TEXT'", f[0])
}
st.arg = f[1]
case "quit", "waittrain", "waitstop":
@@ -124,6 +125,7 @@ type playerTiler interface {
type trainWaiter interface {
TrainHere() bool
RideAtDest() bool
MiniPick(text string) bool
}
// tick körs i början av varje Update. frame är senast ritade bild.
@@ -191,6 +193,11 @@ func (s *script) tick(g *Game, frame *ebiten.Image) error {
if tw, ok := g.scene.(trainWaiter); !ok || tw.RideAtDest() {
s.advance()
}
case "pick":
s.counter++
if tw, ok := g.scene.(trainWaiter); !ok || tw.MiniPick(st.arg) || s.counter > 60 {
s.advance()
}
case "shot":
if err := saveShot(frame, st.arg); err != nil {
return err