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 }