All checks were successful
release / build-release (push) Successful in 1m42s
- level.Schedule.Phase: away/approaching/arriving/dwelling/closing/leaving ur tidtabellen med progress; Boardable (öppna dörrar) och Present; validering av minsta uppehåll/borta-tid; TrackTile för rälsen - sim.Run: ombordstigning bara med öppna dörrar, TrainVisible via Present, PlatformPhase och NearTrack (närmaste perrong inom två rutor) - game/trains.go: rälsrutor längs kantraden (roterade för vertikala spår), tre vagnar som bromsar in och accelererar ut längs rälsen, strålkastare under inrullningen, blinkande lampor vid kanten för den som står nära, dammpartiklar längs perrongen; kameran får en rutas marginal så att kanträlsen syns - game/audio.go: ebiten/v2/audio med avgångsmelodi (approaching) och dörrsignal (closing), volym efter avstånd, -mute; tools/gensound syntetiserar placeholder-WAV:ar - HUD: fasberoende perrongtext och "tåg på väg in" nära rälsen; skriptkommandot waitphase; skärmdump av ankomsten - plan (Björn 2026-10-24): räls/väg-grafik och melodierna i M7b; folk/NPC:er, vänster-/högertrafik, bredare gångar, reklam- och ljudröra som nytt steg M9b – i spelbeskrivning, roadmap, testplan - docs/README/wiki uppdaterade Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
176 lines
5.1 KiB
Go
176 lines
5.1 KiB
Go
// 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 +
|
||
"; waitphase approaching; wait 200; shot $OUT/approach.png; 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{"-mute", "-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)
|
||
}
|