M8: progression, items, bankarta och sparfil
All checks were successful
release / build-release (push) Successful in 1m46s
All checks were successful
release / build-release (push) Successful in 1m46s
- internal/progress: katalog (skills.json 12 noder, items.json 25 items) med validering; XP/nivåer (150·n(n+1)/2) och skillpoäng; skillträd med krav och kostnad; items per plats med buffar, genvägar (JR Pass, kartor, powerbank) och färgton; banresultat och upplåsning (huvudbanor i ordning, sidospår efter föräldern); 33 %-regeln som test - internal/save: versionerad JSON med kunskap, plånbok, debuffar, progression och pågående bana + seed; atomisk skrivning; migrering; ~/.config/metro-puzzle-jp/save.json resp. %AppData% - sim: RunOptions får Extra/FogRadius/Landmarks/FreeGates/RevealAll/ InfiniteBattery; luck påverkar trängselzonen; Fog.RevealAll och extra landmärken - game: bankarta i spelbrädesstil ersätter menyn (⟳ återupptar med sparad seed), skillträd (T), garderob (I), butik i kiosken (B), XP/nivå/items/ upplåsningar på resultatet, spelaren tonas av skins; autospar vid banstart, efter banan och vid köp; flaggor -save/-nosave - level: kind/order/parent/rewards/shop; ny sidobana 002b Kabukicho som belönar skinnet Kimono; 001 belönar Salaryman-skinnet; butikssortiment på 002/003 - tools/shots: demo-sparfil (build/, aldrig den riktiga), bilder av karta, skills, garderob och butik; alla körningar med -nosave - docs/README/wiki uppdaterade Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
227
internal/game/scene_map.go
Normal file
227
internal/game/scene_map.go
Normal file
@@ -0,0 +1,227 @@
|
||||
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()
|
||||
}
|
||||
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), 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)
|
||||
}
|
||||
Reference in New Issue
Block a user