All checks were successful
release / build-release (push) Successful in 1m37s
Resor med byten: en bana kan ha next som pekar på en delbana (kind leg, syns inte på bankartan). När man klivit av rätt tåg fortsätter samma omgång på nästa station med klocka, plånbok, sällskap och räknare; par-tiden adderas, dimma och folk är nya, en Byte!-banner visas och HUD:en visar resmålet via bytesstationen. Tidsgräns och missade tåg gäller hela resan. level.ValidateChains kräver sammanhängande kedjor. Nya resor: 007 Akihabara -> Ueno -> Asakusa och finalen 008 Tokyo (två våningar) -> Nippori -> Narita flygplats. Slutscen när sista huvudbanan klaras första gången: planet lyfter, resan summeras, eftertexter; Progress.StoryCleared sparas; flaggan -ending. tools/levelcheck ritar banorna och rapporterar problem. HUD-nivån kortas när mätarna tar plats; poängraden radbryts. Skärmdumpar och docs uppdaterade. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
640 lines
22 KiB
Go
640 lines
22 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
|
|
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
|
|
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))}
|
|
s.rng = rand.New(rand.NewPCG(1, 2))
|
|
s.rebind(g)
|
|
return s, nil
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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)
|
|
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(NewMapScene(g))
|
|
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.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.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)
|
|
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())
|
|
}
|
|
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)
|
|
}
|
|
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))
|
|
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) {
|
|
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)
|
|
}
|
|
}
|
|
}
|