Files
metro-puzzle-jp/internal/game/minigame_view.go
claude b2995d4ca9
All checks were successful
release / build-release (push) Successful in 1m37s
M7: svårighet, tid, poäng, debuffar, egna kort och sittplatsval
- sim/stats.go: Stats (fart, törst, fokus, toalett, tur), debuffar som
  multiplikatorer, Profile som följer med mellan banor och räknar ned
- sim.Run: tidsgräns och maxantal missade tåg förlorar banan; poängformel
  (tid mot par, pengar, sällskapets mätare, utforskat, avdrag); debuffar
  tjänas av många steg, lång tid, tom matmätare och seedad otur vid
  kiosken; stats påverkar fart, mätartakter, läsning och hjälptid
- egna kort (individualCards): Member.Card, spärren kräver allas saldo och
  drar från alla, automaten frågar vems kort, biljetter köps åt alla
- sittplatsval (seatSelection): Ticket får ett sittplatssteg med seedade
  önskemål (fönster/gång/sida/bredvid); missnöjda ger mätar- och poängavdrag
- level: maxMissedTrains, individualCards, seatSelection; bana 003 Shibuya
  (svårighet 3, fyra i sällskapet) och 002 med tidsgräns och sittplatser;
  ordboken utökad; generatorn är spec-driven
- game: tid kvar/missade i HUD, debuffar i meny/HUD/resultat, poäng och
  förlustorsaker på resultatet, vyer för personval och vagn, skript-pick,
  flaggan -debuffs; HUD och resultat komprimerade för fyra i sällskapet
- plan (Björn 2026-10-24): flera våningar med trappor som M9a, egna kort
  och sittplatsval som svårighetsinställningar i M7 – i spelbeskrivning,
  roadmap, testplan
- docs/README/wiki med sittplatsbilden

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-24 10:18:17 +02:00

237 lines
8.0 KiB
Go

