M3: japanska skyltar – pixelfont, ordbok, kunskap, delvis översättning, hjälp
All checks were successful
release / build-release (push) Successful in 1m42s
All checks were successful
release / build-release (push) Successful in 1m42s
- resources/text/fonts: PixelMplus 10/12 px (M+ FONT LICENSE) + licens; all text ritas via text/v2 på heltalspositioner, debugfonten är borta - internal/lang: ordbok (60 ord, validerad), tokenisering med längsta matchning, Knowledge (ord + kana, nivå 0–5, hintar), Render (kända ord markerade/översatta, okända kvar, kana som romaji), EnglishShare/ ShowEnglish per svårighet (deterministiskt per bana+position), HelpSeconds; datatest kräver att all japanska i banorna finns i ordboken - sim.Run: AskHelp (H) med väntetid efter nivå, avbryts om man går, avslöjar skylten och lär första okända ordet; Knowledge delas mellan banor - game: skylt- och avgångstavlor renderade genom kunskapen med färgkodning, engelsk rad på lätta banor, hjälp-förlopp, "Nytt ord"-notis, HUD visar resmålet på japanska från nivå 1 och japanska-nivå; meny med svårighet; pixelbaserad radbrytning av färgade textsegment; -know-flagga - tools/shots: tre skyltbilder (skylt, hjälp, nytt ord) med förkunskap - docs/README/wiki uppdaterade Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -1,16 +1,26 @@
|
||||
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,
|
||||
@@ -24,6 +34,9 @@ type Assets struct {
|
||||
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.
|
||||
@@ -61,12 +74,62 @@ func LoadAssets() (*Assets, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if err := res.LoadJSON(&a.Text, "text", "sv.json"); err != nil {
|
||||
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 {
|
||||
|
||||
@@ -6,6 +6,8 @@ package game
|
||||
|
||||
import (
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/lang"
|
||||
)
|
||||
|
||||
// Intern upplösning. Allt ritas i denna storlek och skalas sedan upp med
|
||||
@@ -26,6 +28,7 @@ type Scene interface {
|
||||
// Game är Ebitengine-tillståndet för hela programmet.
|
||||
type Game struct {
|
||||
Assets *Assets
|
||||
Know *lang.Knowledge // spelarens japanska – följer med mellan banor (sparas i M8)
|
||||
scene Scene
|
||||
next Scene
|
||||
smokeLeft int // >0: avsluta automatiskt efter så många uppdateringar
|
||||
@@ -35,9 +38,10 @@ type Game struct {
|
||||
|
||||
// Options styr uppstarten.
|
||||
type Options struct {
|
||||
LevelID string // hoppa direkt in i en bana (tom = meny)
|
||||
SmokeFrames int // >0 avslutar efter N uppdateringar (smoke-test)
|
||||
Script string // skriptade steg för skärmdumpar/tester, se script.go
|
||||
LevelID string // hoppa direkt in i en bana (tom = meny)
|
||||
SmokeFrames int // >0 avslutar efter N uppdateringar (smoke-test)
|
||||
Script string // skriptade steg för skärmdumpar/tester, se script.go
|
||||
KnownWords []string // ord spelaren kan från start (för tester/skärmdumpar)
|
||||
}
|
||||
|
||||
// New laddar resurser och bygger spelet.
|
||||
@@ -46,14 +50,17 @@ func New(opt Options) (*Game, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g := &Game{Assets: a, smokeLeft: opt.SmokeFrames, frame: ebiten.NewImage(ScreenW, ScreenH)}
|
||||
g := &Game{Assets: a, Know: lang.NewKnowledge(), smokeLeft: opt.SmokeFrames, frame: ebiten.NewImage(ScreenW, ScreenH)}
|
||||
for _, w := range opt.KnownWords {
|
||||
g.Know.Learn(w)
|
||||
}
|
||||
if opt.Script != "" {
|
||||
if g.script, err = parseScript(opt.Script); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if opt.LevelID != "" {
|
||||
s, err := NewLevelScene(a, opt.LevelID)
|
||||
s, err := NewLevelScene(a, opt.LevelID, g.Know)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -71,6 +71,11 @@ func backJustPressed() bool {
|
||||
return virtual.pressed["esc"] || inpututil.IsKeyJustPressed(ebiten.KeyEscape) || gamepadJustPressed(ebiten.StandardGamepadButtonRightRight)
|
||||
}
|
||||
|
||||
// helpJustPressed: H eller gamepadens övre knapp (Y/triangel) = fråga om hjälp.
|
||||
func helpJustPressed() bool {
|
||||
return virtual.pressed["h"] || inpututil.IsKeyJustPressed(ebiten.KeyH) || gamepadJustPressed(ebiten.StandardGamepadButtonRightTop)
|
||||
}
|
||||
|
||||
// menuMove returnerar -1/+1 när användaren just tryckte upp/ner i en meny.
|
||||
func menuMove() int {
|
||||
switch {
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||||
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||
|
||||
"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/sim"
|
||||
)
|
||||
@@ -17,8 +17,11 @@ import (
|
||||
// Spelarens grundfart i rutor per sekund (blir en stat i M7).
|
||||
const playerSpeed = 6.0
|
||||
|
||||
// Så länge "spärren nekade" visas efter försöket.
|
||||
const deniedShowSec = 2.0
|
||||
// Hur länge tillfälliga meddelanden visas.
|
||||
const (
|
||||
deniedShowSec = 2.0
|
||||
learnedShowSec = 3.0
|
||||
)
|
||||
|
||||
// LevelScene är själva spelandet av en bana. All spellogik ligger i sim.Run;
|
||||
// scenen översätter input och ritar.
|
||||
@@ -29,15 +32,16 @@ type LevelScene struct {
|
||||
animT float64
|
||||
}
|
||||
|
||||
// NewLevelScene laddar banan och ställer spelaren på startrutan.
|
||||
func NewLevelScene(a *Assets, id string) (*LevelScene, error) {
|
||||
// NewLevelScene laddar banan och ställer spelaren på startrutan. know är
|
||||
// spelarens japanska, som delas mellan banor.
|
||||
func NewLevelScene(a *Assets, id string, know *lang.Knowledge) (*LevelScene, error) {
|
||||
lvl, err := level.LoadFile(filepath.Join(a.LevelsDir, id+".json"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := &LevelScene{
|
||||
id: id,
|
||||
run: sim.NewRun(lvl, playerSpeed),
|
||||
run: sim.NewRun(lvl, playerSpeed, a.Dict, know),
|
||||
cam: sim.NewCamera(ScreenW, ScreenH, float64(lvl.Width()*TileSize), float64(lvl.Height()*TileSize)),
|
||||
}
|
||||
s.followPlayer()
|
||||
@@ -67,6 +71,9 @@ func (s *LevelScene) Update(g *Game) error {
|
||||
g.SetScene(m)
|
||||
return nil
|
||||
}
|
||||
if helpJustPressed() {
|
||||
s.run.AskHelp()
|
||||
}
|
||||
s.run.Step(dt, dirInput(), confirmJustPressed())
|
||||
if s.run.Player.Moving() {
|
||||
s.animT += dt
|
||||
@@ -132,51 +139,109 @@ func (s *LevelScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
s.drawHUD(g, screen)
|
||||
}
|
||||
|
||||
// drawHUD ritar den övre raden (bana, resmål, tid, kort, japanska-nivå) och
|
||||
// inforutan (perrong, skylt, nekad spärr, nytt ord).
|
||||
func (s *LevelScene) drawHUD(g *Game, screen *ebiten.Image) {
|
||||
r := s.run
|
||||
a := g.Assets
|
||||
vector.DrawFilledRect(screen, 0, 0, ScreenW, 24, color.RGBA{24, 20, 37, 200}, false)
|
||||
// TODO(M3): visa japanska när en font med kana/kanji finns – debugfonten saknar dem.
|
||||
ebitenutil.DebugPrintAt(screen, fmt.Sprintf("%s %s: %s", r.Level.Name.Get("sv"), a.T("hud.destination"),
|
||||
r.Level.Destination.Get("romaji")), 4, 1)
|
||||
ebitenutil.DebugPrintAt(screen, fmt.Sprintf("%s %3.0fs %s %d %s %d¥ %s", a.T("hud.time"), r.Time,
|
||||
a.T("hud.steps"), r.Steps, a.T("hud.card"), r.Card.Balance, a.T("hud.controls")), 4, 12)
|
||||
|
||||
// Inforutan: perronginfo, skylt eller nekad spärr. Visas inte när banan är avgjord.
|
||||
dest := r.Level.Destination.Get("romaji")
|
||||
if r.Know.ShowsDestinationJP() { // hint från nivå 1: resmålet på japanska, att matcha mot skyltarna
|
||||
dest += " " + r.Level.Destination.Get("ja")
|
||||
}
|
||||
a.Draw(screen, r.Level.Name.Get(UILang), 4, 0, colText)
|
||||
a.Draw(screen, a.T("hud.destination")+": "+dest, 4+a.Width(r.Level.Name.Get(UILang))+12, 0, colKnown)
|
||||
a.Draw(screen, fmt.Sprintf("%s %3.0fs %s %d %s %d¥ %s Lv%d·%d %s", a.T("hud.time"), r.Time,
|
||||
a.T("hud.steps"), r.Steps, a.T("hud.card"), r.Card.Balance, a.T("hud.japanese"), r.Know.Level(), r.Know.Count(),
|
||||
a.T("hud.controls")), 4, LineH, colDim)
|
||||
|
||||
if r.Outcome != sim.Ongoing {
|
||||
return
|
||||
}
|
||||
var lines []string
|
||||
accent := color.RGBA{58, 68, 102, 255}
|
||||
if tr, at, secs := r.PlatformHere(); tr != nil {
|
||||
lines = append(lines, fmt.Sprintf("%s %s %s -> %s", a.T("hud.track"), tr.Track, tr.Line.Get("romaji"), tr.Destination.Get("romaji")))
|
||||
if at {
|
||||
accent = color.RGBA{99, 199, 77, 255}
|
||||
lines = append(lines, fmt.Sprintf("%s %.0f s %s", a.T("hud.departs_in"), math.Ceil(secs), a.T("hud.board")))
|
||||
} else {
|
||||
accent = color.RGBA{254, 174, 52, 255}
|
||||
lines = append(lines, fmt.Sprintf("%s %.0f s", a.T("hud.next_train_in"), math.Ceil(secs)))
|
||||
}
|
||||
} else if sg := r.SignHere(); sg != nil {
|
||||
lines = wrap(a.T("hud.sign")+": "+sg.Text.Get("sv"), textCols)
|
||||
} else if r.DeniedAt >= 0 && r.Time-r.DeniedAt < deniedShowSec {
|
||||
accent = color.RGBA{228, 59, 68, 255}
|
||||
lines = []string{a.T("hud.gate_denied")}
|
||||
lines, accent, bar := s.infoLines(a)
|
||||
if entry := a.Dict.Lookup(r.Learned); entry != nil && r.Time-r.LearnedAt < learnedShowSec {
|
||||
lines = append(lines, plainLine(fmt.Sprintf("%s: %s = %s (%s)", a.T("hud.learned"), r.Learned,
|
||||
entry.Translation(UILang), entry.Romaji), colKnown))
|
||||
}
|
||||
if len(lines) == 0 {
|
||||
return
|
||||
}
|
||||
h := float32(8 + 12*len(lines))
|
||||
h := 8 + LineH*len(lines)
|
||||
if bar {
|
||||
h += 8
|
||||
}
|
||||
// Rutan ligger i nederkant, men flyttas upp under HUD:en om spelaren
|
||||
// (eller tåget under hen) annars skulle hamna bakom den.
|
||||
_, py := r.Player.WorldPos(TileSize)
|
||||
y := float32(ScreenH) - h
|
||||
if float32(py-s.cam.Y)+2*TileSize > y {
|
||||
y := ScreenH - h
|
||||
if int(py-s.cam.Y)+2*TileSize > y {
|
||||
y = 24
|
||||
}
|
||||
vector.DrawFilledRect(screen, 0, y, ScreenW, h, color.RGBA{24, 20, 37, 230}, false)
|
||||
vector.DrawFilledRect(screen, 0, y, ScreenW, 2, accent, false)
|
||||
for i, l := range lines {
|
||||
ebitenutil.DebugPrintAt(screen, l, 4, int(y)+4+12*i)
|
||||
vector.DrawFilledRect(screen, 0, float32(y), ScreenW, float32(h), colPanel, false)
|
||||
vector.DrawFilledRect(screen, 0, float32(y), ScreenW, 2, accent, false)
|
||||
a.drawLines(screen, lines, 4, y+4)
|
||||
if bar {
|
||||
by := float32(y + h - 7)
|
||||
vector.DrawFilledRect(screen, 4, by, ScreenW-8, 4, colBar, false)
|
||||
vector.DrawFilledRect(screen, 4, by, float32((ScreenW-8)*r.HelpProgress()), 4, colKnown, false)
|
||||
}
|
||||
}
|
||||
|
||||
// infoLines bygger inforutans rader: perronginfo, skylt eller nekad spärr.
|
||||
func (s *LevelScene) infoLines(a *Assets) (lines []line, accent color.Color, bar bool) {
|
||||
r := s.run
|
||||
lvl := r.Level
|
||||
tile := r.Player.Tile
|
||||
accent = colBar
|
||||
maxW := ScreenW - 8
|
||||
english := lang.ShowEnglish(lvl.ID, tile.X, tile.Y, lvl.Difficulty)
|
||||
|
||||
if tr, at, secs := r.PlatformHere(); tr != nil {
|
||||
// Avgångstavlan är en skylt som alla andra: japanska, sedd genom kunskapen.
|
||||
ja := fmt.Sprintf("%s%s %s %s%s", tr.Track, a.T("hud.track_suffix_ja"), tr.Line.Get("ja"), tr.Destination.Get("ja"), a.T("hud.for_ja"))
|
||||
rd := lang.Render(a.Dict, r.Know, ja, UILang)
|
||||
lines = append(lines, a.wrapLine(shownLine(rd.Original, colText, r.Know.ShowsReadings()), maxW)...)
|
||||
lines = append(lines, a.wrapLine(shownLine(rd.Partial, colText, false), maxW)...)
|
||||
if english {
|
||||
lines = append(lines, a.wrapLine(plainLine(fmt.Sprintf("%s: Track %s %s for %s", a.T("hud.english"), tr.Track,
|
||||
tr.Line.Get("en"), tr.Destination.Get("en")), colEnglish), maxW)...)
|
||||
}
|
||||
if at {
|
||||
accent = colGood
|
||||
lines = append(lines, plainLine(fmt.Sprintf("%s %.0f s %s", a.T("hud.departs_in"), math.Ceil(secs), a.T("hud.board")), colGood))
|
||||
} else {
|
||||
accent = colWarn
|
||||
lines = append(lines, plainLine(fmt.Sprintf("%s %.0f s", a.T("hud.next_train_in"), math.Ceil(secs)), colWarn))
|
||||
}
|
||||
return lines, accent, false
|
||||
}
|
||||
|
||||
if sg := r.SignHere(); sg != nil {
|
||||
know := r.Know
|
||||
revealed := r.SignRevealed(tile)
|
||||
if revealed {
|
||||
know = nil // allt känt
|
||||
accent = colKnown
|
||||
}
|
||||
rd := lang.Render(a.Dict, know, sg.Text["ja"], UILang)
|
||||
lines = append(lines, a.wrapLine(shownLine(rd.Original, colText, r.Know.ShowsReadings()), maxW)...)
|
||||
lines = append(lines, a.wrapLine(shownLine(rd.Partial, colText, false), maxW)...)
|
||||
if english {
|
||||
lines = append(lines, a.wrapLine(plainLine(a.T("hud.english")+": "+sg.Text.Get("en"), colEnglish), maxW)...)
|
||||
}
|
||||
switch {
|
||||
case r.Help.Active:
|
||||
lines = append(lines, plainLine(a.T("hud.help_progress"), colDim))
|
||||
bar = true
|
||||
case !revealed:
|
||||
lines = append(lines, plainLine(fmt.Sprintf(a.T("hud.help_prompt"), lang.HelpSeconds(r.Know.Level())), colDim))
|
||||
}
|
||||
return lines, accent, bar
|
||||
}
|
||||
|
||||
if r.DeniedAt >= 0 && r.Time-r.DeniedAt < deniedShowSec {
|
||||
return []line{plainLine(a.T("hud.gate_denied"), colBad)}, colBad, false
|
||||
}
|
||||
return nil, accent, false
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||||
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/buildinfo"
|
||||
@@ -14,9 +14,10 @@ import (
|
||||
)
|
||||
|
||||
type menuEntry struct {
|
||||
id string
|
||||
name string
|
||||
dest string
|
||||
id string
|
||||
name string
|
||||
dest string
|
||||
stars string
|
||||
}
|
||||
|
||||
// MenuScene listar banorna och låter spelaren välja.
|
||||
@@ -39,7 +40,12 @@ func NewMenuScene(a *Assets) (*MenuScene, error) {
|
||||
m.err = err
|
||||
continue
|
||||
}
|
||||
m.entries = append(m.entries, menuEntry{id: id, name: l.Name.Get("sv"), dest: l.Destination.Get("romaji")}) // TODO(M3): japanska när fonten finns
|
||||
m.entries = append(m.entries, menuEntry{
|
||||
id: id,
|
||||
name: l.Name.Get(UILang) + " " + l.Name.Get("ja"),
|
||||
dest: l.Destination.Get("ja") + " " + l.Destination.Get("romaji"),
|
||||
stars: strings.Repeat("★", l.Difficulty) + strings.Repeat("☆", 5-l.Difficulty),
|
||||
})
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
@@ -51,7 +57,7 @@ func (m *MenuScene) Update(g *Game) error {
|
||||
if n := len(m.entries); n > 0 {
|
||||
m.sel = (m.sel + menuMove() + n) % n
|
||||
if confirmJustPressed() {
|
||||
s, err := NewLevelScene(g.Assets, m.entries[m.sel].id)
|
||||
s, err := NewLevelScene(g.Assets, m.entries[m.sel].id, g.Know)
|
||||
if err != nil {
|
||||
m.err = err
|
||||
return nil
|
||||
@@ -63,21 +69,25 @@ func (m *MenuScene) Update(g *Game) error {
|
||||
}
|
||||
|
||||
func (m *MenuScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
a := g.Assets
|
||||
screen.Fill(color.RGBA{24, 20, 37, 255})
|
||||
vector.DrawFilledRect(screen, 0, 0, ScreenW, 22, color.RGBA{38, 43, 68, 255}, false)
|
||||
ebitenutil.DebugPrintAt(screen, g.Assets.T("game.title"), 8, 4)
|
||||
ebitenutil.DebugPrintAt(screen, g.Assets.T("menu.choose_level"), 8, 30)
|
||||
a.DrawBig(screen, a.T("game.title"), 8, 5, colText)
|
||||
a.Draw(screen, a.T("menu.choose_level"), 8, 30, colDim)
|
||||
|
||||
for i, e := range m.entries {
|
||||
y := 46 + i*14
|
||||
if i == m.sel {
|
||||
vector.DrawFilledRect(screen, 6, float32(y-2), ScreenW-12, 13, color.RGBA{58, 68, 102, 255}, false)
|
||||
vector.DrawFilledRect(screen, 6, float32(y-2), ScreenW-12, 13, colBar, false)
|
||||
}
|
||||
ebitenutil.DebugPrintAt(screen, fmt.Sprintf("%s -> %s", e.name, e.dest), 12, y)
|
||||
a.Draw(screen, e.name, 12, y, colText)
|
||||
a.Draw(screen, "→ "+e.dest, 150, y, colKnown)
|
||||
a.Draw(screen, e.stars, ScreenW-62, y, colWarn)
|
||||
}
|
||||
if m.err != nil {
|
||||
ebitenutil.DebugPrintAt(screen, "FEL: "+m.err.Error(), 8, ScreenH-40)
|
||||
a.Draw(screen, "FEL: "+m.err.Error(), 8, ScreenH-40, colBad)
|
||||
}
|
||||
ebitenutil.DebugPrintAt(screen, g.Assets.T("menu.hint"), 8, ScreenH-26)
|
||||
ebitenutil.DebugPrintAt(screen, fmt.Sprintf("%s (%s)", buildinfo.Version, buildinfo.Mode), 8, ScreenH-13)
|
||||
a.Draw(screen, fmt.Sprintf("%s Lv%d %d %s", a.T("hud.japanese"), g.Know.Level(), g.Know.Count(), a.T("hud.words")), 8, ScreenH-38, colDim)
|
||||
a.Draw(screen, a.T("menu.hint"), 8, ScreenH-26, colDim)
|
||||
a.Draw(screen, fmt.Sprintf("%s (%s)", buildinfo.Version, buildinfo.Mode), 8, ScreenH-13, colDim)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@ package game
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||||
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/sim"
|
||||
@@ -22,7 +20,7 @@ func NewResultScene(from *LevelScene) *ResultScene { return &ResultScene{from: f
|
||||
func (r *ResultScene) Update(g *Game) error {
|
||||
switch {
|
||||
case inpututil_R():
|
||||
s, err := NewLevelScene(g.Assets, r.from.id)
|
||||
s, err := NewLevelScene(g.Assets, r.from.id, g.Know)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -41,31 +39,35 @@ func (r *ResultScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
r.from.Draw(g, screen) // banan i bakgrunden
|
||||
run := r.from.run
|
||||
a := g.Assets
|
||||
vector.DrawFilledRect(screen, 20, 36, ScreenW-40, 112, color.RGBA{24, 20, 37, 235}, false)
|
||||
vector.DrawFilledRect(screen, 20, 36, ScreenW-40, 112, colPanel, false)
|
||||
|
||||
accent, headline := color.RGBA{228, 59, 68, 255}, a.T("level.wrong_train")
|
||||
accent, headline := colBad, a.T("level.wrong_train")
|
||||
if run.Outcome == sim.Won {
|
||||
accent, headline = color.RGBA{99, 199, 77, 255}, a.T("level.cleared")
|
||||
accent, headline = colGood, a.T("level.cleared")
|
||||
}
|
||||
vector.DrawFilledRect(screen, 20, 36, ScreenW-40, 3, accent, false)
|
||||
ebitenutil.DebugPrintAt(screen, headline, 28, 46)
|
||||
a.DrawBig(screen, headline, 28, 44, accent)
|
||||
|
||||
y := 62
|
||||
if tr := run.Boarded; tr != nil {
|
||||
// TODO(M3): japanska när fonten finns
|
||||
ebitenutil.DebugPrintAt(screen, fmt.Sprintf("%s %s %s -> %s", a.T("hud.track"), tr.Track,
|
||||
tr.Line.Get("romaji"), tr.Destination.Get("romaji")), 28, y)
|
||||
y += 12
|
||||
a.Draw(screen, fmt.Sprintf("%s%s %s %s%s", tr.Track, a.T("hud.track_suffix_ja"), tr.Line.Get("ja"),
|
||||
tr.Destination.Get("ja"), a.T("hud.for_ja")), 28, y, colKnown)
|
||||
y += LineH
|
||||
a.Draw(screen, fmt.Sprintf("%s %s %s → %s", a.T("hud.track"), tr.Track, tr.Line.Get("romaji"), tr.Destination.Get("romaji")), 28, y, colDim)
|
||||
y += LineH
|
||||
if run.Outcome == sim.Lost {
|
||||
for _, l := range wrap(fmt.Sprintf(a.T("result.wrong_detail"), tr.Destination.Get("romaji"),
|
||||
run.Level.Destination.Get("romaji")), 44) {
|
||||
ebitenutil.DebugPrintAt(screen, l, 28, y)
|
||||
y += 12
|
||||
for _, l := range a.wrapLine(plainLine(fmt.Sprintf(a.T("result.wrong_detail"), tr.Destination.Get("romaji"),
|
||||
run.Level.Destination.Get("romaji")), colText), ScreenW-56) {
|
||||
a.drawLines(screen, []line{l}, 28, y)
|
||||
y += LineH
|
||||
}
|
||||
}
|
||||
}
|
||||
y += 6
|
||||
ebitenutil.DebugPrintAt(screen, fmt.Sprintf("%s %.1fs %s %d %s %d %s %d¥", a.T("hud.time"), run.Time,
|
||||
a.T("hud.steps"), run.Steps, a.T("result.gates"), run.GatePasses, a.T("result.card"), run.Card.Balance), 28, y)
|
||||
ebitenutil.DebugPrintAt(screen, a.T("result.hint"), 28, 128)
|
||||
y += 4
|
||||
a.Draw(screen, fmt.Sprintf("%s %.1fs %s %d %s %d %s %d¥", a.T("hud.time"), run.Time,
|
||||
a.T("hud.steps"), run.Steps, a.T("result.gates"), run.GatePasses, a.T("result.card"), run.Card.Balance), 28, y, colText)
|
||||
y += LineH
|
||||
a.Draw(screen, fmt.Sprintf("%s Lv%d %d %s (%d %s)", a.T("hud.japanese"), run.Know.Level(), run.Know.Count(),
|
||||
a.T("hud.words"), run.Know.Helps, a.T("result.helps")), 28, y, colDim)
|
||||
a.Draw(screen, a.T("result.hint"), 28, 130, colDim)
|
||||
}
|
||||
|
||||
@@ -1,27 +1,137 @@
|
||||
package game
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"image/color"
|
||||
"strings"
|
||||
|
||||
// Debugfonten är 6 px bred per tecken; 320 px ger drygt 50 tecken.
|
||||
const textCols = 50
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
|
||||
// wrap radbryter en text på ordgränser till högst width tecken per rad.
|
||||
func wrap(s string, width int) []string {
|
||||
var lines []string
|
||||
cur := ""
|
||||
for _, w := range strings.Fields(s) {
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/lang"
|
||||
)
|
||||
|
||||
type drawTarget = *ebiten.Image
|
||||
|
||||
// Färger för text.
|
||||
var (
|
||||
colText = color.RGBA{255, 255, 255, 255}
|
||||
colDim = color.RGBA{139, 155, 180, 255} // oöversatt / sekundärt
|
||||
colKnown = color.RGBA{254, 174, 52, 255} // kända ord / översättning
|
||||
colReading = color.RGBA{99, 199, 77, 255} // kana som kan läsas men inte förstås
|
||||
colEnglish = color.RGBA{44, 232, 245, 255}
|
||||
colBad = color.RGBA{228, 59, 68, 255}
|
||||
colGood = color.RGBA{99, 199, 77, 255}
|
||||
colWarn = color.RGBA{254, 174, 52, 255}
|
||||
colPanel = color.RGBA{24, 20, 37, 230}
|
||||
colBar = color.RGBA{58, 68, 102, 255}
|
||||
)
|
||||
|
||||
// span är en textbit med färg; en rad är flera spans.
|
||||
type span struct {
|
||||
text string
|
||||
col color.Color
|
||||
}
|
||||
|
||||
type line []span
|
||||
|
||||
// plainLine gör en rad av en enda sträng.
|
||||
func plainLine(s string, col color.Color) line { return line{{s, col}} }
|
||||
|
||||
// shownLine gör en rad av språksystemets segment med färg efter kunskap.
|
||||
// Med readings visas läsningen (romaji) efter kända ord – hinten från nivå 2.
|
||||
func shownLine(shown []lang.Shown, base color.Color, readings bool) line {
|
||||
var l line
|
||||
for _, sh := range shown {
|
||||
col := base
|
||||
switch {
|
||||
case cur == "":
|
||||
cur = w
|
||||
case len([]rune(cur))+1+len([]rune(w)) > width:
|
||||
lines = append(lines, cur)
|
||||
cur = w
|
||||
case sh.Reading:
|
||||
col = colReading
|
||||
case sh.Known && lang.IsJapanese(sh.Text):
|
||||
col = colKnown
|
||||
case sh.Known:
|
||||
col = base
|
||||
default:
|
||||
cur += " " + w
|
||||
col = colDim
|
||||
}
|
||||
l = append(l, span{sh.Text, col})
|
||||
if readings && sh.Romaji != "" {
|
||||
l = append(l, span{"(" + sh.Romaji + ")", colDim})
|
||||
}
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// wrapLine radbryter en rad till maxW pixlar. Bryter vid mellanslag, och
|
||||
// mitt i ett ord bara om ordet ensamt är för långt (japanska saknar mellanslag).
|
||||
func (a *Assets) wrapLine(l line, maxW int) []line {
|
||||
var out []line
|
||||
var cur line
|
||||
curW := 0
|
||||
flush := func() {
|
||||
if len(cur) > 0 {
|
||||
out = append(out, cur)
|
||||
cur, curW = nil, 0
|
||||
}
|
||||
}
|
||||
add := func(s string, col color.Color) {
|
||||
w := a.Width(s)
|
||||
if curW+w > maxW && curW > 0 {
|
||||
flush()
|
||||
s = strings.TrimLeft(s, " ")
|
||||
w = a.Width(s)
|
||||
}
|
||||
if s == "" {
|
||||
return
|
||||
}
|
||||
if n := len(cur); n > 0 && cur[n-1].col == col {
|
||||
cur[n-1].text += s
|
||||
} else {
|
||||
cur = append(cur, span{s, col})
|
||||
}
|
||||
curW += w
|
||||
}
|
||||
for _, sp := range l {
|
||||
for _, tok := range tokens(sp.text) {
|
||||
if a.Width(tok) <= maxW {
|
||||
add(tok, sp.col)
|
||||
continue
|
||||
}
|
||||
for _, r := range tok { // för långt ord: bryt per tecken
|
||||
add(string(r), sp.col)
|
||||
}
|
||||
}
|
||||
}
|
||||
flush()
|
||||
return out
|
||||
}
|
||||
|
||||
// tokens delar en sträng i ord och mellanslagsrunor, båda bevarade.
|
||||
func tokens(s string) []string {
|
||||
var out []string
|
||||
cur := ""
|
||||
for _, r := range s {
|
||||
if r == ' ' {
|
||||
if cur != "" {
|
||||
out = append(out, cur)
|
||||
cur = ""
|
||||
}
|
||||
out = append(out, " ")
|
||||
continue
|
||||
}
|
||||
cur += string(r)
|
||||
}
|
||||
if cur != "" {
|
||||
out = append(out, cur)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// drawLines ritar rader från (x, y) med radhöjd LineH.
|
||||
func (a *Assets) drawLines(dst drawTarget, lines []line, x, y int) {
|
||||
for i, l := range lines {
|
||||
cx := x
|
||||
for _, sp := range l {
|
||||
a.Draw(dst, sp.text, cx, y+i*LineH, sp.col)
|
||||
cx += a.Width(sp.text)
|
||||
}
|
||||
}
|
||||
if cur != "" {
|
||||
lines = append(lines, cur)
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user