All checks were successful
release / build-release (push) Successful in 1m36s
Ett tåg kan vara en buss (vehicle: bus): samma tidtabell, faser, bedömning i nätet och ombordstigning, men ett fordon på en väg i stället för vagnar på räls, hållplatsskylt, "1番" på tavlan, tuta vid inrullning och dörrsignal före avgång. Nätet har tre busslinjer från Narita. Nya sidobanor: 001b Flygbussterminalen (biljettbana med tre bussar) och 003b Harajuku (Yamanote åt rätt eller fel håll runt ringen). Overload-flaggan ger en rullande reklamticker och maximalt med affischer (Ueno, Tokyo). Romaji utan makroner (fonten saknar dem). Skärmdumparna som börjar på bankartan går via titelskärmen. Tester, docs och wiki uppdaterade. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
711 lines
24 KiB
Go
711 lines
24 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/nav"
|
|
"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
|
|
transferBannerSec = 3.0
|
|
)
|
|
|
|
// Mätarnas bredd och mellanrum uppe till höger (per medlem).
|
|
const (
|
|
meterW = 11
|
|
meterGap = 3
|
|
)
|
|
|
|
// 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
|
|
level *level.Level // aktuell delbana (byts när resan fortsätter på nästa station)
|
|
run *sim.Run
|
|
endless bool // målet är ett lottat mål i oändligt läge (inte storyn)
|
|
sfx sfxState
|
|
tips tipState
|
|
highlight bool // tutorialen: resmålets tecken markeras på skyltar, resten tonas ned
|
|
infoBox image.Rectangle // inforutans läge denna bildruta (tom = ingen), så att tipsen inte täcker den
|
|
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)
|
|
|
|
posters map[level.Point]int // affischer per väggruta (visuell röra)
|
|
|
|
// Tåg och effekter (M7b).
|
|
tracks []trackInfo
|
|
rails map[level.Point]bool
|
|
roads map[level.Point]bool // vägrutor för bussar (ritas med vägsprite)
|
|
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
|
|
}
|
|
return newLevelSceneFor(g, lvl, id, seed), nil
|
|
}
|
|
|
|
// newLevelSceneFor startar en redan laddad (eller lottad) bana.
|
|
func newLevelSceneFor(g *Game, lvl *level.Level, id string, seed rng.Seed) *LevelScene {
|
|
s := &LevelScene{id: id, run: sim.NewRun(lvl, g.runOptions(id, seed))}
|
|
s.rng = rand.New(rand.NewPCG(1, 2))
|
|
s.rebind(g)
|
|
return s
|
|
}
|
|
|
|
// ended bokför banans utgång: storyn via levelEnded, oändligt läge via
|
|
// endlessEnded.
|
|
func (s *LevelScene) ended(g *Game) {
|
|
if s.endless {
|
|
s.outcome = g.endlessEnded(s.run)
|
|
return
|
|
}
|
|
s.outcome = g.levelEnded(s.run, s.id)
|
|
}
|
|
|
|
// rebind bygger om allt som hör till den aktuella delbanan: kamera, räls,
|
|
// affischer, melodifaser och bakgrundsprat. Körs vid start och vid byte.
|
|
func (s *LevelScene) rebind(g *Game) {
|
|
lvl := s.run.Level
|
|
s.level = lvl
|
|
s.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.particles = nil
|
|
s.buildTracks()
|
|
s.buildPosters()
|
|
g.startChatter(lvl.Noise)
|
|
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()
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// drawModeHint ritar navigeringslägets ruta nere till vänster när inforutan
|
|
// är tom: listläget säger vilket tåg man ska ta, skyltläget påminner om att
|
|
// man får lista ut det själv (och om bonusen).
|
|
func (s *LevelScene) drawModeHint(a *Assets, screen *ebiten.Image) {
|
|
r := s.run
|
|
txt, col := "", colGood
|
|
if r.Mode == nav.Signs {
|
|
txt, col = a.T("hud.mode_signs"), colWarn
|
|
} else if next := r.NextTrains(); len(next) > 0 {
|
|
txt = fmt.Sprintf("%s %s %s %s", a.T("hud.take"), strings.ToLower(a.T("hud.track")), next[0].Track, next[0].Line.Get("romaji"))
|
|
if len(next) > 1 {
|
|
txt += fmt.Sprintf(" (+%d)", len(next)-1)
|
|
}
|
|
}
|
|
if txt == "" {
|
|
return
|
|
}
|
|
y := ScreenH - LineH - 6
|
|
vector.DrawFilledRect(screen, 0, float32(y), float32(a.Width(txt)+8), float32(LineH+6), colPanel, false)
|
|
a.Draw(screen, txt, 4, y+3, col)
|
|
}
|
|
|
|
// ForceUrge gör en i sällskapet törstig (skriptläget: "urge NAMN" för
|
|
// skärmdumpar av kiosken).
|
|
func (s *LevelScene) ForceUrge(name string) bool {
|
|
for _, m := range s.run.Party.Members {
|
|
if m.Name == name {
|
|
m.Meters.Drink, m.Urge = 5, sim.Thirsty
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// LevelOver rapporterar om banan är avgjord (skriptläget: "waitover").
|
|
func (s *LevelScene) LevelOver() bool { return s.run.Outcome != sim.Ongoing }
|
|
|
|
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)
|
|
s.updateSfx(g)
|
|
s.updateTips(g)
|
|
s.highlight = s.tutorialActive(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.Level != s.level {
|
|
s.rebind(g) // bytet: resan fortsätter på nästa station
|
|
}
|
|
if s.run.Outcome != sim.Ongoing {
|
|
s.outcome = g.levelEnded(s.run, s.id)
|
|
g.SetScene(NewResultScene(s))
|
|
}
|
|
return nil
|
|
}
|
|
if backJustPressed() {
|
|
g.SetScene(NewPauseScene(s)) // Esc pausar; att lämna banan kräver ett val i menyn
|
|
return nil
|
|
}
|
|
if shopJustPressed() && !s.run.Player.Moving() && s.run.Level.At(s.run.Player.Tile) == 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.CarPick:
|
|
if text == "calm" { // lugnaste vagnen som inte är kvinnovagn
|
|
best := -1
|
|
for i, c := range m.Cars {
|
|
if !c.WomenOnly && (best < 0 || c.Crowd < m.Cars[best].Crowd) {
|
|
best = i
|
|
}
|
|
}
|
|
if best < 0 || m.Cursor == best {
|
|
return true
|
|
}
|
|
s.run.StepMini(0, minigame.Input{MoveX: 1})
|
|
return false
|
|
}
|
|
case *minigame.Payer:
|
|
o := m.Options[m.Cursor]
|
|
if o.Name == text || (text == "cash" && o.Cash) {
|
|
return true
|
|
}
|
|
s.run.StepMini(0, minigame.Input{MoveX: 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)
|
|
z := s.run.Player.Tile.Z // bara spelarens våning ritas
|
|
var op ebiten.DrawImageOptions
|
|
for y := y0; y < y1; y++ {
|
|
for x := x0; x < x1; x++ {
|
|
p := level.Point{X: x, Y: y, Z: z}
|
|
if s.rails[p] || s.roads[p] {
|
|
s.drawRails(g, screen, p, ox, oy)
|
|
continue
|
|
}
|
|
op.GeoM.Reset()
|
|
op.GeoM.Translate(ox+float64(x*TileSize), oy+float64(y*TileSize))
|
|
if v, ok := s.posters[p]; ok {
|
|
screen.DrawImage(g.Assets.Posters[v], &op)
|
|
continue
|
|
}
|
|
screen.DrawImage(g.Assets.Tiles[lvl.At(p)], &op)
|
|
}
|
|
}
|
|
s.drawTrains(g, screen, ox, oy)
|
|
s.drawQueueMarks(screen, ox, oy)
|
|
s.drawParticles(screen, ox, oy)
|
|
s.drawNPCs(g, 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)
|
|
if from.Z != z && to.Z != z {
|
|
continue // på en annan våning
|
|
}
|
|
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)
|
|
s.drawTicker(g, screen)
|
|
s.drawTip(g, screen)
|
|
if r := s.run; r.TransferAt >= 0 && r.Time-r.TransferAt < transferBannerSec && r.Mini == nil && r.Outcome == sim.Ongoing {
|
|
txt := g.Assets.T("hud.transfer_done")
|
|
w := g.Assets.Width(txt) + 16
|
|
x := (ScreenW - w) / 2
|
|
vector.DrawFilledRect(screen, float32(x), 60, float32(w), LineH+8, colPanel, false)
|
|
vector.DrawFilledRect(screen, float32(x), 60, float32(w), 2, colWarn, false)
|
|
g.Assets.Draw(screen, txt, x+8, 64, colWarn)
|
|
}
|
|
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.FinalDestination().Get("romaji")
|
|
if !r.IsLastLeg() { // resa med byte: den här delbanan slutar vid bytesstationen
|
|
dest += " " + a.T("hud.via") + " " + r.Level.Destination.Get("romaji")
|
|
}
|
|
if r.Know.ShowsDestinationJP() { // hint från nivå 1: delbanans mål på japanska, att matcha mot skyltarna
|
|
dest += " " + r.Level.Destination.Get("ja")
|
|
}
|
|
name := r.Level.Name.Get(UILang)
|
|
if r.Level.NumFloors() > 1 {
|
|
name += " " + r.Level.FloorName(r.Player.Tile.Z)
|
|
}
|
|
if r.Legs() > 1 {
|
|
name += fmt.Sprintf(" %d/%d", r.LegNo(), r.Legs())
|
|
}
|
|
if s.endless && g.Endless != nil {
|
|
name = fmt.Sprintf("#%d %s", g.Endless.Step+1, name)
|
|
}
|
|
a.Draw(screen, name, 4, 0, colText)
|
|
destX := 4 + a.Width(name) + 12
|
|
a.Draw(screen, a.T("hud.destination")+": "+dest, destX, 0, colKnown)
|
|
// Japanska-nivån får plats till vänster om mätarna; annars kortas den.
|
|
jpX := destX + a.Width(a.T("hud.destination")+": "+dest) + 12
|
|
metersX := ScreenW - 4 - len(r.Party.Members)*(meterW+meterGap)
|
|
jp := fmt.Sprintf("%s Lv%d·%d", a.T("hud.japanese"), r.Know.Level(), r.Know.Count())
|
|
if jpX+a.Width(jp) > metersX-4 {
|
|
jp = fmt.Sprintf("Lv%d", r.Know.Level())
|
|
}
|
|
if jpX+a.Width(jp) <= metersX-4 {
|
|
a.Draw(screen, jp, jpX, 0, colDim)
|
|
}
|
|
timeStr := fmt.Sprintf("%s %3.0fs", a.T("hud.time"), r.Time)
|
|
if r.TimeLimit() > 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.MaxMissed() > 0 {
|
|
wallet += fmt.Sprintf(" %s %d/%d", a.T("hud.missed_short"), r.MissedTrains, r.MaxMissed())
|
|
}
|
|
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.TimeLimit() > 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) && r.FlashKey != sim.FlashTransfer { // bytet har sin egen banner
|
|
col := colBad
|
|
if r.FlashKey == sim.FlashRescan {
|
|
col = colWarn
|
|
}
|
|
lines = append(lines, a.wrapLine(plainLine(a.T(r.FlashKey), col), ScreenW-8)...)
|
|
}
|
|
if r.WrongLane {
|
|
lines = append(lines, plainLine(a.T("hud.wrong_lane"), colWarn))
|
|
} else if r.BumpedAt >= 0 && r.Time-r.BumpedAt < 1.0 {
|
|
lines = append(lines, plainLine(a.T("hud.bump"), colDim))
|
|
}
|
|
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 {
|
|
s.drawModeHint(a, screen) // lägesrutan bara när ingen inforuta visas
|
|
}
|
|
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)
|
|
}
|
|
s.infoBox = box
|
|
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()
|
|
}
|
|
if r.Staff.Active {
|
|
p = r.StaffProgress()
|
|
}
|
|
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)
|
|
if s.highlight {
|
|
lines = append(lines, a.wrapLine(highlightLine(tr.BoardText(), lvl.Destination.Get("ja"), colGood, colDim), maxW)...)
|
|
} else {
|
|
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))
|
|
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)
|
|
if s.highlight {
|
|
lines = append(lines, a.wrapLine(highlightLine(sg.Text["ja"], lvl.Destination.Get("ja"), colGood, colDim), maxW)...)
|
|
} else {
|
|
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) {
|
|
case level.Gate:
|
|
switch {
|
|
case r.Staff.Active:
|
|
return []line{plainLine(a.T("hud.staff_busy"), colDim)}, colGood, true
|
|
case r.NoExitAt >= 0 && r.Time-r.NoExitAt < deniedShowSec:
|
|
return a.wrapLine(plainLine(fmt.Sprintf(a.T("hud.no_exit"), sim.StaffSec), colBad), maxW), colBad, false
|
|
case r.NeedsStaff():
|
|
return []line{plainLine(fmt.Sprintf(a.T("hud.staff_prompt"), sim.StaffSec), colWarn)}, colWarn, false
|
|
case lvl.OneWayGates && r.StaffUnlocked:
|
|
return []line{plainLine(a.T("hud.unlocked_prompt"), colGood)}, colGood, false
|
|
}
|
|
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:
|
|
// Terminalen: linjetavlan för stationen (nätkopplade banor), läst genom kunskapen som en skylt.
|
|
if term := r.TerminalLines(); len(term) > 0 {
|
|
lines = append(lines, plainLine(a.T("hud.terminal"), colDim))
|
|
for _, t := range term {
|
|
rd := lang.Render(a.Dict, r.Know, t, UILang)
|
|
lines = append(lines, a.wrapLine(shownLine(rd.Partial, colText, false), maxW)...)
|
|
}
|
|
}
|
|
if lvl.TicketRequired {
|
|
return append(lines, plainLine(fmt.Sprintf(a.T("hud.machine_ticket"), r.Wallet.Cash), colGood)), colGood, false
|
|
}
|
|
return append(lines, 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 = meterW, meterGap
|
|
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)
|
|
}
|
|
}
|
|
}
|