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>
263 lines
6.9 KiB
Go
263 lines
6.9 KiB
Go
// Genererar placeholder-grafik tills riktig pixel art finns:
|
||
//
|
||
// go run ./tools/genart
|
||
//
|
||
// 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) 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.
|
||
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"image"
|
||
"image/color"
|
||
"image/png"
|
||
"os"
|
||
"path/filepath"
|
||
)
|
||
|
||
const T = 16
|
||
|
||
func rgb(r, g, b uint8) color.RGBA { return color.RGBA{r, g, b, 255} }
|
||
|
||
var (
|
||
wallDark, wallMid, wallLight = rgb(38, 43, 68), rgb(58, 68, 102), rgb(90, 105, 136)
|
||
floorA, floorB, grout = rgb(139, 155, 180), rgb(128, 143, 168), rgb(108, 121, 146)
|
||
yellow, yellowDark = rgb(254, 174, 52), rgb(200, 130, 30)
|
||
metal, metalDark = rgb(190, 198, 208), rgb(120, 130, 145)
|
||
screenC, boxDark = rgb(44, 232, 245), rgb(50, 60, 80)
|
||
signBoard, white = rgb(24, 60, 120), rgb(255, 255, 255)
|
||
green, greenDark = rgb(99, 199, 77), rgb(60, 140, 50)
|
||
skin, hair, capC, capDark = rgb(240, 200, 160), rgb(60, 40, 30), rgb(228, 59, 68), rgb(170, 40, 50)
|
||
jacket, jacketDark = rgb(44, 232, 245), rgb(30, 170, 185)
|
||
pants, shoe, eye = rgb(38, 43, 68), rgb(255, 255, 255), rgb(38, 43, 68)
|
||
trainBody, trainStripe = rgb(230, 234, 240), rgb(228, 59, 68)
|
||
toiletBlue, awningRed = rgb(40, 110, 200), rgb(228, 59, 68)
|
||
)
|
||
|
||
type canvas struct {
|
||
img *image.NRGBA
|
||
ox, oy int
|
||
}
|
||
|
||
func (c canvas) rect(x, y, w, h int, col color.RGBA) {
|
||
for yy := y; yy < y+h; yy++ {
|
||
for xx := x; xx < x+w; xx++ {
|
||
c.img.SetNRGBA(c.ox+xx, c.oy+yy, color.NRGBA{col.R, col.G, col.B, col.A})
|
||
}
|
||
}
|
||
}
|
||
|
||
func floor(c canvas) {
|
||
c.rect(0, 0, 7, 7, floorA)
|
||
c.rect(8, 0, 7, 7, floorB)
|
||
c.rect(0, 8, 7, 7, floorB)
|
||
c.rect(8, 8, 7, 7, floorA)
|
||
c.rect(7, 0, 1, 16, grout)
|
||
c.rect(15, 0, 1, 16, grout)
|
||
c.rect(0, 7, 16, 1, grout)
|
||
c.rect(0, 15, 16, 1, grout)
|
||
}
|
||
|
||
var tiles = []func(canvas){
|
||
func(c canvas) { // vägg
|
||
c.rect(0, 0, 16, 16, wallMid)
|
||
c.rect(0, 0, 16, 2, wallLight)
|
||
c.rect(0, 14, 16, 2, wallDark)
|
||
c.rect(0, 7, 16, 1, wallDark)
|
||
c.rect(7, 2, 1, 5, wallDark)
|
||
c.rect(3, 8, 1, 6, wallDark)
|
||
c.rect(11, 8, 1, 6, wallDark)
|
||
},
|
||
floor, // golv
|
||
func(c canvas) { // start: golv + dörrmatta
|
||
floor(c)
|
||
c.rect(3, 10, 10, 4, green)
|
||
c.rect(3, 10, 10, 1, greenDark)
|
||
c.rect(3, 13, 10, 1, greenDark)
|
||
},
|
||
func(c canvas) { // perrong: golv + gul ledlinje + kant
|
||
floor(c)
|
||
c.rect(0, 11, 16, 3, yellow)
|
||
for x := 1; x < 16; x += 4 {
|
||
c.rect(x, 12, 2, 1, yellowDark)
|
||
}
|
||
c.rect(0, 15, 16, 1, wallDark)
|
||
},
|
||
func(c canvas) { // spärr: två stolpar med grön lampa
|
||
floor(c)
|
||
for _, x := range []int{1, 11} {
|
||
c.rect(x, 1, 4, 13, metal)
|
||
c.rect(x, 1, 4, 1, white)
|
||
c.rect(x, 13, 4, 1, metalDark)
|
||
c.rect(x+1, 3, 2, 2, green)
|
||
}
|
||
},
|
||
func(c canvas) { // automat
|
||
floor(c)
|
||
c.rect(3, 1, 10, 13, boxDark)
|
||
c.rect(4, 2, 8, 5, screenC)
|
||
c.rect(5, 9, 6, 1, metal)
|
||
c.rect(5, 11, 2, 1, yellow)
|
||
c.rect(8, 11, 2, 1, green)
|
||
},
|
||
func(c canvas) { // skylt
|
||
floor(c)
|
||
c.rect(7, 7, 2, 9, boxDark)
|
||
c.rect(2, 1, 12, 7, signBoard)
|
||
c.rect(2, 1, 12, 1, white)
|
||
c.rect(3, 3, 10, 1, white)
|
||
c.rect(3, 5, 6, 1, white)
|
||
},
|
||
func(c canvas) { // tågvagn (ritas på rutan bortom perrongkanten när tåget är inne)
|
||
c.rect(0, 0, 16, 16, wallDark)
|
||
c.rect(0, 2, 16, 12, trainBody)
|
||
c.rect(0, 2, 16, 1, metalDark)
|
||
c.rect(0, 9, 16, 2, trainStripe)
|
||
for _, x := range []int{1, 5, 12} {
|
||
c.rect(x, 4, 3, 4, screenC)
|
||
}
|
||
c.rect(9, 4, 2, 9, metalDark) // dörr
|
||
c.rect(2, 14, 3, 2, boxDark) // hjul
|
||
c.rect(11, 14, 3, 2, boxDark)
|
||
},
|
||
func(c canvas) { // toalett: vit dörr med blå skylt
|
||
floor(c)
|
||
c.rect(3, 1, 10, 13, white)
|
||
c.rect(3, 1, 10, 1, metalDark)
|
||
c.rect(5, 3, 6, 5, toiletBlue)
|
||
c.rect(7, 4, 2, 3, white) // figur
|
||
c.rect(10, 9, 1, 1, boxDark)
|
||
},
|
||
func(c canvas) { // räls (horisontell; roteras för vertikala spår): slipers och två stålskenor
|
||
c.rect(0, 0, 16, 16, wallDark)
|
||
c.rect(0, 0, 16, 1, wallMid)
|
||
for x := 1; x < 16; x += 4 {
|
||
c.rect(x, 3, 2, 10, boxDark) // slipers
|
||
}
|
||
c.rect(0, 5, 16, 1, metal)
|
||
c.rect(0, 6, 16, 1, metalDark)
|
||
c.rect(0, 10, 16, 1, metal)
|
||
c.rect(0, 11, 16, 1, metalDark)
|
||
},
|
||
func(c canvas) { // kiosk: randig markis och disk
|
||
floor(c)
|
||
c.rect(1, 1, 14, 3, awningRed)
|
||
for x := 1; x < 15; x += 4 {
|
||
c.rect(x, 1, 2, 3, white)
|
||
}
|
||
c.rect(2, 4, 12, 6, boxDark)
|
||
c.rect(3, 5, 3, 2, screenC) // flaskor
|
||
c.rect(7, 5, 3, 2, yellow)
|
||
c.rect(11, 5, 2, 2, green)
|
||
c.rect(2, 10, 12, 3, metal) // disk
|
||
},
|
||
}
|
||
|
||
// player ritar en figur som tittar åt facing (0 ner, 1 upp, 2 vänster, 3 höger)
|
||
// i bildruta frame (0 stilla, 1–2 gå).
|
||
func player(c canvas, facing, frame int) {
|
||
// ben + skor
|
||
ly, ry := 11, 11
|
||
lx, rx := 5, 9
|
||
switch frame {
|
||
case 1:
|
||
ly = 10
|
||
case 2:
|
||
ry = 10
|
||
}
|
||
if facing >= 2 { // sidovy: benen närmare varandra, växelvis fram
|
||
lx, rx = 6, 8
|
||
if frame == 1 {
|
||
lx, rx = 5, 8
|
||
} else if frame == 2 {
|
||
lx, rx = 6, 9
|
||
}
|
||
}
|
||
c.rect(lx, ly, 2, 14-ly, pants)
|
||
c.rect(rx, ry, 2, 14-ry, pants)
|
||
c.rect(lx, 14, 2, 1, shoe)
|
||
c.rect(rx, 14, 2, 1, shoe)
|
||
// kropp + armar
|
||
c.rect(4, 6, 8, 5, jacket)
|
||
c.rect(4, 6, 8, 1, jacketDark)
|
||
c.rect(3, 7, 1, 3, jacket)
|
||
c.rect(12, 7, 1, 3, jacket)
|
||
// huvud
|
||
switch facing {
|
||
case 0: // ner
|
||
c.rect(4, 1, 8, 5, skin)
|
||
c.rect(3, 0, 10, 2, capC)
|
||
c.rect(3, 2, 10, 1, capDark)
|
||
c.rect(6, 4, 1, 1, eye)
|
||
c.rect(9, 4, 1, 1, eye)
|
||
case 1: // upp: bakhuvud
|
||
c.rect(4, 1, 8, 5, hair)
|
||
c.rect(3, 0, 10, 2, capC)
|
||
c.rect(3, 2, 10, 1, capDark)
|
||
case 2: // vänster
|
||
c.rect(4, 1, 8, 5, skin)
|
||
c.rect(9, 1, 3, 5, hair)
|
||
c.rect(3, 0, 10, 2, capC)
|
||
c.rect(1, 2, 8, 1, capDark)
|
||
c.rect(5, 4, 1, 1, eye)
|
||
case 3: // höger
|
||
c.rect(4, 1, 8, 5, skin)
|
||
c.rect(4, 1, 3, 5, hair)
|
||
c.rect(3, 0, 10, 2, capC)
|
||
c.rect(7, 2, 8, 1, capDark)
|
||
c.rect(10, 4, 1, 1, eye)
|
||
}
|
||
}
|
||
|
||
func main() {
|
||
out := filepath.Join(repoRoot(), "resources", "sprites")
|
||
|
||
tilesImg := image.NewNRGBA(image.Rect(0, 0, T*len(tiles), T))
|
||
for i, draw := range tiles {
|
||
draw(canvas{tilesImg, i * T, 0})
|
||
}
|
||
save(filepath.Join(out, "tiles.png"), tilesImg)
|
||
|
||
playerImg := image.NewNRGBA(image.Rect(0, 0, T*3, T*4))
|
||
for facing := 0; facing < 4; facing++ {
|
||
for frame := 0; frame < 3; frame++ {
|
||
player(canvas{playerImg, frame * T, facing * T}, facing, frame)
|
||
}
|
||
}
|
||
save(filepath.Join(out, "player.png"), playerImg)
|
||
}
|
||
|
||
func save(path string, img image.Image) {
|
||
f, err := os.Create(path)
|
||
if err != nil {
|
||
fail(err)
|
||
}
|
||
defer f.Close()
|
||
if err := png.Encode(f, img); err != nil {
|
||
fail(err)
|
||
}
|
||
fmt.Println("skrev", path)
|
||
}
|
||
|
||
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 fail(err error) {
|
||
fmt.Fprintln(os.Stderr, "genart:", err)
|
||
os.Exit(1)
|
||
}
|