M3: japanska skyltar – pixelfont, ordbok, kunskap, delvis översättning, hjälp
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:
2026-10-23 20:14:36 +02:00
parent 0e10cbf123
commit b67493fddb
40 changed files with 1846 additions and 174 deletions

90
internal/lang/render.go Normal file
View File

@@ -0,0 +1,90 @@
package lang
import "strings"
// Shown är en bit text som spelaren ser, med hur den ska färgas.
type Shown struct {
Text string
Known bool // ordet är känt (översatt/markerat)
Reading bool // kana-ord som kan läsas men inte förstås
Romaji string // läsning att visa under kända ord vid ShowsReadings
}
// Rendered är en skylt sedd genom spelarens kunskap.
type Rendered struct {
Original []Shown // originaltexten med kända ord markerade
Partial []Shown // delvis översättning: kända ord översatta, resten kvar på japanska
Complete bool // allt gick att läsa
}
// Render tolkar en japansk text med spelarens kunskap. k == nil betyder
// "kan allt" (används när en skylt är avslöjad via hjälp).
//
// Japanska skrivs utan mellanslag, men när ett ord översätts till svenska
// behöver det luft mot grannorden, annars blir det "新宿mot". Därför får
// översatta ord ett mellanslag mot intilliggande text i den delvisa raden.
func Render(d *Dictionary, k *Knowledge, s, uiLang string) Rendered {
var out Rendered
out.Complete = true
prevTranslated := false // förra delen i Partial var ett översatt/läst ord
addPartial := func(sh Shown, translated bool) {
if n := len(out.Partial); n > 0 && (translated || prevTranslated) {
prev := &out.Partial[n-1]
if prev.Text != "" && !strings.HasSuffix(prev.Text, " ") && !strings.HasPrefix(sh.Text, " ") {
prev.Text += " "
}
}
out.Partial = append(out.Partial, sh)
prevTranslated = translated
}
for _, seg := range d.Tokenize(s) {
switch seg.Kind {
case Word:
known := k == nil || k.Knows(seg.Text)
canRead := !known && k != nil && k.Kana && IsAllKana(seg.Text)
orig := Shown{Text: seg.Text, Known: known, Reading: canRead}
if known && k != nil && k.ShowsReadings() {
orig.Romaji = seg.Entry.Romaji
}
out.Original = append(out.Original, orig)
switch {
case known:
addPartial(Shown{Text: seg.Entry.Translation(uiLang), Known: true}, true)
case canRead:
addPartial(Shown{Text: seg.Entry.Romaji, Reading: true}, true)
out.Complete = false
default:
addPartial(Shown{Text: seg.Text}, false)
out.Complete = false
}
case Unknown:
out.Original = append(out.Original, Shown{Text: seg.Text})
addPartial(Shown{Text: seg.Text}, false)
out.Complete = false
default:
out.Original = append(out.Original, Shown{Text: seg.Text, Known: true})
addPartial(Shown{Text: seg.Text, Known: true}, false)
}
}
return out
}
// PlainText slår ihop en rad till en sträng.
func PlainText(line []Shown) string {
s := ""
for _, sh := range line {
s += sh.Text
}
return s
}
// FirstUnknownWord returnerar det första ordboksordet i texten som spelaren
// inte kan, eller "" om hen kan alla.
func FirstUnknownWord(d *Dictionary, k *Knowledge, s string) string {
for _, w := range d.WordsIn(s) {
if !k.Knows(w) {
return w
}
}
return ""
}