Files
metro-puzzle-jp/internal/game/scene_ending.go
claude 86f8130735
All checks were successful
release / build-release (push) Successful in 1m35s
M12: ljud, inställningar och release v0.1.0
tools/gensound syntetiserar 21 ljud: fyra stationsmelodier i klockklang
(en per station via melody-fältet eller banans id), ankomstjingel,
ding-dong vid inrullning, dörrsignal vid avgång, spärrens pip och
nekande, kiosk, toalett, lärt ord, bekräfta/varning, byte, personal,
vinst, förlust, flygplan, sorl och menymusik. sfx.go spelar effekter
utifrån vad som hänt i sim.Run sedan förra bildrutan. Ljudlagret har
huvudvolym × effekter/musik och byter inte slinga i onödan.
Inställningar (S på bankartan, sparas): volymer, språk sv/en som laddas
om direkt, fönsterskala, fullskärm (F11 överallt) och tangentöversikt.
Fönsterikon från genart. CHANGELOG.md. Tester för ljudfiler, melodival
och inställningar. Docs, wiki och skärmdumpar uppdaterade.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-25 08:31:38 +01:00

180 lines
5.3 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 {
g.Audio.Play(SoundPlane, 0.8)
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)
}
}