- Go 1.27 + Ebitengine v2.10, intern upplösning 320x180 med heltalsskalning - internal/res (resursuppslag), internal/rng (seedad slump), internal/level (banformat, validering, BFS-lösbarhet) med tester; internal/archtest kontrollerar att bara presentationslagret beror på Ebitengine - tools/build: debug -> build/debug/, release -> build/release/<os>-<arch>/ med resources/ bredvid binären; Windows korsbyggs utan C-verktyg - VS Code tasks (build/test/release) och launch (dlv) - Första banan 001-flygplatsen, sv/en-texter, skyltordbok, placeholder-sprite - doc/: spelbeskrivning (renskriven), mål och tester, roadmap med teknikval (M0–M12), plan.md; README med bygg- och beroendeinformation Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
136 lines
3.7 KiB
Go
136 lines
3.7 KiB
Go
// 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
|
|
|
|
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/jp-subway-pussel/internal/buildinfo"
|
|
"gitea.brasse-pc.eu/brasse/jp-subway-pussel/internal/level"
|
|
"gitea.brasse-pc.eu/brasse/jp-subway-pussel/internal/res"
|
|
)
|
|
|
|
// Intern upplösning. Allt ritas i denna storlek och skalas sedan upp med
|
|
// heltalsfaktor av Ebitengine så pixlarna förblir skarpa.
|
|
const (
|
|
ScreenW = 320
|
|
ScreenH = 180
|
|
TileSize = 8
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
// Options styr uppstarten.
|
|
type Options struct {
|
|
LevelID string // bana att visa, t.ex. "001-flygplatsen"
|
|
SmokeFrames int // >0 avslutar efter N uppdateringar
|
|
}
|
|
|
|
// New laddar resurser och bygger spelet.
|
|
func New(opt Options) (*Game, error) {
|
|
dir, err := res.Dir()
|
|
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.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
|
|
}
|
|
return g, nil
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Draw ritar aktuell bild.
|
|
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(
|
|
"JP Subway Pussel %s (%s) %.0f fps\nBana: %s\nResurser: %s\nESC avslutar",
|
|
buildinfo.Version, buildinfo.Mode, ebiten.ActualFPS(), name, g.resDir))
|
|
}
|
|
|
|
// 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
|
|
}
|