Inlärning (framsteg, stjärnor, ordlista) och M4: lämmeltåget
All checks were successful
release / build-release (push) Successful in 1m49s
All checks were successful
release / build-release (push) Successful in 1m49s
Inlärning enligt Björns regler: - lang.Knowledge: framsteg per ord (3 läsningar = känt), språkpoäng, köp av valfria ord (kostnad = tier), Award per stjärna - sim.Run: RunOptions med seed; stjärnor 1–3 mot par (parSec eller kortaste vägen × 0,5 s + 20 s); ≥2 stjärnor lär resmålets ord; att stanna på skylt/perrong ger seedad 35 % chans till framsteg per okänt ord - game: ordlista (L) med kända ord, framsteg och kostnad; stjärnor, poäng och inlärt resmål på resultatskärmen; framstegsnotis; flaggor -points/-seed M4 lämmeltåget: - sim.Party: medlemmar med mat/dryck/toa-mätare, behov under tröskel, seedade händelser var 45 s / antal (toa, törst, hunger, distraktion), fartstraff, toalett (W, 3 s/person) och kiosk (K, 150 ¥/person), följare på ledarens spår - level: rutor W och K, party och parSec i bandatan; banor med sällskap måste ha toalett och kiosk (testas); 001 har 2, 002 har 3 i sällskapet - game: följare med färgton, behovsmarkering över huvudet, mätarpanel, facilitetsrutor med förlopp; genart ritar toalett och kiosk - skriptläget: move stannar exakt på målrutan även när farten inte är bildruteexakt (sällskapets fartstraff), stall-skydd mot väggar - docs/README/wiki/spelbeskrivning uppdaterade Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -24,8 +24,12 @@ const LineH = 12
|
||||
// Ordningen på rutorna i resources/sprites/tiles.png (måste matcha tools/genart).
|
||||
var tileOrder = []level.Tile{
|
||||
level.Wall, level.Floor, level.Spawn, level.Platform, level.Gate, level.TicketMachine, level.Sign,
|
||||
level.Wall, // plats 7 = tågvagnen (laddas separat som Train)
|
||||
level.Toilet, level.Kiosk,
|
||||
}
|
||||
|
||||
const trainIndex = 7
|
||||
|
||||
// Assets är allt som laddats från resources/.
|
||||
type Assets struct {
|
||||
ResDir string
|
||||
@@ -55,14 +59,17 @@ func LoadAssets() (*Assets, error) {
|
||||
sub := func(i int) *ebiten.Image {
|
||||
return tiles.SubImage(image.Rect(i*TileSize, 0, (i+1)*TileSize, TileSize)).(*ebiten.Image)
|
||||
}
|
||||
if w, need := tiles.Bounds().Dx(), TileSize*(len(tileOrder)+1); w < need {
|
||||
if w, need := tiles.Bounds().Dx(), TileSize*len(tileOrder); w < need {
|
||||
return nil, fmt.Errorf("tiles.png är %d px bred, behöver minst %d", w, need)
|
||||
}
|
||||
for i, t := range tileOrder {
|
||||
if i == trainIndex {
|
||||
continue
|
||||
}
|
||||
a.Tiles[t] = sub(i)
|
||||
}
|
||||
a.Tiles[level.WrongPlatform] = a.Tiles[level.Platform] // samma utseende; tåget avgör
|
||||
a.Train = sub(len(tileOrder))
|
||||
a.Train = sub(trainIndex)
|
||||
|
||||
player, err := loadImage("sprites", "player.png")
|
||||
if err != nil {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/lang"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/rng"
|
||||
)
|
||||
|
||||
// Intern upplösning. Allt ritas i denna storlek och skalas sedan upp med
|
||||
@@ -29,6 +30,7 @@ type Scene interface {
|
||||
type Game struct {
|
||||
Assets *Assets
|
||||
Know *lang.Knowledge // spelarens japanska – följer med mellan banor (sparas i M8)
|
||||
fixedSeed uint64 // >0: alla banor startar med denna seed (tester/skärmdumpar)
|
||||
scene Scene
|
||||
next Scene
|
||||
smokeLeft int // >0: avsluta automatiskt efter så många uppdateringar
|
||||
@@ -42,6 +44,8 @@ type Options struct {
|
||||
SmokeFrames int // >0 avslutar efter N uppdateringar (smoke-test)
|
||||
Script string // skriptade steg för skärmdumpar/tester, se script.go
|
||||
KnownWords []string // ord spelaren kan från start (för tester/skärmdumpar)
|
||||
Points int // språkpoäng från start (för tester/skärmdumpar)
|
||||
Seed uint64 // >0: fast seed för banorna (för tester/skärmdumpar)
|
||||
}
|
||||
|
||||
// New laddar resurser och bygger spelet.
|
||||
@@ -54,13 +58,15 @@ func New(opt Options) (*Game, error) {
|
||||
for _, w := range opt.KnownWords {
|
||||
g.Know.Learn(w)
|
||||
}
|
||||
g.Know.Points = opt.Points
|
||||
g.fixedSeed = opt.Seed
|
||||
if opt.Script != "" {
|
||||
if g.script, err = parseScript(opt.Script); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if opt.LevelID != "" {
|
||||
s, err := NewLevelScene(a, opt.LevelID, g.Know)
|
||||
s, err := NewLevelScene(a, opt.LevelID, g.Know, g.seed())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -104,3 +110,11 @@ func (g *Game) Draw(screen *ebiten.Image) {
|
||||
|
||||
// Layout talar om för Ebitengine vilken intern upplösning vi ritar i.
|
||||
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { return ScreenW, ScreenH }
|
||||
|
||||
// seed ger seeden för en ny banomgång: fast om -seed angetts, annars färsk.
|
||||
func (g *Game) seed() rng.Seed {
|
||||
if g.fixedSeed > 0 {
|
||||
return rng.FromInt(g.fixedSeed)
|
||||
}
|
||||
return rng.NewSeed()
|
||||
}
|
||||
|
||||
@@ -76,6 +76,11 @@ func helpJustPressed() bool {
|
||||
return virtual.pressed["h"] || inpututil.IsKeyJustPressed(ebiten.KeyH) || gamepadJustPressed(ebiten.StandardGamepadButtonRightTop)
|
||||
}
|
||||
|
||||
// wordsJustPressed: L = ordlistan/språkträdet.
|
||||
func wordsJustPressed() bool {
|
||||
return virtual.pressed["l"] || inpututil.IsKeyJustPressed(ebiten.KeyL) || gamepadJustPressed(ebiten.StandardGamepadButtonLeftRight)
|
||||
}
|
||||
|
||||
// menuMove returnerar -1/+1 när användaren just tryckte upp/ner i en meny.
|
||||
func menuMove() int {
|
||||
switch {
|
||||
|
||||
@@ -5,22 +5,22 @@ import (
|
||||
"image/color"
|
||||
"math"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"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/rng"
|
||||
"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
|
||||
deniedShowSec = 2.0
|
||||
learnedShowSec = 3.0
|
||||
progressedShowSec = 2.5
|
||||
)
|
||||
|
||||
// LevelScene är själva spelandet av en bana. All spellogik ligger i sim.Run;
|
||||
@@ -33,15 +33,15 @@ type LevelScene struct {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// spelarens japanska, som delas mellan banor; seed styr omgångens slump.
|
||||
func NewLevelScene(a *Assets, id string, know *lang.Knowledge, seed rng.Seed) (*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),
|
||||
run: sim.NewRun(lvl, sim.RunOptions{Dict: a.Dict, Know: know, Seed: seed}),
|
||||
cam: sim.NewCamera(ScreenW, ScreenH, float64(lvl.Width()*TileSize), float64(lvl.Height()*TileSize)),
|
||||
}
|
||||
s.followPlayer()
|
||||
@@ -51,6 +51,9 @@ func NewLevelScene(a *Assets, id string, know *lang.Knowledge) (*LevelScene, err
|
||||
// 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 }
|
||||
|
||||
// PlayerMoving rapporterar om spelaren är mellan två rutor (skriptläget).
|
||||
func (s *LevelScene) PlayerMoving() bool { return s.run.Player.Moving() }
|
||||
|
||||
// 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()
|
||||
@@ -131,14 +134,73 @@ func (s *LevelScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
}
|
||||
}
|
||||
|
||||
px, py := s.run.Player.WorldPos(TileSize)
|
||||
// Följarna ritas bakifrån så att ledaren hamnar överst.
|
||||
leader := s.run.Player
|
||||
followers := s.run.Party.Followers()
|
||||
for i := len(followers); i >= 1; i-- {
|
||||
from, to := s.run.Party.FollowerTiles(i)
|
||||
fx := float64(from.X*TileSize) + float64((to.X-from.X)*TileSize)*leader.Progress
|
||||
fy := float64(from.Y*TileSize) + float64((to.Y-from.Y)*TileSize)*leader.Progress
|
||||
facing := sim.Down
|
||||
switch {
|
||||
case to.X > from.X:
|
||||
facing = sim.Right
|
||||
case to.X < from.X:
|
||||
facing = sim.Left
|
||||
case to.Y < from.Y:
|
||||
facing = sim.Up
|
||||
}
|
||||
op.GeoM.Reset()
|
||||
op.GeoM.Translate(ox+math.Round(fx), oy+math.Round(fy))
|
||||
op.ColorScale.Reset()
|
||||
t := memberTints[i%len(memberTints)]
|
||||
op.ColorScale.Scale(t[0], t[1], t[2], 1)
|
||||
screen.DrawImage(g.Assets.PlayerFrame(facing, leader.Moving() && from != to, s.animT), &op)
|
||||
op.ColorScale.Reset()
|
||||
s.drawUrge(g, screen, followers[i-1], int(ox+math.Round(fx)), int(oy+math.Round(fy)))
|
||||
}
|
||||
|
||||
px, py := leader.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)
|
||||
screen.DrawImage(g.Assets.PlayerFrame(leader.Facing, leader.Moving(), s.animT), &op)
|
||||
s.drawUrge(g, screen, s.run.Party.Members[0], int(ox+math.Round(px)), int(oy+math.Round(py)))
|
||||
|
||||
s.drawHUD(g, screen)
|
||||
}
|
||||
|
||||
// memberTints färgar följarnas sprites tills de har egna.
|
||||
var memberTints = [][3]float32{{1, 1, 1}, {1, 0.75, 0.55}, {0.7, 0.95, 0.75}, {0.8, 0.7, 1}, {1, 0.95, 0.55}}
|
||||
|
||||
// urgeMark är tecknet som visas över huvudet på någon med ett behov.
|
||||
func urgeMark(u sim.Urge) (string, color.Color) {
|
||||
switch u {
|
||||
case sim.NeedToilet:
|
||||
return "WC", colBad
|
||||
case sim.Thirsty:
|
||||
return "飲", colEnglish
|
||||
case sim.Hungry:
|
||||
return "食", colWarn
|
||||
case sim.Distracted:
|
||||
return "?", colDim
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (s *LevelScene) drawUrge(g *Game, screen *ebiten.Image, m *sim.Member, x, y int) {
|
||||
mark, col := urgeMark(m.Urge)
|
||||
if mark == "" {
|
||||
if m.Meters.Min() <= 0 {
|
||||
mark, col = "!!", colBad
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
w := g.Assets.Width(mark)
|
||||
vector.DrawFilledRect(screen, float32(x+8-w/2-1), float32(y-11), float32(w+2), 11, colPanel, false)
|
||||
g.Assets.Draw(screen, mark, x+8-w/2, y-12, col)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -152,9 +214,9 @@ func (s *LevelScene) drawHUD(g *Game, screen *ebiten.Image) {
|
||||
}
|
||||
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)
|
||||
a.Draw(screen, fmt.Sprintf("%s %3.0fs %s %d %s %d¥ %s Lv%d·%d", 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()), 4, LineH, colDim)
|
||||
s.drawMeters(g, screen)
|
||||
|
||||
if r.Outcome != sim.Ongoing {
|
||||
return
|
||||
@@ -164,6 +226,16 @@ func (s *LevelScene) drawHUD(g *Game, screen *ebiten.Image) {
|
||||
lines = append(lines, plainLine(fmt.Sprintf("%s: %s = %s (%s)", a.T("hud.learned"), r.Learned,
|
||||
entry.Translation(UILang), entry.Romaji), colKnown))
|
||||
}
|
||||
if len(r.Progressed) > 0 && r.Time-r.ProgressedAt < progressedShowSec {
|
||||
lines = append(lines, a.wrapLine(plainLine(a.T("hud.progress")+": "+strings.Join(r.Progressed, ", "), colWarn), ScreenW-8)...)
|
||||
}
|
||||
if urges := r.Party.Urges(); len(urges) > 0 {
|
||||
var parts []string
|
||||
for _, m := range urges {
|
||||
parts = append(parts, m.Name+": "+a.T("urge."+m.Urge.String()))
|
||||
}
|
||||
lines = append(lines, a.wrapLine(plainLine(strings.Join(parts, " "), colWarn), ScreenW-8)...)
|
||||
}
|
||||
if len(lines) == 0 {
|
||||
return
|
||||
}
|
||||
@@ -182,9 +254,13 @@ func (s *LevelScene) drawHUD(g *Game, screen *ebiten.Image) {
|
||||
vector.DrawFilledRect(screen, 0, float32(y), ScreenW, 2, accent, false)
|
||||
a.drawLines(screen, lines, 4, y+4)
|
||||
if bar {
|
||||
p := r.HelpProgress()
|
||||
if r.Facility.Active {
|
||||
p = r.FacilityProgress()
|
||||
}
|
||||
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)
|
||||
vector.DrawFilledRect(screen, 4, by, float32((ScreenW-8)*p), 4, colKnown, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,8 +275,7 @@ 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.
|
||||
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)
|
||||
rd := lang.Render(a.Dict, r.Know, tr.BoardText(), 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 {
|
||||
@@ -240,8 +315,46 @@ func (s *LevelScene) infoLines(a *Assets) (lines []line, accent color.Color, bar
|
||||
return lines, accent, bar
|
||||
}
|
||||
|
||||
switch lvl.At(tile.X, tile.Y) {
|
||||
case level.Toilet:
|
||||
if r.Facility.Active {
|
||||
return []line{plainLine(a.T("hud.toilet_busy"), colDim)}, colGood, true
|
||||
}
|
||||
if n := len(r.Party.NeedsToilet()); n > 0 {
|
||||
return []line{plainLine(fmt.Sprintf(a.T("hud.toilet_prompt"), n, sim.ToiletSecPerMember*float64(n)), colGood)}, colGood, false
|
||||
}
|
||||
return []line{plainLine(a.T("hud.toilet_none"), colDim)}, accent, false
|
||||
case level.Kiosk:
|
||||
if n := len(r.Party.NeedsKiosk()); n > 0 {
|
||||
return []line{plainLine(fmt.Sprintf(a.T("hud.kiosk_prompt"), n, sim.KioskPriceYen*n), colGood)}, colGood, false
|
||||
}
|
||||
return []line{plainLine(a.T("hud.kiosk_none"), colDim)}, accent, false
|
||||
}
|
||||
if r.DeniedAt >= 0 && r.Time-r.DeniedAt < deniedShowSec {
|
||||
return []line{plainLine(a.T("hud.gate_denied"), colBad)}, colBad, false
|
||||
}
|
||||
return nil, accent, false
|
||||
}
|
||||
|
||||
// drawMeters ritar sällskapets mat/dryck/toa-mätare uppe till höger.
|
||||
func (s *LevelScene) drawMeters(g *Game, screen *ebiten.Image) {
|
||||
members := s.run.Party.Members
|
||||
const w, gap = 14, 4
|
||||
x0 := ScreenW - 4 - len(members)*(w+gap)
|
||||
for i, m := range members {
|
||||
x := float32(x0 + i*(w+gap))
|
||||
g.Assets.Draw(screen, string([]rune(m.Name)[:1]), int(x)+3, 0, colDim)
|
||||
for j, v := range []struct {
|
||||
val float64
|
||||
col color.Color
|
||||
}{{m.Meters.Food, colWarn}, {m.Meters.Drink, colEnglish}, {m.Meters.Toilet, colGood}} {
|
||||
y := float32(13 + j*3)
|
||||
vector.DrawFilledRect(screen, x, y, w, 2, colBar, false)
|
||||
c := v.col
|
||||
if v.val < sim.UrgeThreshold {
|
||||
c = colBad
|
||||
}
|
||||
vector.DrawFilledRect(screen, x, y, float32(w*v.val/100), 2, c, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,10 +54,14 @@ func (m *MenuScene) Update(g *Game) error {
|
||||
if backJustPressed() {
|
||||
return ebiten.Termination
|
||||
}
|
||||
if wordsJustPressed() {
|
||||
g.SetScene(NewWordsScene(g.Assets, m))
|
||||
return nil
|
||||
}
|
||||
if n := len(m.entries); n > 0 {
|
||||
m.sel = (m.sel + menuMove() + n) % n
|
||||
if confirmJustPressed() {
|
||||
s, err := NewLevelScene(g.Assets, m.entries[m.sel].id, g.Know)
|
||||
s, err := NewLevelScene(g.Assets, m.entries[m.sel].id, g.Know, g.seed())
|
||||
if err != nil {
|
||||
m.err = err
|
||||
return nil
|
||||
@@ -87,7 +91,8 @@ func (m *MenuScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
if m.err != nil {
|
||||
a.Draw(screen, "FEL: "+m.err.Error(), 8, ScreenH-40, colBad)
|
||||
}
|
||||
a.Draw(screen, fmt.Sprintf("%s Lv%d %d %s", a.T("hud.japanese"), g.Know.Level(), g.Know.Count(), a.T("hud.words")), 8, ScreenH-38, colDim)
|
||||
a.Draw(screen, fmt.Sprintf("%s Lv%d %d %s %s %d", a.T("hud.japanese"), g.Know.Level(), g.Know.Count(), a.T("hud.words"),
|
||||
a.T("words.points"), g.Know.Points), 8, ScreenH-38, colDim)
|
||||
a.Draw(screen, a.T("menu.hint"), 8, ScreenH-26, colDim)
|
||||
a.Draw(screen, fmt.Sprintf("%s (%s)", buildinfo.Version, buildinfo.Mode), 8, ScreenH-13, colDim)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package game
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||
@@ -19,8 +20,10 @@ func NewResultScene(from *LevelScene) *ResultScene { return &ResultScene{from: f
|
||||
|
||||
func (r *ResultScene) Update(g *Game) error {
|
||||
switch {
|
||||
case wordsJustPressed():
|
||||
g.SetScene(NewWordsScene(g.Assets, r))
|
||||
case inpututil_R():
|
||||
s, err := NewLevelScene(g.Assets, r.from.id, g.Know)
|
||||
s, err := NewLevelScene(g.Assets, r.from.id, g.Know, g.seed())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -47,6 +50,10 @@ func (r *ResultScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
}
|
||||
vector.DrawFilledRect(screen, 20, 36, ScreenW-40, 3, accent, false)
|
||||
a.DrawBig(screen, headline, 28, 44, accent)
|
||||
if run.Outcome == sim.Won {
|
||||
stars := strings.Repeat("★", run.Stars) + strings.Repeat("☆", 3-run.Stars)
|
||||
a.DrawBig(screen, stars, ScreenW-28-a.Width(stars)-8, 44, colWarn)
|
||||
}
|
||||
|
||||
y := 62
|
||||
if tr := run.Boarded; tr != nil {
|
||||
@@ -64,10 +71,18 @@ func (r *ResultScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
}
|
||||
}
|
||||
y += 4
|
||||
a.Draw(screen, fmt.Sprintf("%s %.1fs %s %d %s %d %s %d¥", a.T("hud.time"), run.Time,
|
||||
a.Draw(screen, fmt.Sprintf("%s %.1fs (%s %.0fs) %s %d %s %d %s %d¥", a.T("hud.time"), run.Time, a.T("result.par"), run.ParSec(),
|
||||
a.T("hud.steps"), run.Steps, a.T("result.gates"), run.GatePasses, a.T("result.card"), run.Card.Balance), 28, y, colText)
|
||||
y += LineH
|
||||
a.Draw(screen, fmt.Sprintf("%s Lv%d %d %s (%d %s)", a.T("hud.japanese"), run.Know.Level(), run.Know.Count(),
|
||||
a.T("hud.words"), run.Know.Helps, a.T("result.helps")), 28, y, colDim)
|
||||
if run.Outcome == sim.Won {
|
||||
msg := fmt.Sprintf("+%d %s", run.PointsEarned, a.T("words.points"))
|
||||
if len(run.LearnedDest) > 0 {
|
||||
msg += " " + a.T("hud.learned") + ": " + strings.Join(run.LearnedDest, " ") + " (" + a.T("result.destination") + ")"
|
||||
}
|
||||
a.Draw(screen, msg, 28, y, colKnown)
|
||||
y += LineH
|
||||
}
|
||||
a.Draw(screen, fmt.Sprintf("%s Lv%d %d %s %s %d (%d %s)", a.T("hud.japanese"), run.Know.Level(), run.Know.Count(),
|
||||
a.T("hud.words"), a.T("words.points"), run.Know.Points, run.Know.Helps, a.T("result.helps")), 28, y, colDim)
|
||||
a.Draw(screen, a.T("result.hint"), 28, 130, colDim)
|
||||
}
|
||||
|
||||
112
internal/game/scene_words.go
Normal file
112
internal/game/scene_words.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/lang"
|
||||
)
|
||||
|
||||
// WordsScene är ordlistan/språkträdet: alla ord i ordboken, vad spelaren
|
||||
// kan, framsteg på resten, och möjligheten att lägga språkpoäng på valfria
|
||||
// ord. Blir ett riktigt träd med skills i M8.
|
||||
type WordsScene struct {
|
||||
back Scene
|
||||
entries []*lang.Entry
|
||||
sel int
|
||||
top int
|
||||
msg string
|
||||
}
|
||||
|
||||
const wordsRows = 9
|
||||
|
||||
// NewWordsScene listar ordboken sorterad på tier och ord.
|
||||
func NewWordsScene(a *Assets, back Scene) *WordsScene {
|
||||
var es []*lang.Entry
|
||||
for _, w := range a.Dict.Words() {
|
||||
es = append(es, a.Dict.Lookup(w))
|
||||
}
|
||||
sort.Slice(es, func(i, j int) bool {
|
||||
if es[i].Tier != es[j].Tier {
|
||||
return es[i].Tier < es[j].Tier
|
||||
}
|
||||
return es[i].Word < es[j].Word
|
||||
})
|
||||
return &WordsScene{back: back, entries: es}
|
||||
}
|
||||
|
||||
func (w *WordsScene) Update(g *Game) error {
|
||||
if backJustPressed() || wordsJustPressed() {
|
||||
g.SetScene(w.back)
|
||||
return nil
|
||||
}
|
||||
if n := len(w.entries); n > 0 {
|
||||
w.sel = (w.sel + menuMove() + n) % n
|
||||
if w.sel < w.top {
|
||||
w.top = w.sel
|
||||
}
|
||||
if w.sel >= w.top+wordsRows {
|
||||
w.top = w.sel - wordsRows + 1
|
||||
}
|
||||
if confirmJustPressed() {
|
||||
e := w.entries[w.sel]
|
||||
switch {
|
||||
case g.Know.Knows(e.Word):
|
||||
w.msg = g.Assets.T("words.already")
|
||||
case g.Know.Buy(e):
|
||||
w.msg = fmt.Sprintf(g.Assets.T("words.bought"), e.Word, e.Translation(UILang))
|
||||
default:
|
||||
w.msg = fmt.Sprintf(g.Assets.T("words.too_expensive"), lang.WordCost(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *WordsScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
a := g.Assets
|
||||
k := g.Know
|
||||
screen.Fill(color.RGBA{24, 20, 37, 255})
|
||||
vector.DrawFilledRect(screen, 0, 0, ScreenW, 22, color.RGBA{38, 43, 68, 255}, false)
|
||||
a.DrawBig(screen, a.T("words.title"), 8, 5, colText)
|
||||
hdr := fmt.Sprintf("%s %d %s Lv%d %d/%d %s", a.T("words.points"), k.Points, a.T("hud.japanese"),
|
||||
k.Level(), k.Count(), len(w.entries), a.T("hud.words"))
|
||||
a.Draw(screen, hdr, ScreenW-8-a.Width(hdr), 6, colKnown)
|
||||
|
||||
y0 := 30
|
||||
for i := w.top; i < len(w.entries) && i < w.top+wordsRows; i++ {
|
||||
e := w.entries[i]
|
||||
y := y0 + (i-w.top)*LineH
|
||||
if i == w.sel {
|
||||
vector.DrawFilledRect(screen, 4, float32(y-1), ScreenW-8, LineH, colBar, false)
|
||||
}
|
||||
status, scol := strings.Repeat("●", lang.WordCost(e)), colDim
|
||||
if k.CanBuy(e) {
|
||||
scol = colKnown
|
||||
}
|
||||
wcol := colText
|
||||
switch {
|
||||
case k.Knows(e.Word):
|
||||
status, scol, wcol = "✓", colGood, colKnown
|
||||
case k.ProgressOf(e.Word) > 0:
|
||||
status, scol = fmt.Sprintf("%d/%d", k.ProgressOf(e.Word), lang.ProgressToLearn), colWarn
|
||||
}
|
||||
a.Draw(screen, e.Word, 8, y, wcol)
|
||||
a.Draw(screen, e.Romaji, 92, y, colDim)
|
||||
a.Draw(screen, e.Translation(UILang), 180, y, colText)
|
||||
a.Draw(screen, status, ScreenW-8-a.Width(status), y, scol)
|
||||
}
|
||||
// rullningsindikator
|
||||
if len(w.entries) > wordsRows {
|
||||
a.Draw(screen, fmt.Sprintf("%d-%d / %d", w.top+1, min(len(w.entries), w.top+wordsRows), len(w.entries)), 8, ScreenH-38, colDim)
|
||||
}
|
||||
if w.msg != "" {
|
||||
a.Draw(screen, w.msg, 8, ScreenH-26, colKnown)
|
||||
}
|
||||
a.Draw(screen, a.T("words.hint"), 8, ScreenH-13, colDim)
|
||||
}
|
||||
@@ -20,8 +20,8 @@ import (
|
||||
//
|
||||
// wait N vänta N bildrutor
|
||||
// waittrain vänta tills tåget vid perrongen spelaren står på är inne
|
||||
// press KEY tryck enter | esc | r | up | down en bildruta
|
||||
// move DIR N gå N rutor åt up | down | left | right (släpper när rutorna är avklarade)
|
||||
// press KEY tryck enter | esc | r | h | l | up | down en bildruta
|
||||
// move DIR N gå N rutor åt up | down | left | right (stannar exakt där)
|
||||
// shot PATH spara aktuell bild som PNG (intern upplösning × shotScale)
|
||||
// quit avsluta spelet
|
||||
//
|
||||
@@ -29,18 +29,22 @@ import (
|
||||
|
||||
const shotScale = 3
|
||||
|
||||
// moveStallFrames: så många bildrutor utan ny ruta räknas som "vägg" och
|
||||
// avslutar ett move-steg (annars skulle skriptet hänga för evigt).
|
||||
const moveStallFrames = 120
|
||||
|
||||
type scriptStep struct {
|
||||
cmd string
|
||||
arg string
|
||||
n int
|
||||
dir sim.Dir
|
||||
done bool
|
||||
cmd string
|
||||
arg string
|
||||
n int
|
||||
dir sim.Dir
|
||||
}
|
||||
|
||||
type script struct {
|
||||
steps []scriptStep
|
||||
i int
|
||||
counter int
|
||||
stall int
|
||||
lastPos level.Point
|
||||
havePos bool
|
||||
}
|
||||
@@ -108,7 +112,10 @@ func parseDir(s string) sim.Dir {
|
||||
}
|
||||
|
||||
// playerTiler implementeras av scener som har en spelare i rutnätet.
|
||||
type playerTiler interface{ PlayerTile() level.Point }
|
||||
type playerTiler interface {
|
||||
PlayerTile() level.Point
|
||||
PlayerMoving() bool
|
||||
}
|
||||
|
||||
// trainWaiter implementeras av scener där man kan vänta på ett tåg.
|
||||
type trainWaiter interface{ TrainHere() bool }
|
||||
@@ -139,26 +146,39 @@ func (s *script) tick(g *Game, frame *ebiten.Image) error {
|
||||
}
|
||||
cur := pt.PlayerTile()
|
||||
if !s.havePos {
|
||||
s.lastPos, s.havePos = cur, true
|
||||
s.lastPos, s.havePos, s.stall = cur, true, 0
|
||||
}
|
||||
if cur != s.lastPos {
|
||||
s.lastPos = cur
|
||||
s.counter++
|
||||
s.stall = 0
|
||||
} else {
|
||||
s.stall++
|
||||
}
|
||||
if s.counter >= st.n {
|
||||
if s.counter >= st.n && !pt.PlayerMoving() {
|
||||
s.advance()
|
||||
return nil
|
||||
}
|
||||
virtual.held = st.dir
|
||||
if s.stall > moveStallFrames { // blockerad av vägg: ge upp steget
|
||||
s.advance()
|
||||
return nil
|
||||
}
|
||||
// Håll riktningen tills sista rutan har påbörjats; släpp sedan så att
|
||||
// figuren stannar exakt där. Hålls tangenten kvar i ankomstbildrutan
|
||||
// fortsätter figuren annars en ruta till när farten (t.ex. med
|
||||
// sällskapets fartstraff) inte är bildruteexakt.
|
||||
if s.counter < st.n-1 || (s.counter == st.n-1 && !pt.PlayerMoving()) {
|
||||
virtual.held = st.dir
|
||||
}
|
||||
case "waittrain":
|
||||
if tw, ok := g.scene.(trainWaiter); !ok || tw.TrainHere() {
|
||||
s.advance()
|
||||
}
|
||||
case "shot":
|
||||
if err := saveShot(frame, st.arg); err != nil {
|
||||
return err
|
||||
}
|
||||
s.advance()
|
||||
case "waittrain":
|
||||
if tw, ok := g.scene.(trainWaiter); !ok || tw.TrainHere() {
|
||||
s.advance()
|
||||
}
|
||||
case "quit":
|
||||
return ebiten.Termination
|
||||
}
|
||||
@@ -168,6 +188,7 @@ func (s *script) tick(g *Game, frame *ebiten.Image) error {
|
||||
func (s *script) advance() {
|
||||
s.i++
|
||||
s.counter = 0
|
||||
s.stall = 0
|
||||
s.havePos = false
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user