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