package game import ( "fmt" "image/color" "math" "path/filepath" "github.com/hajimehoshi/ebiten/v2" "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" ) // Spelarens grundfart i rutor per sekund (blir en stat i M7). const playerSpeed = 6.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. type LevelScene struct { id string run *sim.Run cam *sim.Camera animT float64 } // 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, a.Dict, know), cam: sim.NewCamera(ScreenW, ScreenH, float64(lvl.Width()*TileSize), float64(lvl.Height()*TileSize)), } s.followPlayer() return s, nil } // PlayerTile returnerar rutan spelaren står på/lämnar (används av skriptläget). func (s *LevelScene) PlayerTile() level.Point { return s.run.Player.Tile } // TrainHere rapporterar om spelaren står på en perrong vars tåg är inne (skriptläget). func (s *LevelScene) TrainHere() bool { tr, at, _ := s.run.PlatformHere() return tr != nil && at } func (s *LevelScene) followPlayer() { x, y := s.run.Player.WorldPos(TileSize) s.cam.Follow(x+TileSize/2, y+TileSize/2) } func (s *LevelScene) Update(g *Game) error { if backJustPressed() { m, err := NewMenuScene(g.Assets) if err != nil { return err } g.SetScene(m) return nil } if helpJustPressed() { s.run.AskHelp() } s.run.Step(dt, dirInput(), confirmJustPressed()) if s.run.Player.Moving() { s.animT += dt } else { s.animT = 0 } s.followPlayer() if s.run.Outcome != sim.Ongoing { g.SetScene(NewResultScene(s)) } return nil } // trainTile är rutan "bortom perrongkanten" där tågvagnen ritas: första // väggruta nedanför, till höger, till vänster eller ovanför perrongen. func (s *LevelScene) trainTile(p level.Point) level.Point { for _, d := range [...]level.Point{{X: 0, Y: 1}, {X: 1, Y: 0}, {X: -1, Y: 0}, {X: 0, Y: -1}} { n := level.Point{X: p.X + d.X, Y: p.Y + d.Y} if s.run.Level.At(n.X, n.Y) == level.Wall { return n } } return p } func (s *LevelScene) Draw(g *Game, screen *ebiten.Image) { screen.Fill(color.RGBA{24, 20, 37, 255}) lvl := s.run.Level ox, oy := s.cam.Offset() ox, oy = math.Round(ox), math.Round(oy) // Rita bara rutor som syns. x0 := max(0, int(s.cam.X)/TileSize) y0 := max(0, int(s.cam.Y)/TileSize) x1 := min(lvl.Width(), int(s.cam.X+ScreenW)/TileSize+2) y1 := min(lvl.Height(), int(s.cam.Y+ScreenH)/TileSize+2) var op ebiten.DrawImageOptions for y := y0; y < y1; y++ { for x := x0; x < x1; x++ { op.GeoM.Reset() op.GeoM.Translate(ox+float64(x*TileSize), oy+float64(y*TileSize)) screen.DrawImage(g.Assets.Tiles[lvl.At(x, y)], &op) } } // Tåg som står inne. for _, t := range []level.Tile{level.Platform, level.WrongPlatform} { if !s.run.TrainVisible(t) { continue } for _, p := range lvl.Find(t) { tt := s.trainTile(p) op.GeoM.Reset() op.GeoM.Translate(ox+float64(tt.X*TileSize), oy+float64(tt.Y*TileSize)) screen.DrawImage(g.Assets.Train, &op) } } px, py := s.run.Player.WorldPos(TileSize) op.GeoM.Reset() op.GeoM.Translate(ox+math.Round(px), oy+math.Round(py)) screen.DrawImage(g.Assets.PlayerFrame(s.run.Player.Facing, s.run.Player.Moving(), s.animT), &op) 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) 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 } 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 := 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 := ScreenH - h if int(py-s.cam.Y)+2*TileSize > y { y = 24 } 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 }