All checks were successful
release / build-release (push) Successful in 1m46s
- internal/progress: katalog (skills.json 12 noder, items.json 25 items) med validering; XP/nivåer (150·n(n+1)/2) och skillpoäng; skillträd med krav och kostnad; items per plats med buffar, genvägar (JR Pass, kartor, powerbank) och färgton; banresultat och upplåsning (huvudbanor i ordning, sidospår efter föräldern); 33 %-regeln som test - internal/save: versionerad JSON med kunskap, plånbok, debuffar, progression och pågående bana + seed; atomisk skrivning; migrering; ~/.config/metro-puzzle-jp/save.json resp. %AppData% - sim: RunOptions får Extra/FogRadius/Landmarks/FreeGates/RevealAll/ InfiniteBattery; luck påverkar trängselzonen; Fog.RevealAll och extra landmärken - game: bankarta i spelbrädesstil ersätter menyn (⟳ återupptar med sparad seed), skillträd (T), garderob (I), butik i kiosken (B), XP/nivå/items/ upplåsningar på resultatet, spelaren tonas av skins; autospar vid banstart, efter banan och vid köp; flaggor -save/-nosave - level: kind/order/parent/rewards/shop; ny sidobana 002b Kabukicho som belönar skinnet Kimono; 001 belönar Salaryman-skinnet; butikssortiment på 002/003 - tools/shots: demo-sparfil (build/, aldrig den riktiga), bilder av karta, skills, garderob och butik; alla körningar med -nosave - docs/README/wiki uppdaterade Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
527 lines
18 KiB
Go
527 lines
18 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
|
|
|
|
shop *shop // öppen butik i kiosken (pausar banan)
|
|
outcome LevelOutcome // progression från banan (fylls i när den avgjorts)
|
|
|
|
// 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, debuffar, skills och items tas från Game; seed styr slumpen.
|
|
func NewLevelScene(g *Game, id string, seed rng.Seed) (*LevelScene, error) {
|
|
a := g.Assets
|
|
lvl, err := level.LoadFile(filepath.Join(a.LevelsDir, id+".json"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s := &LevelScene{
|
|
id: id,
|
|
run: sim.NewRun(lvl, g.runOptions(id, 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 {
|
|
if s.shop != nil { // butiken pausar banan
|
|
s.updateShop(g)
|
|
return nil
|
|
}
|
|
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 {
|
|
s.outcome = g.levelEnded(s.run, s.id)
|
|
g.SetScene(NewResultScene(s))
|
|
}
|
|
return nil
|
|
}
|
|
if backJustPressed() {
|
|
g.SetScene(NewMapScene(g))
|
|
return nil
|
|
}
|
|
if shopJustPressed() && !s.run.Player.Moving() && s.run.Level.At(s.run.Player.Tile.X, s.run.Player.Tile.Y) == level.Kiosk {
|
|
s.openShop(g)
|
|
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 {
|
|
s.outcome = g.levelEnded(s.run, s.id)
|
|
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))
|
|
tint := g.Effects().Tint
|
|
op.ColorScale.Reset()
|
|
op.ColorScale.Scale(tint[0], tint[1], tint[2], 1)
|
|
screen.DrawImage(g.Assets.PlayerFrame(leader.Facing, leader.Moving(), s.animT), &op)
|
|
op.ColorScale.Reset()
|
|
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)
|
|
}
|
|
if s.shop != nil {
|
|
s.drawShop(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 || s.shop != nil {
|
|
return // resultatpanelen, minispelet eller butiken 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)+" "+a.T("hud.shop_prompt"), colGood)}, colGood, false
|
|
}
|
|
return []line{plainLine(a.T("hud.kiosk_none")+" "+a.T("hud.shop_prompt"), 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)
|
|
}
|
|
}
|
|
}
|