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:
189
internal/game/script.go
Normal file
189
internal/game/script.go
Normal file
@@ -0,0 +1,189 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user