All checks were successful
release / build-release (push) Successful in 1m35s
- level: Schedule (ankomst/intervall/uppehåll) med Status(t), spårnummer, avgift, skylttexter per i-ruta, Path (BFS) och UngatedPlatforms – regeln att perronger bara nås via spärrar testas för alla banor - sim.Run: klocka, tågkort (saldo dras vid spärr, tomt kort stoppas), ombordstigning med Enter när tåget är inne, vinst/förlust; headless bot-test klarar bana 001 och förlorar på fel tåg - game: perronginfo (avgår om / nästa tåg om), skyltruta, nekad spärr, tågvagn som ritas när tåget är inne, inforutan flyttas upp när spelaren står nära nederkanten; resultat med spärrpassager och saldo - bana 001 ombyggd så spärrarna är flaskhalsar; bana 002 omgenererad med spärrar runt perrongerna och skylttexter - tools/shots räknar ut vägar med BFS; skriptkommandot waittrain - docs/README/wiki uppdaterade med nya bilder Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
183 lines
5.7 KiB
Go
183 lines
5.7 KiB
Go
package game
|
||
|
||
import (
|
||
"fmt"
|
||
"image/color"
|
||
"math"
|
||
"path/filepath"
|
||
|
||
"github.com/hajimehoshi/ebiten/v2"
|
||
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||
"github.com/hajimehoshi/ebiten/v2/vector"
|
||
|
||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/sim"
|
||
)
|
||
|
||
// Spelarens grundfart i rutor per sekund (blir en stat i M7).
|
||
const playerSpeed = 6.0
|
||
|
||
// Så länge "spärren nekade" visas efter försöket.
|
||
const deniedShowSec = 2.0
|
||
|
||
// LevelScene är själva spelandet av en bana. All spellogik ligger i sim.Run;
|
||
// scenen översätter input och ritar.
|
||
type LevelScene struct {
|
||
id string
|
||
run *sim.Run
|
||
cam *sim.Camera
|
||
animT float64
|
||
}
|
||
|
||
// NewLevelScene laddar banan och ställer spelaren på startrutan.
|
||
func NewLevelScene(a *Assets, id string) (*LevelScene, error) {
|
||
lvl, err := level.LoadFile(filepath.Join(a.LevelsDir, id+".json"))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
s := &LevelScene{
|
||
id: id,
|
||
run: sim.NewRun(lvl, playerSpeed),
|
||
cam: sim.NewCamera(ScreenW, ScreenH, float64(lvl.Width()*TileSize), float64(lvl.Height()*TileSize)),
|
||
}
|
||
s.followPlayer()
|
||
return s, nil
|
||
}
|
||
|
||
// PlayerTile returnerar rutan spelaren står på/lämnar (används av skriptläget).
|
||
func (s *LevelScene) PlayerTile() level.Point { return s.run.Player.Tile }
|
||
|
||
// TrainHere rapporterar om spelaren står på en perrong vars tåg är inne (skriptläget).
|
||
func (s *LevelScene) TrainHere() bool {
|
||
tr, at, _ := s.run.PlatformHere()
|
||
return tr != nil && at
|
||
}
|
||
|
||
func (s *LevelScene) followPlayer() {
|
||
x, y := s.run.Player.WorldPos(TileSize)
|
||
s.cam.Follow(x+TileSize/2, y+TileSize/2)
|
||
}
|
||
|
||
func (s *LevelScene) Update(g *Game) error {
|
||
if backJustPressed() {
|
||
m, err := NewMenuScene(g.Assets)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
g.SetScene(m)
|
||
return nil
|
||
}
|
||
s.run.Step(dt, dirInput(), confirmJustPressed())
|
||
if s.run.Player.Moving() {
|
||
s.animT += dt
|
||
} else {
|
||
s.animT = 0
|
||
}
|
||
s.followPlayer()
|
||
if s.run.Outcome != sim.Ongoing {
|
||
g.SetScene(NewResultScene(s))
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// trainTile är rutan "bortom perrongkanten" där tågvagnen ritas: första
|
||
// väggruta nedanför, till höger, till vänster eller ovanför perrongen.
|
||
func (s *LevelScene) trainTile(p level.Point) level.Point {
|
||
for _, d := range [...]level.Point{{X: 0, Y: 1}, {X: 1, Y: 0}, {X: -1, Y: 0}, {X: 0, Y: -1}} {
|
||
n := level.Point{X: p.X + d.X, Y: p.Y + d.Y}
|
||
if s.run.Level.At(n.X, n.Y) == level.Wall {
|
||
return n
|
||
}
|
||
}
|
||
return p
|
||
}
|
||
|
||
func (s *LevelScene) Draw(g *Game, screen *ebiten.Image) {
|
||
screen.Fill(color.RGBA{24, 20, 37, 255})
|
||
lvl := s.run.Level
|
||
ox, oy := s.cam.Offset()
|
||
ox, oy = math.Round(ox), math.Round(oy)
|
||
|
||
// Rita bara rutor som syns.
|
||
x0 := max(0, int(s.cam.X)/TileSize)
|
||
y0 := max(0, int(s.cam.Y)/TileSize)
|
||
x1 := min(lvl.Width(), int(s.cam.X+ScreenW)/TileSize+2)
|
||
y1 := min(lvl.Height(), int(s.cam.Y+ScreenH)/TileSize+2)
|
||
var op ebiten.DrawImageOptions
|
||
for y := y0; y < y1; y++ {
|
||
for x := x0; x < x1; x++ {
|
||
op.GeoM.Reset()
|
||
op.GeoM.Translate(ox+float64(x*TileSize), oy+float64(y*TileSize))
|
||
screen.DrawImage(g.Assets.Tiles[lvl.At(x, y)], &op)
|
||
}
|
||
}
|
||
// Tåg som står inne.
|
||
for _, t := range []level.Tile{level.Platform, level.WrongPlatform} {
|
||
if !s.run.TrainVisible(t) {
|
||
continue
|
||
}
|
||
for _, p := range lvl.Find(t) {
|
||
tt := s.trainTile(p)
|
||
op.GeoM.Reset()
|
||
op.GeoM.Translate(ox+float64(tt.X*TileSize), oy+float64(tt.Y*TileSize))
|
||
screen.DrawImage(g.Assets.Train, &op)
|
||
}
|
||
}
|
||
|
||
px, py := s.run.Player.WorldPos(TileSize)
|
||
op.GeoM.Reset()
|
||
op.GeoM.Translate(ox+math.Round(px), oy+math.Round(py))
|
||
screen.DrawImage(g.Assets.PlayerFrame(s.run.Player.Facing, s.run.Player.Moving(), s.animT), &op)
|
||
|
||
s.drawHUD(g, screen)
|
||
}
|
||
|
||
func (s *LevelScene) drawHUD(g *Game, screen *ebiten.Image) {
|
||
r := s.run
|
||
a := g.Assets
|
||
vector.DrawFilledRect(screen, 0, 0, ScreenW, 24, color.RGBA{24, 20, 37, 200}, false)
|
||
// TODO(M3): visa japanska när en font med kana/kanji finns – debugfonten saknar dem.
|
||
ebitenutil.DebugPrintAt(screen, fmt.Sprintf("%s %s: %s", r.Level.Name.Get("sv"), a.T("hud.destination"),
|
||
r.Level.Destination.Get("romaji")), 4, 1)
|
||
ebitenutil.DebugPrintAt(screen, fmt.Sprintf("%s %3.0fs %s %d %s %d¥ %s", a.T("hud.time"), r.Time,
|
||
a.T("hud.steps"), r.Steps, a.T("hud.card"), r.Card.Balance, a.T("hud.controls")), 4, 12)
|
||
|
||
// Inforutan: perronginfo, skylt eller nekad spärr. Visas inte när banan är avgjord.
|
||
if r.Outcome != sim.Ongoing {
|
||
return
|
||
}
|
||
var lines []string
|
||
accent := color.RGBA{58, 68, 102, 255}
|
||
if tr, at, secs := r.PlatformHere(); tr != nil {
|
||
lines = append(lines, fmt.Sprintf("%s %s %s -> %s", a.T("hud.track"), tr.Track, tr.Line.Get("romaji"), tr.Destination.Get("romaji")))
|
||
if at {
|
||
accent = color.RGBA{99, 199, 77, 255}
|
||
lines = append(lines, fmt.Sprintf("%s %.0f s %s", a.T("hud.departs_in"), math.Ceil(secs), a.T("hud.board")))
|
||
} else {
|
||
accent = color.RGBA{254, 174, 52, 255}
|
||
lines = append(lines, fmt.Sprintf("%s %.0f s", a.T("hud.next_train_in"), math.Ceil(secs)))
|
||
}
|
||
} else if sg := r.SignHere(); sg != nil {
|
||
lines = wrap(a.T("hud.sign")+": "+sg.Text.Get("sv"), textCols)
|
||
} else if r.DeniedAt >= 0 && r.Time-r.DeniedAt < deniedShowSec {
|
||
accent = color.RGBA{228, 59, 68, 255}
|
||
lines = []string{a.T("hud.gate_denied")}
|
||
}
|
||
if len(lines) == 0 {
|
||
return
|
||
}
|
||
h := float32(8 + 12*len(lines))
|
||
// Rutan ligger i nederkant, men flyttas upp under HUD:en om spelaren
|
||
// (eller tåget under hen) annars skulle hamna bakom den.
|
||
_, py := r.Player.WorldPos(TileSize)
|
||
y := float32(ScreenH) - h
|
||
if float32(py-s.cam.Y)+2*TileSize > y {
|
||
y = 24
|
||
}
|
||
vector.DrawFilledRect(screen, 0, y, ScreenW, h, color.RGBA{24, 20, 37, 230}, false)
|
||
vector.DrawFilledRect(screen, 0, y, ScreenW, 2, accent, false)
|
||
for i, l := range lines {
|
||
ebitenutil.DebugPrintAt(screen, l, 4, int(y)+4+12*i)
|
||
}
|
||
}
|