All checks were successful
release / build-release (push) Successful in 1m49s
Inlärning enligt Björns regler: - lang.Knowledge: framsteg per ord (3 läsningar = känt), språkpoäng, köp av valfria ord (kostnad = tier), Award per stjärna - sim.Run: RunOptions med seed; stjärnor 1–3 mot par (parSec eller kortaste vägen × 0,5 s + 20 s); ≥2 stjärnor lär resmålets ord; att stanna på skylt/perrong ger seedad 35 % chans till framsteg per okänt ord - game: ordlista (L) med kända ord, framsteg och kostnad; stjärnor, poäng och inlärt resmål på resultatskärmen; framstegsnotis; flaggor -points/-seed M4 lämmeltåget: - sim.Party: medlemmar med mat/dryck/toa-mätare, behov under tröskel, seedade händelser var 45 s / antal (toa, törst, hunger, distraktion), fartstraff, toalett (W, 3 s/person) och kiosk (K, 150 ¥/person), följare på ledarens spår - level: rutor W och K, party och parSec i bandatan; banor med sällskap måste ha toalett och kiosk (testas); 001 har 2, 002 har 3 i sällskapet - game: följare med färgton, behovsmarkering över huvudet, mätarpanel, facilitetsrutor med förlopp; genart ritar toalett och kiosk - skriptläget: move stannar exakt på målrutan även när farten inte är bildruteexakt (sällskapets fartstraff), stall-skydd mot väggar - docs/README/wiki/spelbeskrivning uppdaterade Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
173 lines
4.8 KiB
Go
173 lines
4.8 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.Kiosk,
|
||
}
|
||
|
||
const trainIndex = 7
|
||
|
||
// 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 vid perrongen när tåget är inne
|
||
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 {
|
||
continue
|
||
}
|
||
a.Tiles[t] = sub(i)
|
||
}
|
||
a.Tiles[level.WrongPlatform] = a.Tiles[level.Platform] // samma utseende; tåget avgör
|
||
a.Train = sub(trainIndex)
|
||
|
||
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
|
||
}
|