M9: navigeringslägen, linjenät och terminal
All checks were successful
release / build-release (push) Successful in 1m34s
All checks were successful
release / build-release (push) Successful in 1m34s
internal/nav med resources/data/network.json (76 stationer, 11 linjer, Yamanote som ringlinje). Tåg bedöms från lineId + terminus mot banans station/to: rätt, fel håll, för långt bort (fler än 12 stationer) eller ingen trafik; correct räknas ut för nätkopplade banor, 1-4 rätta tåg. Tågresan visar linjens riktiga stationer. Nya förlustorsaker "Fel håll" och "För långt bort". Listläge (nästa tåg nere till vänster) och skyltläge (+50 % poäng) växlas med N på bankartan och sparas; -mode. Terminalen: biljettautomaten visar linjetavlan på nätbanor. Perronger P p Q q R r. Bana 006 Ikebukuro med fyra perronger. Banorna 001-005 kopplade till nätet. Skript: waitover och pick calm; poängraden radbryts; skärmdumpar uppdaterade. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -80,7 +80,9 @@ func LoadAssets() (*Assets, error) {
|
||||
}
|
||||
a.Tiles[t] = sub(i)
|
||||
}
|
||||
a.Tiles[level.WrongPlatform] = a.Tiles[level.Platform] // samma utseende; tåget avgör
|
||||
for _, t := range level.PlatformTiles[1:] {
|
||||
a.Tiles[t] = a.Tiles[level.Platform] // samma utseende; tåget avgör
|
||||
}
|
||||
a.Train = sub(trainIndex)
|
||||
a.Rails = sub(railsIndex)
|
||||
for i := 0; i < posterCount; i++ {
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"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/nav"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/progress"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/res"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/rng"
|
||||
@@ -45,6 +46,7 @@ type Game struct {
|
||||
Catalog *progress.Catalog
|
||||
Levels []*level.Level // alla banor, sorterade på id
|
||||
Audio *Audio // nil = tyst
|
||||
Mode nav.Mode // navigeringsläge (= State.NavMode), växlas på bankartan
|
||||
|
||||
savePath string // "" = spara inte
|
||||
fixedSeed uint64 // >0: alla banor startar med denna seed (tester/skärmdumpar)
|
||||
@@ -69,6 +71,7 @@ type Options struct {
|
||||
Mute bool // inget ljud
|
||||
SavePath string // sparfil ("" = standardplatsen)
|
||||
NoSave bool // läs sparfilen men skriv aldrig
|
||||
Mode string // navigeringsläge från start: "list" eller "signs" (tomt = sparfilens)
|
||||
}
|
||||
|
||||
// New laddar resurser, sparfil och bygger spelet.
|
||||
@@ -109,6 +112,10 @@ func New(opt Options) (*Game, error) {
|
||||
}
|
||||
g.State = st
|
||||
g.Know, g.Wallet, g.Profile, g.Progress = st.Know, st.Wallet, st.Profile, st.Progress
|
||||
g.Mode = nav.ParseMode(st.NavMode)
|
||||
if opt.Mode != "" {
|
||||
g.Mode = nav.ParseMode(opt.Mode)
|
||||
}
|
||||
if !opt.NoSave {
|
||||
g.savePath = path
|
||||
}
|
||||
@@ -233,10 +240,21 @@ func (g *Game) runOptions(id string, seed rng.Seed) sim.RunOptions {
|
||||
return sim.RunOptions{
|
||||
Dict: g.Assets.Dict, Know: g.Know, Wallet: g.Wallet, Debuffs: g.Profile.Debuffs, Seed: seed,
|
||||
Extra: eff.Stats, FogRadius: eff.FogRadius, Landmarks: eff.Landmarks, FreeGates: eff.FreeGates,
|
||||
RevealAll: eff.Reveal[id], InfiniteBattery: eff.Battery,
|
||||
RevealAll: eff.Reveal[id], InfiniteBattery: eff.Battery, Mode: g.Mode,
|
||||
}
|
||||
}
|
||||
|
||||
// toggleMode växlar navigeringsläge och sparar.
|
||||
func (g *Game) toggleMode() {
|
||||
if g.Mode == nav.List {
|
||||
g.Mode = nav.Signs
|
||||
} else {
|
||||
g.Mode = nav.List
|
||||
}
|
||||
g.State.NavMode = g.Mode.String()
|
||||
g.Save()
|
||||
}
|
||||
|
||||
// startLevel startar (eller återupptar) en bana och sparar seeden så att en
|
||||
// laddad omgång ger samma slump.
|
||||
func (g *Game) startLevel(id string) (*LevelScene, error) {
|
||||
|
||||
@@ -126,6 +126,9 @@ func shopJustPressed() bool { return virtual.pressed["b"] || inpututil.IsKeyJust
|
||||
// phoneJustPressed: P = kortet i telefonen av/på (meny).
|
||||
func phoneJustPressed() bool { return virtual.pressed["p"] || inpututil.IsKeyJustPressed(ebiten.KeyP) }
|
||||
|
||||
// modeJustPressed: N växlar navigeringsläge (lista/skyltar) på bankartan.
|
||||
func modeJustPressed() bool { return virtual.pressed["n"] || inpututil.IsKeyJustPressed(ebiten.KeyN) }
|
||||
|
||||
// mapJustPressed: M eller gamepadens vänstra mittknapp (Back/Select) = minikartan av/på.
|
||||
func mapJustPressed() bool {
|
||||
return virtual.pressed["m"] || inpututil.IsKeyJustPressed(ebiten.KeyM) || gamepadJustPressed(ebiten.StandardGamepadButtonCenterLeft)
|
||||
|
||||
@@ -24,6 +24,10 @@ var minimapColors = map[level.Tile]color.RGBA{
|
||||
level.Spawn: {99, 199, 77, 255},
|
||||
level.Platform: {254, 174, 52, 255},
|
||||
level.WrongPlatform: {254, 174, 52, 255},
|
||||
level.Platform2: {254, 174, 52, 255},
|
||||
level.Platform3: {254, 174, 52, 255},
|
||||
level.Platform4: {254, 174, 52, 255},
|
||||
level.Platform5: {254, 174, 52, 255},
|
||||
level.Gate: {44, 232, 245, 255},
|
||||
level.TicketMachine: {247, 118, 34, 255},
|
||||
level.Sign: {90, 140, 230, 255},
|
||||
@@ -111,7 +115,7 @@ func (s *LevelScene) drawMinimap(g *Game, screen *ebiten.Image, box image.Rectan
|
||||
}
|
||||
t := l.At(p)
|
||||
c := minimapColors[t]
|
||||
if (t == level.Platform || t == level.WrongPlatform) && r.TrainVisible(t) {
|
||||
if t.IsPlatform() && r.TrainVisible(t) {
|
||||
c = color.RGBA{255, 255, 255, 255} // tåget är inne
|
||||
}
|
||||
vector.DrawFilledRect(screen, float32(x0+x*sc), float32(y0+y*sc), float32(sc), float32(sc), c, false)
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"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/minigame"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/nav"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/rng"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/sim"
|
||||
)
|
||||
@@ -104,6 +105,31 @@ func (s *LevelScene) RideAtDest() bool {
|
||||
return !ok || g.Index == g.Dest
|
||||
}
|
||||
|
||||
// drawModeHint ritar navigeringslägets ruta nere till vänster när inforutan
|
||||
// är tom: listläget säger vilket tåg man ska ta, skyltläget påminner om att
|
||||
// man får lista ut det själv (och om bonusen).
|
||||
func (s *LevelScene) drawModeHint(a *Assets, screen *ebiten.Image) {
|
||||
r := s.run
|
||||
txt, col := "", colGood
|
||||
if r.Mode == nav.Signs {
|
||||
txt, col = a.T("hud.mode_signs"), colWarn
|
||||
} else if next := r.NextTrains(); len(next) > 0 {
|
||||
txt = fmt.Sprintf("%s %s %s %s", a.T("hud.take"), strings.ToLower(a.T("hud.track")), next[0].Track, next[0].Line.Get("romaji"))
|
||||
if len(next) > 1 {
|
||||
txt += fmt.Sprintf(" (+%d)", len(next)-1)
|
||||
}
|
||||
}
|
||||
if txt == "" {
|
||||
return
|
||||
}
|
||||
y := ScreenH - LineH - 6
|
||||
vector.DrawFilledRect(screen, 0, float32(y), float32(a.Width(txt)+8), float32(LineH+6), colPanel, false)
|
||||
a.Draw(screen, txt, 4, y+3, col)
|
||||
}
|
||||
|
||||
// LevelOver rapporterar om banan är avgjord (skriptläget: "waitover").
|
||||
func (s *LevelScene) LevelOver() bool { return s.run.Outcome != sim.Ongoing }
|
||||
|
||||
func (s *LevelScene) followPlayer() {
|
||||
x, y := s.run.Player.WorldPos(TileSize)
|
||||
s.cam.Follow(x+TileSize/2, y+TileSize/2)
|
||||
@@ -165,6 +191,20 @@ func (s *LevelScene) MiniPick(text string) bool {
|
||||
s.run.StepMini(0, minigame.Input{Move: 1})
|
||||
return false
|
||||
}
|
||||
case *minigame.CarPick:
|
||||
if text == "calm" { // lugnaste vagnen som inte är kvinnovagn
|
||||
best := -1
|
||||
for i, c := range m.Cars {
|
||||
if !c.WomenOnly && (best < 0 || c.Crowd < m.Cars[best].Crowd) {
|
||||
best = i
|
||||
}
|
||||
}
|
||||
if best < 0 || m.Cursor == best {
|
||||
return true
|
||||
}
|
||||
s.run.StepMini(0, minigame.Input{MoveX: 1})
|
||||
return false
|
||||
}
|
||||
case *minigame.TopUp:
|
||||
switch m.Stage {
|
||||
case minigame.TopUpPickMember:
|
||||
@@ -345,6 +385,9 @@ func (s *LevelScene) drawHUD(g *Game, screen *ebiten.Image) {
|
||||
return // resultatpanelen, minispelet eller butiken tar över; ingen inforuta eller minikarta
|
||||
}
|
||||
lines, accent, bar := s.infoLines(a)
|
||||
if len(lines) == 0 {
|
||||
s.drawModeHint(a, screen)
|
||||
}
|
||||
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))
|
||||
@@ -499,10 +542,18 @@ func (s *LevelScene) infoLines(a *Assets) (lines []line, accent color.Color, bar
|
||||
}
|
||||
return []line{plainLine(a.T("hud.kiosk_none")+" "+a.T("hud.shop_prompt"), colDim)}, accent, false
|
||||
case level.TicketMachine:
|
||||
if lvl.TicketRequired {
|
||||
return []line{plainLine(fmt.Sprintf(a.T("hud.machine_ticket"), r.Wallet.Cash), colGood)}, colGood, false
|
||||
// Terminalen: linjetavlan för stationen (nätkopplade banor), läst genom kunskapen som en skylt.
|
||||
if term := r.TerminalLines(); len(term) > 0 {
|
||||
lines = append(lines, plainLine(a.T("hud.terminal"), colDim))
|
||||
for _, t := range term {
|
||||
rd := lang.Render(a.Dict, r.Know, t, UILang)
|
||||
lines = append(lines, a.wrapLine(shownLine(rd.Partial, colText, false), maxW)...)
|
||||
}
|
||||
}
|
||||
return []line{plainLine(fmt.Sprintf(a.T("hud.machine_topup"), r.Wallet.Card, r.Wallet.Cash), colGood)}, colGood, false
|
||||
if lvl.TicketRequired {
|
||||
return append(lines, plainLine(fmt.Sprintf(a.T("hud.machine_ticket"), r.Wallet.Cash), colGood)), colGood, false
|
||||
}
|
||||
return append(lines, plainLine(fmt.Sprintf(a.T("hud.machine_topup"), r.Wallet.Card, r.Wallet.Cash), colGood)), colGood, false
|
||||
}
|
||||
if r.DeniedAt >= 0 && r.Time-r.DeniedAt < deniedShowSec {
|
||||
key := "hud.gate_denied"
|
||||
|
||||
@@ -101,6 +101,8 @@ func (m *MapScene) Update(g *Game) error {
|
||||
case phoneJustPressed():
|
||||
g.Wallet.Phone = !g.Wallet.Phone
|
||||
g.Save()
|
||||
case modeJustPressed():
|
||||
g.toggleMode()
|
||||
}
|
||||
if n := len(m.mains); n > 0 {
|
||||
mx, my := miniMoveXY()
|
||||
@@ -209,7 +211,7 @@ func (m *MapScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
foot := fmt.Sprintf("%s %d %s %d %s %d¥", a.T("map.skill_points"), p.SkillPoints, a.T("words.points"), g.Know.Points,
|
||||
a.T("hud.cash"), g.Wallet.Cash)
|
||||
a.Draw(screen, foot, 8, 30, colDim)
|
||||
a.Draw(screen, fmt.Sprintf(a.T("menu.phone"), onoff), 8, 42, colDim)
|
||||
a.Draw(screen, fmt.Sprintf(a.T("menu.phone"), onoff)+" "+fmt.Sprintf(a.T("menu.mode"), a.T("mode."+g.Mode.String())), 8, 42, colDim)
|
||||
if len(g.Profile.Debuffs) > 0 {
|
||||
var parts []string
|
||||
for _, d := range g.Profile.Debuffs {
|
||||
|
||||
@@ -62,8 +62,12 @@ func (r *ResultScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
y += LineH
|
||||
if run.Outcome == sim.Lost {
|
||||
reason := a.T(run.LostReason)
|
||||
if run.LostReason == sim.LostWrongTrain {
|
||||
reason = fmt.Sprintf(a.T("result.wrong_detail"), tr.Destination.Get("romaji"), run.Level.Destination.Get("romaji"))
|
||||
if detail, ok := map[string]string{
|
||||
sim.LostWrongTrain: "result.wrong_detail",
|
||||
sim.LostWrongDirection: "result.wrong_direction_detail",
|
||||
sim.LostTooFar: "result.too_far_detail",
|
||||
}[run.LostReason]; ok {
|
||||
reason += " " + fmt.Sprintf(a.T(detail), tr.Destination.Get("romaji"), run.Level.Destination.Get("romaji"))
|
||||
}
|
||||
for _, l := range a.wrapLine(plainLine(reason, colText), ScreenW-56) {
|
||||
a.drawLines(screen, []line{l}, 28, y)
|
||||
@@ -80,11 +84,26 @@ func (r *ResultScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
y += LineH
|
||||
if run.Outcome == sim.Won {
|
||||
sc := run.Score
|
||||
a.Draw(screen, fmt.Sprintf("%s %d", a.T("result.score"), sc.Total), 28, y, colText)
|
||||
y += LineH
|
||||
a.Draw(screen, fmt.Sprintf("%s %d %s %d %s %d %s %d %s %d %s %d", strings.ToLower(a.T("hud.time")), sc.Time, a.T("result.money"), sc.Money,
|
||||
a.T("result.meters"), sc.Meters, a.T("result.map"), sc.Explore, a.T("result.car"), sc.Car, a.T("result.penalty"), sc.Penalty), 28, y, colDim)
|
||||
y += LineH
|
||||
// Poängen som en uträkning; radbryts till högst två rader.
|
||||
parts := []string{
|
||||
fmt.Sprintf("%s %d", strings.ToLower(a.T("hud.time")), sc.Time), fmt.Sprintf("%s %d", a.T("result.money"), sc.Money),
|
||||
fmt.Sprintf("%s %d", a.T("result.meters"), sc.Meters), fmt.Sprintf("%s %d", a.T("result.map"), sc.Explore),
|
||||
fmt.Sprintf("%s %d", a.T("result.car"), sc.Car),
|
||||
}
|
||||
if sc.Bonus > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%s %d", a.T("result.bonus"), sc.Bonus))
|
||||
}
|
||||
total := fmt.Sprintf("%s %d = %s", a.T("result.score"), sc.Total, strings.Join(parts, " + "))
|
||||
if sc.Penalty > 0 {
|
||||
total += fmt.Sprintf(" - %s %d", a.T("result.penalty"), sc.Penalty)
|
||||
}
|
||||
for i, l := range a.wrapLine(plainLine(total, colText), ScreenW-56) {
|
||||
if i > 1 {
|
||||
break
|
||||
}
|
||||
a.drawLines(screen, []line{l}, 28, y)
|
||||
y += LineH
|
||||
}
|
||||
msg := fmt.Sprintf("+%d %s", run.PointsEarned, a.T("result.lang_points"))
|
||||
if len(run.LearnedDest) > 0 {
|
||||
msg += " " + a.T("hud.learned") + ": " + strings.Join(run.LearnedDest, " ") + " (" + a.T("result.destination") + ")"
|
||||
|
||||
@@ -93,7 +93,7 @@ func parseScript(src string) (*script, error) {
|
||||
return nil, fmt.Errorf("script: '%s TEXT'", f[0])
|
||||
}
|
||||
st.arg = f[1]
|
||||
case "quit", "waittrain", "waitstop":
|
||||
case "quit", "waittrain", "waitstop", "waitover":
|
||||
default:
|
||||
return nil, fmt.Errorf("script: okänt kommando %q", f[0])
|
||||
}
|
||||
@@ -126,6 +126,7 @@ type playerTiler interface {
|
||||
type trainWaiter interface {
|
||||
TrainHere() bool
|
||||
RideAtDest() bool
|
||||
LevelOver() bool
|
||||
MiniPick(text string) bool
|
||||
TrackPhaseName() string
|
||||
}
|
||||
@@ -195,6 +196,11 @@ func (s *script) tick(g *Game, frame *ebiten.Image) error {
|
||||
if tw, ok := g.scene.(trainWaiter); !ok || tw.RideAtDest() {
|
||||
s.advance()
|
||||
}
|
||||
case "waitover":
|
||||
s.counter++
|
||||
if tw, ok := g.scene.(trainWaiter); !ok || tw.LevelOver() || s.counter > 60*120 {
|
||||
s.advance()
|
||||
}
|
||||
case "waitphase":
|
||||
s.counter++
|
||||
if tw, ok := g.scene.(trainWaiter); !ok || tw.TrackPhaseName() == st.arg || s.counter > 60*60 {
|
||||
|
||||
@@ -36,7 +36,7 @@ type trackInfo struct {
|
||||
func (s *LevelScene) buildTracks() {
|
||||
s.tracks = nil
|
||||
s.rails = map[level.Point]bool{}
|
||||
for _, t := range []level.Tile{level.Platform, level.WrongPlatform} {
|
||||
for _, t := range level.PlatformTiles {
|
||||
for _, p := range s.run.Level.Find(t) {
|
||||
tt, horizontal := s.run.Level.TrackTile(p)
|
||||
s.tracks = append(s.tracks, trackInfo{tile: t, platform: p, track: tt, horizontal: horizontal})
|
||||
|
||||
Reference in New Issue
Block a user