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>
190 lines
4.0 KiB
Go
190 lines
4.0 KiB
Go
package game
|
||
|
||
import (
|
||
"fmt"
|
||
"image"
|
||
"image/png"
|
||
"os"
|
||
"path/filepath"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/hajimehoshi/ebiten/v2"
|
||
|
||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/sim"
|
||
)
|
||
|
||
// Skriptläge (-script) för automatiserade skärmdumpar och smoke-tester.
|
||
// Stegen skiljs med ';':
|
||
//
|
||
// wait N vänta N bildrutor
|
||
// press KEY tryck enter | esc | r | up | down en bildruta
|
||
// move DIR N gå N rutor åt up | down | left | right (släpper när rutorna är avklarade)
|
||
// shot PATH spara aktuell bild som PNG (intern upplösning × shotScale)
|
||
// quit avsluta spelet
|
||
//
|
||
// Exempel: -level 001-flygplatsen -script "move down 2; move right 2; shot doc/img/level.png; quit"
|
||
|
||
const shotScale = 3
|
||
|
||
type scriptStep struct {
|
||
cmd string
|
||
arg string
|
||
n int
|
||
dir sim.Dir
|
||
done bool
|
||
}
|
||
|
||
type script struct {
|
||
steps []scriptStep
|
||
i int
|
||
counter int
|
||
lastPos level.Point
|
||
havePos bool
|
||
}
|
||
|
||
// virtual är den simulerade inputen som input.go läser före riktig input.
|
||
var virtual struct {
|
||
held sim.Dir
|
||
pressed map[string]bool
|
||
}
|
||
|
||
func parseScript(src string) (*script, error) {
|
||
s := &script{}
|
||
for _, raw := range strings.Split(src, ";") {
|
||
f := strings.Fields(raw)
|
||
if len(f) == 0 {
|
||
continue
|
||
}
|
||
st := scriptStep{cmd: f[0]}
|
||
switch f[0] {
|
||
case "wait":
|
||
if len(f) != 2 {
|
||
return nil, fmt.Errorf("script: 'wait N'")
|
||
}
|
||
st.n, _ = strconv.Atoi(f[1])
|
||
case "press":
|
||
if len(f) != 2 {
|
||
return nil, fmt.Errorf("script: 'press KEY'")
|
||
}
|
||
st.arg = f[1]
|
||
case "move":
|
||
if len(f) != 3 {
|
||
return nil, fmt.Errorf("script: 'move DIR N'")
|
||
}
|
||
st.dir = parseDir(f[1])
|
||
if st.dir == sim.None {
|
||
return nil, fmt.Errorf("script: okänd riktning %q", f[1])
|
||
}
|
||
st.n, _ = strconv.Atoi(f[2])
|
||
case "shot":
|
||
if len(f) != 2 {
|
||
return nil, fmt.Errorf("script: 'shot PATH'")
|
||
}
|
||
st.arg = f[1]
|
||
case "quit":
|
||
default:
|
||
return nil, fmt.Errorf("script: okänt kommando %q", f[0])
|
||
}
|
||
s.steps = append(s.steps, st)
|
||
}
|
||
return s, nil
|
||
}
|
||
|
||
func parseDir(s string) sim.Dir {
|
||
switch s {
|
||
case "up":
|
||
return sim.Up
|
||
case "down":
|
||
return sim.Down
|
||
case "left":
|
||
return sim.Left
|
||
case "right":
|
||
return sim.Right
|
||
}
|
||
return sim.None
|
||
}
|
||
|
||
// playerTiler implementeras av scener som har en spelare i rutnätet.
|
||
type playerTiler interface{ PlayerTile() level.Point }
|
||
|
||
// tick körs i början av varje Update. frame är senast ritade bild.
|
||
// Returnerar ebiten.Termination när skriptet vill avsluta.
|
||
func (s *script) tick(g *Game, frame *ebiten.Image) error {
|
||
virtual.held = sim.None
|
||
virtual.pressed = map[string]bool{}
|
||
if s.i >= len(s.steps) {
|
||
return nil
|
||
}
|
||
st := &s.steps[s.i]
|
||
switch st.cmd {
|
||
case "wait":
|
||
s.counter++
|
||
if s.counter >= st.n {
|
||
s.advance()
|
||
}
|
||
case "press":
|
||
virtual.pressed[st.arg] = true
|
||
s.advance()
|
||
case "move":
|
||
pt, ok := g.scene.(playerTiler)
|
||
if !ok {
|
||
s.advance() // ingen spelare att flytta i den här scenen
|
||
return nil
|
||
}
|
||
cur := pt.PlayerTile()
|
||
if !s.havePos {
|
||
s.lastPos, s.havePos = cur, true
|
||
}
|
||
if cur != s.lastPos {
|
||
s.lastPos = cur
|
||
s.counter++
|
||
}
|
||
if s.counter >= st.n {
|
||
s.advance()
|
||
return nil
|
||
}
|
||
virtual.held = st.dir
|
||
case "shot":
|
||
if err := saveShot(frame, st.arg); err != nil {
|
||
return err
|
||
}
|
||
s.advance()
|
||
case "quit":
|
||
return ebiten.Termination
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (s *script) advance() {
|
||
s.i++
|
||
s.counter = 0
|
||
s.havePos = false
|
||
}
|
||
|
||
func saveShot(frame *ebiten.Image, path string) error {
|
||
w, h := frame.Bounds().Dx(), frame.Bounds().Dy()
|
||
buf := make([]byte, 4*w*h)
|
||
frame.ReadPixels(buf)
|
||
out := image.NewNRGBA(image.Rect(0, 0, w*shotScale, h*shotScale))
|
||
for y := 0; y < h*shotScale; y++ {
|
||
for x := 0; x < w*shotScale; x++ {
|
||
src := 4 * ((y/shotScale)*w + x/shotScale)
|
||
dst := out.PixOffset(x, y)
|
||
copy(out.Pix[dst:dst+4], buf[src:src+4])
|
||
out.Pix[dst+3] = 255
|
||
}
|
||
}
|
||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||
return err
|
||
}
|
||
f, err := os.Create(path)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer f.Close()
|
||
fmt.Println("skärmdump:", path)
|
||
return png.Encode(f, out)
|
||
}
|