M6: plånbok, biljetter, kortet i telefonen och seedade minispel
All checks were successful
release / build-release (push) Successful in 1m41s
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
156
internal/game/minigame_view.go
Normal file
156
internal/game/minigame_view.go
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user