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.Tile{level.Platform, level.WrongPlatform} { 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}) for k := -railsReach; k <= railsReach; k++ { c := tt if horizontal { c.X += k } else { c.Y += k } if s.run.Level.At(c.X, c.Y) == level.Wall && (c.X == 0 || c.Y == 0 || c.X == s.run.Level.Width()-1 || c.Y == s.run.Level.Height()-1) { 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) { 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) { for _, tr := range s.tracks { 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 { s.lastPhase[tr.tile] = phase dist := math.Hypot(float64(tr.platform.X*TileSize)-px, float64(tr.platform.Y*TileSize)-py) / TileSize switch phase { case level.Approaching: g.Audio.Play(SoundApproach, volumeAt(dist)) case level.Closing: g.Audio.Play(SoundDoors, volumeAt(dist)) } } // 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 && 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) } }