All checks were successful
release / build-release (push) Successful in 1m34s
internal/nav med resources/data/network.json (76 stationer, 11 linjer, Yamanote som ringlinje). Tåg bedöms från lineId + terminus mot banans station/to: rätt, fel håll, för långt bort (fler än 12 stationer) eller ingen trafik; correct räknas ut för nätkopplade banor, 1-4 rätta tåg. Tågresan visar linjens riktiga stationer. Nya förlustorsaker "Fel håll" och "För långt bort". Listläge (nästa tåg nere till vänster) och skyltläge (+50 % poäng) växlas med N på bankartan och sparas; -mode. Terminalen: biljettautomaten visar linjetavlan på nätbanor. Perronger P p Q q R r. Bana 006 Ikebukuro med fyra perronger. Banorna 001-005 kopplade till nätet. Skript: waitover och pick calm; poängraden radbryts; skärmdumpar uppdaterade. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
209 lines
7.0 KiB
Go
209 lines
7.0 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
|
|
}
|
|
|
|
func (s *LevelScene) buildTracks() {
|
|
s.tracks = nil
|
|
s.rails = map[level.Point]bool{}
|
|
for _, t := range level.PlatformTiles {
|
|
for _, p := range s.run.Level.Find(t) {
|
|
tt, horizontal := s.run.Level.TrackTile(p)
|
|
s.tracks = append(s.tracks, trackInfo{tile: t, platform: p, track: tt, horizontal: horizontal})
|
|
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 {
|
|
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))
|
|
screen.DrawImage(g.Assets.Rails, &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)
|
|
// 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
|
|
}
|
|
for k := 0; k < trainCars; k++ {
|
|
d := off + float64(k-trainCars/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(g.Assets.Train, &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 phase {
|
|
case level.Approaching:
|
|
g.Audio.Play(SoundApproach, volumeAt(dist)*mask)
|
|
case level.Closing:
|
|
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)
|
|
}
|
|
}
|