All checks were successful
release / build-release (push) Successful in 1m37s
Resor med byten: en bana kan ha next som pekar på en delbana (kind leg, syns inte på bankartan). När man klivit av rätt tåg fortsätter samma omgång på nästa station med klocka, plånbok, sällskap och räknare; par-tiden adderas, dimma och folk är nya, en Byte!-banner visas och HUD:en visar resmålet via bytesstationen. Tidsgräns och missade tåg gäller hela resan. level.ValidateChains kräver sammanhängande kedjor. Nya resor: 007 Akihabara -> Ueno -> Asakusa och finalen 008 Tokyo (två våningar) -> Nippori -> Narita flygplats. Slutscen när sista huvudbanan klaras första gången: planet lyfter, resan summeras, eftertexter; Progress.StoryCleared sparas; flaggan -ending. tools/levelcheck ritar banorna och rapporterar problem. HUD-nivån kortas när mätarna tar plats; poängraden radbryts. Skärmdumpar och docs uppdaterade. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
342 lines
9.5 KiB
Go
342 lines
9.5 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
|
||
},
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
|
||
// 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)
|
||
}
|