package game import ( "bytes" "fmt" "image" "image/color" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/text/v2" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/lang" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/res" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/sim" ) // UILang är UI-språket (sv/en) – blir en inställning i M12. const UILang = "sv" // Radhöjd för 10-pixelsfonten. const LineH = 12 // Ordningen på rutorna i resources/sprites/tiles.png (måste matcha tools/genart). var tileOrder = []level.Tile{ level.Wall, level.Floor, level.Spawn, level.Platform, level.Gate, level.TicketMachine, level.Sign, level.Wall, // plats 7 = tågvagnen (laddas separat som Train) level.Toilet, level.Wall, // plats 9 = rälsen (laddas separat som Rails) level.StairsUp, level.StairsDown, level.LaneLeft, level.LaneRight, level.Wall, level.Wall, level.Wall, // plats 13–15 = affischer (laddas separat som Posters) level.Kiosk, } const ( trainIndex = 7 railsIndex = 9 posterIndex = 14 posterCount = 3 ) // Assets är allt som laddats från resources/. type Assets struct { ResDir string LevelsDir string Tiles map[level.Tile]*ebiten.Image Train *ebiten.Image // tågvagn, ritas längs rälsen när tåget syns Rails *ebiten.Image // rälsruta (horisontell), roteras för vertikala spår Posters []*ebiten.Image // affischer för visuell röra (ritas över väggrutor) Player [4][3]*ebiten.Image // [riktning: ner, upp, vänster, höger][bildruta: stilla, gå1, gå2] Text map[string]string Dict *lang.Dictionary Font *text.GoTextFace // PixelMplus 10 px – UI och skyltar FontBig *text.GoTextFace // PixelMplus 12 px – rubriker } // LoadAssets läser grafik och texter. func LoadAssets() (*Assets, error) { dir, err := res.Dir() if err != nil { return nil, err } a := &Assets{ResDir: dir, Tiles: map[level.Tile]*ebiten.Image{}} a.LevelsDir, _ = res.Path("data", "levels") tiles, err := loadImage("sprites", "tiles.png") if err != nil { return nil, err } sub := func(i int) *ebiten.Image { return tiles.SubImage(image.Rect(i*TileSize, 0, (i+1)*TileSize, TileSize)).(*ebiten.Image) } if w, need := tiles.Bounds().Dx(), TileSize*len(tileOrder); w < need { return nil, fmt.Errorf("tiles.png är %d px bred, behöver minst %d", w, need) } for i, t := range tileOrder { if i == trainIndex || i == railsIndex || (i >= posterIndex && i < posterIndex+posterCount) { continue } a.Tiles[t] = sub(i) } a.Tiles[level.WrongPlatform] = a.Tiles[level.Platform] // samma utseende; tåget avgör a.Train = sub(trainIndex) a.Rails = sub(railsIndex) for i := 0; i < posterCount; i++ { a.Posters = append(a.Posters, sub(posterIndex+i)) } player, err := loadImage("sprites", "player.png") if err != nil { return nil, err } for row := 0; row < 4; row++ { for col := 0; col < 3; col++ { a.Player[row][col] = player.SubImage(image.Rect(col*TileSize, row*TileSize, (col+1)*TileSize, (row+1)*TileSize)).(*ebiten.Image) } } if err := res.LoadJSON(&a.Text, UILang+".json"); err != nil { if err := res.LoadJSON(&a.Text, "text", UILang+".json"); err != nil { return nil, err } } dict, err := res.ReadFile("text", "jp-dictionary.json") if err != nil { return nil, err } if a.Dict, err = lang.ParseDictionary(dict); err != nil { return nil, err } if a.Font, err = loadFont("PixelMplus10-Regular.ttf", 10); err != nil { return nil, err } if a.FontBig, err = loadFont("PixelMplus12-Regular.ttf", 12); err != nil { return nil, err } return a, nil } func loadFont(name string, size float64) (*text.GoTextFace, error) { b, err := res.ReadFile("text", "fonts", name) if err != nil { return nil, err } src, err := text.NewGoTextFaceSource(bytes.NewReader(b)) if err != nil { return nil, fmt.Errorf("font %s: %w", name, err) } return &text.GoTextFace{Source: src, Size: size}, nil } // Draw ritar en rad text med 10-pixelsfonten; (x, y) är övre vänstra hörnet. // Heltalspositioner ger pixelskarpa glyfer. func (a *Assets) Draw(dst *ebiten.Image, s string, x, y int, col color.Color) { a.drawFace(dst, a.Font, s, x, y, col) } // DrawBig ritar med 12-pixelsfonten. func (a *Assets) DrawBig(dst *ebiten.Image, s string, x, y int, col color.Color) { a.drawFace(dst, a.FontBig, s, x, y, col) } func (a *Assets) drawFace(dst *ebiten.Image, face *text.GoTextFace, s string, x, y int, col color.Color) { op := &text.DrawOptions{} op.GeoM.Translate(float64(x), float64(y)) op.ColorScale.ScaleWithColor(col) text.Draw(dst, s, face, op) } // Width är textens bredd i pixlar med 10-pixelsfonten. func (a *Assets) Width(s string) int { return int(text.Advance(s, a.Font)) } // T slår upp en UI-text; saknas nyckeln visas nyckeln själv så att det syns. func (a *Assets) T(key string) string { if s, ok := a.Text[key]; ok { return s } return key } // PlayerFrame returnerar rätt bildruta för riktning och gångfas. func (a *Assets) PlayerFrame(facing sim.Dir, moving bool, animT float64) *ebiten.Image { row := 0 switch facing { case sim.Up: row = 1 case sim.Left: row = 2 case sim.Right: row = 3 } col := 0 if moving { col = 1 + int(animT/0.12)%2 } return a.Player[row][col] } func loadImage(parts ...string) (*ebiten.Image, error) { img, err := res.LoadImage(parts...) if err != nil { return nil, err } return ebiten.NewImageFromImage(img), nil }