All checks were successful
release / build-release (push) Successful in 1m34s
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>
230 lines
6.6 KiB
Go
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)
|
|
}
|