M13: titelskärm, språkval, tutorial, tips och pausmeny
All checks were successful
release / build-release (push) Successful in 1m40s
All checks were successful
release / build-release (push) Successful in 1m40s
Björns feedback efter v0.1.0. Titelskärm med premissen i text och menyn Fortsätt/Starta, Tutorial, Inställningar, Avsluta (Enter två gånger); språkval första gången spelet startar. Tutorial i fem sidor med riktiga sprites och en exempelskylt där resmålet markeras och resten tonas ned, plus nyckeln 番線/方面/改札 och biljettipset. Kontextuella tips i spelet tills första banan är klarad, och resmålets tecken markeras på skyltar och avgångstavlor under tiden. Esc i en bana öppnar en pausmeny (Fortsätt, Lämna banan, Inställningar, Avsluta spelet) i stället för att lämna banan; Esc på bankartan går till titeln. Kortare lägestexter så att bankartans rader ryms på svenska. Test för markeringen, docs, wiki och skärmdumpar. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -162,8 +162,10 @@ func New(opt Options) (*Game, error) {
|
||||
return nil, err
|
||||
}
|
||||
g.scene = s
|
||||
case !exists:
|
||||
g.scene = &LangScene{} // första starten: välj språk, sedan titelskärmen
|
||||
default:
|
||||
g.scene = NewMapScene(g)
|
||||
g.scene = NewTitleScene()
|
||||
}
|
||||
return g, nil
|
||||
}
|
||||
@@ -284,8 +286,8 @@ func (g *Game) Save() {
|
||||
// till banan och tystnar när man lämnar den.
|
||||
func (g *Game) SetScene(s Scene) {
|
||||
switch s.(type) {
|
||||
case *LevelScene, *ResultScene: // banans sorl fortsätter
|
||||
case *MapScene, *SettingsScene, *EndingScene, *WordsScene, *SkillsScene, *ItemsScene:
|
||||
case *LevelScene, *ResultScene, *PauseScene: // banans sorl fortsätter
|
||||
case *MapScene, *SettingsScene, *EndingScene, *WordsScene, *SkillsScene, *ItemsScene, *TitleScene, *LangScene, *TutorialScene:
|
||||
g.Audio.Loop(SoundMusicMap, 0.6) // menymusiken (fortsätter om den redan spelas)
|
||||
default:
|
||||
g.Audio.StopLoop()
|
||||
|
||||
@@ -38,14 +38,17 @@ const (
|
||||
// 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
|
||||
level *level.Level // aktuell delbana (byts när resan fortsätter på nästa station)
|
||||
run *sim.Run
|
||||
endless bool // målet är ett lottat mål i oändligt läge (inte storyn)
|
||||
sfx sfxState
|
||||
cam *sim.Camera
|
||||
animT float64
|
||||
hideMap bool // M döljer/visar minikartan
|
||||
id string
|
||||
level *level.Level // aktuell delbana (byts när resan fortsätter på nästa station)
|
||||
run *sim.Run
|
||||
endless bool // målet är ett lottat mål i oändligt läge (inte storyn)
|
||||
sfx sfxState
|
||||
tips tipState
|
||||
highlight bool // tutorialen: resmålets tecken markeras på skyltar, resten tonas ned
|
||||
infoBox image.Rectangle // inforutans läge denna bildruta (tom = ingen), så att tipsen inte täcker den
|
||||
cam *sim.Camera
|
||||
animT float64
|
||||
hideMap bool // M döljer/visar minikartan
|
||||
|
||||
shop *shop // öppen butik i kiosken (pausar banan)
|
||||
outcome LevelOutcome // progression från banan (fylls i när den avgjorts)
|
||||
@@ -185,6 +188,8 @@ func (s *LevelScene) Update(g *Game) error {
|
||||
}
|
||||
s.updateEffects(g)
|
||||
s.updateSfx(g)
|
||||
s.updateTips(g)
|
||||
s.highlight = s.tutorialActive(g)
|
||||
if s.run.Mini != nil {
|
||||
// Minispel: all input går dit; Esc avbryter minispelet, inte banan.
|
||||
mx, my := miniMoveXY()
|
||||
@@ -199,7 +204,7 @@ func (s *LevelScene) Update(g *Game) error {
|
||||
return nil
|
||||
}
|
||||
if backJustPressed() {
|
||||
g.SetScene(NewMapScene(g))
|
||||
g.SetScene(NewPauseScene(s)) // Esc pausar; att lämna banan kräver ett val i menyn
|
||||
return nil
|
||||
}
|
||||
if shopJustPressed() && !s.run.Player.Moving() && s.run.Level.At(s.run.Player.Tile) == level.Kiosk {
|
||||
@@ -353,6 +358,7 @@ func (s *LevelScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
s.drawIndicator(screen, ox, oy)
|
||||
|
||||
s.drawHUD(g, screen)
|
||||
s.drawTip(g, screen)
|
||||
if r := s.run; r.TransferAt >= 0 && r.Time-r.TransferAt < transferBannerSec && r.Mini == nil && r.Outcome == sim.Ongoing {
|
||||
txt := g.Assets.T("hud.transfer_done")
|
||||
w := g.Assets.Width(txt) + 16
|
||||
@@ -517,6 +523,7 @@ func (s *LevelScene) drawHUD(g *Game, screen *ebiten.Image) {
|
||||
}
|
||||
box = image.Rect(0, y, ScreenW, y+h)
|
||||
}
|
||||
s.infoBox = box
|
||||
if !s.hideMap {
|
||||
s.drawMinimap(g, screen, box, hasBox)
|
||||
}
|
||||
@@ -553,7 +560,11 @@ func (s *LevelScene) infoLines(a *Assets) (lines []line, accent color.Color, bar
|
||||
if tr, at, secs := r.PlatformHere(); tr != nil {
|
||||
// Avgångstavlan är en skylt som alla andra: japanska, sedd genom kunskapen.
|
||||
rd := lang.Render(a.Dict, r.Know, tr.BoardText(), UILang)
|
||||
lines = append(lines, a.wrapLine(shownLine(rd.Original, colText, r.Know.ShowsReadings()), maxW)...)
|
||||
if s.highlight {
|
||||
lines = append(lines, a.wrapLine(highlightLine(tr.BoardText(), lvl.Destination.Get("ja"), colGood, colDim), maxW)...)
|
||||
} else {
|
||||
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,
|
||||
@@ -594,7 +605,11 @@ func (s *LevelScene) infoLines(a *Assets) (lines []line, accent color.Color, bar
|
||||
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)...)
|
||||
if s.highlight {
|
||||
lines = append(lines, a.wrapLine(highlightLine(sg.Text["ja"], lvl.Destination.Get("ja"), colGood, colDim), maxW)...)
|
||||
} else {
|
||||
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)...)
|
||||
|
||||
@@ -91,7 +91,8 @@ func (m *MapScene) current() *level.Level {
|
||||
func (m *MapScene) Update(g *Game) error {
|
||||
switch {
|
||||
case backJustPressed():
|
||||
return ebiten.Termination
|
||||
g.SetScene(NewTitleScene())
|
||||
return nil
|
||||
case wordsJustPressed():
|
||||
g.SetScene(NewWordsScene(g.Assets, m))
|
||||
return nil
|
||||
|
||||
211
internal/game/scene_title.go
Normal file
211
internal/game/scene_title.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"math"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/buildinfo"
|
||||
)
|
||||
|
||||
// TitleScene är titelskärmen: premissen i text och menyn Fortsätt/Starta,
|
||||
// Tutorial, Inställningar, Avsluta (Enter två gånger).
|
||||
type TitleScene struct {
|
||||
sel int
|
||||
t float64
|
||||
confirm bool // Avsluta valt en gång
|
||||
}
|
||||
|
||||
// NewTitleScene öppnar titelskärmen.
|
||||
func NewTitleScene() *TitleScene { return &TitleScene{} }
|
||||
|
||||
const titleItems = 4
|
||||
|
||||
func (s *TitleScene) Update(g *Game) error {
|
||||
s.t += dt
|
||||
if mv := menuMove(); mv != 0 {
|
||||
s.sel = (s.sel + mv + titleItems) % titleItems
|
||||
s.confirm = false
|
||||
}
|
||||
if backJustPressed() {
|
||||
s.confirm = false
|
||||
}
|
||||
if !confirmJustPressed() {
|
||||
return nil
|
||||
}
|
||||
g.Audio.Play(SoundConfirm, 0.6)
|
||||
switch s.sel {
|
||||
case 0:
|
||||
g.SetScene(NewMapScene(g))
|
||||
case 1:
|
||||
g.SetScene(NewTutorialScene(s))
|
||||
case 2:
|
||||
g.SetScene(NewSettingsScene(s))
|
||||
case 3:
|
||||
if s.confirm {
|
||||
return ebiten.Termination
|
||||
}
|
||||
s.confirm = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *TitleScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
a := g.Assets
|
||||
screen.Fill(color.RGBA{24, 20, 37, 255})
|
||||
// Räls längs nederkanten med ett tåg som rullar förbi.
|
||||
railY := ScreenH - 28
|
||||
for x := 0; x < ScreenW; x += TileSize {
|
||||
var op ebiten.DrawImageOptions
|
||||
op.GeoM.Translate(float64(x), float64(railY))
|
||||
screen.DrawImage(a.Rails, &op)
|
||||
}
|
||||
tx := math.Mod(s.t*40, float64(ScreenW+6*TileSize)) - 3*TileSize
|
||||
for i := 0; i < 3; i++ {
|
||||
var op ebiten.DrawImageOptions
|
||||
op.GeoM.Translate(tx+float64(i*TileSize), float64(railY-TileSize+4))
|
||||
screen.DrawImage(a.Train, &op)
|
||||
}
|
||||
a.DrawBig(screen, a.T("game.title"), 16, 8, colKnown)
|
||||
ver := fmt.Sprintf("%s (%s)", buildinfo.Version, buildinfo.Mode)
|
||||
a.Draw(screen, ver, ScreenW-8-a.Width(ver), 10, colDim)
|
||||
y := 30
|
||||
for _, l := range a.wrapLine(plainLine(a.T("title.premise"), colText), ScreenW-32) {
|
||||
a.drawLines(screen, []line{l}, 16, y)
|
||||
y += LineH
|
||||
}
|
||||
start := a.T("menu.continue")
|
||||
if g.Progress.TotalStars() == 0 && len(g.Progress.Levels) == 0 {
|
||||
start = a.T("menu.start")
|
||||
}
|
||||
items := [titleItems]string{start, a.T("title.tutorial"), a.T("menu.settings"), a.T("menu.quit")}
|
||||
y += 6
|
||||
for i, it := range items {
|
||||
col := colText
|
||||
if i == s.sel {
|
||||
vector.DrawFilledRect(screen, 12, float32(y-2), 130, LineH, colBar, false)
|
||||
col = colKnown
|
||||
a.Draw(screen, "▶", 14, y, colKnown)
|
||||
}
|
||||
a.Draw(screen, it, 26, y, col)
|
||||
y += LineH
|
||||
}
|
||||
if s.confirm {
|
||||
a.Draw(screen, a.T("title.quit_confirm"), 150, y-LineH, colWarn)
|
||||
}
|
||||
a.Draw(screen, a.T("title.hint"), 8, ScreenH-12, colDim)
|
||||
}
|
||||
|
||||
// LangScene visas första gången spelet startar: välj språk.
|
||||
type LangScene struct{ sel int }
|
||||
|
||||
var langChoices = []string{"sv", "en"}
|
||||
|
||||
func (s *LangScene) Update(g *Game) error {
|
||||
if mv := menuMove(); mv != 0 {
|
||||
s.sel = (s.sel + mv + len(langChoices)) % len(langChoices)
|
||||
}
|
||||
if confirmJustPressed() {
|
||||
g.State.Settings.Lang = langChoices[s.sel]
|
||||
g.applySettings()
|
||||
g.Save()
|
||||
g.Audio.Play(SoundConfirm, 0.6)
|
||||
g.SetScene(NewTitleScene())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *LangScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
a := g.Assets
|
||||
screen.Fill(color.RGBA{24, 20, 37, 255})
|
||||
a.DrawBig(screen, a.T("game.title"), 16, 10, colKnown)
|
||||
a.Draw(screen, "Välj språk / Choose language", 16, 50, colText)
|
||||
y := 72
|
||||
for i, l := range langChoices {
|
||||
col := colText
|
||||
if i == s.sel {
|
||||
vector.DrawFilledRect(screen, 12, float32(y-2), 130, LineH+2, colBar, false)
|
||||
col = colKnown
|
||||
a.Draw(screen, "▶", 14, y, colKnown)
|
||||
}
|
||||
a.Draw(screen, a.T("lang."+l), 26, y, col)
|
||||
y += LineH + 1
|
||||
}
|
||||
a.Draw(screen, "Pilar + Enter / Arrows + Enter", 16, y+8, colDim)
|
||||
}
|
||||
|
||||
// PauseScene är pausmenyn i en bana (Esc): Fortsätt, Lämna banan,
|
||||
// Inställningar, Avsluta spelet (Enter två gånger). Esc igen fortsätter.
|
||||
type PauseScene struct {
|
||||
from *LevelScene
|
||||
sel int
|
||||
confirm bool
|
||||
}
|
||||
|
||||
const pauseItems = 4
|
||||
|
||||
// NewPauseScene pausar banan.
|
||||
func NewPauseScene(from *LevelScene) *PauseScene { return &PauseScene{from: from} }
|
||||
|
||||
func (s *PauseScene) Update(g *Game) error {
|
||||
if backJustPressed() {
|
||||
g.SetScene(s.from)
|
||||
return nil
|
||||
}
|
||||
if mv := menuMove(); mv != 0 {
|
||||
s.sel = (s.sel + mv + pauseItems) % pauseItems
|
||||
s.confirm = false
|
||||
}
|
||||
if !confirmJustPressed() {
|
||||
return nil
|
||||
}
|
||||
g.Audio.Play(SoundConfirm, 0.6)
|
||||
switch s.sel {
|
||||
case 0:
|
||||
g.SetScene(s.from)
|
||||
case 1:
|
||||
if s.from.endless {
|
||||
g.endEndless()
|
||||
}
|
||||
g.SetScene(NewMapScene(g))
|
||||
case 2:
|
||||
g.SetScene(NewSettingsScene(s))
|
||||
case 3:
|
||||
if s.confirm {
|
||||
return ebiten.Termination
|
||||
}
|
||||
s.confirm = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PauseScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
s.from.Draw(g, screen)
|
||||
a := g.Assets
|
||||
vector.DrawFilledRect(screen, 0, 0, ScreenW, ScreenH, color.RGBA{0, 0, 0, 120}, false)
|
||||
vector.DrawFilledRect(screen, 40, 40, ScreenW-80, 104, colPanel, false)
|
||||
vector.DrawFilledRect(screen, 40, 40, ScreenW-80, 2, colWarn, false)
|
||||
a.DrawBig(screen, a.T("pause.title"), 48, 46, colText)
|
||||
items := [pauseItems]string{a.T("pause.resume"), a.T("pause.leave"), a.T("menu.settings"), a.T("pause.quit")}
|
||||
y := 68
|
||||
for i, it := range items {
|
||||
col := colText
|
||||
if i == s.sel {
|
||||
vector.DrawFilledRect(screen, 44, float32(y-2), ScreenW-88, LineH+2, colBar, false)
|
||||
col = colKnown
|
||||
}
|
||||
a.Draw(screen, it, 50, y, col)
|
||||
y += LineH + 2
|
||||
}
|
||||
msg := a.T("pause.hint")
|
||||
if s.confirm {
|
||||
msg = a.T("pause.quit_confirm")
|
||||
}
|
||||
for _, l := range a.wrapLine(plainLine(msg, colDim), ScreenW-96) {
|
||||
a.drawLines(screen, []line{l}, 48, y+2)
|
||||
y += LineH
|
||||
}
|
||||
}
|
||||
92
internal/game/scene_tutorial.go
Normal file
92
internal/game/scene_tutorial.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"strings"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
||||
)
|
||||
|
||||
// TutorialScene är genomgången av grundmekaniken i fem sidor: målet och
|
||||
// stationen, hur man läser en skylt (med resmålet markerat och resten
|
||||
// nedtonat), kort/biljetter/spärrar, sällskapet och tåget, lägen och
|
||||
// tangenter. ←/→ bläddrar, Esc går tillbaka.
|
||||
type TutorialScene struct {
|
||||
back Scene
|
||||
page int
|
||||
}
|
||||
|
||||
const tutorialPages = 5
|
||||
|
||||
// NewTutorialScene öppnar tutorialen.
|
||||
func NewTutorialScene(back Scene) *TutorialScene { return &TutorialScene{back: back} }
|
||||
|
||||
func (s *TutorialScene) Update(g *Game) error {
|
||||
if backJustPressed() {
|
||||
g.SetScene(s.back)
|
||||
return nil
|
||||
}
|
||||
dx, _ := miniMoveXY()
|
||||
if confirmJustPressed() {
|
||||
dx = 1
|
||||
}
|
||||
if dx != 0 {
|
||||
s.page += dx
|
||||
if s.page >= tutorialPages {
|
||||
g.SetScene(s.back)
|
||||
return nil
|
||||
}
|
||||
if s.page < 0 {
|
||||
s.page = 0
|
||||
}
|
||||
g.Audio.Play(SoundConfirm, 0.4)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *TutorialScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
a := g.Assets
|
||||
screen.Fill(color.RGBA{24, 20, 37, 255})
|
||||
vector.DrawFilledRect(screen, 0, 0, ScreenW, 22, color.RGBA{38, 43, 68, 255}, false)
|
||||
key := fmt.Sprintf("tutorial.p%d", s.page+1)
|
||||
a.DrawBig(screen, a.T(key+".title"), 8, 5, colText)
|
||||
a.Draw(screen, fmt.Sprintf(a.T("tutorial.page"), s.page+1, tutorialPages), ScreenW-70, 8, colDim)
|
||||
y := 28
|
||||
for _, para := range strings.Split(a.T(key), "\n") {
|
||||
for _, l := range a.wrapLine(plainLine(para, colText), ScreenW-16) {
|
||||
a.drawLines(screen, []line{l}, 8, y)
|
||||
y += LineH
|
||||
}
|
||||
y += 3
|
||||
}
|
||||
switch s.page {
|
||||
case 0: // rutlegend med riktiga sprites
|
||||
x := 12
|
||||
for _, t := range []struct {
|
||||
tile level.Tile
|
||||
key string
|
||||
}{{level.Sign, "tile.sign"}, {level.Gate, "tile.gate"}, {level.TicketMachine, "tile.machine"}, {level.Toilet, "tile.toilet"}, {level.Kiosk, "tile.kiosk"}, {level.Platform, "tile.platform"}} {
|
||||
var op ebiten.DrawImageOptions
|
||||
op.GeoM.Scale(2, 2)
|
||||
op.GeoM.Translate(float64(x), float64(y))
|
||||
screen.DrawImage(a.Tiles[t.tile], &op)
|
||||
a.Draw(screen, a.T(t.key), x, y+2*TileSize+2, colDim)
|
||||
x += 52
|
||||
}
|
||||
case 1: // exempelskylten: resmålet markerat, resten nedtonat
|
||||
sample := "14番線 山手線 渋谷方面 → 南東口 改札"
|
||||
body := append([]line{highlightLine(sample, "渋谷", colGood, colDim)}, a.wrapLine(line{{"→ ", colDim}, {a.T("tutorial.p2.sample"), colText}}, ScreenW-28)...)
|
||||
vector.DrawFilledRect(screen, 8, float32(y), ScreenW-16, float32(LineH*len(body)+8), colPanel, false)
|
||||
a.drawLines(screen, body, 14, y+4)
|
||||
y += LineH*len(body) + 12
|
||||
for _, l := range a.wrapLine(plainLine(a.T("tutorial.p2b"), colText), ScreenW-16) {
|
||||
a.drawLines(screen, []line{l}, 8, y)
|
||||
y += LineH
|
||||
}
|
||||
}
|
||||
a.Draw(screen, a.T("tutorial.hint"), 8, ScreenH-12, colDim)
|
||||
}
|
||||
@@ -135,3 +135,26 @@ func (a *Assets) drawLines(dst drawTarget, lines []line, x, y int) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if needle == "" || !strings.Contains(text, needle) {
|
||||
return plainLine(text, dim)
|
||||
}
|
||||
var l line
|
||||
for {
|
||||
i := strings.Index(text, needle)
|
||||
if i < 0 {
|
||||
if text != "" {
|
||||
l = append(l, span{text, dim})
|
||||
}
|
||||
return l
|
||||
}
|
||||
if i > 0 {
|
||||
l = append(l, span{text[:i], dim})
|
||||
}
|
||||
l = append(l, span{needle, hi})
|
||||
text = text[i+len(needle):]
|
||||
}
|
||||
}
|
||||
|
||||
16
internal/game/text_test.go
Normal file
16
internal/game/text_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package game
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestHighlightDestination(t *testing.T) {
|
||||
l := highlightLine("14番線 山手線 渋谷方面 → 南東", "渋谷", colGood, colDim)
|
||||
if len(l) != 3 || l[0].text != "14番線 山手線 " || l[1].text != "渋谷" || l[1].col != colGood || l[2].col != colDim {
|
||||
t.Fatalf("spans = %+v", l)
|
||||
}
|
||||
if l := highlightLine("渋谷 渋谷", "渋谷", colGood, colDim); len(l) != 3 || l[0].col != colGood || l[2].col != colGood {
|
||||
t.Fatalf("flera träffar: %+v", l)
|
||||
}
|
||||
if l := highlightLine("東京方面", "渋谷", colGood, colDim); len(l) != 1 || l[0].col != colDim {
|
||||
t.Fatalf("ingen träff: allt nedtonat, fick %+v", l)
|
||||
}
|
||||
}
|
||||
85
internal/game/tips.go
Normal file
85
internal/game/tips.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||
)
|
||||
|
||||
// Tutorialtips i spelet: visas en gång var, i ordning när villkoret
|
||||
// uppfylls, tills första banan är klarad. {dest} och {ja} byts mot
|
||||
// resmålet i latinska bokstäver respektive japanska tecken.
|
||||
type tipDef struct {
|
||||
key string
|
||||
when func(s *LevelScene) bool
|
||||
}
|
||||
|
||||
var tutorialTips = []tipDef{
|
||||
{"tip.start", func(s *LevelScene) bool { return true }},
|
||||
{"tip.sign", func(s *LevelScene) bool { return s.run.SignHere() != nil }},
|
||||
{"tip.party", func(s *LevelScene) bool { return len(s.run.Party.Urges()) > 0 }},
|
||||
{"tip.gate", func(s *LevelScene) bool { return s.run.DeniedAt >= 0 || s.run.GatePasses > 0 }},
|
||||
{"tip.platform", func(s *LevelScene) bool { tr, _, _ := s.run.PlatformHere(); return tr != nil }},
|
||||
}
|
||||
|
||||
const tipShowSec = 7.0
|
||||
|
||||
type tipState struct {
|
||||
shown map[string]bool
|
||||
current string
|
||||
since float64
|
||||
}
|
||||
|
||||
// tutorialActive: tipsen och skyltmarkeringen gäller tills första banan är
|
||||
// klarad (och aldrig i oändligt läge).
|
||||
func (s *LevelScene) tutorialActive(g *Game) bool {
|
||||
return !s.endless && !g.Progress.Cleared("001-flygplatsen")
|
||||
}
|
||||
|
||||
// updateTips väljer nästa tips som ska visas.
|
||||
func (s *LevelScene) updateTips(g *Game) {
|
||||
if !s.tutorialActive(g) {
|
||||
return
|
||||
}
|
||||
r := s.run
|
||||
t := &s.tips
|
||||
if t.shown == nil {
|
||||
t.shown = map[string]bool{}
|
||||
}
|
||||
if t.current != "" && r.Time-t.since < tipShowSec {
|
||||
return
|
||||
}
|
||||
t.current = ""
|
||||
for _, d := range tutorialTips {
|
||||
if !t.shown[d.key] && d.when(s) {
|
||||
t.shown[d.key] = true
|
||||
t.current, t.since = d.key, r.Time
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tipText fyller i resmålet i ett tips.
|
||||
func (s *LevelScene) tipText(a *Assets, key string) string {
|
||||
d := s.run.Level.Destination
|
||||
return strings.NewReplacer("{dest}", d.Get("romaji"), "{ja}", d.Get("ja")).Replace(a.T(key))
|
||||
}
|
||||
|
||||
// drawTip ritar det aktuella tipset under HUD:en.
|
||||
func (s *LevelScene) drawTip(g *Game, screen *ebiten.Image) {
|
||||
t := &s.tips
|
||||
if t.current == "" || s.run.Mini != nil || s.run.Outcome != 0 {
|
||||
return
|
||||
}
|
||||
a := g.Assets
|
||||
lines := a.wrapLine(highlightLine(s.tipText(a, t.current), s.run.Level.Destination.Get("ja"), colGood, colText), ScreenW-16)
|
||||
h := 6 + LineH*len(lines)
|
||||
y := 26
|
||||
if s.infoBox.Dy() > 0 && s.infoBox.Min.Y < ScreenH/2 {
|
||||
y = s.infoBox.Max.Y + 2 // inforutan ligger upptill: lägg tipset under den
|
||||
}
|
||||
vector.DrawFilledRect(screen, 4, float32(y), ScreenW-8, float32(h), colPanel, false)
|
||||
vector.DrawFilledRect(screen, 4, float32(y), 2, float32(h), colKnown, false)
|
||||
a.drawLines(screen, lines, 10, y+3)
|
||||
}
|
||||
Reference in New Issue
Block a user