M9b: folk, filer, reklam, sorl och vagnval
All checks were successful
release / build-release (push) Successful in 1m33s

Folk på stationen (NPC:er som vandrar, kliver av tåg och går mot
utgången, blockerar spelaren men inte varandra), vänster-/högerfiler
som bromsar på fel sida, bredare gångar, reklamaffischer (clutter),
japanskt sorl i loop (noise), vagnval med kvinnovagn (stort avdrag med
man i sällskapet, tidsstyrd), kömarkeringar på perrongen, trängsel per
vagn (bonus för tom vagn, svettig-debuff och svårare resa i full vagn,
släpp förbi passagerare) och att folk kliver av innan man får gå på.
Ny bana 005 Ueno. Skärmdumpsverktyget kan filtrera på bana/skript.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 12:33:44 +02:00
parent 8fca14e42d
commit de61568e44
49 changed files with 1568 additions and 136 deletions

View File

@@ -4,7 +4,7 @@
//
// Skriver resources/sprites/tiles.png (en remsa 16×16-rutor i ordningen
// vägg, golv, start, perrong, spärr, automat, skylt, tågvagn, toalett, kiosk, räls,
// trappa upp, trappa ner) och
// trappa upp, trappa ner, fil vänster, fil höger, affisch ×3) och
// resources/sprites/player.png (3 bildrutor × 4 riktningar: ner, upp,
// vänster, höger; kolumn 0 = stilla, 1–2 = gå). Ritas med enkla rektanglar
// så att allt är deterministiskt och lätt att justera.
@@ -163,6 +163,44 @@ var tiles = []func(canvas){
c.rect(6, 13, 4, 1, boxDark)
c.rect(5, 12, 6, 1, boxDark)
},
func(c canvas) { // fil vänster: golv med vänsterpilar
floor(c)
for _, y := range []int{3, 11} {
c.rect(4, y, 8, 1, metalDark)
c.rect(3, y-1, 1, 3, metalDark)
}
},
func(c canvas) { // fil höger: golv med högerpilar
floor(c)
for _, y := range []int{3, 11} {
c.rect(4, y, 8, 1, metalDark)
c.rect(12, y-1, 1, 3, metalDark)
}
},
func(c canvas) { // affisch 1: färgglad reklam
c.rect(0, 0, 16, 16, wallMid)
c.rect(2, 1, 12, 14, rgb(250, 230, 120))
c.rect(3, 2, 10, 5, rgb(228, 59, 68))
c.rect(4, 8, 8, 1, boxDark)
c.rect(4, 10, 6, 1, boxDark)
c.rect(4, 12, 7, 1, boxDark)
},
func(c canvas) { // affisch 2: blå lockskylt som liknar en riktig skylt
c.rect(0, 0, 16, 16, wallMid)
c.rect(1, 2, 14, 12, signBoard)
c.rect(1, 2, 14, 1, white)
c.rect(3, 5, 10, 1, white)
c.rect(3, 8, 6, 1, white)
c.rect(3, 11, 8, 1, yellow)
},
func(c canvas) { // affisch 3: rosa med stort tecken
c.rect(0, 0, 16, 16, wallMid)
c.rect(2, 1, 12, 14, rgb(255, 170, 200))
c.rect(5, 4, 6, 6, white)
c.rect(7, 4, 2, 6, rgb(200, 60, 120))
c.rect(5, 6, 6, 2, rgb(200, 60, 120))
c.rect(4, 12, 8, 1, rgb(120, 30, 70))
},
func(c canvas) { // kiosk: randig markis och disk
floor(c)
c.rect(1, 1, 14, 3, awningRed)

View File

@@ -83,12 +83,39 @@ func writeWAV(path string, pcm []int16) error {
return binary.Write(f, binary.LittleEndian, pcm)
}
// chatter syntetiserar sex sekunder loopbart stationssorl: lågpassat brus
// med stavelseliknande amplitudmodulation från en deterministisk LCG.
func chatter(seconds float64) []int16 {
n := int(seconds * rate)
out := make([]int16, n)
var lcg uint32 = 12345
next := func() float64 { lcg = lcg*1664525 + 1013904223; return float64(lcg>>8)/float64(1<<24)*2 - 1 }
lp := 0.0
env, envTarget, envT := 0.3, 0.3, 0.0
for i := 0; i < n; i++ {
t := float64(i) / rate
// ny "stavelse" var 120–250 ms
if t >= envT {
envTarget = 0.1 + 0.6*math.Abs(next())
envT = t + 0.12 + 0.13*math.Abs(next())
}
env += (envTarget - env) * 0.002
lp += (next() - lp) * 0.08 // lågpass ≈ 600 Hz
v := lp * env * 0.6
// mjuk in-/utfasning för sömlös loop
fade := math.Min(1, math.Min(t, seconds-t)/0.2)
out[i] = int16(v * fade * 32767)
}
return out
}
func main() {
out := filepath.Join(repoRoot(), "resources", "sound")
must(os.MkdirAll(out, 0o755))
must(writeWAV(filepath.Join(out, "melody-approach.wav"), render(approachMelody, 0.01, 0.06, 0.3)))
must(writeWAV(filepath.Join(out, "door-chime.wav"), render(doorChime, 0.005, 0.04, 0.1)))
fmt.Println("skrev", filepath.Join(out, "melody-approach.wav"), "och door-chime.wav")
must(writeWAV(filepath.Join(out, "chatter.wav"), chatter(6)))
fmt.Println("skrev", filepath.Join(out, "melody-approach.wav"), ", door-chime.wav och chatter.wav")
}
func repoRoot() string {

View File

@@ -102,6 +102,14 @@ func buildShots(levelsDir string) error {
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
}
// 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"}
@@ -118,6 +126,7 @@ func buildShots(levelsDir string) error {
{"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"}},
}
return nil
}
@@ -190,6 +199,9 @@ func main() {
}
_ = 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)
@@ -234,3 +246,13 @@ 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
}