Files
metro-puzzle-jp/internal/game/trains.go
claude 0c47b0ad00
All checks were successful
release / build-release (push) Successful in 1m36s
M10b: bussar, fler sidospår och överstimulerande UI
Ett tåg kan vara en buss (vehicle: bus): samma tidtabell, faser,
bedömning i nätet och ombordstigning, men ett fordon på en väg i stället
för vagnar på räls, hållplatsskylt, "1番" på tavlan, tuta vid inrullning
och dörrsignal före avgång. Nätet har tre busslinjer från Narita. Nya
sidobanor: 001b Flygbussterminalen (biljettbana med tre bussar) och 003b
Harajuku (Yamanote åt rätt eller fel håll runt ringen). Overload-flaggan
ger en rullande reklamticker och maximalt med affischer (Ueno, Tokyo).
Romaji utan makroner (fonten saknar dem). Skärmdumparna som börjar på
bankartan går via titelskärmen. Tester, docs och wiki uppdaterade.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-25 17:33:55 +01:00

241 lines
8.3 KiB
Go

package game
import (
"image/color"
"math"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/vector"
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
)
// Tågens ankomst och avgång (M7b): räls längs perrongkanten, vagnar som
// rullar in och ut, vind, ankomstindikator och melodier.
const (
trainCars = 3 // vagnar per tåg
trainRunTiles = 6.0 // hur långt bort tåget startar/försvinner
railsReach = 8 // räls ritas så här många rutor åt varje håll
maxParticles = 40
indicatorDist = 2 // rutor från perrongen där man ser/hör tåget komma
)
type particle struct {
x, y, vx, vy, life float64
}
// trackInfo är det som behövs för att rita ett tågs räls och rörelse.
type trackInfo struct {
tile level.Tile // P eller p
platform level.Point // perrongrutan
track level.Point // rälsrutan bortom kanten
horizontal bool // rälsens riktning
bus bool // buss: väg och ett fordon i stället för räls och vagnar
}
func (s *LevelScene) buildTracks() {
s.tracks = nil
s.rails = map[level.Point]bool{}
s.roads = map[level.Point]bool{}
for _, t := range level.PlatformTiles {
for _, p := range s.run.Level.Find(t) {
tt, horizontal := s.run.Level.TrackTile(p)
bus := false
if tr := s.run.Level.TrainAt(t); tr != nil && tr.IsBus() {
bus = true
}
s.tracks = append(s.tracks, trackInfo{tile: t, platform: p, track: tt, horizontal: horizontal, bus: bus})
out := level.Point{X: tt.X - p.X, Y: tt.Y - p.Y} // riktningen bort från perrongen
for k := -railsReach; k <= railsReach; k++ {
c := tt
if horizontal {
c.X += k
} else {
c.Y += k
}
beyond := level.Point{X: c.X + out.X, Y: c.Y + out.Y, Z: c.Z}
// Räls på väggrutor längs spåret, så länge det är vägg (eller kartkant) bortom dem.
if s.run.Level.At(c) == level.Wall && s.run.Level.At(beyond) == level.Wall {
if bus {
s.roads[c] = true
} else {
s.rails[c] = true
}
}
}
}
}
}
// trainOffset är tågets förskjutning i rutor längs rälsen i en fas:
// det kommer in från minus-sidan och lämnar åt plus-sidan.
func trainOffset(phase level.Phase, prog float64) (float64, bool) {
switch phase {
case level.Arriving:
return -trainRunTiles * (1 - prog) * (1 - prog), true // bromsar in
case level.Dwelling, level.Closing:
return 0, true
case level.Leaving:
return trainRunTiles * prog * prog, true // accelererar ut
}
return 0, false
}
// drawRails ritar rälsrutan (roterad för vertikala spår).
func (s *LevelScene) drawRails(g *Game, screen *ebiten.Image, c level.Point, ox, oy float64) {
var op ebiten.DrawImageOptions
vertical := false
for _, tr := range s.tracks {
if !tr.horizontal && c.X == tr.track.X && c.Z == tr.track.Z {
vertical = true
}
}
if vertical {
op.GeoM.Translate(-TileSize/2, -TileSize/2)
op.GeoM.Rotate(math.Pi / 2)
op.GeoM.Translate(TileSize/2, TileSize/2)
}
op.GeoM.Translate(ox+float64(c.X*TileSize), oy+float64(c.Y*TileSize))
img := g.Assets.Rails
if s.roads[c] {
img = g.Assets.Road
}
screen.DrawImage(img, &op)
}
// drawTrains ritar tågen i rätt läge för sin fas.
func (s *LevelScene) drawTrains(g *Game, screen *ebiten.Image, ox, oy float64) {
z := s.run.Player.Tile.Z
for _, tr := range s.tracks {
if tr.platform.Z != z {
continue // tågen på andra våningar syns inte härifrån
}
phase, prog := s.run.PlatformPhase(tr.tile)
if tr.bus { // hållplatsskylten står på hållplatsen
var op ebiten.DrawImageOptions
op.GeoM.Translate(ox+float64(tr.platform.X*TileSize), oy+float64(tr.platform.Y*TileSize))
screen.DrawImage(g.Assets.StopSign, &op)
}
// Strålkastare när tåget är på väg in: ett sken från det håll det kommer.
if phase == level.Approaching {
s.drawHeadlight(screen, tr, prog, ox, oy)
}
off, show := trainOffset(phase, prog)
if !show {
continue
}
cars, sprite := trainCars, g.Assets.Train
if tr.bus {
cars, sprite = 1, g.Assets.Bus
}
for k := 0; k < cars; k++ {
d := off + float64(k-cars/2)
var op ebiten.DrawImageOptions
x, y := float64(tr.track.X*TileSize), float64(tr.track.Y*TileSize)
if tr.horizontal {
x += d * TileSize
} else {
y += d * TileSize
op.GeoM.Translate(-TileSize/2, -TileSize/2)
op.GeoM.Rotate(math.Pi / 2)
op.GeoM.Translate(TileSize/2, TileSize/2)
}
op.GeoM.Translate(ox+math.Round(x), oy+math.Round(y))
screen.DrawImage(sprite, &op)
}
}
}
func (s *LevelScene) drawHeadlight(screen *ebiten.Image, tr trackInfo, prog float64, ox, oy float64) {
a := 0.25 + 0.55*prog // Ebitengine vill ha premultiplicerad alfa
c := color.RGBA{uint8(255 * a), uint8(240 * a), uint8(180 * a), uint8(255 * a)}
x, y := float64(tr.track.X*TileSize), float64(tr.track.Y*TileSize)
length := float32(TileSize * (1 + 4*prog))
if tr.horizontal {
vector.DrawFilledRect(screen, float32(ox+x)-length, float32(oy+y)+4, length, 8, c, false)
} else {
vector.DrawFilledRect(screen, float32(ox+x)+4, float32(oy+y)-length, 8, length, c, false)
}
}
// drawIndicator: blinkande lampor vid perrongkanten när tåget är på väg in
// och spelaren är nära rälsen.
func (s *LevelScene) drawIndicator(screen *ebiten.Image, ox, oy float64) {
_, platform, phase, _, _, ok := s.run.NearTrack(indicatorDist)
if !ok || phase != level.Approaching || math.Mod(s.run.Time, 0.5) > 0.25 {
return
}
x, y := float32(ox+float64(platform.X*TileSize)), float32(oy+float64(platform.Y*TileSize))
vector.DrawFilledRect(screen, x+2, y+TileSize-3, 3, 2, colWarn, false)
vector.DrawFilledRect(screen, x+TileSize-5, y+TileSize-3, 3, 2, colWarn, false)
}
// updateEffects sköter fasbyten (ljud), vind och partiklar.
func (s *LevelScene) updateEffects(g *Game) {
r := s.run
px, py := r.Player.WorldPos(TileSize)
for _, tr := range s.tracks {
phase, prog := r.PlatformPhase(tr.tile)
if last, seen := s.lastPhase[tr.tile]; !seen || last != phase {
if tr.platform.Z != r.Player.Tile.Z {
s.lastPhase[tr.tile] = phase
continue // hörs inte från en annan våning
}
s.lastPhase[tr.tile] = phase
dist := math.Hypot(float64(tr.platform.X*TileSize)-px, float64(tr.platform.Y*TileSize)-py) / TileSize
mask := noiseMask(r.Level.Noise)
switch {
case tr.bus && phase == level.Arriving: // bussen tutar när den svänger in
g.Audio.Play(SoundBusHorn, volumeAt(dist)*mask)
case tr.bus && phase == level.Closing:
g.Audio.Play(SoundDoors, volumeAt(dist)*mask)
case tr.bus:
case phase == level.Approaching: // ankomstjingeln: tåget är på väg
g.Audio.Play(SoundApproach, volumeAt(dist)*mask)
case phase == level.Arriving: // ding-dong när det rullar in
g.Audio.Play(SoundAnnounce, volumeAt(dist)*mask)
case phase == level.Closing: // stationens avgångsmelodi innan dörrarna stängs
g.Audio.Play(MelodyFor(r.Level), volumeAt(dist)*mask)
case phase == level.Leaving: // dörrsignalen när det går
g.Audio.Play(SoundDoors, volumeAt(dist)*mask)
}
}
// Vind längs perrongen när tåget rullar in eller ut (och strax före).
windy := phase == level.Arriving || phase == level.Leaving || (phase == level.Approaching && prog > 0.7)
if windy && tr.platform.Z == r.Player.Tile.Z && len(s.particles) < maxParticles {
dir := 1.0
if phase == level.Arriving {
dir = 1.0 // luften trycks framför tåget: samma håll som tåget rör sig (in från minus-sidan)
}
spread := float64(s.rng.IntN(5*TileSize)) - 2*TileSize
x := float64(tr.platform.X*TileSize) + spread
y := float64(tr.platform.Y*TileSize) + float64(s.rng.IntN(TileSize))
vx, vy := dir*(60+float64(s.rng.IntN(60))), float64(s.rng.IntN(10))-5
if !tr.horizontal {
x, y = float64(tr.platform.X*TileSize)+float64(s.rng.IntN(TileSize)), float64(tr.platform.Y*TileSize)+spread
vx, vy = vy, vx
}
s.particles = append(s.particles, particle{x, y, vx, vy, 0.4 + 0.3*s.rng.Float64()})
}
}
kept := s.particles[:0]
for _, p := range s.particles {
p.life -= dt
if p.life <= 0 {
continue
}
p.x += p.vx * dt
p.y += p.vy * dt
kept = append(kept, p)
}
s.particles = kept
}
func (s *LevelScene) drawParticles(screen *ebiten.Image, ox, oy float64) {
for _, p := range s.particles {
a := uint8(200 * math.Min(1, p.life*3))
vector.DrawFilledRect(screen, float32(ox+p.x), float32(oy+p.y), 2, 1, color.RGBA{a, a, a, a}, false)
}
}