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:
@@ -1,19 +1,11 @@
|
||||
// Package game innehåller Ebitengine-spelloopen. Detta är det enda paketet
|
||||
// (tillsammans med cmd/) som får känna till Ebitengine; all spellogik ska
|
||||
// ligga i paket som går att testa utan fönster.
|
||||
// Package game innehåller Ebitengine-spelloopen och scenerna. Detta är det
|
||||
// enda paketet (tillsammans med cmd/) som får känna till Ebitengine; all
|
||||
// spellogik ligger i paket som går att testa utan fönster (internal/sim,
|
||||
// internal/level …).
|
||||
package game
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/buildinfo"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/res"
|
||||
)
|
||||
|
||||
// Intern upplösning. Allt ritas i denna storlek och skalas sedan upp med
|
||||
@@ -21,115 +13,87 @@ import (
|
||||
const (
|
||||
ScreenW = 320
|
||||
ScreenH = 180
|
||||
TileSize = 8
|
||||
TileSize = 16
|
||||
dt = 1.0 / 60 // Ebitengine kör Update med fast 60 TPS
|
||||
)
|
||||
|
||||
// Scene är ett spelläge: meny, bana, resultat …
|
||||
type Scene interface {
|
||||
Update(g *Game) error
|
||||
Draw(g *Game, screen *ebiten.Image)
|
||||
}
|
||||
|
||||
// Game är Ebitengine-tillståndet för hela programmet.
|
||||
type Game struct {
|
||||
lvl *level.Level
|
||||
player *ebiten.Image
|
||||
tiles map[level.Tile]*ebiten.Image
|
||||
resDir string
|
||||
smokeLeft int // >0: avsluta automatiskt efter så många uppdateringar (för smoke-test)
|
||||
frames int
|
||||
levelError error
|
||||
Assets *Assets
|
||||
scene Scene
|
||||
next Scene
|
||||
smokeLeft int // >0: avsluta automatiskt efter så många uppdateringar
|
||||
script *script
|
||||
frame *ebiten.Image // allt ritas hit och blittas sedan till skärmen (gör skärmdumpar möjliga)
|
||||
}
|
||||
|
||||
// Options styr uppstarten.
|
||||
type Options struct {
|
||||
LevelID string // bana att visa, t.ex. "001-flygplatsen"
|
||||
SmokeFrames int // >0 avslutar efter N uppdateringar
|
||||
LevelID string // hoppa direkt in i en bana (tom = meny)
|
||||
SmokeFrames int // >0 avslutar efter N uppdateringar (smoke-test)
|
||||
Script string // skriptade steg för skärmdumpar/tester, se script.go
|
||||
}
|
||||
|
||||
// New laddar resurser och bygger spelet.
|
||||
func New(opt Options) (*Game, error) {
|
||||
dir, err := res.Dir()
|
||||
a, err := LoadAssets()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g := &Game{resDir: dir, smokeLeft: opt.SmokeFrames}
|
||||
|
||||
img, err := res.LoadImage("sprites", "placeholder.png")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
g := &Game{Assets: a, smokeLeft: opt.SmokeFrames, frame: ebiten.NewImage(ScreenW, ScreenH)}
|
||||
if opt.Script != "" {
|
||||
if g.script, err = parseScript(opt.Script); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
g.player = ebiten.NewImageFromImage(img)
|
||||
|
||||
var raw []byte
|
||||
if raw, err = res.ReadFile("data", "levels", opt.LevelID+".json"); err == nil {
|
||||
g.lvl, err = level.Parse(raw)
|
||||
}
|
||||
if err != nil {
|
||||
g.levelError = err
|
||||
}
|
||||
|
||||
g.tiles = map[level.Tile]*ebiten.Image{}
|
||||
for t, c := range map[level.Tile]color.RGBA{
|
||||
level.Wall: {38, 43, 68, 255},
|
||||
level.Floor: {139, 155, 180, 255},
|
||||
level.Spawn: {99, 199, 77, 255},
|
||||
level.Platform: {254, 174, 52, 255},
|
||||
level.WrongPlatform: {228, 59, 68, 255},
|
||||
level.Gate: {44, 232, 245, 255},
|
||||
level.TicketMachine: {247, 118, 34, 255},
|
||||
level.Sign: {255, 255, 255, 255},
|
||||
} {
|
||||
im := ebiten.NewImage(TileSize, TileSize)
|
||||
im.Fill(c)
|
||||
g.tiles[t] = im
|
||||
if opt.LevelID != "" {
|
||||
s, err := NewLevelScene(a, opt.LevelID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g.scene = s
|
||||
} else {
|
||||
g.scene, err = NewMenuScene(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return g, nil
|
||||
}
|
||||
|
||||
// SetScene byter scen från och med nästa uppdatering.
|
||||
func (g *Game) SetScene(s Scene) { g.next = s }
|
||||
|
||||
// Update körs 60 ggr/s.
|
||||
func (g *Game) Update() error {
|
||||
g.frames++
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
|
||||
return ebiten.Termination
|
||||
}
|
||||
if g.smokeLeft > 0 {
|
||||
g.smokeLeft--
|
||||
if g.smokeLeft == 0 {
|
||||
return ebiten.Termination
|
||||
}
|
||||
}
|
||||
return nil
|
||||
if g.next != nil {
|
||||
g.scene, g.next = g.next, nil
|
||||
}
|
||||
if g.script != nil {
|
||||
if err := g.script.tick(g, g.frame); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return g.scene.Update(g)
|
||||
}
|
||||
|
||||
// Draw ritar aktuell bild.
|
||||
// Draw ritar aktuell scen till den interna bilden och sedan till skärmen.
|
||||
func (g *Game) Draw(screen *ebiten.Image) {
|
||||
screen.Fill(color.RGBA{24, 20, 37, 255})
|
||||
|
||||
if g.lvl != nil {
|
||||
// Centrera banan på skärmen (riktiga banor får kamera senare).
|
||||
ox := (ScreenW - g.lvl.Width()*TileSize) / 2
|
||||
oy := (ScreenH - g.lvl.Height()*TileSize) / 2
|
||||
var op ebiten.DrawImageOptions
|
||||
for y := 0; y < g.lvl.Height(); y++ {
|
||||
for x := 0; x < g.lvl.Width(); x++ {
|
||||
op.GeoM.Reset()
|
||||
op.GeoM.Translate(float64(ox+x*TileSize), float64(oy+y*TileSize))
|
||||
screen.DrawImage(g.tiles[g.lvl.At(x, y)], &op)
|
||||
}
|
||||
}
|
||||
s := g.lvl.SpawnPoint()
|
||||
op.GeoM.Reset()
|
||||
op.GeoM.Translate(float64(ox+s.X*TileSize)-4, float64(oy+s.Y*TileSize)-8)
|
||||
screen.DrawImage(g.player, &op)
|
||||
}
|
||||
|
||||
name := "(ingen bana)"
|
||||
if g.lvl != nil {
|
||||
name = g.lvl.Name.Get("sv") + " -> " + g.lvl.Destination.Get("ja")
|
||||
} else if g.levelError != nil {
|
||||
name = "FEL: " + g.levelError.Error()
|
||||
}
|
||||
ebitenutil.DebugPrint(screen, fmt.Sprintf(
|
||||
"Metro Puzzle JP %s (%s) %.0f fps\nBana: %s\nResurser: %s\nESC avslutar",
|
||||
buildinfo.Version, buildinfo.Mode, ebiten.ActualFPS(), name, g.resDir))
|
||||
g.scene.Draw(g, g.frame)
|
||||
screen.DrawImage(g.frame, nil)
|
||||
}
|
||||
|
||||
// Layout talar om för Ebitengine vilken intern upplösning vi ritar i.
|
||||
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
|
||||
return ScreenW, ScreenH
|
||||
}
|
||||
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { return ScreenW, ScreenH }
|
||||
|
||||
Reference in New Issue
Block a user