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) }