Files
metro-puzzle-jp/internal/game/scene_map.go
claude 0c47b0ad00
All checks were successful
release / build-release (push) Successful in 1m36s
M10b: bussar, fler sidospår och överstimulerande UI
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>
2026-10-25 17:33:55 +01:00

276 lines
8.0 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/buildinfo"
"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, fmt.Sprintf("%s (%s)", buildinfo.Version, buildinfo.Mode), 8, ScreenH-12, colDim)
}
// 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)
}