M2: tåg med tidtabell, spärrar med kort, skyltar, vinst/förlust
All checks were successful
release / build-release (push) Successful in 1m35s
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>
This commit is contained in:
@@ -17,15 +17,16 @@ import (
|
||||
// Spelarens grundfart i rutor per sekund (blir en stat i M7).
|
||||
const playerSpeed = 6.0
|
||||
|
||||
// LevelScene är själva spelandet av en bana.
|
||||
// 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
|
||||
lvl *level.Level
|
||||
player *sim.Mover
|
||||
cam *sim.Camera
|
||||
animT float64
|
||||
elapsed float64
|
||||
steps int
|
||||
id string
|
||||
run *sim.Run
|
||||
cam *sim.Camera
|
||||
animT float64
|
||||
}
|
||||
|
||||
// NewLevelScene laddar banan och ställer spelaren på startrutan.
|
||||
@@ -35,20 +36,25 @@ func NewLevelScene(a *Assets, id string) (*LevelScene, error) {
|
||||
return nil, err
|
||||
}
|
||||
s := &LevelScene{
|
||||
id: id,
|
||||
lvl: lvl,
|
||||
player: sim.NewMover(lvl.SpawnPoint(), playerSpeed),
|
||||
cam: sim.NewCamera(ScreenW, ScreenH, float64(lvl.Width()*TileSize), float64(lvl.Height()*TileSize)),
|
||||
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.player.Tile }
|
||||
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.player.WorldPos(TileSize)
|
||||
x, y := s.run.Player.WorldPos(TileSize)
|
||||
s.cam.Follow(x+TileSize/2, y+TileSize/2)
|
||||
}
|
||||
|
||||
@@ -61,50 +67,116 @@ func (s *LevelScene) Update(g *Game) error {
|
||||
g.SetScene(m)
|
||||
return nil
|
||||
}
|
||||
s.elapsed += dt
|
||||
s.steps += s.player.Step(dt, dirInput(), s.lvl.CanEnter)
|
||||
if s.player.Moving() {
|
||||
s.run.Step(dt, dirInput(), confirmJustPressed())
|
||||
if s.run.Player.Moving() {
|
||||
s.animT += dt
|
||||
} else {
|
||||
s.animT = 0
|
||||
// Står vi på en perrong är banan slut (M2 ger tåg, tidtabell och spärrar).
|
||||
if t := s.lvl.At(s.player.Tile.X, s.player.Tile.Y); t == level.Platform || t == level.WrongPlatform {
|
||||
g.SetScene(NewResultScene(s, s.lvl.TrainAt(t)))
|
||||
}
|
||||
}
|
||||
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(s.lvl.Width(), int(s.cam.X+ScreenW)/TileSize+2)
|
||||
y1 := min(s.lvl.Height(), int(s.cam.Y+ScreenH)/TileSize+2)
|
||||
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[s.lvl.At(x, y)], &op)
|
||||
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.player.WorldPos(TileSize)
|
||||
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.player.Facing, s.player.Moving(), s.animT), &op)
|
||||
screen.DrawImage(g.Assets.PlayerFrame(s.run.Player.Facing, s.run.Player.Moving(), s.animT), &op)
|
||||
|
||||
// HUD
|
||||
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", s.lvl.Name.Get("sv"), g.Assets.T("hud.destination"),
|
||||
s.lvl.Destination.Get("romaji")), 4, 1)
|
||||
ebitenutil.DebugPrintAt(screen, fmt.Sprintf("%s %3.0fs %s %d %s", g.Assets.T("hud.time"), s.elapsed,
|
||||
g.Assets.T("hud.steps"), s.steps, g.Assets.T("hud.controls")), 4, 12)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user