All checks were successful
release / build-release (push) Successful in 1m34s
- level: Point har Z; banor kan ha floors (lika stora rutnät med namn), gamla grid-banor läses som en våning; trappor U/D på samma x,y en våning upp/ner valideras parvis; Neighbors ger grannar över våningar så att Path, lösbarhet, spärregel och Unreachable fungerar; skyltar har z - sim: att kliva på en trappa flyttar spelaren till andra änden (FloorChanges); dimman är per våning och avslöjar trappans andra ände; tåg hörs/syns bara på egen våning - game: bara spelarens våning ritas (följare på andra våningar döljs), HUD och minikarta visar våningens namn, trappgrafik i genart och färg på minikartan - bana 004 Tokyo (svårighet 3, tre i sällskapet): start under mark på B1, trappa upp till 1F med perronger; generatorn gör våningar med trappor som flaskhalsar; ordboken utökad - testbotar och tools/shots förstår trappor; två våningsbilder - docs/README/wiki uppdaterade Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
144 lines
4.5 KiB
Go
144 lines
4.5 KiB
Go
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},
|
|
level.StairsUp: {200, 150, 255, 255},
|
|
level.StairsDown: {150, 100, 220, 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 })
|
|
|
|
z := r.Player.Tile.Z
|
|
floorLabel := ""
|
|
if l.NumFloors() > 1 {
|
|
floorLabel = l.FloorName(z)
|
|
}
|
|
w := max(mapW, 44) + 6
|
|
h := mapH + 6 + len(deps)*LineH
|
|
if floorLabel != "" {
|
|
h += 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
|
|
if floorLabel != "" {
|
|
a.Draw(screen, floorLabel, x0, y0, colKnown)
|
|
y0 += LineH
|
|
}
|
|
for y := 0; y < l.Height(); y++ {
|
|
for x := 0; x < l.Width(); x++ {
|
|
p := level.Point{X: x, Y: y, Z: z}
|
|
if !r.Fog.Visible(l, p) {
|
|
continue
|
|
}
|
|
t := l.At(p)
|
|
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)
|
|
if from.Z != z {
|
|
continue
|
|
}
|
|
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
|
|
}
|