Files
metro-puzzle-jp/internal/game/assets.go
claude 4ae6a1d24c
All checks were successful
release / build-release (push) Successful in 1m37s
M10: byten, hemresan och levelcheck
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>
2026-10-24 13:37:48 +02:00

195 lines
5.6 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.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)
Plane *ebiten.Image // flygplanet i slutscenen
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)
}
for _, t := range level.PlatformTiles[1:] {
a.Tiles[t] = 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
}
if a.Plane, err = loadImage("sprites", "plane.png"); 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
}