All checks were successful
release / build-release (push) Successful in 1m36s
Ett tåg kan vara en buss (vehicle: bus): samma tidtabell, faser, bedömning i nätet och ombordstigning, men ett fordon på en väg i stället för vagnar på räls, hållplatsskylt, "1番" på tavlan, tuta vid inrullning och dörrsignal före avgång. Nätet har tre busslinjer från Narita. Nya sidobanor: 001b Flygbussterminalen (biljettbana med tre bussar) och 003b Harajuku (Yamanote åt rätt eller fel håll runt ringen). Overload-flaggan ger en rullande reklamticker och maximalt med affischer (Ueno, Tokyo). Romaji utan makroner (fonten saknar dem). Skärmdumparna som börjar på bankartan går via titelskärmen. Tester, docs och wiki uppdaterade. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
221 lines
6.5 KiB
Go
221 lines
6.5 KiB
Go
package game
|
||
|
||
import (
|
||
"bytes"
|
||
"fmt"
|
||
"image"
|
||
"image/color"
|
||
"image/png"
|
||
|
||
"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), från inställningarna (Assets.LoadLang).
|
||
var 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,
|
||
level.Wall, level.Wall, level.Wall, // plats 18–20 = buss, väg, hållplatsskylt (laddas separat)
|
||
}
|
||
|
||
const (
|
||
trainIndex = 7
|
||
railsIndex = 9
|
||
posterIndex = 14
|
||
posterCount = 3
|
||
busIndex = 18
|
||
roadIndex = 19
|
||
stopIndex = 20
|
||
)
|
||
|
||
// 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
|
||
Bus *ebiten.Image // bussen (ett fordon, inte vagnar)
|
||
Road *ebiten.Image // vägruta (horisontell), roteras för vertikala
|
||
StopSign *ebiten.Image // hållplatsskylt ovanpå busshållplatsen
|
||
Icon image.Image // fönsterikonen (nil om den saknas)
|
||
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) || i >= busIndex {
|
||
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)
|
||
a.Bus, a.Road, a.StopSign = sub(busIndex), sub(roadIndex), sub(stopIndex)
|
||
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 := a.LoadLang(UILang); err != nil {
|
||
return nil, err
|
||
}
|
||
if b, err := res.ReadFile("sprites", "icon.png"); err == nil {
|
||
if img, err := png.Decode(bytes.NewReader(b)); err == nil {
|
||
a.Icon = img
|
||
}
|
||
}
|
||
|
||
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)) }
|
||
|
||
// LoadLang läser UI-texterna för ett språk och gör det till UILang.
|
||
func (a *Assets) LoadLang(lang string) error {
|
||
texts := map[string]string{}
|
||
if err := res.LoadJSON(&texts, lang+".json"); err != nil {
|
||
if err := res.LoadJSON(&texts, "text", lang+".json"); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
a.Text = texts
|
||
UILang = lang
|
||
return nil
|
||
}
|
||
|
||
// 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
|
||
}
|