All checks were successful
release / build-release (push) Successful in 1m42s
- level.Schedule.Phase: away/approaching/arriving/dwelling/closing/leaving ur tidtabellen med progress; Boardable (öppna dörrar) och Present; validering av minsta uppehåll/borta-tid; TrackTile för rälsen - sim.Run: ombordstigning bara med öppna dörrar, TrainVisible via Present, PlatformPhase och NearTrack (närmaste perrong inom två rutor) - game/trains.go: rälsrutor längs kantraden (roterade för vertikala spår), tre vagnar som bromsar in och accelererar ut längs rälsen, strålkastare under inrullningen, blinkande lampor vid kanten för den som står nära, dammpartiklar längs perrongen; kameran får en rutas marginal så att kanträlsen syns - game/audio.go: ebiten/v2/audio med avgångsmelodi (approaching) och dörrsignal (closing), volym efter avstånd, -mute; tools/gensound syntetiserar placeholder-WAV:ar - HUD: fasberoende perrongtext och "tåg på väg in" nära rälsen; skriptkommandot waitphase; skärmdump av ankomsten - plan (Björn 2026-10-24): räls/väg-grafik och melodierna i M7b; folk/NPC:er, vänster-/högertrafik, bredare gångar, reklam- och ljudröra som nytt steg M9b – i spelbeskrivning, roadmap, testplan - docs/README/wiki uppdaterade Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
180 lines
5.0 KiB
Go
180 lines
5.0 KiB
Go
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.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
|
||
}
|