All checks were successful
release / build-release (push) Successful in 1m34s
- level: Point har Z; banor kan ha floors (lika stora rutnät med namn), gamla grid-banor läses som en våning; trappor U/D på samma x,y en våning upp/ner valideras parvis; Neighbors ger grannar över våningar så att Path, lösbarhet, spärregel och Unreachable fungerar; skyltar har z - sim: att kliva på en trappa flyttar spelaren till andra änden (FloorChanges); dimman är per våning och avslöjar trappans andra ände; tåg hörs/syns bara på egen våning - game: bara spelarens våning ritas (följare på andra våningar döljs), HUD och minikarta visar våningens namn, trappgrafik i genart och färg på minikartan - bana 004 Tokyo (svårighet 3, tre i sällskapet): start under mark på B1, trappa upp till 1F med perronger; generatorn gör våningar med trappor som flaskhalsar; ordboken utökad - testbotar och tools/shots förstår trappor; två våningsbilder - docs/README/wiki uppdaterade Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
284 lines
7.6 KiB
Go
284 lines
7.6 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) 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) { // 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)
|
||
}
|