Health, mana and experience bars, quarter hearts, lives, a spinning coin counter, score board, stage timer, minimap with fog, compass, hotbar, paper-doll equipment, skill tree, skill buttons with cooldown wipes, status effects, a boss bar, damage numbers, quest log and tracker, party frames, character sheet, level-up screen and shop. data exports DrawSlot so every slot looks alike; pixel adds TextVeil for dissolving text. Messages the font cannot draw are fixed and TestMessagesHaveGlyphs holds every message to the font.
209 lines
6.8 KiB
Go
209 lines
6.8 KiB
Go
package game_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"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"
|
|
)
|
|
|
|
var bag = []data.Item{{Sprite: icon.Potion, Name: "Potion", Count: 5}, {Sprite: icon.Sword, Name: "Sword"}, {Sprite: icon.Bolt, Name: "Bolt"}, {}, {Sprite: icon.Key, Name: "Key"}}
|
|
|
|
func TestHotbarKeysWheelAndClicks(t *testing.T) {
|
|
sel, changes, used := 0, 0, -1
|
|
off := false
|
|
wd := &world{clk: newClock()}
|
|
tt := view(400, 100, wd, func(c *ui.Context) {
|
|
r := game.Hotbar(c, "Hotbar", bag, &sel, game.HotbarOptions{Disabled: off})
|
|
if r.Changed {
|
|
changes++
|
|
}
|
|
if r.Used >= 0 {
|
|
used = r.Used
|
|
}
|
|
})
|
|
// Slot i is 22 dots wide, its middle at 22i+11.
|
|
tt.ClickAt(2*(22*2+11), 22)
|
|
if sel != 2 || used != 2 || changes != 1 {
|
|
t.Fatalf("click: slot %d, used %d, %d changes", sel, used, changes)
|
|
}
|
|
tt.Key(0, ui.Key5)
|
|
tt.Key(0, ui.Key9)
|
|
tt.Key(0, ui.KeyLeft)
|
|
if sel != 3 || changes != 3 {
|
|
t.Fatalf("5, 9, Left: slot %d, %d changes", sel, changes)
|
|
}
|
|
// A notch is a 16-dot line; the bar wraps.
|
|
tt.Scroll(22, 22, 0, 32)
|
|
tt.Scroll(22, 22, 0, 32)
|
|
tt.Scroll(22, 22, 0, -16)
|
|
if sel != 0 || changes != 5 {
|
|
t.Fatalf("two notches and a half: slot %d, %d changes", sel, changes)
|
|
}
|
|
tt.Key(0, ui.KeyEnter)
|
|
if used != 0 {
|
|
t.Fatalf("Enter used %d", used)
|
|
}
|
|
keeps(t, tt.Image())
|
|
off = true
|
|
tt.Frame()
|
|
tt.Key(0, ui.Key3)
|
|
tt.Scroll(22, 22, 0, 32)
|
|
if sel != 0 {
|
|
t.Fatalf("a disabled bar moved to %d", sel)
|
|
}
|
|
panics(t, "ten slots", func() {
|
|
view(400, 100, wd, func(c *ui.Context) {
|
|
game.Hotbar(c, "x", make([]data.Item, 10), &sel, game.HotbarOptions{})
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestInertHotbarIgnoresTheWheel(t *testing.T) {
|
|
sel := 0
|
|
wd := &world{clk: newClock(), inert: true}
|
|
tt := view(400, 100, wd, func(c *ui.Context) { game.Hotbar(c, "Hotbar", bag, &sel, game.HotbarOptions{}) })
|
|
tt.Scroll(22, 22, 0, 32)
|
|
tt.ClickAt(2*(22*3+11), 22)
|
|
if sel != 0 {
|
|
t.Fatalf("an inert bar moved to %d", sel)
|
|
}
|
|
}
|
|
|
|
func TestEquipmentWalksTheDoll(t *testing.T) {
|
|
gear := []data.Item{{Sprite: icon.Crown, Name: "Crown"}, {}, {Sprite: icon.Sword, Name: "Iron sword"}, {}, {}}
|
|
sel, picked := game.Weapon, -1
|
|
tt := view(400, 200, &world{clk: newClock()}, func(c *ui.Context) {
|
|
if r := game.Equipment(c, "Gear", gear, &sel, game.EquipmentOptions{}); r.Picked >= 0 {
|
|
picked = r.Picked
|
|
}
|
|
})
|
|
r := find(t, tt, "Gear")
|
|
// The doll is centered under the widest caption.
|
|
w := 3*26 - 4
|
|
for _, s := range []string{"HEAD: Crown", "BODY", "WEAPON: Iron sword", "SHIELD", "RING"} {
|
|
w = max(w, pixel.Measure(s))
|
|
}
|
|
left := (w - (3*26 - 4)) / 2
|
|
click := func(x, y int) { tt.ClickAt(r.X+float32(2*(left+x*26+11)), r.Y+float32(2*(y*26+11))) }
|
|
click(2, 1)
|
|
if sel != game.Shield || picked != game.Shield {
|
|
t.Fatalf("clicking the shield slot chose %d, picked %d", sel, picked)
|
|
}
|
|
// Left of the doll, nothing is hit.
|
|
tt.ClickAt(r.X+float32(2*(left-5)), r.Y+float32(2*(26+11)))
|
|
if sel != game.Shield {
|
|
t.Fatalf("a click beside the doll chose %d", sel)
|
|
}
|
|
for _, step := range []struct {
|
|
key ui.Key
|
|
want int
|
|
}{{ui.KeyLeft, game.Body}, {ui.KeyUp, game.Head}, {ui.KeyLeft, game.Weapon}, {ui.KeyDown, game.Ring}, {ui.KeyUp, game.Weapon}, {ui.KeyRight, game.Body}, {ui.KeyDown, game.Ring}} {
|
|
tt.Key(0, step.key)
|
|
if sel != step.want {
|
|
t.Fatalf("%v went to %d, not %d", step.key, sel, step.want)
|
|
}
|
|
}
|
|
tt.Key(0, ui.KeySpace)
|
|
if picked != game.Ring {
|
|
t.Fatalf("Space picked %d", picked)
|
|
}
|
|
keeps(t, tt.Image())
|
|
panics(t, "four slots", func() {
|
|
view(400, 200, &world{clk: newClock()}, func(c *ui.Context) {
|
|
game.Equipment(c, "x", gear[:4], &sel, game.EquipmentOptions{})
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestSkillTreeLearnsOpenSkillsWithPoints(t *testing.T) {
|
|
skills := []game.Skill{
|
|
{Name: "Slash", Sprite: icon.Sword, At: game.Spot{X: 1}, Learned: true},
|
|
{Name: "Guard", Sprite: icon.Shield, At: game.Spot{Y: 1}, Needs: []int{0}},
|
|
{Name: "Fire", Sprite: icon.Flame, At: game.Spot{X: 2, Y: 1}, Needs: []int{0}, Learned: true},
|
|
{Name: "Meteor", Sprite: icon.Star, At: game.Spot{X: 2, Y: 2}, Needs: []int{2, 1}},
|
|
}
|
|
cur, points, learned := 0, 1, -1
|
|
tt := view(400, 200, &world{clk: newClock()}, func(c *ui.Context) {
|
|
if r := game.SkillTree(c, "Skills", skills, &cur, &points, game.SkillTreeOptions{}); r.Learned >= 0 {
|
|
learned = r.Learned
|
|
}
|
|
})
|
|
r := find(t, tt, "Skills")
|
|
// Nodes sit 28 dots apart under a 20-dot header.
|
|
node := func(x, y int) { tt.ClickAt(r.X+float32(2*(x*28+10)), r.Y+float32(2*(20+y*28+10))) }
|
|
node(1, 0)
|
|
tt.Key(0, ui.KeyEnter)
|
|
if points != 1 {
|
|
t.Fatal("a learned skill took a point")
|
|
}
|
|
node(2, 2)
|
|
tt.Key(0, ui.KeyEnter)
|
|
if cur != 3 || skills[3].Learned || points != 1 {
|
|
t.Fatal("a locked skill was learned")
|
|
}
|
|
node(0, 1)
|
|
if cur != 1 || skills[1].Learned {
|
|
t.Fatal("the first click on a skill learned it")
|
|
}
|
|
node(0, 1)
|
|
if !skills[1].Learned || points != 0 || learned != 1 {
|
|
t.Fatalf("the second click: learned %d, %d points", learned, points)
|
|
}
|
|
tt.Key(0, ui.KeyRight)
|
|
tt.Key(0, ui.KeyDown)
|
|
tt.Key(0, ui.KeyEnter)
|
|
if cur != 3 || skills[3].Learned {
|
|
t.Fatal("Meteor was learned with no points")
|
|
}
|
|
points = 1
|
|
tt.Key(0, ui.KeyEnter)
|
|
if !skills[3].Learned || points != 0 || learned != 3 {
|
|
t.Fatal("Meteor was not learned with a point")
|
|
}
|
|
keeps(t, tt.Image())
|
|
first := 0
|
|
panics(t, "skills needing each other", func() {
|
|
view(400, 200, &world{clk: newClock()}, func(c *ui.Context) {
|
|
game.SkillTree(c, "x", []game.Skill{{Name: "a", Sprite: icon.Star, Needs: []int{1}}, {Name: "b", Sprite: icon.Star, At: game.Spot{X: 1}, Needs: []int{0}}}, &first, &points, game.SkillTreeOptions{})
|
|
})
|
|
})
|
|
panics(t, "a skill off the grid", func() {
|
|
view(400, 200, &world{clk: newClock()}, func(c *ui.Context) {
|
|
game.SkillTree(c, "x", []game.Skill{{Name: "a", Sprite: icon.Star, At: game.Spot{X: -1}}}, &first, &points, game.SkillTreeOptions{})
|
|
})
|
|
})
|
|
panics(t, "a skill needing itself", func() {
|
|
view(400, 200, &world{clk: newClock()}, func(c *ui.Context) {
|
|
game.SkillTree(c, "x", []game.Skill{{Name: "a", Sprite: icon.Star, Needs: []int{0}}}, &cur, &points, game.SkillTreeOptions{})
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestSkillButtonFiresOnlyWhenReady(t *testing.T) {
|
|
cool, clicks := 0.5, 0
|
|
tt := view(200, 100, &world{clk: newClock()}, func(c *ui.Context) {
|
|
if game.SkillButton(c, "Fireball", icon.Flame, "Q", cool, game.SkillButtonOptions{}).Clicked {
|
|
clicks++
|
|
}
|
|
})
|
|
// The wipe darkens the left half for half a cooldown.
|
|
wiped := count(t, tt.Image(), 2, 2, 9, 20, pixel.Edge)
|
|
if err := tt.Click("Fireball"); err != nil || clicks != 0 {
|
|
t.Fatalf("a cooling skill fired: %v", err)
|
|
}
|
|
cool = 0
|
|
tt.Frame()
|
|
if n := count(t, tt.Image(), 2, 2, 9, 20, pixel.Edge); wiped < n+50 {
|
|
t.Fatalf("wipe dots %d, ready %d", wiped, n)
|
|
}
|
|
tt.Click("Fireball")
|
|
if clicks != 1 {
|
|
t.Fatal("a ready skill did not fire")
|
|
}
|
|
}
|