Texterna ryms på skärmen på svenska och engelska
All checks were successful
release / build-release (push) Successful in 1m37s
All checks were successful
release / build-release (push) Successful in 1m37s
Björn såg att bankartans tangentrad klipptes på engelska. Nu mäts alla
enradstexter med den riktiga fonten i båda språken mot sitt utrymme
(game.TestSingleLineTextsFit) och HUD-raden för alla banor
(TestLevelNamesFitHUD). Åtgärder: kortare etiketter ("Mål"/"To"),
bankartans tangentrad på två rader (versionen står på titelskärmen),
HUD:en stryker japansk hint, "via" och valfria plånboksdelar som inte
får plats och kortar med "..", fler rutor radbryts, kortare bannamn,
kortare minispelstexter. Skärmdumpar på engelska (map-en, sign-en) via en
engelsk demosparfil. Alla skärmdumpar omtagna.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -416,13 +416,6 @@ func (s *LevelScene) drawHUD(g *Game, screen *ebiten.Image) {
|
||||
a := g.Assets
|
||||
vector.DrawFilledRect(screen, 0, 0, ScreenW, 24, color.RGBA{24, 20, 37, 200}, false)
|
||||
|
||||
dest := r.FinalDestination().Get("romaji")
|
||||
if !r.IsLastLeg() { // resa med byte: den här delbanan slutar vid bytesstationen
|
||||
dest += " " + a.T("hud.via") + " " + r.Level.Destination.Get("romaji")
|
||||
}
|
||||
if r.Know.ShowsDestinationJP() { // hint från nivå 1: delbanans mål på japanska, att matcha mot skyltarna
|
||||
dest += " " + r.Level.Destination.Get("ja")
|
||||
}
|
||||
name := r.Level.Name.Get(UILang)
|
||||
if r.Level.NumFloors() > 1 {
|
||||
name += " " + r.Level.FloorName(r.Player.Tile.Z)
|
||||
@@ -433,39 +426,72 @@ func (s *LevelScene) drawHUD(g *Game, screen *ebiten.Image) {
|
||||
if s.endless && g.Endless != nil {
|
||||
name = fmt.Sprintf("#%d %s", g.Endless.Step+1, name)
|
||||
}
|
||||
metersX := ScreenW - 4 - len(r.Party.Members)*(meterW+meterGap)
|
||||
name = a.fitText(name, metersX/2)
|
||||
a.Draw(screen, name, 4, 0, colText)
|
||||
destX := 4 + a.Width(name) + 12
|
||||
a.Draw(screen, a.T("hud.destination")+": "+dest, destX, 0, colKnown)
|
||||
// Japanska-nivån får plats till vänster om mätarna; annars kortas den.
|
||||
jpX := destX + a.Width(a.T("hud.destination")+": "+dest) + 12
|
||||
metersX := ScreenW - 4 - len(r.Party.Members)*(meterW+meterGap)
|
||||
jp := fmt.Sprintf("%s Lv%d·%d", a.T("hud.japanese"), r.Know.Level(), r.Know.Count())
|
||||
if jpX+a.Width(jp) > metersX-4 {
|
||||
jp = fmt.Sprintf("Lv%d", r.Know.Level())
|
||||
// Resmålet: slutmålet, "via" bytesstationen och japanska hinten – i den
|
||||
// ordningen stryks delar som inte får plats före mätarna.
|
||||
final := r.FinalDestination().Get("romaji")
|
||||
via, jp := "", ""
|
||||
if !r.IsLastLeg() { // resa med byte: den här delbanan slutar vid bytesstationen
|
||||
via = " " + a.T("hud.via") + " " + r.Level.Destination.Get("romaji")
|
||||
}
|
||||
if jpX+a.Width(jp) <= metersX-4 {
|
||||
a.Draw(screen, jp, jpX, 0, colDim)
|
||||
if r.Know.ShowsDestinationJP() { // hint från nivå 1: delbanans mål på japanska, att matcha mot skyltarna
|
||||
jp = " " + r.Level.Destination.Get("ja")
|
||||
}
|
||||
label := a.T("hud.destination") + ": "
|
||||
dest := label + final + via + jp
|
||||
for _, cand := range []string{label + final + via + jp, label + final + via, label + final + jp, label + final} {
|
||||
dest = cand
|
||||
if destX+a.Width(cand) <= metersX-4 {
|
||||
break
|
||||
}
|
||||
}
|
||||
dest = a.fitText(dest, metersX-4-destX)
|
||||
a.Draw(screen, dest, destX, 0, colKnown)
|
||||
// Japanska-nivån får plats till vänster om mätarna; annars kortas den.
|
||||
jpX := destX + a.Width(dest) + 12
|
||||
lv := fmt.Sprintf("%s Lv%d·%d", a.T("hud.japanese"), r.Know.Level(), r.Know.Count())
|
||||
if jpX+a.Width(lv) > metersX-4 {
|
||||
lv = fmt.Sprintf("Lv%d", r.Know.Level())
|
||||
}
|
||||
if jpX+a.Width(lv) <= metersX-4 {
|
||||
a.Draw(screen, lv, jpX, 0, colDim)
|
||||
}
|
||||
timeStr := fmt.Sprintf("%s %3.0fs", a.T("hud.time"), r.Time)
|
||||
if r.TimeLimit() > 0 {
|
||||
left := r.TimeLeft()
|
||||
timeStr = fmt.Sprintf("%s %d:%02d", a.T("hud.time_left"), int(left)/60, int(left)%60)
|
||||
}
|
||||
wallet := fmt.Sprintf("%s %s %d¥ %s %d¥", timeStr, a.T("hud.card"), r.Wallet.Card, a.T("hud.cash_short"), r.Wallet.Cash)
|
||||
// Plånboksraden: tid, kort, kontanter, sedan valfria delar (missade tåg,
|
||||
// biljett, telefon) som stryks bakifrån om de inte får plats före mätarna.
|
||||
must := fmt.Sprintf("%s %s %d¥ %s %d¥", timeStr, a.T("hud.card"), r.Wallet.Card, a.T("hud.cash_short"), r.Wallet.Cash)
|
||||
var optional []string
|
||||
if r.MaxMissed() > 0 {
|
||||
wallet += fmt.Sprintf(" %s %d/%d", a.T("hud.missed_short"), r.MissedTrains, r.MaxMissed())
|
||||
}
|
||||
if r.Wallet.Phone {
|
||||
wallet += fmt.Sprintf(" %s %.0f%%", a.T("hud.phone"), r.Wallet.Battery)
|
||||
optional = append(optional, fmt.Sprintf("%s %d/%d", a.T("hud.missed_short"), r.MissedTrains, r.MaxMissed()))
|
||||
}
|
||||
if r.Wallet.Ticket != "" {
|
||||
wallet += " " + a.T("hud.ticket") + " " + r.Wallet.Ticket
|
||||
optional = append(optional, a.T("hud.ticket")+" "+r.Wallet.Ticket)
|
||||
}
|
||||
if r.Wallet.Phone {
|
||||
optional = append(optional, fmt.Sprintf("%s %.0f%%", a.T("hud.phone"), r.Wallet.Battery))
|
||||
}
|
||||
wallet := must
|
||||
for n := len(optional); n >= 0; n-- {
|
||||
wallet = must
|
||||
for _, o := range optional[:n] {
|
||||
wallet += " " + o
|
||||
}
|
||||
if 4+a.Width(wallet) <= metersX-4 {
|
||||
break
|
||||
}
|
||||
}
|
||||
walletCol := colDim
|
||||
if r.TimeLimit() > 0 && r.TimeLeft() < 30 {
|
||||
walletCol = colBad
|
||||
}
|
||||
a.Draw(screen, wallet, 4, LineH, walletCol)
|
||||
a.Draw(screen, a.fitText(wallet, metersX-8), 4, LineH, walletCol)
|
||||
s.drawMeters(g, screen)
|
||||
|
||||
if r.Outcome != sim.Ongoing || r.Mini != nil || s.shop != nil {
|
||||
@@ -634,9 +660,9 @@ func (s *LevelScene) infoLines(a *Assets) (lines []line, accent color.Color, bar
|
||||
case r.NoExitAt >= 0 && r.Time-r.NoExitAt < deniedShowSec:
|
||||
return a.wrapLine(plainLine(fmt.Sprintf(a.T("hud.no_exit"), sim.StaffSec), colBad), maxW), colBad, false
|
||||
case r.NeedsStaff():
|
||||
return []line{plainLine(fmt.Sprintf(a.T("hud.staff_prompt"), sim.StaffSec), colWarn)}, colWarn, false
|
||||
return a.wrapLine(plainLine(fmt.Sprintf(a.T("hud.staff_prompt"), sim.StaffSec), colWarn), maxW), colWarn, false
|
||||
case lvl.OneWayGates && r.StaffUnlocked:
|
||||
return []line{plainLine(a.T("hud.unlocked_prompt"), colGood)}, colGood, false
|
||||
return a.wrapLine(plainLine(a.T("hud.unlocked_prompt"), colGood), maxW), colGood, false
|
||||
}
|
||||
case level.Toilet:
|
||||
if r.Facility.Active {
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/buildinfo"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
||||
)
|
||||
|
||||
@@ -266,7 +265,7 @@ func (m *MapScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
a.Draw(screen, a.T("hud.debuffs")+": "+strings.Join(parts, ", "), 8, y, colBad)
|
||||
}
|
||||
a.Draw(screen, a.T("map.hint"), 8, ScreenH-24, colDim)
|
||||
a.Draw(screen, fmt.Sprintf("%s (%s)", buildinfo.Version, buildinfo.Mode), 8, ScreenH-12, colDim)
|
||||
a.Draw(screen, a.T("map.hint2"), 8, ScreenH-12, colDim) // versionen står på titelskärmen
|
||||
}
|
||||
|
||||
// drawLine ritar en tjock linje mellan två punkter.
|
||||
|
||||
@@ -123,8 +123,10 @@ func (r *ResultScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
if len(run.LearnedDest) > 0 {
|
||||
msg += " " + a.T("hud.learned") + ": " + strings.Join(run.LearnedDest, " ") + " (" + a.T("result.destination") + ")"
|
||||
}
|
||||
a.Draw(screen, msg, 28, y, colKnown)
|
||||
y += LineH
|
||||
for _, l := range a.wrapLine(plainLine(msg, colKnown), ScreenW-56) {
|
||||
a.drawLines(screen, []line{l}, 28, y)
|
||||
y += LineH
|
||||
}
|
||||
}
|
||||
if o := r.from.outcome; run.Outcome == sim.Won {
|
||||
prog := fmt.Sprintf("+%d %s", o.XP, a.T("map.xp"))
|
||||
|
||||
@@ -94,7 +94,7 @@ func (s *TitleScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
y += LineH
|
||||
}
|
||||
if s.confirm {
|
||||
a.Draw(screen, a.T("title.quit_confirm"), 150, y-LineH, colWarn)
|
||||
a.Draw(screen, a.T("title.quit_confirm"), 16, y+2, colWarn)
|
||||
}
|
||||
a.Draw(screen, a.T("title.hint"), 8, ScreenH-12, colDim)
|
||||
}
|
||||
|
||||
@@ -136,6 +136,21 @@ func (a *Assets) drawLines(dst drawTarget, lines []line, x, y int) {
|
||||
}
|
||||
}
|
||||
|
||||
// fitText kortar en text med ".." tills den ryms i maxW pixlar.
|
||||
func (a *Assets) fitText(s string, maxW int) string {
|
||||
if a.Width(s) <= maxW {
|
||||
return s
|
||||
}
|
||||
r := []rune(s)
|
||||
for len(r) > 0 {
|
||||
r = r[:len(r)-1]
|
||||
if a.Width(string(r)+"..") <= maxW {
|
||||
return string(r) + ".."
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// highlightLine är tutorialens sätt att läsa en skylt: resmålets japanska
|
||||
// tecken i markeringsfärg, allt annat nedtonat.
|
||||
func highlightLine(text, needle string, hi, dim color.Color) line {
|
||||
|
||||
156
internal/game/textwidth_test.go
Normal file
156
internal/game/textwidth_test.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2/text/v2"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/res"
|
||||
)
|
||||
|
||||
// loadTexts läser ett språks UI-texter.
|
||||
func loadTexts(t *testing.T, lang string) map[string]string {
|
||||
t.Helper()
|
||||
m := map[string]string{}
|
||||
if err := res.LoadJSON(&m, "text", lang+".json"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func uiFace(t *testing.T) *text.GoTextFace {
|
||||
t.Helper()
|
||||
face, err := loadFont("PixelMplus10-Regular.ttf", 10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return face
|
||||
}
|
||||
|
||||
// TestSingleLineTextsFit: texter som ritas på en rad utan radbrytning ska
|
||||
// rymmas i sitt utrymme på båda språken. Bredderna mäts med den riktiga
|
||||
// fonten. Texter som radbryts vid visning (tutorial, tips, premiss …) ingår
|
||||
// inte.
|
||||
func TestSingleLineTextsFit(t *testing.T) {
|
||||
face := uiFace(t)
|
||||
width := func(s string) int { return int(text.Advance(s, face)) }
|
||||
const (
|
||||
full = ScreenW - 16 // en rad över hela skärmen med marginal
|
||||
panel = ScreenW - 24 - 16 // minispelspanelens innerbredd
|
||||
resW = ScreenW - 56 // resultatpanelen
|
||||
pause = ScreenW - 96 // pausmenyn
|
||||
)
|
||||
type check struct {
|
||||
key string
|
||||
max int
|
||||
args []any
|
||||
}
|
||||
checks := []check{
|
||||
{"map.hint", full, nil}, {"map.hint2", full, nil}, {"menu.hint", full, nil}, {"settings.hint", full, nil},
|
||||
{"title.hint", full, nil}, {"tutorial.hint", full, nil}, {"ending.hint", full, nil},
|
||||
{"result.hint", resW, nil}, {"result.hint_endless", resW, nil}, {"result.hint_endless_over", resW, nil},
|
||||
{"pause.hint", pause, nil}, {"pause.quit_confirm", pause, nil}, {"pause.resume", pause, nil}, {"pause.leave", pause, nil}, {"pause.quit", pause, nil},
|
||||
{"title.quit_confirm", ScreenW - 32, nil}, {"title.tutorial", 110, nil}, {"menu.settings", 110, nil}, {"menu.quit", 110, nil}, {"menu.continue", 110, nil}, {"menu.start", 110, nil},
|
||||
{"items.hint", full, nil}, {"skills.hint", full, nil}, {"words.hint", full, nil}, {"shop.hint", panel, nil},
|
||||
{"mini.car.hint", panel, nil}, {"mini.car.pick", panel, nil}, {"mini.car.alighting", panel, nil}, {"mini.car.men", panel, nil},
|
||||
{"mini.crowd.title", panel - 60, nil}, {"mini.crowd.hint", panel, []any{2}},
|
||||
{"mini.ride.title", panel - 60, nil}, {"mini.ride.hint", panel, nil}, {"mini.ride.let_pass", panel, nil},
|
||||
{"mini.ticket.title", panel - 60, nil}, {"mini.ticket.hint", panel, []any{"Narita Airport", 5000}},
|
||||
{"mini.seat.hint", panel, []any{"Ville"}}, {"mini.seat.prefs", panel, nil},
|
||||
{"mini.topup.title", panel - 60, nil}, {"mini.topup.feed", panel, []any{5000}}, {"mini.topup.pick", panel, nil}, {"mini.topup.amount", panel, nil}, {"mini.topup.member", panel, nil}, {"mini.topup.wrong", panel - 60, nil},
|
||||
{"mini.payer.title", panel - 60, nil}, {"mini.payer.price", panel, []any{900}}, {"mini.payer.short", panel, nil}, {"mini.payer.hint", panel, nil},
|
||||
{"hud.kiosk_prompt", full - 60, []any{4, 800}}, {"hud.toilet_prompt", full, []any{4, 32.0}}, {"hud.machine_topup", full, []any{100000, 100000}}, {"hud.machine_ticket", full, []any{100000}},
|
||||
{"hud.help_prompt", full, []any{30.0}}, {"hud.train_near", full, nil}, {"hud.staff_busy", full, nil}, {"hud.toilet_busy", full, nil}, {"hud.kiosk_none", full - 60, nil}, {"hud.toilet_none", full, nil},
|
||||
{"settings.master", 118, nil}, {"settings.effects", 118, nil}, {"settings.music", 118, nil}, {"settings.lang", 118, nil}, {"settings.scale", 118, nil}, {"settings.fullscreen", 118, nil},
|
||||
{"settings.scale_auto", ScreenW - 130 - 8, nil},
|
||||
{"ending.summary", full, nil}, {"ending.story_done", full * 2, nil},
|
||||
}
|
||||
for _, lang := range []string{"sv", "en"} {
|
||||
m := loadTexts(t, lang)
|
||||
for _, c := range checks {
|
||||
s, ok := m[c.key]
|
||||
if !ok {
|
||||
t.Errorf("%s: nyckeln %s saknas", lang, c.key)
|
||||
continue
|
||||
}
|
||||
if len(c.args) > 0 {
|
||||
s = fmt.Sprintf(s, c.args...)
|
||||
}
|
||||
if w := width(s); w > c.max {
|
||||
t.Errorf("%s %s är %d px, får vara %d: %q", lang, c.key, w, c.max, s)
|
||||
}
|
||||
}
|
||||
// Bankartans sammansatta rader.
|
||||
line2 := fmt.Sprintf(m["menu.phone"], m["menu.on"]) + " " + fmt.Sprintf(m["menu.mode"], m["mode.signs"])
|
||||
if w := width(line2); w > full {
|
||||
t.Errorf("%s bankartans lägesrad är %d px: %q", lang, w, line2)
|
||||
}
|
||||
endless := m["map.endless"] + " (" + fmt.Sprintf(m["map.endless_best"], 99999, 99) + ") " + m["map.ngplus"] + " (" + fmt.Sprintf(m["map.ngplus_level"], 9) + ")"
|
||||
if w := width(endless); w > full {
|
||||
t.Errorf("%s bankartans oändligt-rad är %d px: %q", lang, w, endless)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLevelNamesFitHUD: bannamnet, resmålet och mätarna ska rymmas på
|
||||
// HUD:ens första rad även med fullt sällskap och byte, på båda språken.
|
||||
func TestLevelNamesFitHUD(t *testing.T) {
|
||||
face := uiFace(t)
|
||||
width := func(s string) int { return int(text.Advance(s, face)) }
|
||||
dir, err := res.Path("data", "levels")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
all, err := level.LoadAll(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, lang := range []string{"sv", "en"} {
|
||||
m := loadTexts(t, lang)
|
||||
for _, l := range all {
|
||||
name := l.Name.Get(lang)
|
||||
if l.NumFloors() > 1 {
|
||||
name += " " + l.FloorName(0)
|
||||
}
|
||||
chain := level.ChainOf(all, l.ID)
|
||||
if len(chain) > 1 {
|
||||
name += " 1/2"
|
||||
}
|
||||
dest := m["hud.destination"] + ": " + level.FinalOf(all, l).Destination.Get("romaji")
|
||||
if len(chain) > 1 {
|
||||
dest += " " + m["hud.via"] + " " + l.Destination.Get("romaji")
|
||||
}
|
||||
metersX := ScreenW - 4 - max(1, l.Party)*(meterW+meterGap)
|
||||
if w := 4 + width(name) + 12 + width(dest); w > metersX-4 {
|
||||
t.Errorf("%s %s: HUD-raden %q + %q är %d px, mätarna börjar vid %d", lang, l.ID, name, dest, w, metersX)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestReportTextWidths skriver (med -v) ut alla texter bredare än skärmen,
|
||||
// som hjälp vid textgranskning. Fäller inget: de flesta radbryts vid visning.
|
||||
func TestReportTextWidths(t *testing.T) {
|
||||
face := uiFace(t)
|
||||
for _, lang := range []string{"sv", "en"} {
|
||||
m := loadTexts(t, lang)
|
||||
var keys []string
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
for _, line := range strings.Split(m[k], "\n") {
|
||||
if w := int(text.Advance(line, face)); w > ScreenW-16 {
|
||||
t.Logf("WIDE %s %-28s %4d px %q", lang, k, w, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = filepath.Base
|
||||
}
|
||||
Reference in New Issue
Block a user