All checks were successful
release / build-release (push) Successful in 1m35s
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>
180 lines
5.3 KiB
Go
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)
|
|
}
|
|
}
|