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 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.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.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) } } }