Files
metro-puzzle-jp/internal/game/assets.go
claude 8fca14e42d
All checks were successful
release / build-release (push) Successful in 1m34s
M9a: stationer i flera våningar med trappor
- 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>
2026-10-24 11:54:07 +02:00

181 lines
5.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.Kiosk,
}
const (
trainIndex = 7
railsIndex = 9
)
// 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
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 {
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)
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
}