Files
metro-puzzle-jp/internal/game/scene_map.go
claude 5114115976
All checks were successful
release / build-release (push) Successful in 1m37s
Texterna ryms på skärmen på svenska och engelska
Björn såg att bankartans tangentrad klipptes på engelska. Nu mäts alla
enradstexter med den riktiga fonten i båda språken mot sitt utrymme
(game.TestSingleLineTextsFit) och HUD-raden för alla banor
(TestLevelNamesFitHUD). Åtgärder: kortare etiketter ("Mål"/"To"),
bankartans tangentrad på två rader (versionen står på titelskärmen),
HUD:en stryker japansk hint, "via" och valfria plånboksdelar som inte
får plats och kortar med "..", fler rutor radbryts, kortare bannamn,
kortare minispelstexter. Skärmdumpar på engelska (map-en, sign-en) via en
engelsk demosparfil. Alla skärmdumpar omtagna.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-25 18:10:26 +01:00

275 lines
7.9 KiB
Go

package game
import (
"fmt"
"image/color"
"sort"
"strings"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/vector"
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
)
// MapScene är bankartan i spelbrädesstil: huvudbanorna på en slingrande
// linje från flygplatsen, sidobanorna som avstickare ovanför. Låsta banor
// är grå, klarade visar stjärnor.
type MapScene struct {
confirmNG bool // G tryckt en gång: nästa G startar New Game+
mains []*level.Level
sides map[string][]*level.Level // parent -> sidobanor
sel int // vald huvudbana
side int // 0 = huvudbanan, 1.. = sidobana nr
msg string
}
type mapNode struct {
lvl *level.Level
x, y int
}
// NewMapScene bygger kartan från alla banor.
func NewMapScene(g *Game) *MapScene {
m := &MapScene{sides: map[string][]*level.Level{}}
for _, l := range g.Levels {
switch {
case l.IsLeg(): // delbanor nås via sin resa
case l.IsSide():
m.sides[l.Parent] = append(m.sides[l.Parent], l)
default:
m.mains = append(m.mains, l)
}
}
sort.Slice(m.mains, func(i, j int) bool { return m.mains[i].Order < m.mains[j].Order })
// Välj den första banan som inte är klarad.
for i, l := range m.mains {
if !g.Progress.Cleared(l.ID) {
m.sel = i
break
}
}
return m
}
// nodes ger positionerna på kartan.
func (m *MapScene) nodes() ([]mapNode, map[string][]mapNode) {
n := len(m.mains)
var mains []mapNode
sides := map[string][]mapNode{}
for i, l := range m.mains {
x := 36
if n > 1 {
x = 36 + i*(ScreenW-72)/(n-1)
}
y := 104
if i%2 == 1 {
y = 116
}
mains = append(mains, mapNode{l, x, y})
for k, s := range m.sides[l.ID] {
sides[l.ID] = append(sides[l.ID], mapNode{s, x + 12 + k*18, y - 28})
}
}
return mains, sides
}
func (m *MapScene) current() *level.Level {
if len(m.mains) == 0 {
return nil
}
main := m.mains[m.sel]
if m.side > 0 {
if s := m.sides[main.ID]; m.side-1 < len(s) {
return s[m.side-1]
}
}
return main
}
func (m *MapScene) Update(g *Game) error {
switch {
case backJustPressed():
g.SetScene(NewTitleScene())
return nil
case wordsJustPressed():
g.SetScene(NewWordsScene(g.Assets, m))
return nil
case skillsJustPressed():
g.SetScene(NewSkillsScene(g, m))
return nil
case itemsJustPressed():
g.SetScene(NewItemsScene(g, m))
return nil
case phoneJustPressed():
g.Wallet.Phone = !g.Wallet.Phone
g.Save()
case modeJustPressed():
g.toggleMode()
case settingsJustPressed():
g.SetScene(NewSettingsScene(m))
return nil
case endlessJustPressed() && g.Progress.StoryCleared:
s, err := g.startEndless(g.seed())
if err != nil {
m.msg = err.Error()
return nil
}
g.SetScene(s)
return nil
case ngPlusJustPressed() && g.Progress.StoryCleared:
if !m.confirmNG {
m.confirmNG = true
m.msg = g.Assets.T("map.ngplus_confirm")
return nil
}
g.newGamePlus()
g.SetScene(NewMapScene(g))
return nil
}
if m.confirmNG && (confirmJustPressed() || miniMoveXYAny()) {
m.confirmNG, m.msg = false, "" // något annat tryckt: ingen NG+
}
if n := len(m.mains); n > 0 {
mx, my := miniMoveXY()
if mx != 0 {
m.sel = (m.sel + mx + n) % n
m.side = 0
}
if my != 0 {
sides := len(m.sides[m.mains[m.sel].ID])
m.side = max(0, min(sides, m.side-my)) // upp = ut på sidospåret
}
if confirmJustPressed() {
l := m.current()
if !g.Progress.Unlocked(g.Levels, l.ID) {
m.msg = g.Assets.T("map.locked")
return nil
}
s, err := g.startLevel(l.ID)
if err != nil {
m.msg = err.Error()
return nil
}
g.SetScene(s)
}
}
return nil
}
func (m *MapScene) Draw(g *Game, screen *ebiten.Image) {
a := g.Assets
p := g.Progress
screen.Fill(color.RGBA{24, 20, 37, 255})
vector.DrawFilledRect(screen, 0, 0, ScreenW, 22, color.RGBA{38, 43, 68, 255}, false)
a.DrawBig(screen, a.T("game.title"), 8, 5, colText)
hdr := fmt.Sprintf("%s %d %s %d/%d ★ %d", a.T("map.level"), p.Level(), a.T("map.xp"), p.XP, p.XP+progress_XPToNext(p.XP), p.TotalStars())
a.Draw(screen, hdr, ScreenW-8-a.Width(hdr), 6, colKnown)
mains, sides := m.nodes()
// Linjen mellan huvudbanorna.
for i := 1; i < len(mains); i++ {
drawLine(screen, mains[i-1].x, mains[i-1].y, mains[i].x, mains[i].y, colBar)
}
for _, mn := range mains {
for _, sn := range sides[mn.lvl.ID] {
drawLine(screen, mn.x, mn.y, sn.x, sn.y, colBar)
}
}
cur := m.current()
drawNode := func(n mapNode, side bool) {
sz := float32(14)
if side {
sz = 10
}
col := colDim
unlocked := p.Unlocked(g.Levels, n.lvl.ID)
switch {
case p.Cleared(n.lvl.ID):
col = colGood
case unlocked:
col = colText
}
vector.DrawFilledRect(screen, float32(n.x)-sz/2, float32(n.y)-sz/2, sz, sz, col, false)
if n.lvl == cur {
vector.DrawFilledRect(screen, float32(n.x)-sz/2-2, float32(n.y)-sz/2-2, sz+4, 2, colKnown, false)
vector.DrawFilledRect(screen, float32(n.x)-sz/2-2, float32(n.y)+sz/2, sz+4, 2, colKnown, false)
}
if r := p.Levels[n.lvl.ID]; r != nil && r.Cleared {
st := strings.Repeat("★", r.Stars)
a.Draw(screen, st, n.x-a.Width(st)/2, n.y+int(sz)/2+2, colWarn)
}
if !unlocked { // lås: en liten bygel + låda
vector.DrawFilledRect(screen, float32(n.x)-3, float32(n.y)-1, 6, 5, colPanel, false)
vector.DrawFilledRect(screen, float32(n.x)-2, float32(n.y)-4, 4, 3, colPanel, false)
vector.DrawFilledRect(screen, float32(n.x)-1, float32(n.y)-3, 2, 2, colDim, false)
}
}
for _, n := range mains {
drawNode(n, false)
for _, s := range sides[n.lvl.ID] {
drawNode(s, true)
}
}
// Info om vald bana.
if cur != nil {
name := cur.Name.Get(UILang) + " " + cur.Name.Get("ja")
if cur.IsSide() {
name = a.T("map.side") + ": " + name
}
a.Draw(screen, name, 8, 134, colText)
fd := level.FinalOf(g.Levels, cur).Destination
info := fmt.Sprintf("→ %s %s %s %s %d", fd.Get("ja"), fd.Get("romaji"),
strings.Repeat("★", cur.Difficulty)+strings.Repeat("☆", 5-cur.Difficulty), a.T("map.party"), cur.Party)
if n := len(level.ChainOf(g.Levels, cur.ID)); n > 1 {
info += fmt.Sprintf(" %s %d", a.T("map.transfers"), n-1)
}
if g.State.Current != nil && g.State.Current.LevelID == cur.ID {
info += " " + a.T("map.resume")
}
a.Draw(screen, info, 8, 146, colDim)
if !p.Unlocked(g.Levels, cur.ID) {
a.Draw(screen, a.T("map.locked"), 150, 30, colBad)
} else if m.msg != "" {
for i, l := range a.wrapLine(plainLine(m.msg, colBad), ScreenW-158) {
a.drawLines(screen, []line{l}, 150, 30+i*LineH)
}
}
}
onoff := a.T("menu.off")
if g.Wallet.Phone {
onoff = a.T("menu.on")
}
foot := fmt.Sprintf("%s %d %s %d %s %d¥", a.T("map.skill_points"), p.SkillPoints, a.T("words.points"), g.Know.Points,
a.T("hud.cash"), g.Wallet.Cash)
a.Draw(screen, foot, 8, 30, colDim)
a.Draw(screen, fmt.Sprintf(a.T("menu.phone"), onoff)+" "+fmt.Sprintf(a.T("menu.mode"), a.T("mode."+g.Mode.String())), 8, 42, colDim)
y := 54
if p.StoryCleared { // efter storyn: oändligt läge och New Game+
line := a.T("map.endless")
if b := g.State.BestEndless(); b != nil {
line += fmt.Sprintf(" (%s)", fmt.Sprintf(a.T("map.endless_best"), b.Score, b.Goals))
}
line += " " + a.T("map.ngplus")
if p.NGPlus > 0 {
line += fmt.Sprintf(" (%s)", fmt.Sprintf(a.T("map.ngplus_level"), p.NGPlus))
}
a.Draw(screen, line, 8, y, colKnown)
y += LineH
}
if len(g.Profile.Debuffs) > 0 {
var parts []string
for _, d := range g.Profile.Debuffs {
parts = append(parts, fmt.Sprintf("%s (%d)", a.T("debuff."+string(d.Kind)), d.LevelsLeft))
}
a.Draw(screen, a.T("hud.debuffs")+": "+strings.Join(parts, ", "), 8, y, colBad)
}
a.Draw(screen, a.T("map.hint"), 8, ScreenH-24, colDim)
a.Draw(screen, a.T("map.hint2"), 8, ScreenH-12, colDim) // versionen står på titelskärmen
}
// drawLine ritar en tjock linje mellan två punkter.
func drawLine(screen *ebiten.Image, x0, y0, x1, y1 int, col color.Color) {
vector.StrokeLine(screen, float32(x0), float32(y0), float32(x1), float32(y1), 2, col, false)
}