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) } }