Files
8bitMyGoUI/shop/vendingmachine.go
ZacharyZhang-NYandClaude Opus 5.5 03e1f91706 Upgrade MyGo to v0.5.5
Elements are now pass-scoped value handles: tests that read bounds
after a frame bind a ui.Handle. pixel tells frames apart by the build
pass's root, since the Tester clock no longer moves on its own.

Fix the regressions the upgrade brought:
- Clipboard reads in input handlers go through c.Services().
- MyGo notes an overlay's opener after building it, so AutoFocus
  inside lost it; pixel.AutoFocus focuses through a Handle, and
  archtest bans the raw call. Dialogs hand the focus back again.
- Elements around a press stay hovered: Tooltip tells a press-given
  focus by focus arriving under the pointer.
- The web host moves the Tester clock with real time, at most 500 ms
  a call, and gives components the real clock.

The gallery's source index also accepts CRLF checkouts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 01:31:08 -04:00

178 lines
4.9 KiB
Go

package shop
import (
"fmt"
"log/slog"
"strconv"
"strings"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/icon"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
// Snack is a slot in the machine; nameless means empty.
type Snack struct {
Name string
// Sprite is at most 16 dots square.
Sprite *pixel.Sprite
Price int
Stock int
}
type VendingMachineOptions struct {
Disabled bool
}
type VendingMachineResult struct {
Element ui.Element
// Vended is the slot dropped this frame, else -1.
Vended int
}
type vendKey struct{}
// vend is the code typed, the line shown, the drops.
type vend struct {
code, msg string
cur, drops int
last int
}
var keys = []string{"A", "B", "C", "D", "1", "2", "3", "4", "CLR", "OK"}
const vcell = 30
// VendingMachine sells slots A1 to D4 by code; credit pays.
func VendingMachine(c *ui.Context, text string, slots [16]Snack, credit *int, o VendingMachineOptions) VendingMachineResult {
k := pixel.K(c)
amount("VendingMachine credit", *credit)
said := make([]string, 0, 16)
for i, s := range slots {
if s.Name == "" {
continue
}
amount("VendingMachine price", s.Price)
if s.Sprite == nil || s.Sprite.W > 16 || s.Sprite.H > 16 || s.Stock < 0 {
panic(fmt.Sprintf("shop: VendingMachine slot %d %q needs a small sprite and stock", i, s.Name))
}
said = append(said, code(i)+" "+s.Name+" "+strconv.Itoa(s.Price))
}
row := ui.Row(c).Gap(k.D(8)).AlignItems(ui.Start).Role(ui.RoleGroup).Label(text)
v := ui.Local(row, vendKey{}, func() vend { return vend{last: -1} })
res := VendingMachineResult{Vended: -1}
res.Element = row.Children(func() {
glass := ui.Box(c).Size(k.D(4*vcell+4), k.D(4*vcell+28)).Role(ui.RoleImage).Label(k.Msg("snacks")).Description(strings.Join(said, ", "))
// The bought snack bounces into the tray in steps.
bounce, last := k.Shot(c, glass, v.drops, 8, 4), v.last
glass.Draw(func(p *ui.Painter, r ui.Rect) {
g := k.Canvas(p, r)
g.Frame(0, 0, g.W, g.H, pixel.Sunken, pixel.Well)
for i, s := range slots {
x, y := 2+i%4*vcell, 2+i/4*vcell
g.Micro(x+2, y+2, code(i), pixel.Faint, 1)
if s.Name == "" {
continue
}
g.Sprite(x+(vcell-s.Sprite.W)/2, y+8, s.Sprite, 0, pixel.Ink, 1)
p := strconv.Itoa(s.Price)
g.Micro(x+vcell-2-pixel.MicroWidth(p), y+vcell-7, p, pixel.Warn, 1)
if s.Stock == 0 {
g.Veil(x+1, y+1, vcell-2, vcell-2, pixel.Shade, 8)
}
}
ty := 4*vcell + 6
g.Rect(4, ty, g.W-8, 18, pixel.Shade)
if last >= 0 && slots[last].Sprite != nil {
sp := slots[last].Sprite
dy := []int{-6, 0, -2, 0}[min(bounce, 3)]
g.Sprite((g.W-sp.W)/2, ty+18-sp.H-1+dy, sp, 0, pixel.Ink, 1)
}
})
ui.Column(c).Gap(k.D(4)).Children(func() {
line := v.msg
if line == "" {
line = v.code
}
ui.Box(c).Size(k.D(4*22), k.D(pixel.LineHeight+4)).Role(ui.RoleStatus).Label(line).Draw(func(p *ui.Painter, r ui.Rect) {
g := k.Canvas(p, r)
g.Frame(0, 0, g.W, g.H, pixel.Sunken, pixel.Well)
g.Text(4, 2, pixel.Wrap(line, g.W-8, 1)[0], pixel.Good, 1)
})
shell, base, st, picked := pixel.Pad(c, k.Msg("keypad"), 4, len(keys), 22, 18, &v.cur, o.Disabled)
if picked >= 0 {
res.Vended = press(k, v, keys[picked], slots, credit)
}
say := keys[v.cur]
switch say {
case "CLR":
say = k.Msg("clear")
case "OK":
say = k.Msg("ok")
}
base.Description(say)
cur := v.cur
shell.Draw(func(p *ui.Painter, r ui.Rect) {
g := k.Canvas(p, r)
for i, key := range keys {
x, y := i%4*22, i/4*18
g.Frame(x+1, y+1, 20, 16, pixel.Raised, pixel.Face)
if i == cur && st.Focus {
g.Cursor(x, y, 22, 18)
}
// CLR and OK are a cross and a check.
switch key {
case "CLR":
g.Sprite(x+5, y+3, icon.Close, 0, pixel.Bad, 1)
case "OK":
g.Sprite(x+5, y+3, icon.Check, 0, pixel.Good, 1)
default:
g.Micro(x+(22-pixel.MicroWidth(key))/2, y+7, key, pixel.Ink, 1)
}
}
g.Finish(st)
})
money(c, k, Gold, *credit, pixel.Ink)
})
})
if res.Vended >= 0 {
slog.Debug("8bit: vended", "label", text, "slot", code(res.Vended))
}
return res
}
// code names slot i: A1 to D4.
func code(i int) string {
return string(rune('A'+i/4)) + strconv.Itoa(i%4+1)
}
// press takes a key; OK on a full code vends.
func press(k pixel.Kit, v *vend, key string, slots [16]Snack, credit *int) int {
switch {
case len(key) == 1 && key[0] >= 'A' && key[0] <= 'D':
v.code, v.msg = key, ""
case len(key) == 1 && len(v.code) == 1:
v.code += key
case key == "CLR":
v.code, v.msg = "", ""
case key == "OK" && len(v.code) == 2:
i := int(v.code[0]-'A')*4 + int(v.code[1]-'1')
s := slots[i]
v.code = ""
switch {
case s.Name == "" || s.Stock == 0:
v.msg = k.Msg("sold_out")
case *credit < s.Price:
v.msg = k.Msg("insert_coin")
default:
*credit -= s.Price
v.msg, v.drops, v.last = k.Msg("enjoy"), v.drops+1, i
return i
}
case key == "OK":
v.msg = k.Msg("code_prompt")
}
return -1
}