// Genererar dokumentationens skärmdumpar deterministiskt: // // go run ./tools/shots // // Bygger debug-binären, kör den med -script för varje bild och skriver // PNG:er till doc/img/. Vägarna genom banorna räknas ut med BFS från // bandatan. Kör om detta när grafik, UI eller banor ändrats, och committa // bilderna – README och wikin pekar på dem. // // På Linux körs spelet på en osynlig skärm (headless Wayfire + Xwayland, // se headless.go) så att inget fönster syns eller tar fokus. METRO_HEADLESS=0 // stänger av det. package main import ( "fmt" "os" "os/exec" "path/filepath" "runtime" "strings" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/save" ) // demoSave skriver en sparfil med lite progression så att kartan, skills // och garderoben har något att visa. Används bara av skärmdumparna. func demoSave(path string) error { st := save.New() st.Know.Learn("改札") st.Know.Learn("出口") st.Know.Points = 2 st.Progress.RecordLevel("001-flygplatsen", true, 3, 941) st.Progress.RecordLevel("002-shinjuku", true, 2, 760) st.Progress.AddXP(600) st.Progress.Items["skin_salaryman"] = true st.Progress.Items["geta"] = true st.Progress.Items["water_bottle"] = true st.Progress.Equipped["body"] = "skin_salaryman" st.Progress.Equipped["feet"] = "geta" st.Progress.Skills["focus1"] = true return save.Store(path, st) } type shot struct { level string script string // $OUT ersätts med doc/img-mappen args []string // extra flaggor till spelet } // shots byggs i main eftersom vägarna räknas ut från bandatan (BFS), så att // bilderna överlever ändringar i banorna. var shots []shot var demo string func buildShots(levelsDir string) error { l, err := level.LoadFile(filepath.Join(levelsDir, "001-flygplatsen.json")) if err != nil { return err } sign := l.Find(level.Sign)[1] // skylten vid spärrarna goal := l.Find(level.Platform)[0] toSign, err := moves(l, l.SpawnPoint(), sign) if err != nil { return err } toGoal, err := moves(l, sign, goal) if err != nil { return err } toMachine, err := moves(l, l.SpawnPoint(), l.Find(level.TicketMachine)[0]) if err != nil { return err } toKiosk, err := moves(l, l.SpawnPoint(), l.Find(level.Kiosk)[0]) if err != nil { return err } l2, err := level.LoadFile(filepath.Join(levelsDir, "002-shinjuku.json")) if err != nil { return err } toMachine2, err := moves(l2, l2.SpawnPoint(), l2.Find(level.TicketMachine)[0]) if err != nil { return err } l3, err := level.LoadFile(filepath.Join(levelsDir, "003-shibuya.json")) if err != nil { return err } toMachine3, err := moves(l3, l3.SpawnPoint(), l3.Find(level.TicketMachine)[0]) if err != nil { return err } l4, err := level.LoadFile(filepath.Join(levelsDir, "004-tokyo.json")) if err != nil { return err } toStairs, err := moves(l4, l4.SpawnPoint(), l4.Find(level.StairsUp)[0]) if err != nil { return err } l5, err := level.LoadFile(filepath.Join(levelsDir, "005-ueno.json")) if err != nil { return err } toPlatform5, err := moves(l5, l5.SpawnPoint(), l5.CorrectPlatform()) if err != nil { return err } l6, err := level.LoadFile(filepath.Join(levelsDir, "006-ikebukuro.json")) if err != nil { return err } toMachine6, err := moves(l6, l6.SpawnPoint(), l6.Find(level.TicketMachine)[0]) if err != nil { return err } toTrap6, err := moves(l6, l6.SpawnPoint(), l6.Find(level.Platform3)[0]) // Marunouchi: för långt bort if err != nil { return err } // Skyltbilderna tas med några kända ord så att markeringen och den // delvisa översättningen syns; hjälpen lär sedan in nästa ord. known := []string{"-know", "改札,番線,方面", "-seed", "7"} shots = []shot{ {"", "wait 10; shot $OUT/map.png; press l; wait 5; press down; press down; press down; wait 5; shot $OUT/words.png; press l; wait 3; press t; wait 5; press down; wait 3; shot $OUT/skills.png; press t; wait 3; press i; wait 5; shot $OUT/items.png; quit", []string{"-know", "改札,出口,トイレ", "-points", "3", "-save", "$DEMO"}}, {"001-flygplatsen", toKiosk + "; press b; wait 5; press down; wait 3; shot $OUT/shop.png; quit", []string{"-seed", "7", "-save", "$DEMO"}}, {"001-flygplatsen", "move right 4; move down 2; wait 5; shot $OUT/level-001.png; quit", nil}, {"002-shinjuku", "move down 6; move right 4; wait 5; shot $OUT/level-002.png; quit", nil}, {"001-flygplatsen", toSign + "; wait 5; shot $OUT/sign.png; press h; wait 200; shot $OUT/sign-help.png; wait 200; shot $OUT/sign-learned.png; " + toGoal + "; waitphase approaching; wait 200; shot $OUT/approach.png; waittrain; wait 30; shot $OUT/platform.png; press enter; wait 100; pick calm; press enter; wait 40; shot $OUT/ride.png; waitstop; press enter; wait 5; shot $OUT/result.png; quit", known}, {"001-flygplatsen", toMachine + "; press enter; wait 5; press right; wait 5; shot $OUT/topup.png; quit", []string{"-seed", "7", "-card", "0"}}, {"002-shinjuku", toMachine2 + "; press enter; wait 5; shot $OUT/ticket.png; quit", []string{"-seed", "7", "-know", "渋谷"}}, {"003-shibuya", toMachine3 + "; press enter; pick 池袋; press enter; wait 5; press enter; press right; wait 5; shot $OUT/seats.png; quit", []string{"-seed", "7", "-know", "池袋", "-debuffs", "sore_feet"}}, {"004-tokyo", "wait 10; shot $OUT/floors-b1.png; " + toStairs + "; wait 20; shot $OUT/floors-1f.png; quit", []string{"-seed", "7"}}, {"005-ueno", "wait 90; shot $OUT/crowd.png; " + toPlatform5 + "; waittrain; wait 5; press enter; wait 20; shot $OUT/cars-alight.png; wait 80; press right; wait 5; shot $OUT/cars.png; quit", []string{"-seed", "7"}}, {"006-ikebukuro", "move right 2; wait 5; shot $OUT/nav-list.png; quit", []string{"-seed", "7", "-mode", "list"}}, {"006-ikebukuro", toMachine6 + "; wait 5; shot $OUT/terminal.png; " + "quit", []string{"-seed", "7", "-mode", "signs", "-know", "山手線,番線,方面,新宿,池袋,埼京線"}}, {"006-ikebukuro", toTrap6 + "; waittrain; wait 5; press enter; wait 100; pick calm; press enter; wait 60; shot $OUT/ride-real.png; waitover; wait 5; shot $OUT/too-far.png; quit", []string{"-seed", "7", "-mode", "signs"}}, {"", "wait 10; press n; wait 5; shot $OUT/map-signs.png; quit", []string{"-save", "$DEMO"}}, } return nil } // moves gör om kortaste vägen mellan två rutor till "move DIR N"-steg. func moves(l *level.Level, from, to level.Point) (string, error) { path := l.Path(from, to, l.CanEnter) if path == nil { return "", fmt.Errorf("ingen väg %v -> %v i %s", from, to, l.ID) } var parts []string dir, n := "", 0 flush := func() { if n > 0 { parts = append(parts, fmt.Sprintf("move %s %d", dir, n)) } } for i := 1; i < len(path); i++ { if path[i].Z != path[i-1].Z { continue // trappan flyttar en till andra våningen i samma steg } d := dirName(path[i-1], path[i]) if d != dir { flush() dir, n = d, 0 } n++ } flush() return strings.Join(parts, "; "), nil } func dirName(a, b level.Point) string { switch { case b.X > a.X: return "right" case b.X < a.X: return "left" case b.Y > a.Y: return "down" default: return "up" } } func main() { root := repoRoot() out := filepath.Join(root, "doc", "img") must(os.MkdirAll(out, 0o755)) demo = filepath.Join(root, "build", "demo-save.json") must(os.MkdirAll(filepath.Dir(demo), 0o755)) must(demoSave(demo)) must(buildShots(filepath.Join(root, "resources", "data", "levels"))) build := exec.Command("go", "run", "./tools/build", "-mode", "debug") build.Dir, build.Stdout, build.Stderr = root, os.Stdout, os.Stderr must(build.Run()) exe := filepath.Join(root, "build", "debug", "metro-puzzle-jp") if runtime.GOOS == "windows" { exe += ".exe" } hl, err := startHeadless(filepath.Join(root, "build")) must(err) if hl != nil { fmt.Printf("» osynlig skärm: %s / %s (inget syns på skrivbordet)\n", hl.wayland, hl.x11) defer hl.stop() } else { fmt.Println("» ingen headless-kompositor (wayfire saknas eller METRO_HEADLESS=0): använder skärmen") } _ = demo for _, s := range shots { if len(os.Args) > 1 && !matchesAny(s, os.Args[1:]) { continue // `go run ./tools/shots 005 ride` kör bara skott vars namn eller bana innehåller något av orden } args := []string{"-mute", "-nosave", "-script", strings.ReplaceAll(s.script, "$OUT", out)} if s.level != "" { args = append(args, "-level", s.level) } for _, a := range s.args { args = append(args, strings.ReplaceAll(a, "$DEMO", demo)) } run := exec.Command(exe, args...) run.Dir, run.Stdout, run.Stderr = filepath.Dir(exe), os.Stdout, os.Stderr if hl != nil { run.Env = hl.env() } if err := run.Run(); err != nil { hl.stop() fail(fmt.Errorf("%s: %w", s.script, err)) } } fmt.Println("klart:", out) } func repoRoot() string { dir, _ := os.Getwd() for { if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { return dir } parent := filepath.Dir(dir) if parent == dir { fail(fmt.Errorf("hittade ingen go.mod")) } dir = parent } } func must(err error) { if err != nil { fail(err) } } func fail(err error) { fmt.Fprintln(os.Stderr, "shots:", err) os.Exit(1) } // matchesAny rapporterar om skottets bana eller skript innehåller något av orden. func matchesAny(s shot, words []string) bool { for _, w := range words { if strings.Contains(s.level, w) || strings.Contains(s.script, w) { return true } } return false }