Add game components 269-290; CommandMenu AutoFocus
Press start, title screen, save slots, options menu, game over with CONTINUE, credits roll, ending card, loading tips, letterbox, mission brief, achievements, result screen, crafting grid, loot popup, treasure chest, gacha reel, interact prompt, turn order, battle log, target selector, ATB gauge and stamina bar. Cued effects count runs, so each cue change plays once and reports once. CommandMenu takes AutoFocus when enabled. HealthBar, ManaBar and CountUp keep exact colors, ticks and counts for the largest ints.
This commit is contained in:
38 files changed
+2663
-8
No files matched your search
@@ -0,0 +1,379 @@
|
||||
package game_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"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/pixel"
|
||||
)
|
||||
|
||||
func TestLetterboxSlidesInSteps(t *testing.T) {
|
||||
for _, reduce := range []bool{false, true} {
|
||||
wd := &world{clk: newClock(), reduce: reduce}
|
||||
on := false
|
||||
tt := view(300, 200, wd, func(c *ui.Context) {
|
||||
game.Letterbox(c, on, game.LetterboxOptions{}, func() { ui.Box(c).Size(pixel.K(c).D(100), pixel.K(c).D(60)) })
|
||||
})
|
||||
bar := func() int { return count(t, tt.Image(), 50, 0, 1, 30, pixel.Ink) }
|
||||
on = true
|
||||
tt.Frame()
|
||||
// Two of four steps in, a bar is 8 dots.
|
||||
wd.clk.t = wd.clk.t.Add(250 * time.Millisecond)
|
||||
tt.Frame()
|
||||
if half := bar(); (half == 8) == reduce || reduce && half != 16 {
|
||||
t.Fatalf("reduced %v: a quarter second in, the bar is %d dots", reduce, half)
|
||||
}
|
||||
wd.clk.t = wd.clk.t.Add(time.Second)
|
||||
tt.Frame()
|
||||
if bar() != 16 {
|
||||
t.Fatalf("reduced %v: the bar settled at %d", reduce, bar())
|
||||
}
|
||||
// Turned off mid-slide, the bars return from there.
|
||||
on = false
|
||||
tt.Frame()
|
||||
wd.clk.t = wd.clk.t.Add(250 * time.Millisecond)
|
||||
tt.Frame()
|
||||
on = true
|
||||
tt.Frame()
|
||||
if now := bar(); !reduce && now != 8 || reduce && now != 16 {
|
||||
t.Fatalf("reduced %v: turned back on at half, the bar is %d", reduce, now)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissionBriefStarts(t *testing.T) {
|
||||
started := false
|
||||
tt := view(500, 200, &world{clk: newClock()}, func(c *ui.Context) {
|
||||
started = started || game.MissionBrief(c, game.Quest{Title: "Rescue", Objectives: []game.Objective{{Text: "Go", Need: 1}}}, []string{"####", "#..#", "####"}, game.Spot{X: 1, Y: 1}, nil, game.MissionBriefOptions{}).Start
|
||||
})
|
||||
tt.Click("START")
|
||||
if !started {
|
||||
t.Fatal("START did not report")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAchievementsHideLockedNames(t *testing.T) {
|
||||
awards := []game.Award{{Sprite: icon.Trophy, Name: "First blood", How: "Win a fight", Unlocked: true}, {Sprite: icon.Coin, Name: "King", How: "Beat the king"}}
|
||||
sel := 0
|
||||
tt := view(400, 200, &world{clk: newClock()}, func(c *ui.Context) {
|
||||
game.Achievements(c, "Awards", awards, &sel, game.AchievementsOptions{})
|
||||
})
|
||||
if !tt.HasText("UNLOCKED 1/2") || !tt.HasText("First blood") {
|
||||
t.Fatalf("texts %q", tt.Texts())
|
||||
}
|
||||
tt.Click("Awards")
|
||||
tt.Key(0, ui.KeyHome)
|
||||
tt.Key(0, ui.KeyRight)
|
||||
if sel != 1 || tt.HasText("King") || !tt.HasText("???") || !tt.HasText("Beat the king") {
|
||||
t.Fatalf("a locked award showed %q", tt.Texts())
|
||||
}
|
||||
r := find(t, tt, "Awards")
|
||||
x, y := dots(r)
|
||||
// The locked coin is a Shade shadow: no Warn gold.
|
||||
if count(t, tt.Image(), x+24, y, 24, 24, pixel.Warn) != 0 || count(t, tt.Image(), x+24, y, 24, 24, pixel.Shade) == 0 {
|
||||
t.Fatal("the locked award is not a silhouette")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResultScreenCountsThenRanks(t *testing.T) {
|
||||
for _, reduce := range []bool{false, true} {
|
||||
wd := &world{clk: newClock(), reduce: reduce}
|
||||
tt := view(400, 200, wd, func(c *ui.Context) {
|
||||
game.ResultScreen(c, "Result", 83*time.Second, 12500, "S", game.ResultScreenOptions{})
|
||||
})
|
||||
rank := func() int { return count(t, tt.Image(), 0, 0, 200, 100, pixel.Warn) }
|
||||
if (rank() > 0) != reduce {
|
||||
t.Fatalf("reduced %v: the rank showed before the counts", reduce)
|
||||
}
|
||||
wd.clk.t = wd.clk.t.Add(2 * time.Second)
|
||||
tt.Frame()
|
||||
if rank() == 0 {
|
||||
t.Fatalf("reduced %v: no rank after the counts", reduce)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResultScreenFitsABigScore(t *testing.T) {
|
||||
tt := view(600, 200, &world{clk: newClock()}, func(c *ui.Context) {
|
||||
ui.Column(c).Padding(pixel.K(c).D(20)).Children(func() {
|
||||
game.ResultScreen(c, "Result", 83*time.Second, 1000000000000000, "S", game.ResultScreenOptions{})
|
||||
})
|
||||
})
|
||||
if n := count(t, tt.Image(), 0, 0, 20, 100, pixel.Ink); n != 0 {
|
||||
t.Fatalf("%d score dots left of the screen", n)
|
||||
}
|
||||
if r := find(t, tt, "Result"); int(r.W)/2 < pixel.Measure("SCORE")+12+pixel.Measure("1000000000000000") {
|
||||
t.Fatalf("the screen is %v px wide", r.W)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCraftingShowsAKnownRecipe(t *testing.T) {
|
||||
grid := make([]data.Item, 9)
|
||||
grid[4] = data.Item{Sprite: icon.Leaf, Name: "Herb"}
|
||||
recipes := []game.Recipe{{Grid: [9]string{4: "Herb"}, Result: data.Item{Sprite: icon.Potion, Name: "Potion"}}}
|
||||
cell, crafted, picked := 0, 0, -1
|
||||
tt := view(500, 200, &world{clk: newClock()}, func(c *ui.Context) {
|
||||
r := game.Crafting(c, "Craft", grid, recipes, &cell, game.CraftingOptions{})
|
||||
if r.Crafted {
|
||||
crafted++
|
||||
}
|
||||
if r.Picked >= 0 {
|
||||
picked = r.Picked
|
||||
}
|
||||
})
|
||||
if !tt.HasText("Potion") {
|
||||
t.Fatalf("no result shown: %q", tt.Texts())
|
||||
}
|
||||
tt.Click("CRAFT")
|
||||
grid[4] = data.Item{}
|
||||
tt.Frame()
|
||||
tt.Click("CRAFT")
|
||||
if crafted != 1 || tt.HasText("Potion") {
|
||||
t.Fatalf("crafted %d times; texts %q", crafted, tt.Texts())
|
||||
}
|
||||
r := find(t, tt, "Craft")
|
||||
tt.ClickAt(r.X+float32(2*(22*2+11)), r.Y+float32(2*(22*2+11)))
|
||||
if cell != 8 || picked != 8 {
|
||||
t.Fatalf("clicking the last cell picked %d", picked)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLootPopupRisesAndCloses(t *testing.T) {
|
||||
wd := &world{clk: newClock()}
|
||||
open, closed := true, 0
|
||||
tt := view(500, 300, wd, func(c *ui.Context) {
|
||||
if game.LootPopup(c, data.Item{Sprite: icon.Potion, Name: "POTION", Count: 3}, &open, game.LootPopupOptions{}).Closed {
|
||||
closed++
|
||||
}
|
||||
})
|
||||
if !tt.HasText("YOU GOT: POTION ×3!") {
|
||||
t.Fatalf("texts %q", tt.Texts())
|
||||
}
|
||||
r := find(t, tt, "POTION ×3")
|
||||
x, y := dots(r)
|
||||
top := func() int {
|
||||
for j := range int(r.H) / 2 {
|
||||
if count(t, tt.Image(), x, y+j, int(r.W)/2, 1, pixel.Bad) > 0 {
|
||||
return j
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
low := top()
|
||||
wd.clk.t = wd.clk.t.Add(time.Second)
|
||||
tt.Frame()
|
||||
if rose := low - top(); rose != 8 {
|
||||
t.Fatalf("the potion rose %d dots", rose)
|
||||
}
|
||||
tt.Click("OK")
|
||||
if open || closed != 1 {
|
||||
t.Fatal("OK did not close it")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreasureChestOpensOnce(t *testing.T) {
|
||||
for _, reduce := range []bool{false, true} {
|
||||
wd := &world{clk: newClock(), reduce: reduce}
|
||||
open, opened := false, 0
|
||||
tt := view(200, 200, wd, func(c *ui.Context) {
|
||||
if game.TreasureChest(c, "Chest", data.Item{Sprite: icon.Gem, Name: "Gem"}, &open, game.TreasureChestOptions{}).Opened {
|
||||
opened++
|
||||
}
|
||||
})
|
||||
gem := func() int { return count(t, tt.Image(), 0, 0, 24, 21, pixel.Info) }
|
||||
tt.Click("Chest")
|
||||
if !open || opened != 1 || (gem() > 0) != reduce {
|
||||
t.Fatalf("reduced %v: the press showed the gem %v", reduce, gem() > 0)
|
||||
}
|
||||
wd.clk.t = wd.clk.t.Add(time.Second)
|
||||
tt.Frame()
|
||||
tt.Click("Chest")
|
||||
if gem() == 0 || opened != 1 {
|
||||
t.Fatalf("reduced %v: no gem, or opened twice", reduce)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreasureChestGemStaysInside(t *testing.T) {
|
||||
open := true
|
||||
tt := view(200, 200, &world{clk: newClock()}, func(c *ui.Context) {
|
||||
ui.Column(c).Padding(pixel.K(c).D(20)).Children(func() {
|
||||
game.TreasureChest(c, "Chest", data.Item{Sprite: icon.Gem, Name: "Gem"}, &open, game.TreasureChestOptions{})
|
||||
})
|
||||
})
|
||||
if n, in := count(t, tt.Image(), 0, 0, 100, 20, pixel.Info), count(t, tt.Image(), 20, 20, 24, 50, pixel.Info); n != 0 || in == 0 {
|
||||
t.Fatalf("%d gem dots above the chest, %d in it", n, in)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGachaReelSpinsAndStops(t *testing.T) {
|
||||
faces := []data.Item{{Sprite: icon.Star, Name: "Star"}, {Sprite: icon.Heart, Name: "Heart"}, {Sprite: icon.Coin, Name: "Coin"}}
|
||||
for _, reduce := range []bool{false, true} {
|
||||
wd := &world{clk: newClock(), reduce: reduce}
|
||||
spin, done := 0, 0
|
||||
tt := view(300, 100, wd, func(c *ui.Context) {
|
||||
if game.GachaReel(c, "Reels", faces, [3]int{2, 2, 2}, spin, game.GachaReelOptions{}).Done {
|
||||
done++
|
||||
}
|
||||
})
|
||||
coins := func() int { return count(t, tt.Image(), 0, 0, 84, 28, pixel.Warn) }
|
||||
still := coins()
|
||||
spin = 1
|
||||
tt.Frame()
|
||||
var seen []int
|
||||
for range 4 {
|
||||
wd.clk.t = wd.clk.t.Add(100 * time.Millisecond)
|
||||
tt.Frame()
|
||||
seen = append(seen, coins())
|
||||
}
|
||||
spun := seen[0] != still || seen[1] != still || seen[2] != still
|
||||
if spun == reduce || (done == 1) != reduce {
|
||||
t.Fatalf("reduced %v: spun %v, done %d", reduce, spun, done)
|
||||
}
|
||||
wd.clk.t = wd.clk.t.Add(2 * time.Second)
|
||||
tt.Frame()
|
||||
if done != 1 || coins() != still {
|
||||
t.Fatalf("reduced %v: done %d, coins %d of %d", reduce, done, coins(), still)
|
||||
}
|
||||
// Spinning back to an old cue lands; reduced, both do.
|
||||
spin = 2
|
||||
tt.Frame()
|
||||
spin = 1
|
||||
tt.Frame()
|
||||
wd.clk.t = wd.clk.t.Add(2 * time.Second)
|
||||
tt.Frame()
|
||||
if want := map[bool]int{false: 2, true: 3}[reduce]; done != want {
|
||||
t.Fatalf("reduced %v: spinning back to 1 landed %d times in all", reduce, done)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGachaReelRefusesAFaceWithoutSprite(t *testing.T) {
|
||||
panics(t, "a face with no sprite", func() {
|
||||
view(300, 100, &world{clk: newClock()}, func(c *ui.Context) {
|
||||
game.GachaReel(c, "x", []data.Item{{Sprite: icon.Star, Name: "Star"}, {Name: "Blank"}}, [3]int{}, 0, game.GachaReelOptions{})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestInteractPromptSitsAboveItsTarget(t *testing.T) {
|
||||
var target *ui.Element
|
||||
pad := 40
|
||||
tt := view(400, 300, &world{clk: newClock(), reduce: true}, func(c *ui.Context) {
|
||||
ui.Column(c).Padding(pixel.K(c).D(pad)).Children(func() {
|
||||
target = ui.Box(c).Size(pixel.K(c).D(20), pixel.K(c).D(20)).Label("Mayor")
|
||||
})
|
||||
game.InteractPrompt(c, target, "TALK", game.InteractPromptOptions{})
|
||||
})
|
||||
// Right from the start, and after a move.
|
||||
for range 2 {
|
||||
bubble, mayor := find(t, tt, "A TALK"), find(t, tt, "Mayor")
|
||||
if bubble.Y+bubble.H > mayor.Y || bubble.Y+bubble.H < mayor.Y-20 || bubble.X > mayor.X+mayor.W/2 {
|
||||
t.Fatalf("the bubble %v does not sit above %v", bubble, mayor)
|
||||
}
|
||||
pad = 70
|
||||
tt.Frame()
|
||||
}
|
||||
panics(t, "a two-letter key", func() {
|
||||
view(300, 200, &world{clk: newClock()}, func(c *ui.Context) {
|
||||
game.InteractPrompt(c, ui.Box(c), "TALK", game.InteractPromptOptions{Key: "AB"})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestTurnOrderRaisesTheActive(t *testing.T) {
|
||||
tt := view(200, 60, &world{clk: newClock()}, func(c *ui.Context) {
|
||||
game.TurnOrder(c, "Turns", []game.Fighter{{Name: "Aria", Portrait: game.Hero}, {Name: "Slime", Portrait: icon.Skull, Foe: true}}, 1, game.TurnOrderOptions{})
|
||||
})
|
||||
img := tt.Image()
|
||||
if at(t, img, 19+8, 0) != pixel.Accent || at(t, img, 8, 0) == pixel.Accent || at(t, img, 8, 5) != pixel.Good {
|
||||
t.Fatal("the active portrait is not raised in Accent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBattleLogKeepsTheNewest(t *testing.T) {
|
||||
lines := []game.Line{{Text: "one", Slot: pixel.Good}, {Text: "two"}, {Text: "three"}, {Text: "four"}, {Text: "five", Slot: pixel.Bad}}
|
||||
tt := view(400, 200, &world{clk: newClock()}, func(c *ui.Context) { game.BattleLog(c, "Log", lines, game.BattleLogOptions{}) })
|
||||
// 68 dots hold four lines: the first rolled off.
|
||||
if count(t, tt.Image(), 0, 0, 180, 68, pixel.Good) != 0 || count(t, tt.Image(), 0, 50, 180, 18, pixel.Bad) == 0 {
|
||||
t.Fatal("the log does not keep the newest lines at the bottom")
|
||||
}
|
||||
// Two lines sit at the bottom, not the top.
|
||||
lines = lines[3:]
|
||||
lines[0].Slot = pixel.Good
|
||||
tt.Frame()
|
||||
if count(t, tt.Image(), 0, 34, 180, 16, pixel.Good) == 0 || count(t, tt.Image(), 0, 0, 180, 30, pixel.Good) != 0 {
|
||||
t.Fatal("a short log is not at the bottom")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTargetSelectorMovesTheCursor(t *testing.T) {
|
||||
sel, chosen := 0, -1
|
||||
enemies := []game.Enemy{{Name: "Slime", Sprite: icon.Skull, HP: 3, MaxHP: 9}, {Name: "Bat", Sprite: icon.Bug, HP: 6, MaxHP: 6}}
|
||||
tt := view(300, 200, &world{clk: newClock()}, func(c *ui.Context) {
|
||||
if r := game.TargetSelector(c, "Targets", enemies, &sel, game.TargetSelectorOptions{}); r.Chosen >= 0 {
|
||||
chosen = r.Chosen
|
||||
}
|
||||
})
|
||||
cursor := func(i int) int { return count(t, tt.Image(), i*40, 0, 40, 12, pixel.Accent) }
|
||||
if cursor(0) == 0 || cursor(1) != 0 {
|
||||
t.Fatal("the ▼ is not over the first target")
|
||||
}
|
||||
tt.Click("Targets")
|
||||
tt.Key(0, ui.KeyHome)
|
||||
tt.Key(0, ui.KeyRight)
|
||||
tt.Key(0, ui.KeyEnter)
|
||||
if sel != 1 || chosen != 1 || cursor(1) == 0 {
|
||||
t.Fatalf("Right, Enter chose %d", chosen)
|
||||
}
|
||||
// A long name stays in its 40-dot cell.
|
||||
enemies[0].Name = "Slime of the Very Long Name"
|
||||
tt.Frame()
|
||||
if n := count(t, tt.Image(), 80, 36, 60, 20, pixel.Ink); n != 0 {
|
||||
t.Fatalf("%d name dots spilled past the cells", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestATBGaugeFlashesReady(t *testing.T) {
|
||||
for _, reduce := range []bool{false, true} {
|
||||
wd := &world{clk: newClock(), reduce: reduce}
|
||||
v := 0.5
|
||||
tt := view(200, 60, wd, func(c *ui.Context) { game.ATBGauge(c, "ARIA", v, game.ATBGaugeOptions{}) })
|
||||
if count(t, tt.Image(), 0, 9, 64, 1, pixel.Info) != 30 {
|
||||
t.Fatal("half a gauge is not 30 dots")
|
||||
}
|
||||
v = 1
|
||||
tt.Frame()
|
||||
var ready []int
|
||||
for range 2 {
|
||||
ready = append(ready, count(t, tt.Image(), 30, 0, 34, 6, pixel.Warn))
|
||||
wd.clk.t = wd.clk.t.Add(125 * time.Millisecond)
|
||||
tt.Frame()
|
||||
}
|
||||
if flashed := (ready[0] == 0) != (ready[1] == 0); flashed == reduce || ready[0]+ready[1] == 0 {
|
||||
t.Fatalf("reduced %v: READY dots %v", reduce, ready)
|
||||
}
|
||||
}
|
||||
zh := view(200, 60, &world{clk: newClock(), zh: true}, func(c *ui.Context) { game.ATBGauge(c, "ARIA", 1, game.ATBGaugeOptions{}) })
|
||||
// 就绪 needs a 14-row label line above the 8-row bar.
|
||||
if count(t, zh.Image(), 20, 0, 44, 14, pixel.Warn) == 0 || find(t, zh, "ARIA").H != 2*(14+8) {
|
||||
t.Fatal("a Chinese READY is missing or crowds the bar")
|
||||
}
|
||||
panics(t, "a gauge too narrow", func() {
|
||||
view(200, 60, &world{clk: newClock()}, func(c *ui.Context) { game.ATBGauge(c, "ARIA", 1, game.ATBGaugeOptions{Width: 10}) })
|
||||
})
|
||||
}
|
||||
|
||||
func TestStaminaBarDithersThePartBlock(t *testing.T) {
|
||||
tt := view(200, 60, &world{clk: newClock()}, func(c *ui.Context) { game.StaminaBar(c, "STAMINA", 0.65, game.StaminaBarOptions{}) })
|
||||
img := tt.Image()
|
||||
// Six blocks full, the seventh half, the rest empty.
|
||||
if full, part, empty := count(t, img, 0, 7, 36, 6, pixel.Good), count(t, img, 36, 7, 5, 6, pixel.Good), count(t, img, 42, 7, 18, 6, pixel.Good); full != 180 || part == 0 || part == 30 || empty != 0 {
|
||||
t.Fatalf("blocks: full %d, part %d, empty %d", full, part, empty)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user