M5: minikarta med dimma, riktmärken och avgångslista
All checks were successful
release / build-release (push) Successful in 1m40s
All checks were successful
release / build-release (push) Successful in 1m40s
- sim.Fog: avslöjar tre gångsteg från spelaren plus gränsande väggar (ser längs gången, inte genom väggen); riktmärken (perronger, start, spärrar) syns alltid; Explored ger utforskad andel; nollställs per omgång - game/minimap: 1–3 px per ruta i det hörn som varken täcker inforutan eller spelaren, perrong vit när tåget är inne, spelaren blinkar, följarna i sina toner, avgångslista per spår (● inne / ○ kommer); M döljer/visar - resultatskärmen visar utforskad andel och antal försörjda; layout justerad - docs/README/wiki uppdaterade Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -81,6 +81,11 @@ func wordsJustPressed() bool {
|
||||
return virtual.pressed["l"] || inpututil.IsKeyJustPressed(ebiten.KeyL) || gamepadJustPressed(ebiten.StandardGamepadButtonLeftRight)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// menuMove returnerar -1/+1 när användaren just tryckte upp/ner i en meny.
|
||||
func menuMove() int {
|
||||
switch {
|
||||
|
||||
126
internal/game/minimap.go
Normal file
126
internal/game/minimap.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"math"
|
||||
"sort"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/sim"
|
||||
)
|
||||
|
||||
// Minikartan: bara det spelaren sett plus riktmärkena (perronger, start,
|
||||
// spärrar), och en liten avgångslista per spår. Den lägger sig i det hörn
|
||||
// som varken täcker inforutan eller spelaren.
|
||||
|
||||
var minimapColors = map[level.Tile]color.RGBA{
|
||||
level.Wall: {58, 68, 102, 255},
|
||||
level.Floor: {139, 155, 180, 255},
|
||||
level.Spawn: {99, 199, 77, 255},
|
||||
level.Platform: {254, 174, 52, 255},
|
||||
level.WrongPlatform: {254, 174, 52, 255},
|
||||
level.Gate: {44, 232, 245, 255},
|
||||
level.TicketMachine: {247, 118, 34, 255},
|
||||
level.Sign: {90, 140, 230, 255},
|
||||
level.Toilet: {235, 240, 250, 255},
|
||||
level.Kiosk: {228, 59, 68, 255},
|
||||
}
|
||||
|
||||
// minimapScale väljer pixlar per ruta så att kartan ryms i ungefär en
|
||||
// tredjedel av skärmen.
|
||||
func minimapScale(l *level.Level) int {
|
||||
sc := min(3, (ScreenW/3)/max(1, l.Width()), (ScreenH/3)/max(1, l.Height()))
|
||||
return max(1, sc)
|
||||
}
|
||||
|
||||
// minimapRect väljer hörn: nedre höger som standard, annars det första hörnet
|
||||
// som inte krockar med inforutan (full bredd vid box) eller spelaren.
|
||||
func minimapRect(w, h int, box image.Rectangle, hasBox bool, player image.Rectangle) image.Rectangle {
|
||||
const m = 4
|
||||
candidates := []image.Rectangle{
|
||||
image.Rect(ScreenW-m-w, ScreenH-m-h, ScreenW-m, ScreenH-m), // nedre höger
|
||||
image.Rect(m, ScreenH-m-h, m+w, ScreenH-m), // nedre vänster
|
||||
image.Rect(ScreenW-m-w, 26, ScreenW-m, 26+h), // övre höger (under HUD:en)
|
||||
image.Rect(m, 26, m+w, 26+h), // övre vänster
|
||||
}
|
||||
for _, c := range candidates {
|
||||
if hasBox && c.Overlaps(box) {
|
||||
continue
|
||||
}
|
||||
if c.Overlaps(player.Inset(-8)) {
|
||||
continue
|
||||
}
|
||||
return c
|
||||
}
|
||||
return candidates[0]
|
||||
}
|
||||
|
||||
func (s *LevelScene) drawMinimap(g *Game, screen *ebiten.Image, box image.Rectangle, hasBox bool) {
|
||||
a := g.Assets
|
||||
r := s.run
|
||||
l := r.Level
|
||||
sc := minimapScale(l)
|
||||
mapW, mapH := l.Width()*sc, l.Height()*sc
|
||||
|
||||
// Avgångslistan: ett spår per rad.
|
||||
type dep struct {
|
||||
track string
|
||||
at bool
|
||||
secs float64
|
||||
}
|
||||
var deps []dep
|
||||
for _, tr := range l.Trains {
|
||||
at, secs := tr.Schedule.Status(r.Time)
|
||||
deps = append(deps, dep{tr.Track, at, secs})
|
||||
}
|
||||
sort.Slice(deps, func(i, j int) bool { return deps[i].track < deps[j].track })
|
||||
|
||||
w := max(mapW, 44) + 6
|
||||
h := mapH + 6 + len(deps)*LineH
|
||||
px, py := r.Player.WorldPos(TileSize)
|
||||
ox, oy := s.cam.Offset()
|
||||
playerRect := image.Rect(int(px+ox), int(py+oy), int(px+ox)+TileSize, int(py+oy)+TileSize)
|
||||
rc := minimapRect(w, h, box, hasBox, playerRect)
|
||||
|
||||
vector.DrawFilledRect(screen, float32(rc.Min.X), float32(rc.Min.Y), float32(w), float32(h), colPanel, false)
|
||||
x0, y0 := rc.Min.X+3, rc.Min.Y+3
|
||||
for y := 0; y < l.Height(); y++ {
|
||||
for x := 0; x < l.Width(); x++ {
|
||||
p := level.Point{X: x, Y: y}
|
||||
if !r.Fog.Visible(l, p) {
|
||||
continue
|
||||
}
|
||||
t := l.At(x, y)
|
||||
c := minimapColors[t]
|
||||
if (t == level.Platform || t == level.WrongPlatform) && 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)
|
||||
}
|
||||
}
|
||||
// Sällskapet: följarna i sina toner, spelaren blinkande vit.
|
||||
for i := len(r.Party.Followers()); i >= 1; i-- {
|
||||
from, _ := r.Party.FollowerTiles(i)
|
||||
tint := memberTints[i%len(memberTints)]
|
||||
vector.DrawFilledRect(screen, float32(x0+from.X*sc), float32(y0+from.Y*sc), float32(sc), float32(sc),
|
||||
color.RGBA{uint8(255 * tint[0]), uint8(255 * tint[1]), uint8(255 * tint[2]), 255}, false)
|
||||
}
|
||||
if math.Mod(r.Time, 0.6) < 0.4 {
|
||||
pt := r.Player.Tile
|
||||
vector.DrawFilledRect(screen, float32(x0+pt.X*sc), float32(y0+pt.Y*sc), float32(sc), float32(sc), colText, false)
|
||||
}
|
||||
// Avgångar: "2 ●6s" = inne, avgår om 6 s; "1 ○14s" = kommer om 14 s.
|
||||
for i, d := range deps {
|
||||
mark, col := "○", colDim
|
||||
if d.at {
|
||||
mark, col = "●", colGood
|
||||
}
|
||||
a.Draw(screen, fmt.Sprintf("%s %s%.0fs", d.track, mark, math.Ceil(d.secs)), x0, y0+mapH+2+i*LineH, col)
|
||||
}
|
||||
_ = sim.FogRadius
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package game
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"math"
|
||||
"path/filepath"
|
||||
@@ -26,10 +27,11 @@ 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
|
||||
run *sim.Run
|
||||
cam *sim.Camera
|
||||
animT float64
|
||||
id string
|
||||
run *sim.Run
|
||||
cam *sim.Camera
|
||||
animT float64
|
||||
hideMap bool // M döljer/visar minikartan
|
||||
}
|
||||
|
||||
// NewLevelScene laddar banan och ställer spelaren på startrutan. know är
|
||||
@@ -77,6 +79,9 @@ func (s *LevelScene) Update(g *Game) error {
|
||||
if helpJustPressed() {
|
||||
s.run.AskHelp()
|
||||
}
|
||||
if mapJustPressed() {
|
||||
s.hideMap = !s.hideMap
|
||||
}
|
||||
s.run.Step(dt, dirInput(), confirmJustPressed())
|
||||
if s.run.Player.Moving() {
|
||||
s.animT += dt
|
||||
@@ -219,7 +224,7 @@ func (s *LevelScene) drawHUD(g *Game, screen *ebiten.Image) {
|
||||
s.drawMeters(g, screen)
|
||||
|
||||
if r.Outcome != sim.Ongoing {
|
||||
return
|
||||
return // resultatpanelen tar över; ingen inforuta eller minikarta
|
||||
}
|
||||
lines, accent, bar := s.infoLines(a)
|
||||
if entry := a.Dict.Lookup(r.Learned); entry != nil && r.Time-r.LearnedAt < learnedShowSec {
|
||||
@@ -236,20 +241,30 @@ func (s *LevelScene) drawHUD(g *Game, screen *ebiten.Image) {
|
||||
}
|
||||
lines = append(lines, a.wrapLine(plainLine(strings.Join(parts, " "), colWarn), ScreenW-8)...)
|
||||
}
|
||||
if len(lines) == 0 {
|
||||
|
||||
// Inforutans läge: nederkant, men flyttas upp under HUD:en om spelaren
|
||||
// (eller tåget under hen) annars skulle hamna bakom den.
|
||||
var box image.Rectangle
|
||||
hasBox := len(lines) > 0
|
||||
if hasBox {
|
||||
h := 8 + LineH*len(lines)
|
||||
if bar {
|
||||
h += 8
|
||||
}
|
||||
_, py := r.Player.WorldPos(TileSize)
|
||||
y := ScreenH - h
|
||||
if int(py-s.cam.Y)+2*TileSize > y {
|
||||
y = 24
|
||||
}
|
||||
box = image.Rect(0, y, ScreenW, y+h)
|
||||
}
|
||||
if !s.hideMap {
|
||||
s.drawMinimap(g, screen, box, hasBox)
|
||||
}
|
||||
if !hasBox {
|
||||
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
|
||||
}
|
||||
y, h := box.Min.Y, box.Dy()
|
||||
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)
|
||||
|
||||
@@ -42,7 +42,7 @@ func (r *ResultScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
r.from.Draw(g, screen) // banan i bakgrunden
|
||||
run := r.from.run
|
||||
a := g.Assets
|
||||
vector.DrawFilledRect(screen, 20, 36, ScreenW-40, 112, colPanel, false)
|
||||
vector.DrawFilledRect(screen, 20, 36, ScreenW-40, 120, colPanel, false)
|
||||
|
||||
accent, headline := colBad, a.T("level.wrong_train")
|
||||
if run.Outcome == sim.Won {
|
||||
@@ -71,8 +71,11 @@ func (r *ResultScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
}
|
||||
}
|
||||
y += 4
|
||||
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)
|
||||
a.Draw(screen, fmt.Sprintf("%s %.1fs (%s %.0fs) %s %d %s %.0f %%", a.T("hud.time"), run.Time, a.T("result.par"), run.ParSec(),
|
||||
a.T("hud.steps"), run.Steps, a.T("result.explored"), 100*run.Fog.Explored(run.Level)), 28, y, colText)
|
||||
y += LineH
|
||||
a.Draw(screen, fmt.Sprintf("%s %d %s %d¥ %s %d", a.T("result.gates"), run.GatePasses, a.T("result.card"), run.Card.Balance,
|
||||
a.T("result.served"), run.Served), 28, y, colText)
|
||||
y += LineH
|
||||
if run.Outcome == sim.Won {
|
||||
msg := fmt.Sprintf("+%d %s", run.PointsEarned, a.T("words.points"))
|
||||
@@ -84,5 +87,5 @@ func (r *ResultScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
}
|
||||
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)
|
||||
a.Draw(screen, a.T("result.hint"), 28, 141, colDim)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user