package game
import (
"fmt"
"image"
"image/color"
"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/minigame"
)
// Minispelen ritas som en panel över banan. Logiken bor i internal/minigame.
func (s *LevelScene) drawMinigame(g *Game, screen *ebiten.Image) {
a := g.Assets
r := s.run
panel := image.Rect(12, 28, ScreenW-12, ScreenH-10)
vector.DrawFilledRect(screen, float32(panel.Min.X), float32(panel.Min.Y), float32(panel.Dx()), float32(panel.Dy()), colPanel, false)
vector.DrawFilledRect(screen, float32(panel.Min.X), float32(panel.Min.Y), float32(panel.Dx()), 3, colKnown, false)
switch m := r.Mini.(type) {
case *minigame.TopUp:
s.drawTopUp(a, screen, m, panel)
case *minigame.Ticket:
s.drawTicket(a, screen, m, panel)
case *minigame.Crowd:
s.drawCrowd(a, screen, m, panel)
case *minigame.Ride:
s.drawRide(a, screen, m, panel)
}
}
// jp ritar japansk text sedd genom spelarens kunskap (kända ord i gult).
func (s *LevelScene) jp(a *Assets, screen *ebiten.Image, text string, x, y int) {
rd := lang.Render(a.Dict, s.run.Know, text, UILang)
a.drawLines(screen, []line{shownLine(rd.Original, colText, false)}, x, y)
}
func (s *LevelScene) drawTopUp(a *Assets, screen *ebiten.Image, m *minigame.TopUp, p image.Rectangle) {
x, y := p.Min.X+8, p.Min.Y+8
a.DrawBig(screen, "チャージ", x, y, colKnown)
a.Draw(screen, a.T("mini.topup.title"), x+60, y+2, colText)
y += 20
switch m.Stage {
case minigame.TopUpPickMember:
a.Draw(screen, a.T("mini.topup.member"), x, y, colDim)
y += LineH + 4
for i, name := range m.Members {
col := colText
if i == m.Cursor {
vector.DrawFilledRect(screen, float32(x-2), float32(y+i*LineH-1), 200, LineH, colBar, false)
col = colKnown
}
bal := s.run.Wallet.Card
if i > 0 {
bal = s.run.Party.Members[i].Card
}
a.Draw(screen, fmt.Sprintf("%-8s %5d¥", name, bal), x, y+i*LineH, col)
}
case minigame.TopUpPickButton:
a.Draw(screen, a.T("mini.topup.pick"), x, y, colDim)
y += LineH + 4
for i, b := range m.Buttons {
bx, by := x+(i%2)*140, y+(i/2)*26
col := colBar
if i == m.Cursor {
col = color.RGBA{90, 105, 136, 255}
}
vector.DrawFilledRect(screen, float32(bx), float32(by), 132, 22, col, false)
s.jp(a, screen, b, bx+6, by+5)
}
if m.Penalty > 0 {
a.Draw(screen, "ピッ! "+a.T("mini.topup.wrong"), x, y+56, colBad)
}
case minigame.TopUpPickAmount:
a.Draw(screen, a.T("mini.topup.amount"), x, y, colDim)
y += LineH + 4
for i, amt := range m.Amounts {
col := colText
if i == m.Cursor {
vector.DrawFilledRect(screen, float32(x-2), float32(y+i*LineH-1), 120, LineH, colBar, false)
col = colKnown
}
a.Draw(screen, fmt.Sprintf("%d円 (%d¥)", amt, amt), x, y+i*LineH, col)
}
case minigame.TopUpFeed:
a.Draw(screen, fmt.Sprintf(a.T("mini.topup.feed"), m.Amount), x, y, colText)
y += LineH + 8
vector.DrawFilledRect(screen, float32(x), float32(y), float32(p.Dx()-16), 8, colBar, false)
vector.DrawFilledRect(screen, float32(x), float32(y), float32(float64(p.Dx()-16)*m.Fill), 8, colGood, false)
}
a.Draw(screen, a.T("mini.cancel"), x, p.Max.Y-16, colDim)
}
func (s *LevelScene) drawTicket(a *Assets, screen *ebiten.Image, m *minigame.Ticket, p image.Rectangle) {
x, y := p.Min.X+8, p.Min.Y+8
a.DrawBig(screen, "きっぷ", x, y, colKnown)
a.Draw(screen, a.T("mini.ticket.title"), x+48, y+2, colText)
y += 20
if m.Stage == minigame.TicketPickSeat {
s.drawSeats(a, screen, m, p, x, y)
return
}
a.Draw(screen, fmt.Sprintf(a.T("mini.ticket.hint"), s.run.Level.Destination.Get("romaji"), m.Cash), x, y, colDim)
y += LineH + 4
for i, o := range m.Options {
ry := y + i*LineH
col := colText
if i == m.Cursor {
vector.DrawFilledRect(screen, float32(x-2), float32(ry-1), float32(p.Dx()-12), LineH, colBar, false)
}
if o.Fare > m.Cash {
col = colDim
}
s.jp(a, screen, o.Dest, x, ry)
fare := fmt.Sprintf("%d円", o.Fare)
a.Draw(screen, fare, p.Max.X-10-a.Width(fare), ry, col)
}
a.Draw(screen, a.T("mini.cancel"), x, p.Max.Y-16, colDim)
}
func (s *LevelScene) drawCrowd(a *Assets, screen *ebiten.Image, m *minigame.Crowd, p image.Rectangle) {
x, y := p.Min.X+8, p.Min.Y+8
a.DrawBig(screen, "満員", x, y, colBad)
a.Draw(screen, a.T("mini.crowd.title"), x+36, y+2, colText)
y += 24
a.Draw(screen, fmt.Sprintf(a.T("mini.crowd.hint"), m.MaxTries-m.Attempts), x, y, colDim)
y += LineH + 10
w := float32(p.Dx() - 16)
vector.DrawFilledRect(screen, float32(x), float32(y), w, 14, colBar, false)
vector.DrawFilledRect(screen, float32(x)+w*float32(m.ZoneLo), float32(y), w*float32(m.ZoneHi-m.ZoneLo), 14, colGood, false)
vector.DrawFilledRect(screen, float32(x)+w*float32(m.Pos)-1, float32(y)-3, 3, 20, colText, false)
}
func (s *LevelScene) drawRide(a *Assets, screen *ebiten.Image, m *minigame.Ride, p image.Rectangle) {
x, y := p.Min.X+8, p.Min.Y+8
a.DrawBig(screen, "次は", x, y, colDim)
a.Draw(screen, a.T("mini.ride.title"), x+36, y+2, colText)
y += 22
cur := m.Current()
a.DrawBig(screen, cur, x, y, colText)
rd := lang.Render(a.Dict, s.run.Know, cur, UILang)
a.drawLines(screen, []line{shownLine(rd.Partial, colText, false)}, x+a.Width(cur)*12/10+12, y+2)
lvl := s.run.Level
if lang.ShowEnglish(lvl.ID, m.Index, 77, lvl.Difficulty) {
if e := a.Dict.Lookup(cur); e != nil {
a.Draw(screen, e.Romaji, x, y+16, colEnglish)
}
}
y += 34
// Linjen: en prick per station, den aktuella större.
for i := range m.Stops {
cx := x + 6 + i*22
col := colDim
if i == m.Index {
col = colText
}
if i < m.Index {
col = colBar
}
vector.DrawFilledRect(screen, float32(cx), float32(y+3), 14, 2, colBar, false)
vector.DrawFilledRect(screen, float32(cx-3), float32(y), 6, 8, col, false)
}
y += 14
vector.DrawFilledRect(screen, float32(x), float32(y), float32(p.Dx()-16), 3, colBar, false)
vector.DrawFilledRect(screen, float32(x), float32(y), float32(float64(p.Dx()-16)*(1-m.Timer/m.StopSec)), 3, colKnown, false)
hint := a.T("mini.ride.hint")
if dest := lvl.Destination.Get("romaji"); dest != "" {
hint = strings.Replace(hint, "%s", dest, 1)
}
a.Draw(screen, hint, x, p.Max.Y-16, colGood)
}
// prefText beskriver ett önskemål.
func prefText(a *Assets, m *minigame.Ticket, p minigame.SeatPref) string {
switch p.Kind {
case minigame.PrefWindow:
return a.T("seat.window")
case minigame.PrefAisle:
return a.T("seat.aisle")
case minigame.PrefLeft:
return a.T("seat.left")
case minigame.PrefRight:
return a.T("seat.right")
default:
return fmt.Sprintf(a.T("seat.with"), m.Members[p.With])
}
}
// drawSeats ritar vagnen: tre rader, fönster V | gång | fönster H, och
// sällskapets önskemål till höger.
func (s *LevelScene) drawSeats(a *Assets, screen *ebiten.Image, m *minigame.Ticket, p image.Rectangle, x, y int) {
a.Draw(screen, fmt.Sprintf(a.T("mini.seat.hint"), m.Members[m.Current]), x, y, colDim)
y += LineH + 4
const sw, sh, gap = 22, 18, 10
a.Draw(screen, a.T("seat.front"), x+2, y-2, colDim)
y += 10
for row := 0; row < minigame.SeatRows; row++ {
for col := 0; col < minigame.SeatCols; col++ {
sx := x + col*(sw+2)
if col >= 2 {
sx += gap
}
sy := y + row*(sh+3)
seat := minigame.Seat{Row: row, Col: col}
bg := colBar
if m.SeatTaken(seat) {
bg = color.RGBA{90, 105, 136, 255}
}
vector.DrawFilledRect(screen, float32(sx), float32(sy), sw, sh, bg, false)
if seat == m.Seat {
vector.DrawFilledRect(screen, float32(sx), float32(sy), sw, 2, colKnown, false)
vector.DrawFilledRect(screen, float32(sx), float32(sy+sh-2), sw, 2, colKnown, false)
}
for i, as := range m.Assigned {
if as == seat {
a.Draw(screen, string([]rune(m.Members[i])[:1]), sx+8, sy+3, colText)
}
}
}
}
// Önskemål.
px := x + 4*(sw+2) + gap + 14
a.Draw(screen, a.T("mini.seat.prefs"), px, y-10, colDim)
for i, pr := range m.Prefs {
col := colText
if pr.Member == m.Current {
col = colKnown
}
a.Draw(screen, m.Members[pr.Member]+": "+prefText(a, m, pr), px, y+i*LineH, col)
}
a.Draw(screen, a.T("mini.cancel"), x, p.Max.Y-16, colDim)
}