Files
metro-puzzle-jp/internal/game/scene_ending.go
claude 4ae6a1d24c
All checks were successful
release / build-release (push) Successful in 1m37s
M10: byten, hemresan och levelcheck
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>
2026-10-24 13:37:48 +02:00

179 lines
5.2 KiB
Go

package game
import (
"fmt"
"image/color"
"math"
"sort"
"strings"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/vector"
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/sim"
)
// EndingScene är storyns slut: sällskapet flyger hem, resan summeras och
// eftertexterna rullar. Enter går vidare mellan delarna, Esc hoppar till
// bankartan.
type EndingScene struct {
t float64 // sekunder i aktuell del
phase int // 0 flyget, 1 summeringen, 2 eftertexterna
lines []string
}
const (
endingFlightSec = 7.0 // flyget innan summeringen kommer av sig själv
endingCreditsPxS = 14.0 // eftertexternas rullhastighet
)
// NewEndingScene startar hemresan.
func NewEndingScene(g *Game) *EndingScene {
return &EndingScene{lines: strings.Split(g.Assets.T("ending.credits"), "\n")}
}
func (e *EndingScene) Update(g *Game) error {
e.t += dt
switch {
case backJustPressed():
g.SetScene(NewMapScene(g))
return nil
case confirmJustPressed():
e.phase++
e.t = 0
}
if e.phase == 0 && e.t > endingFlightSec {
e.phase, e.t = 1, 0
}
if e.phase == 2 && e.t*endingCreditsPxS > float64(len(e.lines)*LineH+ScreenH) {
e.phase++ // eftertexterna har rullat förbi
}
if e.phase > 2 {
g.SetScene(NewMapScene(g))
}
return nil
}
func (e *EndingScene) Draw(g *Game, screen *ebiten.Image) {
a := g.Assets
e.drawSky(screen)
switch e.phase {
case 0:
e.drawFlight(g, screen)
a.DrawBig(screen, a.T("ending.title"), 24, 20, colText)
a.Draw(screen, a.T("ending.sub"), 24, 44, colDim)
case 1:
e.drawSummary(g, screen)
default:
e.drawCredits(g, screen)
}
a.Draw(screen, a.T("ending.hint"), 8, ScreenH-12, colDim)
}
// drawSky är en skymningshimmel i band, från mörkblått upptill till orange
// vid horisonten, med långsamt drivande moln.
func (e *EndingScene) drawSky(screen *ebiten.Image) {
top, bottom := [3]float64{30, 32, 80}, [3]float64{230, 120, 70}
const bands = 12
for i := 0; i < bands; i++ {
f := float64(i) / float64(bands-1)
col := color.RGBA{uint8(top[0] + (bottom[0]-top[0])*f), uint8(top[1] + (bottom[1]-top[1])*f), uint8(top[2] + (bottom[2]-top[2])*f), 255}
vector.DrawFilledRect(screen, 0, float32(i*ScreenH/bands), ScreenW, float32(ScreenH/bands+1), col, false)
}
cloud := color.RGBA{250, 220, 200, 120}
for i := 0; i < 5; i++ {
x := math.Mod(float64(i)*73+e.t*(4+float64(i)), ScreenW+60) - 30
y := float64(40 + i*22)
vector.DrawFilledRect(screen, float32(x), float32(y), 28, 6, cloud, false)
vector.DrawFilledRect(screen, float32(x+6), float32(y-4), 14, 5, cloud, false)
}
vector.DrawFilledRect(screen, 0, ScreenH-20, ScreenW, 20, color.RGBA{24, 20, 37, 255}, false) // marken
}
// drawFlight: planet lyfter åt höger medan sällskapet vinkar på marken.
func (e *EndingScene) drawFlight(g *Game, screen *ebiten.Image) {
a := g.Assets
f := min(1, e.t/endingFlightSec)
px := -40 + f*(ScreenW+80)
py := 120 - f*80 + 3*math.Sin(e.t*2)
var op ebiten.DrawImageOptions
op.GeoM.Scale(2, 2)
op.GeoM.Translate(px, py)
screen.DrawImage(a.Plane, &op)
for i := range memberTints {
var o ebiten.DrawImageOptions
t := memberTints[i]
o.ColorScale.Scale(t[0], t[1], t[2], 1)
o.GeoM.Translate(float64(ScreenW/2-50+i*20), ScreenH-20-TileSize+math.Abs(2*math.Sin(e.t*3+float64(i))))
screen.DrawImage(a.PlayerFrame(sim.Up, true, e.t+float64(i)*0.3), &o)
}
}
// drawSummary är resan i siffror: banor, stjärnor, poäng, ord och souvenirer.
func (e *EndingScene) drawSummary(g *Game, screen *ebiten.Image) {
a := g.Assets
p := g.Progress
vector.DrawFilledRect(screen, 16, 14, ScreenW-32, ScreenH-40, colPanel, false)
vector.DrawFilledRect(screen, 16, 14, ScreenW-32, 2, colGood, false)
a.DrawBig(screen, a.T("ending.summary"), 24, 20, colGood)
levels, cleared, score := 0, 0, 0
for _, l := range g.Levels {
if l.IsLeg() {
continue
}
levels++
if r := p.Levels[l.ID]; r != nil && r.Cleared {
cleared++
score += r.Score
}
}
y := 44
rows := []string{
fmt.Sprintf("%s: %d/%d", a.T("ending.levels"), cleared, levels),
fmt.Sprintf("%s: %d/%d", a.T("ending.stars"), p.TotalStars(), 3*levels),
fmt.Sprintf("%s: %d", a.T("ending.score"), score),
fmt.Sprintf("%s: %d", a.T("ending.words"), g.Know.Count()),
}
for _, r := range rows {
a.Draw(screen, r, 24, y, colText)
y += LineH
}
var items []string
for id := range p.Items {
if it := g.Catalog.Item(id); it != nil {
items = append(items, it.Name.Get(UILang))
}
}
sort.Strings(items)
souvenirs := a.T("ending.none")
if len(items) > 0 {
souvenirs = strings.Join(items, ", ")
}
for _, l := range a.wrapLine(plainLine(a.T("ending.items")+": "+souvenirs, colKnown), ScreenW-48) {
a.drawLines(screen, []line{l}, 24, y)
y += LineH
}
y += 4
for _, l := range a.wrapLine(plainLine(a.T("ending.story_done"), colWarn), ScreenW-48) {
a.drawLines(screen, []line{l}, 24, y)
y += LineH
}
}
// drawCredits rullar eftertexterna nerifrån och upp.
func (e *EndingScene) drawCredits(g *Game, screen *ebiten.Image) {
a := g.Assets
y0 := ScreenH - int(e.t*endingCreditsPxS)
for i, l := range e.lines {
y := y0 + i*LineH
if y < -LineH || y > ScreenH {
continue
}
col := colDim
if i == 0 || (i > 0 && e.lines[i-1] == "" && i+1 < len(e.lines) && e.lines[i+1] != "") {
col = colText // rubriker
}
a.Draw(screen, l, (ScreenW-a.Width(l))/2, y, col)
}
}