Files
metro-puzzle-jp/tools/shots/main.go
claude b2995d4ca9
All checks were successful
release / build-release (push) Successful in 1m37s
M7: svårighet, tid, poäng, debuffar, egna kort och sittplatsval
- sim/stats.go: Stats (fart, törst, fokus, toalett, tur), debuffar som
  multiplikatorer, Profile som följer med mellan banor och räknar ned
- sim.Run: tidsgräns och maxantal missade tåg förlorar banan; poängformel
  (tid mot par, pengar, sällskapets mätare, utforskat, avdrag); debuffar
  tjänas av många steg, lång tid, tom matmätare och seedad otur vid
  kiosken; stats påverkar fart, mätartakter, läsning och hjälptid
- egna kort (individualCards): Member.Card, spärren kräver allas saldo och
  drar från alla, automaten frågar vems kort, biljetter köps åt alla
- sittplatsval (seatSelection): Ticket får ett sittplatssteg med seedade
  önskemål (fönster/gång/sida/bredvid); missnöjda ger mätar- och poängavdrag
- level: maxMissedTrains, individualCards, seatSelection; bana 003 Shibuya
  (svårighet 3, fyra i sällskapet) och 002 med tidsgräns och sittplatser;
  ordboken utökad; generatorn är spec-driven
- game: tid kvar/missade i HUD, debuffar i meny/HUD/resultat, poäng och
  förlustorsaker på resultatet, vyer för personval och vagn, skript-pick,
  flaggan -debuffs; HUD och resultat komprimerade för fyra i sällskapet
- plan (Björn 2026-10-24): flera våningar med trappor som M9a, egna kort
  och sittplatsval som svårighetsinställningar i M7 – i spelbeskrivning,
  roadmap, testplan
- docs/README/wiki med sittplatsbilden

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-24 10:18:17 +02:00

176 lines
5.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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.
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
)
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
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
}
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
}
// 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/menu.png; press l; wait 5; press down; press down; press down; wait 5; shot $OUT/words.png; quit",
[]string{"-know", "改札,出口,トイレ", "-points", "3"}},
{"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 +
"; waittrain; wait 30; shot $OUT/platform.png; 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"}},
}
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++ {
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))
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"
}
for _, s := range shots {
args := []string{"-script", strings.ReplaceAll(s.script, "$OUT", out)}
if s.level != "" {
args = append(args, "-level", s.level)
}
args = append(args, s.args...)
run := exec.Command(exe, args...)
run.Dir, run.Stdout, run.Stderr = filepath.Dir(exe), os.Stdout, os.Stderr
if err := run.Run(); err != nil {
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)
}