All checks were successful
release / build-release (push) Successful in 1m36s
Ett tåg kan vara en buss (vehicle: bus): samma tidtabell, faser, bedömning i nätet och ombordstigning, men ett fordon på en väg i stället för vagnar på räls, hållplatsskylt, "1番" på tavlan, tuta vid inrullning och dörrsignal före avgång. Nätet har tre busslinjer från Narita. Nya sidobanor: 001b Flygbussterminalen (biljettbana med tre bussar) och 003b Harajuku (Yamanote åt rätt eller fel håll runt ringen). Overload-flaggan ger en rullande reklamticker och maximalt med affischer (Ueno, Tokyo). Romaji utan makroner (fonten saknar dem). Skärmdumparna som börjar på bankartan går via titelskärmen. Tester, docs och wiki uppdaterade. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
384 lines
11 KiB
Go
384 lines
11 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,
|
||
// 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.
|
||
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) { // trappa upp: steg som blir ljusare uppåt + pil
|
||
floor(c)
|
||
for i := 0; i < 5; i++ {
|
||
shade := uint8(90 + i*25)
|
||
c.rect(2, 13-i*3, 12, 3, color.RGBA{shade, shade + 10, shade + 30, 255})
|
||
}
|
||
c.rect(7, 1, 2, 4, white) // pil upp
|
||
c.rect(6, 2, 4, 1, white)
|
||
c.rect(5, 3, 6, 1, white)
|
||
},
|
||
func(c canvas) { // trappa ner: steg som blir mörkare neråt + pil
|
||
floor(c)
|
||
for i := 0; i < 5; i++ {
|
||
shade := uint8(190 - i*25)
|
||
c.rect(2, 1+i*3, 12, 3, color.RGBA{shade, shade + 10, shade + 30, 255})
|
||
}
|
||
c.rect(7, 11, 2, 4, boxDark) // pil ner
|
||
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)
|
||
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
|
||
},
|
||
func(c canvas) { // buss (ritas på vägrutan bortom hållplatsen när bussen är inne)
|
||
c.rect(0, 0, 16, 16, wallDark)
|
||
c.rect(1, 2, 14, 11, color.RGBA{240, 200, 60, 255}) // gul kaross
|
||
c.rect(1, 2, 14, 1, metalDark)
|
||
c.rect(2, 4, 12, 4, screenC) // fönsterrad
|
||
c.rect(7, 4, 1, 4, metalDark) // fönsterspröjs
|
||
c.rect(1, 9, 14, 2, awningRed) // rand
|
||
c.rect(11, 8, 2, 5, metalDark) // dörr
|
||
c.rect(3, 13, 3, 2, boxDark) // hjul
|
||
c.rect(10, 13, 3, 2, boxDark)
|
||
},
|
||
func(c canvas) { // väg (horisontell; roteras för vertikala): asfalt med mittlinje och kantsten
|
||
c.rect(0, 0, 16, 16, color.RGBA{70, 72, 80, 255})
|
||
c.rect(0, 0, 16, 1, wallMid)
|
||
c.rect(0, 15, 16, 1, metal) // trottoarkant
|
||
for x := 0; x < 16; x += 6 {
|
||
c.rect(x, 7, 3, 1, color.RGBA{230, 220, 150, 255}) // streckad mittlinje
|
||
}
|
||
},
|
||
func(c canvas) { // hållplatsskylt: stolpe med bussymbol (ritas över hållplatsrutan)
|
||
c.rect(7, 4, 2, 12, metalDark)
|
||
c.rect(3, 1, 10, 6, color.RGBA{240, 200, 60, 255})
|
||
c.rect(4, 2, 8, 3, boxDark)
|
||
c.rect(5, 3, 2, 1, white)
|
||
c.rect(9, 3, 2, 1, white)
|
||
},
|
||
}
|
||
|
||
// 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)
|
||
|
||
planeImg := image.NewNRGBA(image.Rect(0, 0, 32, 12))
|
||
plane(canvas{planeImg, 0, 0})
|
||
save(filepath.Join(out, "plane.png"), planeImg)
|
||
|
||
iconImg := image.NewNRGBA(image.Rect(0, 0, 32, 32))
|
||
icon(canvas{iconImg, 0, 0})
|
||
save(filepath.Join(out, "icon.png"), iconImg)
|
||
}
|
||
|
||
// icon är fönsterikonen (32×32): ett tågfront på mörk platta.
|
||
func icon(c canvas) {
|
||
c.rect(0, 0, 32, 32, rgb(38, 43, 68))
|
||
c.rect(6, 4, 20, 24, rgb(236, 240, 245)) // front
|
||
c.rect(8, 7, 16, 8, rgb(90, 140, 220)) // vindruta
|
||
c.rect(6, 17, 20, 3, rgb(214, 60, 60)) // rand
|
||
c.rect(9, 22, 4, 3, rgb(255, 230, 120)) // strålkastare
|
||
c.rect(19, 22, 4, 3, rgb(255, 230, 120)) // strålkastare
|
||
c.rect(4, 28, 24, 2, rgb(150, 160, 175)) // underrede
|
||
c.rect(13, 1, 6, 3, rgb(150, 160, 175)) // strömavtagare
|
||
}
|
||
|
||
// plane är flygplanet i slutscenen (32×12, nosen åt höger).
|
||
func plane(c canvas) {
|
||
white, grey, blue, red := rgb(236, 240, 245), rgb(150, 160, 175), rgb(90, 140, 220), rgb(214, 60, 60)
|
||
c.rect(4, 5, 24, 4, white) // kropp
|
||
c.rect(28, 6, 3, 2, white) // nos
|
||
c.rect(2, 4, 3, 5, white) // akter
|
||
c.rect(1, 1, 4, 4, red) // fena
|
||
c.rect(10, 3, 10, 2, grey) // bortre vingen
|
||
c.rect(12, 9, 8, 2, grey) // främre vingen
|
||
c.rect(14, 10, 3, 2, grey) // motor
|
||
for x := 8; x < 26; x += 3 {
|
||
c.rect(x, 6, 1, 1, blue) // fönster
|
||
}
|
||
c.rect(26, 6, 2, 1, blue) // cockpit
|
||
}
|
||
|
||
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)
|
||
}
|