All checks were successful
release / build-release (push) Successful in 1m42s
- level.Schedule.Phase: away/approaching/arriving/dwelling/closing/leaving ur tidtabellen med progress; Boardable (öppna dörrar) och Present; validering av minsta uppehåll/borta-tid; TrackTile för rälsen - sim.Run: ombordstigning bara med öppna dörrar, TrainVisible via Present, PlatformPhase och NearTrack (närmaste perrong inom två rutor) - game/trains.go: rälsrutor längs kantraden (roterade för vertikala spår), tre vagnar som bromsar in och accelererar ut längs rälsen, strålkastare under inrullningen, blinkande lampor vid kanten för den som står nära, dammpartiklar längs perrongen; kameran får en rutas marginal så att kanträlsen syns - game/audio.go: ebiten/v2/audio med avgångsmelodi (approaching) och dörrsignal (closing), volym efter avstånd, -mute; tools/gensound syntetiserar placeholder-WAV:ar - HUD: fasberoende perrongtext och "tåg på väg in" nära rälsen; skriptkommandot waitphase; skärmdump av ankomsten - plan (Björn 2026-10-24): räls/väg-grafik och melodierna i M7b; folk/NPC:er, vänster-/högertrafik, bredare gångar, reklam- och ljudröra som nytt steg M9b – i spelbeskrivning, roadmap, testplan - docs/README/wiki uppdaterade Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
512 lines
17 KiB
Go
512 lines
17 KiB
Go
package game
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"math"
|
|
"math/rand/v2"
|
|
"path/filepath"
|
|
"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/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"
|
|
)
|
|
|
|
// Hur länge tillfälliga meddelanden visas.
|
|
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;
|
|
// scenen översätter input och ritar.
|
|
type LevelScene struct {
|
|
id string
|
|
run *sim.Run
|
|
cam *sim.Camera
|
|
animT float64
|
|
hideMap bool // M döljer/visar minikartan
|
|
|
|
// Tåg och effekter (M7b).
|
|
tracks []trackInfo
|
|
rails map[level.Point]bool
|
|
lastPhase map[level.Tile]level.Phase
|
|
particles []particle
|
|
rng *rand.Rand // bara för partiklar (ingen spellogik)
|
|
}
|
|
|
|
// 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: 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.cam.Margin = TileSize // rälsen ligger i kantraden: visa en ruta utanför kartan
|
|
s.lastPhase = map[level.Tile]level.Phase{}
|
|
s.rng = rand.New(rand.NewPCG(1, 2))
|
|
s.buildTracks()
|
|
for _, tr := range s.tracks { // ingen melodi för fasen banan råkar börja i
|
|
s.lastPhase[tr.tile], _ = s.run.PlatformPhase(tr.tile)
|
|
}
|
|
s.followPlayer()
|
|
return s, nil
|
|
}
|
|
|
|
// TrackPhaseName är fasen för närmaste perrong inom indikatoravståndet
|
|
// (skriptläget), "" om ingen är nära.
|
|
func (s *LevelScene) TrackPhaseName() string {
|
|
_, _, phase, _, _, ok := s.run.NearTrack(indicatorDist)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return phase.String()
|
|
}
|
|
|
|
// PlayerTile returnerar rutan spelaren står på/lämnar (används av skriptläget).
|
|
func (s *LevelScene) PlayerTile() level.Point { return s.run.Player.Tile }
|
|
|
|
// PlayerMoving rapporterar om spelaren är mellan två rutor (skriptläget).
|
|
func (s *LevelScene) PlayerMoving() bool { return s.run.Player.Moving() }
|
|
|
|
// TrainHere rapporterar om spelaren står på en perrong vars tåg är inne (skriptläget).
|
|
func (s *LevelScene) TrainHere() bool {
|
|
tr, at, _ := s.run.PlatformHere()
|
|
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 {
|
|
s.updateEffects(g)
|
|
if s.run.Mini != nil {
|
|
// Minispel: all input går dit; Esc avbryter minispelet, inte banan.
|
|
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
|
|
}
|
|
if backJustPressed() {
|
|
m, err := NewMenuScene(g.Assets)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
g.SetScene(m)
|
|
return nil
|
|
}
|
|
if helpJustPressed() {
|
|
s.run.AskHelp()
|
|
}
|
|
if mapJustPressed() {
|
|
s.hideMap = !s.hideMap
|
|
}
|
|
s.run.Step(dt, dirInput(), confirmJustPressed())
|
|
if s.run.Player.Moving() {
|
|
s.animT += dt
|
|
} else {
|
|
s.animT = 0
|
|
}
|
|
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
|
|
}
|
|
|
|
func (s *LevelScene) Draw(g *Game, screen *ebiten.Image) {
|
|
screen.Fill(color.RGBA{24, 20, 37, 255})
|
|
lvl := s.run.Level
|
|
ox, oy := s.cam.Offset()
|
|
ox, oy = math.Round(ox), math.Round(oy)
|
|
|
|
// Rita bara rutor som syns.
|
|
x0 := max(0, int(s.cam.X)/TileSize)
|
|
y0 := max(0, int(s.cam.Y)/TileSize)
|
|
x1 := min(lvl.Width(), int(s.cam.X+ScreenW)/TileSize+2)
|
|
y1 := min(lvl.Height(), int(s.cam.Y+ScreenH)/TileSize+2)
|
|
var op ebiten.DrawImageOptions
|
|
for y := y0; y < y1; y++ {
|
|
for x := x0; x < x1; x++ {
|
|
if s.rails[level.Point{X: x, Y: y}] {
|
|
s.drawRails(g, screen, level.Point{X: x, Y: y}, ox, oy)
|
|
continue
|
|
}
|
|
op.GeoM.Reset()
|
|
op.GeoM.Translate(ox+float64(x*TileSize), oy+float64(y*TileSize))
|
|
screen.DrawImage(g.Assets.Tiles[lvl.At(x, y)], &op)
|
|
}
|
|
}
|
|
s.drawTrains(g, screen, ox, oy)
|
|
s.drawParticles(screen, ox, oy)
|
|
|
|
// Följarna ritas bakifrån så att ledaren hamnar överst.
|
|
leader := s.run.Player
|
|
followers := s.run.Party.Followers()
|
|
for i := len(followers); i >= 1; i-- {
|
|
from, to := s.run.Party.FollowerTiles(i)
|
|
fx := float64(from.X*TileSize) + float64((to.X-from.X)*TileSize)*leader.Progress
|
|
fy := float64(from.Y*TileSize) + float64((to.Y-from.Y)*TileSize)*leader.Progress
|
|
facing := sim.Down
|
|
switch {
|
|
case to.X > from.X:
|
|
facing = sim.Right
|
|
case to.X < from.X:
|
|
facing = sim.Left
|
|
case to.Y < from.Y:
|
|
facing = sim.Up
|
|
}
|
|
op.GeoM.Reset()
|
|
op.GeoM.Translate(ox+math.Round(fx), oy+math.Round(fy))
|
|
op.ColorScale.Reset()
|
|
t := memberTints[i%len(memberTints)]
|
|
op.ColorScale.Scale(t[0], t[1], t[2], 1)
|
|
screen.DrawImage(g.Assets.PlayerFrame(facing, leader.Moving() && from != to, s.animT), &op)
|
|
op.ColorScale.Reset()
|
|
s.drawUrge(g, screen, followers[i-1], int(ox+math.Round(fx)), int(oy+math.Round(fy)))
|
|
}
|
|
|
|
px, py := leader.WorldPos(TileSize)
|
|
op.GeoM.Reset()
|
|
op.GeoM.Translate(ox+math.Round(px), oy+math.Round(py))
|
|
screen.DrawImage(g.Assets.PlayerFrame(leader.Facing, leader.Moving(), s.animT), &op)
|
|
s.drawUrge(g, screen, s.run.Party.Members[0], int(ox+math.Round(px)), int(oy+math.Round(py)))
|
|
s.drawIndicator(screen, ox, oy)
|
|
|
|
s.drawHUD(g, screen)
|
|
if s.run.Mini != nil {
|
|
s.drawMinigame(g, screen)
|
|
}
|
|
}
|
|
|
|
// memberTints färgar följarnas sprites tills de har egna.
|
|
var memberTints = [][3]float32{{1, 1, 1}, {1, 0.75, 0.55}, {0.7, 0.95, 0.75}, {0.8, 0.7, 1}, {1, 0.95, 0.55}}
|
|
|
|
// urgeMark är tecknet som visas över huvudet på någon med ett behov.
|
|
func urgeMark(u sim.Urge) (string, color.Color) {
|
|
switch u {
|
|
case sim.NeedToilet:
|
|
return "WC", colBad
|
|
case sim.Thirsty:
|
|
return "飲", colEnglish
|
|
case sim.Hungry:
|
|
return "食", colWarn
|
|
case sim.Distracted:
|
|
return "?", colDim
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
func (s *LevelScene) drawUrge(g *Game, screen *ebiten.Image, m *sim.Member, x, y int) {
|
|
mark, col := urgeMark(m.Urge)
|
|
if mark == "" {
|
|
if m.Meters.Min() <= 0 {
|
|
mark, col = "!!", colBad
|
|
} else {
|
|
return
|
|
}
|
|
}
|
|
w := g.Assets.Width(mark)
|
|
vector.DrawFilledRect(screen, float32(x+8-w/2-1), float32(y-11), float32(w+2), 11, colPanel, false)
|
|
g.Assets.Draw(screen, mark, x+8-w/2, y-12, col)
|
|
}
|
|
|
|
// drawHUD ritar den övre raden (bana, resmål, tid, kort, japanska-nivå) och
|
|
// inforutan (perrong, skylt, nekad spärr, nytt ord).
|
|
func (s *LevelScene) drawHUD(g *Game, screen *ebiten.Image) {
|
|
r := s.run
|
|
a := g.Assets
|
|
vector.DrawFilledRect(screen, 0, 0, ScreenW, 24, color.RGBA{24, 20, 37, 200}, false)
|
|
|
|
dest := r.Level.Destination.Get("romaji")
|
|
if r.Know.ShowsDestinationJP() { // hint från nivå 1: resmålet på japanska, att matcha mot skyltarna
|
|
dest += " " + r.Level.Destination.Get("ja")
|
|
}
|
|
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)
|
|
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
|
|
}
|
|
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 {
|
|
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 {
|
|
lines = append(lines, plainLine(fmt.Sprintf("%s: %s = %s (%s)", a.T("hud.learned"), r.Learned,
|
|
entry.Translation(UILang), entry.Romaji), colKnown))
|
|
}
|
|
if len(r.Progressed) > 0 && r.Time-r.ProgressedAt < progressedShowSec {
|
|
lines = append(lines, a.wrapLine(plainLine(a.T("hud.progress")+": "+strings.Join(r.Progressed, ", "), colWarn), ScreenW-8)...)
|
|
}
|
|
if urges := r.Party.Urges(); len(urges) > 0 {
|
|
var parts []string
|
|
for _, m := range urges {
|
|
parts = append(parts, m.Name+": "+a.T("urge."+m.Urge.String()))
|
|
}
|
|
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)...)
|
|
}
|
|
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.
|
|
var box image.Rectangle
|
|
hasBox := len(lines) > 0
|
|
if hasBox {
|
|
h := 8 + LineH*len(lines)
|
|
if bar {
|
|
h += 8
|
|
}
|
|
_, py := r.Player.WorldPos(TileSize)
|
|
y := ScreenH - h
|
|
if int(py-s.cam.Y)+2*TileSize > y {
|
|
y = 24
|
|
}
|
|
box = image.Rect(0, y, ScreenW, y+h)
|
|
}
|
|
if !s.hideMap {
|
|
s.drawMinimap(g, screen, box, hasBox)
|
|
}
|
|
if !hasBox {
|
|
return
|
|
}
|
|
y, h := box.Min.Y, box.Dy()
|
|
vector.DrawFilledRect(screen, 0, float32(y), ScreenW, float32(h), colPanel, false)
|
|
vector.DrawFilledRect(screen, 0, float32(y), ScreenW, 2, accent, false)
|
|
a.drawLines(screen, lines, 4, y+4)
|
|
if bar {
|
|
p := r.HelpProgress()
|
|
if r.Facility.Active {
|
|
p = r.FacilityProgress()
|
|
}
|
|
by := float32(y + h - 7)
|
|
vector.DrawFilledRect(screen, 4, by, ScreenW-8, 4, colBar, false)
|
|
vector.DrawFilledRect(screen, 4, by, float32((ScreenW-8)*p), 4, colKnown, false)
|
|
}
|
|
}
|
|
|
|
// infoLines bygger inforutans rader: perronginfo, skylt eller nekad spärr.
|
|
func (s *LevelScene) infoLines(a *Assets) (lines []line, accent color.Color, bar bool) {
|
|
r := s.run
|
|
lvl := r.Level
|
|
tile := r.Player.Tile
|
|
accent = colBar
|
|
maxW := ScreenW - 8
|
|
english := lang.ShowEnglish(lvl.ID, tile.X, tile.Y, lvl.Difficulty)
|
|
|
|
if tr, at, secs := r.PlatformHere(); tr != nil {
|
|
// Avgångstavlan är en skylt som alla andra: japanska, sedd genom kunskapen.
|
|
rd := lang.Render(a.Dict, r.Know, tr.BoardText(), UILang)
|
|
lines = append(lines, a.wrapLine(shownLine(rd.Original, colText, r.Know.ShowsReadings()), maxW)...)
|
|
lines = append(lines, a.wrapLine(shownLine(rd.Partial, colText, false), maxW)...)
|
|
if english {
|
|
lines = append(lines, a.wrapLine(plainLine(fmt.Sprintf("%s: Track %s %s for %s", a.T("hud.english"), tr.Track,
|
|
tr.Line.Get("en"), tr.Destination.Get("en")), colEnglish), maxW)...)
|
|
}
|
|
phase, _ := r.PlatformPhase(lvl.At(tile.X, tile.Y))
|
|
switch {
|
|
case phase == level.Closing:
|
|
accent = colWarn
|
|
lines = append(lines, plainLine(fmt.Sprintf("%s %.0f s", a.T("hud.closing"), math.Ceil(secs)), colWarn))
|
|
case at:
|
|
accent = colGood
|
|
lines = append(lines, plainLine(fmt.Sprintf("%s %.0f s %s", a.T("hud.departs_in"), math.Ceil(secs), a.T("hud.board")), colGood))
|
|
case phase == level.Arriving:
|
|
accent = colWarn
|
|
lines = append(lines, plainLine(a.T("hud.arriving"), colWarn))
|
|
case phase == level.Approaching:
|
|
accent = colWarn
|
|
lines = append(lines, plainLine(fmt.Sprintf(a.T("hud.approaching"), math.Ceil(secs)), colWarn))
|
|
case phase == level.Leaving:
|
|
lines = append(lines, plainLine(fmt.Sprintf("%s %s %.0f s", a.T("hud.leaving"), a.T("hud.next_train_in"), math.Ceil(secs)), colDim))
|
|
default:
|
|
accent = colWarn
|
|
lines = append(lines, plainLine(fmt.Sprintf("%s %.0f s", a.T("hud.next_train_in"), math.Ceil(secs)), colWarn))
|
|
}
|
|
return lines, accent, false
|
|
}
|
|
// Nära rälsen (men inte på perrongen): man ser och hör tåget komma.
|
|
if _, _, phase, _, dist, ok := r.NearTrack(indicatorDist); ok && dist > 0 && phase == level.Approaching && r.SignHere() == nil {
|
|
return []line{plainLine(a.T("hud.train_near"), colWarn)}, colWarn, false
|
|
}
|
|
|
|
if sg := r.SignHere(); sg != nil {
|
|
know := r.Know
|
|
revealed := r.SignRevealed(tile)
|
|
if revealed {
|
|
know = nil // allt känt
|
|
accent = colKnown
|
|
}
|
|
rd := lang.Render(a.Dict, know, sg.Text["ja"], UILang)
|
|
lines = append(lines, a.wrapLine(shownLine(rd.Original, colText, r.Know.ShowsReadings()), maxW)...)
|
|
lines = append(lines, a.wrapLine(shownLine(rd.Partial, colText, false), maxW)...)
|
|
if english {
|
|
lines = append(lines, a.wrapLine(plainLine(a.T("hud.english")+": "+sg.Text.Get("en"), colEnglish), maxW)...)
|
|
}
|
|
switch {
|
|
case r.Help.Active:
|
|
lines = append(lines, plainLine(a.T("hud.help_progress"), colDim))
|
|
bar = true
|
|
case !revealed:
|
|
lines = append(lines, plainLine(fmt.Sprintf(a.T("hud.help_prompt"), lang.HelpSeconds(r.Know.Level())), colDim))
|
|
}
|
|
return lines, accent, bar
|
|
}
|
|
|
|
switch lvl.At(tile.X, tile.Y) {
|
|
case level.Toilet:
|
|
if r.Facility.Active {
|
|
return []line{plainLine(a.T("hud.toilet_busy"), colDim)}, colGood, true
|
|
}
|
|
if n := len(r.Party.NeedsToilet()); n > 0 {
|
|
return []line{plainLine(fmt.Sprintf(a.T("hud.toilet_prompt"), n, sim.ToiletSecPerMember*float64(n)), colGood)}, colGood, false
|
|
}
|
|
return []line{plainLine(a.T("hud.toilet_none"), colDim)}, accent, false
|
|
case level.Kiosk:
|
|
if n := len(r.Party.NeedsKiosk()); n > 0 {
|
|
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 {
|
|
key := "hud.gate_denied"
|
|
switch {
|
|
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
|
|
}
|
|
return nil, accent, false
|
|
}
|
|
|
|
// 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 = 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)+2, 0, colDim)
|
|
for j, v := range []struct {
|
|
val float64
|
|
col color.Color
|
|
}{{m.Meters.Food, colWarn}, {m.Meters.Drink, colEnglish}, {m.Meters.Toilet, colGood}} {
|
|
y := float32(13 + j*3)
|
|
vector.DrawFilledRect(screen, x, y, w, 2, colBar, false)
|
|
c := v.col
|
|
if v.val < sim.UrgeThreshold {
|
|
c = colBad
|
|
}
|
|
vector.DrawFilledRect(screen, x, y, float32(w*v.val/100), 2, c, false)
|
|
}
|
|
}
|
|
}
|