Fifteen shop components: PriceTag, Wallet, ShopItem, Cart, Receipt, Checkout, CurrencyConvert, SaleBadge, Quantity, PricingTiers, Coupon, DailyReward, BidTicker, TradeWindow and VendingMachine. Receipt prints dot by dot with feed holes and a torn edge; Coupon cuts real notches; SaleBadge is a sixteen-point starburst; VendingMachine sells A1 to D4 by code and drops the snack into its tray. Sums are checked against overflow, TradeWindow withdraws both confirms on any change to an offer, and a disabled Checkout veils its inert content. text.Odometer now counts digits as text, steps at 15 fps as S5 asks, draws exact digits at rest and lands rolls that floats would stall.
178 lines
4.9 KiB
Go
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
|
|
}
|