M1: rörelse, kollision, kamera, scener, tileset, skärmdumpar, CI
Some checks failed
release / build-release (push) Failing after 27s
Some checks failed
release / build-release (push) Failing after 27s
- internal/sim: rutbaserad Mover (4 riktningar, riktningsbyte vid rutgräns, bildrute-exakt ankomst) och Camera (följer, klampar, centrerar små kartor), med tester - internal/game: scener meny -> bana -> resultat, tangentbord + gamepad, tileset och spelaranimation, HUD, skriptläge (-script) för deterministiska skärmdumpar och tester - tools/genart genererar placeholder-grafik (tiles.png, player.png); tools/shots tar doc/img/*.png som README och wiki visar - bana 002-shinjuku (33x21) för kamerascroll; level.List/LoadFile/TrainAt - .gitea/workflows/release.yaml: vet + test + linux/windows-zippar till rullande latest på brasse-linux01-runnern (linux-amd64) - docs: roadmap M1 klar, tester, plan, README med bilder och styrning Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
110
internal/game/scene_level.go
Normal file
110
internal/game/scene_level.go
Normal file
@@ -0,0 +1,110 @@
|
||||
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
|
||||
|
||||
// LevelScene är själva spelandet av en bana.
|
||||
type LevelScene struct {
|
||||
id string
|
||||
lvl *level.Level
|
||||
player *sim.Mover
|
||||
cam *sim.Camera
|
||||
animT float64
|
||||
elapsed float64
|
||||
steps int
|
||||
}
|
||||
|
||||
// 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,
|
||||
lvl: lvl,
|
||||
player: sim.NewMover(lvl.SpawnPoint(), 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) followPlayer() {
|
||||
x, y := s.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.elapsed += dt
|
||||
s.steps += s.player.Step(dt, dirInput(), s.lvl.CanEnter)
|
||||
if s.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()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *LevelScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
screen.Fill(color.RGBA{24, 20, 37, 255})
|
||||
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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
px, py := s.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)
|
||||
|
||||
// HUD
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user