All checks were successful
release / build-release (push) Successful in 1m37s
Resor med byten: en bana kan ha next som pekar på en delbana (kind leg, syns inte på bankartan). När man klivit av rätt tåg fortsätter samma omgång på nästa station med klocka, plånbok, sällskap och räknare; par-tiden adderas, dimma och folk är nya, en Byte!-banner visas och HUD:en visar resmålet via bytesstationen. Tidsgräns och missade tåg gäller hela resan. level.ValidateChains kräver sammanhängande kedjor. Nya resor: 007 Akihabara -> Ueno -> Asakusa och finalen 008 Tokyo (två våningar) -> Nippori -> Narita flygplats. Slutscen när sista huvudbanan klaras första gången: planet lyfter, resan summeras, eftertexter; Progress.StoryCleared sparas; flaggan -ending. tools/levelcheck ritar banorna och rapporterar problem. HUD-nivån kortas när mätarna tar plats; poängraden radbryts. Skärmdumpar och docs uppdaterade. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
236 lines
6.8 KiB
Go
236 lines
6.8 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 {
|
|
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 - 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)
|
|
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 != "" {
|
|
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)
|
|
}
|