190 lines
4.4 KiB
Go
190 lines
4.4 KiB
Go
package navigation
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"math"
|
|
"time"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
|
)
|
|
|
|
// hero is the walker, two frames of stepping feet.
|
|
var hero = pixel.MustSprite(`
|
|
..eee...
|
|
.eaaae..
|
|
.efffe..
|
|
..eee...
|
|
.eaaae..
|
|
e.aaa.e.
|
|
..e.e...
|
|
.e...e..
|
|
|
|
..eee...
|
|
.eaaae..
|
|
.efffe..
|
|
..eee...
|
|
.eaaae..
|
|
.eaaae..
|
|
..e.e...
|
|
..e.e...
|
|
`)
|
|
|
|
type MapNode struct {
|
|
Name string
|
|
// X and Y place it on the map, in dots.
|
|
X, Y int
|
|
Locked bool
|
|
}
|
|
|
|
type WorldMapOptions struct {
|
|
// Width and Height are dots; zeros mean 240 by 140.
|
|
Width, Height int
|
|
Disabled bool
|
|
}
|
|
|
|
type WorldMapResult struct {
|
|
Element *ui.Element
|
|
// Entered reports Enter, or a click, at the hero's node.
|
|
Changed, Entered bool
|
|
}
|
|
|
|
type walkKey struct{}
|
|
|
|
// walk is the node the hero left and when.
|
|
type walk struct {
|
|
from int
|
|
at time.Time
|
|
}
|
|
|
|
// WorldMap joins nodes with dotted paths; arrows walk the hero.
|
|
func WorldMap(c *ui.Context, text string, nodes []MapNode, paths [][2]int, at *int, o WorldMapOptions) WorldMapResult {
|
|
k := pixel.K(c)
|
|
if *at < 0 || *at >= len(nodes) {
|
|
panic(fmt.Sprintf("navigation: node %d of %d", *at, len(nodes)))
|
|
}
|
|
w, h := pixel.Or("WorldMap Width", o.Width, 240), pixel.Or("WorldMap Height", o.Height, 140)
|
|
shell, base, st := pixel.Shell(c, func() *ui.Element {
|
|
return ui.Box(c).Focusable().Role(ui.RoleGroup).Label(text).Description(nodes[*at].Name).Disabled(o.Disabled)
|
|
})
|
|
wk := ui.Local(base, walkKey{}, func() walk { return walk{from: *at} })
|
|
res := WorldMapResult{Element: shell}
|
|
walkTo := func(j int) {
|
|
if j != *at && !nodes[j].Locked {
|
|
wk.from, wk.at, *at, res.Changed = *at, k.Now(), j, true
|
|
}
|
|
}
|
|
for _, m := range []struct {
|
|
key ui.Key
|
|
dx, dy int
|
|
}{{ui.KeyLeft, -1, 0}, {ui.KeyRight, 1, 0}, {ui.KeyUp, 0, -1}, {ui.KeyDown, 0, 1}} {
|
|
if base.Shortcut(0, m.key) {
|
|
if j := toward(nodes, paths, *at, m.dx, m.dy); j >= 0 {
|
|
walkTo(j)
|
|
}
|
|
}
|
|
}
|
|
enter, space := base.Shortcut(0, ui.KeyEnter), base.Shortcut(0, ui.KeySpace)
|
|
res.Entered = enter || space
|
|
// Enter and Space are taken, so clicks come from pointers.
|
|
if base.Clicked() {
|
|
px, py, _ := base.PointerPosition()
|
|
if j := nearest(nodes, int(px/k.Dot()), int(py/k.Dot())); j == *at {
|
|
res.Entered = true
|
|
} else if j >= 0 && linked(paths, *at, j) {
|
|
walkTo(j)
|
|
}
|
|
}
|
|
// Six steps carry the hero; Reduce Motion lands it.
|
|
step := 6
|
|
if !k.Reduce() {
|
|
step = k.Since(c, wk.at, 12, 6)
|
|
}
|
|
a, b := nodes[wk.from], nodes[*at]
|
|
hx, hy := a.X+(b.X-a.X)*step/6, a.Y+(b.Y-a.Y)*step/6
|
|
shell.Size(k.D(w), k.D(h)).Draw(func(p *ui.Painter, r ui.Rect) {
|
|
g := k.Canvas(p, r)
|
|
g.Dither(0, 0, g.W, g.H, pixel.Face, pixel.Good, 2)
|
|
for _, e := range paths {
|
|
trail(g, nodes[e[0]], nodes[e[1]])
|
|
}
|
|
for i, n := range nodes {
|
|
fill := pixel.Accent
|
|
if n.Locked {
|
|
fill = pixel.Shade
|
|
}
|
|
g.Disc(n.X, n.Y, 4, pixel.Edge)
|
|
g.Disc(n.X, n.Y, 3, fill)
|
|
ink := pixel.Faint
|
|
if i == *at {
|
|
ink = pixel.Ink
|
|
}
|
|
g.Text(n.X-pixel.Measure(n.Name)/2, n.Y+6, n.Name, ink, 1)
|
|
}
|
|
g.Sprite(hx-4, hy-12, hero, g.Step(4)%2, pixel.Ink, 1)
|
|
g.Finish(st)
|
|
})
|
|
switch {
|
|
case res.Changed:
|
|
slog.Debug("8bit: world map", "label", text, "node", nodes[*at].Name)
|
|
case res.Entered:
|
|
slog.Debug("8bit: world map enter", "label", text, "node", nodes[*at].Name)
|
|
}
|
|
return res
|
|
}
|
|
|
|
// toward is the linked node lying most along dx, dy.
|
|
func toward(nodes []MapNode, paths [][2]int, at, dx, dy int) int {
|
|
best, score := -1, 0.0
|
|
for _, e := range paths {
|
|
j := e[1]
|
|
if e[1] == at {
|
|
j = e[0]
|
|
} else if e[0] != at {
|
|
continue
|
|
}
|
|
vx, vy := float64(nodes[j].X-nodes[at].X), float64(nodes[j].Y-nodes[at].Y)
|
|
if cos := (vx*float64(dx) + vy*float64(dy)) / math.Hypot(vx, vy); cos > 0.3 && cos > score {
|
|
best, score = j, cos
|
|
}
|
|
}
|
|
return best
|
|
}
|
|
|
|
// linked reports a path between nodes a and b.
|
|
func linked(paths [][2]int, a, b int) bool {
|
|
for _, e := range paths {
|
|
if e == [2]int{a, b} || e == [2]int{b, a} {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// nearest is the node within 8 dots, or -1.
|
|
func nearest(nodes []MapNode, x, y int) int {
|
|
for i, n := range nodes {
|
|
if (n.X-x)*(n.X-x)+(n.Y-y)*(n.Y-y) <= 64 {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// trail dots the path between two nodes.
|
|
func trail(g pixel.Canvas, a, b MapNode) {
|
|
n := max(abs(b.X-a.X), abs(b.Y-a.Y))
|
|
for i := 0; i <= n; i += 3 {
|
|
g.Dot(a.X+(b.X-a.X)*i/n, a.Y+(b.Y-a.Y)*i/n, pixel.Edge)
|
|
}
|
|
}
|
|
|
|
func abs(v int) int {
|
|
if v < 0 {
|
|
return -v
|
|
}
|
|
return v
|
|
}
|