Files
ZacharyZhang-NY b4b4d67b92 Audit keyboard, focus, veils, motion and clocks across all 500
A cross-cutting pass, each fix with a test that failed first.

Veils: MyGo still hands a key to a base that turned off this frame,
so every element key now goes through pixel.Shortcut, which an
archtest enforces. Input handlers record and the build applies them
only while enabled; the editor turns away a veil-frame letter or
copy; value bases get a copy and keep only enabled turns. Popups
give a held focus back to their opener when the veil lifts.

Motion: S5 is enforced at the clock. Every Tick, Since, Step,
Toward and Shot runs at 4-15 fps; blinks keep 1 Hz on 4 fps steps,
fast animations step at 15, and drawn time is quantized to frames.

Clocks stepping back: Since holds at zero without overflow, and
k.Elapsed moves a stored start to now, so nothing goes negative,
runs backward or indexes out of range.

Also: logs count typed text instead of holding it, gallery names
use glyphs the font has, removable rows are keyed, components take
their own Options (archtest), and sweeps over all 508 demos check
veiled keys, clicks, wheels, the clock stepping back and names.
2026-10-12 16:03:04 -04:00

245 lines
8.5 KiB
Go

package gallery
import (
"fmt"
"time"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/data"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/game"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/icon"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/input"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
var lootEntries = []Entry{
{280, "ResultScreen", "结算画面", "game", "Time and score count up; then the rank lands.", demoResultScreen},
{281, "Crafting", "合成台", "game", "Click a cell to swap ingredients. Herb over water makes a potion.", demoCrafting},
{282, "LootPopup", "获得物品", "game", "YOU GOT: something nice! The item rises out of the box.", demoLootPopup},
{283, "TreasureChest", "宝箱", "game", "Press the chest. It creaks open; the gem floats up.", demoTreasureChest},
{284, "GachaReel", "扭蛋转轮", "game", "SPIN: three reels, stopping one by one. No real money involved.", demoGachaReel},
{285, "InteractPrompt", "互动提示", "game", "Walk up to the mayor and a TALK bubble bobs above him.", demoInteractPrompt},
{286, "TurnOrder", "行动顺序", "game", "Who acts next. The active one stands raised.", demoTurnOrder},
{287, "BattleLog", "战斗日志", "game", "Combat lines roll up as the fight goes on.", demoBattleLog},
{288, "TargetSelector", "目标选择", "game", "A ▼ over the target. Arrows choose; Enter attacks.", demoTargetSelector},
{289, "ATBGauge", "行动槽", "game", "Each gauge fills at its hero's speed and flashes READY.", demoATBGauge},
{290, "StaminaBar", "耐力条", "game", "Sprint and it drains; rest and it refills.", demoStaminaBar},
}
type lootDemo struct {
result, cell, spin, turn, target int
grid []data.Item
stops [3]int
popup, chest, near, sprint bool
lines []game.Line
ready [3]time.Time
stamina float64
last time.Time
}
func lootState(c *ui.Context) *lootDemo {
now := pixel.K(c).Now()
return ui.Local(c.Root(), demoKey("loot"), func() lootDemo {
s := lootDemo{grid: make([]data.Item, 9), near: true, stamina: 1, last: now, ready: [3]time.Time{now, now, now}}
s.grid[1], s.grid[4] = herb, water
return s
})
}
var (
herb = data.Item{Sprite: icon.Leaf, Name: "Herb"}
water = data.Item{Sprite: icon.Drop, Name: "Water"}
)
func demoResultScreen(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := lootState(c)
ui.Column(c).Gap(k.D(6)).Children(func() {
// A new key makes a fresh screen; counts run again.
ui.Box(c).Key(s.result).Children(func() {
game.ResultScreen(c, "Stage clear", 83*time.Second+450*time.Millisecond, 12500, "S", game.ResultScreenOptions{})
})
if small(c, "AGAIN", false) {
s.result++
log("counting again")
}
})
}
func demoCrafting(c *ui.Context, log func(string)) {
s := lootState(c)
recipes := []game.Recipe{{Grid: [9]string{1: "Herb", 4: "Water"}, Result: data.Item{Sprite: icon.Potion, Name: "Potion"}}}
r := game.Crafting(c, "Workbench", s.grid, recipes, &s.cell, game.CraftingOptions{})
// A picked cell cycles: empty, herb, water.
if r.Picked >= 0 {
switch s.grid[r.Picked].Name {
case "":
s.grid[r.Picked] = herb
case "Herb":
s.grid[r.Picked] = water
default:
s.grid[r.Picked] = data.Item{}
}
log(fmt.Sprintf("cell %d: %s", r.Picked+1, s.grid[r.Picked].Name))
}
if r.Crafted {
s.grid = make([]data.Item, 9)
log("made a potion!")
}
}
func demoLootPopup(c *ui.Context, log func(string)) {
s := lootState(c)
if small(c, "OPEN THE BOX", s.popup) {
s.popup = true
log("ooh")
}
if game.LootPopup(c, data.Item{Sprite: icon.Potion, Name: "POTION", Count: 3}, &s.popup, game.LootPopupOptions{}).Closed {
log("put it in the bag")
}
}
func demoTreasureChest(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := lootState(c)
ui.Row(c).Gap(k.D(10)).AlignItems(ui.End).Children(func() {
if game.TreasureChest(c, "Chest", data.Item{Sprite: icon.Gem, Name: "Gem"}, &s.chest, game.TreasureChestOptions{}).Opened {
log("a gem!")
}
if small(c, "CLOSE", !s.chest) {
s.chest = false
log("closed")
}
})
}
// faces are the gacha reel's faces.
var faces = []data.Item{{Sprite: icon.Star, Name: "Star"}, {Sprite: icon.Heart, Name: "Heart"}, {Sprite: icon.Coin, Name: "Coin"}, {Sprite: icon.Skull, Name: "Skull"}, {Sprite: icon.Gem, Name: "Gem"}}
func demoGachaReel(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := lootState(c)
dice := gameState(c).dice
ui.Row(c).Gap(k.D(8)).AlignItems(ui.Center).Children(func() {
if game.GachaReel(c, "Reels", faces, s.stops, s.spin, game.GachaReelOptions{}).Done {
log(fmt.Sprintf("%s %s %s", faces[s.stops[0]].Name, faces[s.stops[1]].Name, faces[s.stops[2]].Name))
}
if small(c, "SPIN", false) {
s.stops = [3]int{dice.IntN(len(faces)), dice.IntN(len(faces)), dice.IntN(len(faces))}
s.spin++
log("spinning")
}
})
}
func demoInteractPrompt(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := lootState(c)
ui.Row(c).Gap(k.D(16)).AlignItems(ui.End).Padding(k.D(24), 0, 0, 0).Children(func() {
mayor := ui.Box(c).Size(k.D(24), k.D(24)).Label("Mayor").Draw(func(p *ui.Painter, r ui.Rect) {
k.Canvas(p, r).Sprite(0, 0, game.Hero, 0, pixel.Ink, 2)
})
if s.near {
game.InteractPrompt(c, mayor, "TALK", game.InteractPromptOptions{})
}
if input.Toggle(c, "NEAR THE MAYOR", &s.near, input.ToggleOptions{Size: input.Small}).Changed {
log(fmt.Sprintf("near %v", s.near))
}
})
}
// fighters are the turn order demo's lineup.
var fighters = []game.Fighter{{Name: "Aria", Portrait: game.Hero}, {Name: "Slime", Portrait: slime, Foe: true}, {Name: "Brom", Portrait: game.Hero}, {Name: "Bat", Portrait: icon.Bug, Foe: true}}
func demoTurnOrder(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := lootState(c)
ui.Row(c).Gap(k.D(10)).AlignItems(ui.Center).Children(func() {
game.TurnOrder(c, "Turns", fighters, s.turn, game.TurnOrderOptions{})
if small(c, "NEXT TURN", false) {
s.turn = (s.turn + 1) % len(fighters)
log(fighters[s.turn].Name + "'s turn")
}
})
}
func demoBattleLog(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := lootState(c)
dice := gameState(c).dice
ui.Column(c).Gap(k.D(6)).Children(func() {
game.BattleLog(c, "Battle", s.lines, game.BattleLogOptions{})
ui.Row(c).Gap(k.D(4)).Children(func() {
if small(c, "ATTACK", false) {
line := game.Line{Text: fmt.Sprintf("Slime takes %d.", 3+dice.IntN(9)), Slot: pixel.Bad}
s.lines = append(s.lines, game.Line{Text: "Aria attacks!"}, line)
log(line.Text)
}
if small(c, "HEAL", false) {
s.lines = append(s.lines, game.Line{Text: "Brom heals 8.", Slot: pixel.Good})
log("healed")
}
})
})
}
func demoTargetSelector(c *ui.Context, log func(string)) {
s := lootState(c)
enemies := []game.Enemy{{Name: "Slime", Sprite: slime, HP: 6, MaxHP: 9}, {Name: "Bat", Sprite: icon.Bug, HP: 5, MaxHP: 5}, {Name: "Ghost", Sprite: icon.Skull, MaxHP: 7}}
r := game.TargetSelector(c, "Targets", enemies, &s.target, game.TargetSelectorOptions{})
if r.Changed {
log("aim at " + enemies[s.target].Name)
}
if r.Chosen >= 0 {
log("attack " + enemies[r.Chosen].Name)
}
}
func demoATBGauge(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := lootState(c)
ui.Column(c).Gap(k.D(4)).Children(func() {
for i, h := range []struct {
name string
speed int
}{{"ARIA", 30}, {"BROM", 45}, {"CYRA", 20}} {
ui.Row(c).Gap(k.D(6)).AlignItems(ui.End).Children(func() {
// Each gauge fills in its hero's speed, in tenths.
fill := k.Since(c, s.ready[i], 10, h.speed)
game.ATBGauge(c, h.name, float64(fill)/float64(h.speed), game.ATBGaugeOptions{Width: 80})
if small(c, "ACT", fill < h.speed) {
s.ready[i] = k.Now()
log(h.name + " acts")
}
})
}
})
}
func demoStaminaBar(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := lootState(c)
now := k.Now()
// Sprinting drains a quarter a second; rest refills a tenth.
if dt := max(0, now.Sub(s.last).Seconds()); s.sprint {
s.stamina = max(0, s.stamina-dt/4)
} else {
s.stamina = min(1, s.stamina+dt/10)
}
s.last = now
if s.sprint && s.stamina == 0 {
s.sprint = false
log("out of breath")
}
if s.sprint || s.stamina < 1 {
k.Tick(c, 10)
}
ui.Row(c).Gap(k.D(10)).AlignItems(ui.End).Children(func() {
game.StaminaBar(c, "STAMINA", s.stamina, game.StaminaBarOptions{})
if input.Toggle(c, "SPRINT", &s.sprint, input.ToggleOptions{Size: input.Small, Disabled: s.stamina == 0}).Changed {
log(fmt.Sprintf("sprint %v", s.sprint))
}
})
}