Files
metro-puzzle-jp/internal/game/scene_map.go
claude f762f9554e
All checks were successful
release / build-release (push) Successful in 1m34s
M9: navigeringslägen, linjenät och terminal
internal/nav med resources/data/network.json (76 stationer, 11 linjer,
Yamanote som ringlinje). Tåg bedöms från lineId + terminus mot banans
station/to: rätt, fel håll, för långt bort (fler än 12 stationer) eller
ingen trafik; correct räknas ut för nätkopplade banor, 1-4 rätta tåg.
Tågresan visar linjens riktiga stationer. Nya förlustorsaker "Fel håll"
och "För långt bort". Listläge (nästa tåg nere till vänster) och
skyltläge (+50 % poäng) växlas med N på bankartan och sparas; -mode.
Terminalen: biljettautomaten visar linjetavlan på nätbanor. Perronger
P p Q q R r. Bana 006 Ikebukuro med fyra perronger. Banorna 001-005
kopplade till nätet. Skript: waitover och pick calm; poängraden
radbryts; skärmdumpar uppdaterade.

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

230 lines
6.6 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 {
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 {
if l.IsSide() {
m.sides[l.Parent] = append(m.sides[l.Parent], l)
} else {
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 - 40})
}
}
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():
return ebiten.Termination
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()
}
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)
info := fmt.Sprintf("→ %s %s %s %s %d", cur.Destination.Get("ja"), cur.Destination.Get("romaji"),
strings.Repeat("★", cur.Difficulty)+strings.Repeat("☆", 5-cur.Difficulty), a.T("map.party"), cur.Party)
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 != "" {
a.Draw(screen, m.msg, 150, 30, colBad)
}
}
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)
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, 54, 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)
}