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 - 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() 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) }