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>
219 lines
5.9 KiB
Go
219 lines
5.9 KiB
Go
package sim
|
||
|
||
import (
|
||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/lang"
|
||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
||
)
|
||
|
||
// Outcome är hur en banomgång står sig.
|
||
type Outcome int
|
||
|
||
// Utfall.
|
||
const (
|
||
Ongoing Outcome = iota
|
||
Won // klev på rätt tåg
|
||
Lost // klev på fel tåg
|
||
)
|
||
|
||
// Card är tågkortet (IC-kortet). Saldo i yen.
|
||
type Card struct{ Balance int }
|
||
|
||
// CanPay rapporterar om saldot räcker.
|
||
func (c *Card) CanPay(yen int) bool { return c.Balance >= yen }
|
||
|
||
// Pay drar beloppet om det går.
|
||
func (c *Card) Pay(yen int) bool {
|
||
if !c.CanPay(yen) {
|
||
return false
|
||
}
|
||
c.Balance -= yen
|
||
return true
|
||
}
|
||
|
||
// StartBalanceYen är kortets saldo vid banstart tills M6 ger riktig ekonomi.
|
||
const StartBalanceYen = 2000
|
||
|
||
// Run är tillståndet för en pågående bana: spelaren, klockan, kortet och
|
||
// utfallet. Ren logik – LevelScene ritar den, testerna kör den headless.
|
||
type Run struct {
|
||
Level *level.Level
|
||
Player *Mover
|
||
Card Card
|
||
Time float64 // sekunder sedan banstart (styr tidtabellerna)
|
||
Steps int
|
||
GatePasses int
|
||
DeniedAt float64 // när spärren senast nekade (för HUD); < 0 = aldrig
|
||
Outcome Outcome
|
||
Boarded *level.Train
|
||
|
||
// Språk: ordboken, spelarens kunskap (delas mellan banor) och hjälpen.
|
||
Dict *lang.Dictionary
|
||
Know *lang.Knowledge
|
||
Help HelpState
|
||
Revealed map[level.Point]bool // skyltar som översatts helt via hjälp
|
||
Learned string // senast inlärda ord (för HUD-notis)
|
||
LearnedAt float64 // när det lärdes in; < 0 = aldrig
|
||
}
|
||
|
||
// HelpState är en pågående "fråga någon om hjälp".
|
||
type HelpState struct {
|
||
Active bool
|
||
At level.Point // skyltrutan hjälpen gäller
|
||
Remaining float64
|
||
Total float64
|
||
}
|
||
|
||
// NewRun startar banan med spelaren på startrutan. dict och know får vara
|
||
// nil (tom ordbok respektive ny kunskap).
|
||
func NewRun(l *level.Level, speed float64, dict *lang.Dictionary, know *lang.Knowledge) *Run {
|
||
if dict == nil {
|
||
dict = lang.NewDictionary()
|
||
}
|
||
if know == nil {
|
||
know = lang.NewKnowledge()
|
||
}
|
||
return &Run{
|
||
Level: l,
|
||
Player: NewMover(l.SpawnPoint(), speed),
|
||
Card: Card{Balance: StartBalanceYen},
|
||
DeniedAt: -1,
|
||
Dict: dict,
|
||
Know: know,
|
||
Revealed: map[level.Point]bool{},
|
||
LearnedAt: -1,
|
||
}
|
||
}
|
||
|
||
// CanEnter avgör om spelaren får gå in på en ruta: inte vägg, och en spärr
|
||
// kräver att kortet kan betala avgiften (att gå mellan två spärrutor är gratis).
|
||
func (r *Run) CanEnter(p level.Point) bool {
|
||
t := r.Level.At(p.X, p.Y)
|
||
if !t.Passable() {
|
||
return false
|
||
}
|
||
if t == level.Gate && !r.inGate() && !r.Card.CanPay(r.Level.FareYen) {
|
||
r.DeniedAt = r.Time
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
func (r *Run) inGate() bool {
|
||
return r.Level.At(r.Player.Tile.X, r.Player.Tile.Y) == level.Gate
|
||
}
|
||
|
||
// Step för banan framåt dt sekunder. want är hållen riktning, board=true
|
||
// betyder att spelaren försöker kliva på tåget där hen står.
|
||
func (r *Run) Step(dt float64, want Dir, board bool) {
|
||
if r.Outcome != Ongoing {
|
||
return
|
||
}
|
||
r.Time += dt
|
||
before := r.Player.Tile
|
||
wasGate := r.inGate()
|
||
r.Steps += r.Player.Step(dt, want, r.CanEnter)
|
||
r.stepHelp(dt)
|
||
if r.Player.Tile != before && r.inGate() && !wasGate {
|
||
r.Card.Pay(r.Level.FareYen)
|
||
r.GatePasses++
|
||
}
|
||
if board && !r.Player.Moving() {
|
||
if tr, at, _ := r.PlatformHere(); tr != nil && at {
|
||
r.Board(tr)
|
||
}
|
||
}
|
||
}
|
||
|
||
// PlatformHere returnerar tåget vid perrongen spelaren står på (nil om
|
||
// spelaren inte står på en perrong), om det är inne, och sekunder kvar
|
||
// till avgång respektive ankomst.
|
||
func (r *Run) PlatformHere() (tr *level.Train, atPlatform bool, seconds float64) {
|
||
t := r.Level.At(r.Player.Tile.X, r.Player.Tile.Y)
|
||
if t != level.Platform && t != level.WrongPlatform {
|
||
return nil, false, 0
|
||
}
|
||
tr = r.Level.TrainAt(t)
|
||
if tr == nil {
|
||
return nil, false, 0
|
||
}
|
||
atPlatform, seconds = tr.Schedule.Status(r.Time)
|
||
return tr, atPlatform, seconds
|
||
}
|
||
|
||
// SignHere returnerar skylten spelaren står stilla på, annars nil.
|
||
func (r *Run) SignHere() *level.SignText {
|
||
if r.Player.Moving() {
|
||
return nil
|
||
}
|
||
return r.Level.SignAt(r.Player.Tile)
|
||
}
|
||
|
||
// Board kliver på ett tåg och avgör banan.
|
||
func (r *Run) Board(tr *level.Train) {
|
||
r.Boarded = tr
|
||
if tr.Correct {
|
||
r.Outcome = Won
|
||
} else {
|
||
r.Outcome = Lost
|
||
}
|
||
}
|
||
|
||
// TrainVisible rapporterar om tåget för en perrongruta står inne just nu.
|
||
func (r *Run) TrainVisible(t level.Tile) bool {
|
||
tr := r.Level.TrainAt(t)
|
||
if tr == nil {
|
||
return false
|
||
}
|
||
at, _ := tr.Schedule.Status(r.Time)
|
||
return at
|
||
}
|
||
|
||
// AskHelp börjar fråga en förbipasserande om vad skylten betyder. Kräver att
|
||
// spelaren står stilla på en skylt som inte redan är avslöjad. Tiden beror
|
||
// på japanska-nivån (lang.HelpSeconds).
|
||
func (r *Run) AskHelp() bool {
|
||
sg := r.SignHere()
|
||
if sg == nil || r.Help.Active || r.Revealed[r.Player.Tile] || r.Outcome != Ongoing {
|
||
return false
|
||
}
|
||
secs := lang.HelpSeconds(r.Know.Level())
|
||
r.Help = HelpState{Active: true, At: r.Player.Tile, Remaining: secs, Total: secs}
|
||
return true
|
||
}
|
||
|
||
// stepHelp räknar ned hjälpen; att gå därifrån avbryter. När den är klar
|
||
// avslöjas skylten och spelaren lär sig det första okända ordet på den.
|
||
func (r *Run) stepHelp(dt float64) {
|
||
if !r.Help.Active {
|
||
return
|
||
}
|
||
if r.Player.Moving() || r.Player.Tile != r.Help.At {
|
||
r.Help = HelpState{}
|
||
return
|
||
}
|
||
r.Help.Remaining -= dt
|
||
if r.Help.Remaining > 0 {
|
||
return
|
||
}
|
||
r.Revealed[r.Help.At] = true
|
||
r.Know.Helps++
|
||
if sg := r.Level.SignAt(r.Help.At); sg != nil {
|
||
if w := lang.FirstUnknownWord(r.Dict, r.Know, sg.Text["ja"]); w != "" {
|
||
r.Know.Learn(w)
|
||
r.Learned, r.LearnedAt = w, r.Time
|
||
}
|
||
}
|
||
r.Help = HelpState{}
|
||
}
|
||
|
||
// SignRevealed rapporterar om skylten på rutan är helt översatt.
|
||
func (r *Run) SignRevealed(p level.Point) bool { return r.Revealed[p] }
|
||
|
||
// HelpProgress är 0..1 för en pågående hjälp.
|
||
func (r *Run) HelpProgress() float64 {
|
||
if !r.Help.Active || r.Help.Total <= 0 {
|
||
return 0
|
||
}
|
||
return 1 - r.Help.Remaining/r.Help.Total
|
||
}
|