308 lines
7.7 KiB
Go
308 lines
7.7 KiB
Go
package tween
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"aego/contracts/scriptwait"
|
|
"aego/core/curve"
|
|
"aego/core/mathx"
|
|
"aego/engine"
|
|
"aego/scene"
|
|
"aego/schema"
|
|
"aego/script/abi"
|
|
)
|
|
|
|
const dt = float32(1) / 64
|
|
|
|
type probe struct {
|
|
Value float32 `aego:"value"`
|
|
Count int32 `aego:"count"`
|
|
Tint mathx.Color `aego:"tint"`
|
|
Pos mathx.Vec2 `aego:"pos"`
|
|
Name string `aego:"name"`
|
|
}
|
|
|
|
type waker struct {
|
|
calls []uint64
|
|
types []uint64
|
|
}
|
|
|
|
func (w *waker) Complete(typ, id uint64) {
|
|
w.types = append(w.types, typ)
|
|
w.calls = append(w.calls, id)
|
|
}
|
|
|
|
func newScene(t *testing.T) *scene.Scene {
|
|
t.Helper()
|
|
types := scene.NewTypes(schema.NewRegistry())
|
|
if err := scene.RegisterComponent[probe](types, schema.Name("test.Probe")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := types.Schema().Freeze(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return scene.New(types, nil, scene.Options{})
|
|
}
|
|
|
|
func setup(t *testing.T) (*Service, *scene.Scene, scene.NodeHandle) {
|
|
t.Helper()
|
|
s := newScene(t)
|
|
h := s.Create(s.Root(), "n")
|
|
scene.Add[probe](s, h)
|
|
s.Flush()
|
|
return NewService(s, nil), s, h
|
|
}
|
|
|
|
func get(s *scene.Scene, h scene.NodeHandle) *probe {
|
|
p, _ := scene.Get[probe](s, h)
|
|
return p
|
|
}
|
|
|
|
func steps(svc *Service, n int) {
|
|
for range n {
|
|
svc.Step(dt, false)
|
|
}
|
|
}
|
|
|
|
func TestLinearEndsExactlyWithoutOvershoot(t *testing.T) {
|
|
svc, s, h := setup(t)
|
|
tw, err := svc.ToFloat(h, "Probe.value", 10, 1, Options{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
prev := float32(0)
|
|
for range 63 {
|
|
svc.Step(dt, false)
|
|
v := get(s, h).Value
|
|
if v < prev || v > 10 {
|
|
t.Fatalf("value %v after %v", v, prev)
|
|
}
|
|
prev = v
|
|
}
|
|
if svc.Done(tw) {
|
|
t.Fatal("finished early")
|
|
}
|
|
svc.Step(dt, false)
|
|
if v := get(s, h).Value; v != 10 || !svc.Done(tw) {
|
|
t.Fatalf("end value %v, done %v", v, svc.Done(tw))
|
|
}
|
|
}
|
|
|
|
func TestDelayReadsStartWhenItEnds(t *testing.T) {
|
|
svc, s, h := setup(t)
|
|
if _, err := svc.ToFloat(h, "Probe.value", 4, 1, Options{Delay: 0.5}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
steps(svc, 16)
|
|
get(s, h).Value = 2
|
|
steps(svc, 16)
|
|
if v := get(s, h).Value; v != 2 {
|
|
t.Fatalf("value moved during the delay: %v", v)
|
|
}
|
|
steps(svc, 32)
|
|
if v := get(s, h).Value; v != 3 {
|
|
t.Fatalf("halfway from 2 to 4 is 3, got %v", v)
|
|
}
|
|
}
|
|
|
|
func TestPingPongEndsAtStart(t *testing.T) {
|
|
svc, s, h := setup(t)
|
|
get(s, h).Value = 1
|
|
tw, _ := svc.ToFloat(h, "Probe.value", 5, 0.5, Options{Legs: 2, PingPong: true})
|
|
steps(svc, 32)
|
|
if v := get(s, h).Value; v != 5 {
|
|
t.Fatalf("after the first leg %v", v)
|
|
}
|
|
steps(svc, 32)
|
|
if v := get(s, h).Value; v != 1 || !svc.Done(tw) {
|
|
t.Fatalf("ping-pong must end at the start, got %v", v)
|
|
}
|
|
}
|
|
|
|
func TestStopKeepsCurrentValue(t *testing.T) {
|
|
svc, s, h := setup(t)
|
|
tw, _ := svc.ToFloat(h, "Probe.value", 8, 1, Options{Legs: -1, PingPong: true})
|
|
steps(svc, 96)
|
|
at := get(s, h).Value
|
|
svc.Stop(tw)
|
|
steps(svc, 10)
|
|
if v := get(s, h).Value; v != at || v == 8 || !svc.Done(tw) {
|
|
t.Fatalf("stop jumped: %v → %v", at, v)
|
|
}
|
|
}
|
|
|
|
func TestDestroyedNodeStopsTween(t *testing.T) {
|
|
svc, s, h := setup(t)
|
|
tw, _ := svc.ToFloat(h, "Probe.value", 1, 1, Options{})
|
|
s.Destroy(h)
|
|
svc.Step(dt, false)
|
|
if !svc.Done(tw) || svc.Tweens() != 0 {
|
|
t.Fatal("tween of a destroyed node is still running")
|
|
}
|
|
}
|
|
|
|
func TestUnscaledRunsOnlyInItsDomain(t *testing.T) {
|
|
svc, s, h := setup(t)
|
|
svc.ToFloat(h, "Probe.value", 1, 1, Options{Unscaled: true})
|
|
steps(svc, 16)
|
|
if get(s, h).Value != 0 {
|
|
t.Fatal("game time moved an unscaled tween")
|
|
}
|
|
for range 16 {
|
|
svc.Step(dt, true)
|
|
}
|
|
if v := get(s, h).Value; v != 0.25 {
|
|
t.Fatalf("unscaled value %v", v)
|
|
}
|
|
}
|
|
|
|
func TestLanesNamesAndKinds(t *testing.T) {
|
|
svc, s, h := setup(t)
|
|
get(s, h).Tint = mathx.Color{R: 1, G: 1, B: 1, A: 1}
|
|
if _, err := svc.ToFloat(h, "Probe.Tint.a", 0, 0.25, Options{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := svc.ToFloat(h, "test.Probe.count", 5, 0.25, Options{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := svc.ToVec2(h, "Probe.pos", mathx.Vec2{X: 2, Y: -2}, 0.25, Options{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
steps(svc, 16)
|
|
p := get(s, h)
|
|
if p.Tint != (mathx.Color{R: 1, G: 1, B: 1}) || p.Count != 5 || p.Pos != (mathx.Vec2{X: 2, Y: -2}) {
|
|
t.Fatalf("probe %+v", p)
|
|
}
|
|
if _, err := svc.ToVec2(h, "Probe.value", mathx.Vec2{}, 1, Options{}); err == nil {
|
|
t.Fatal("a Vec2 tween on a float field must fail")
|
|
}
|
|
if _, err := svc.ToFloat(h, "Probe.name", 1, 1, Options{}); err == nil {
|
|
t.Fatal("a string field must not tween")
|
|
}
|
|
if _, err := svc.ToFloat(h, "Nope.value", 1, 1, Options{}); err == nil {
|
|
t.Fatal("unknown component accepted")
|
|
}
|
|
}
|
|
|
|
func TestTransformPosition(t *testing.T) {
|
|
svc, s, h := setup(t)
|
|
if _, err := svc.Position(h, mathx.Vec2{X: 4, Y: 8}, 0.5, Options{Ease: curve.InOutSine}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
steps(svc, 32)
|
|
if p := s.Local(h).Position; p != (mathx.Vec2{X: 4, Y: 8}) {
|
|
t.Fatalf("position %+v", p)
|
|
}
|
|
}
|
|
|
|
func TestNewTweenReplacesRunningOne(t *testing.T) {
|
|
svc, s, h := setup(t)
|
|
a, _ := svc.ToFloat(h, "Probe.value", 10, 1, Options{})
|
|
steps(svc, 16)
|
|
b, _ := svc.ToFloat(h, "Probe.value", 0, 0.25, Options{})
|
|
if !svc.Done(a) || svc.Done(b) {
|
|
t.Fatal("the second tween must stop the first")
|
|
}
|
|
steps(svc, 16)
|
|
if v := get(s, h).Value; v != 0 {
|
|
t.Fatalf("value %v", v)
|
|
}
|
|
}
|
|
|
|
func TestCurveTween(t *testing.T) {
|
|
svc, s, h := setup(t)
|
|
c := curve.Keyed(curve.Key{T: 0, V: 0, Interp: curve.InterpStep}, curve.Key{T: 1, V: 1})
|
|
svc.ToFloat(h, "Probe.value", 10, 1, Options{Curve: &c})
|
|
steps(svc, 32)
|
|
if v := get(s, h).Value; v != 0 {
|
|
t.Fatalf("a step curve holds the start, got %v", v)
|
|
}
|
|
steps(svc, 32)
|
|
if v := get(s, h).Value; v != 10 {
|
|
t.Fatalf("end %v", v)
|
|
}
|
|
}
|
|
|
|
func TestWakerIsCalled(t *testing.T) {
|
|
s := newScene(t)
|
|
h := s.Create(s.Root(), "n")
|
|
scene.Add[probe](s, h)
|
|
s.Flush()
|
|
w := engine.NewWorld("t")
|
|
wk := &waker{}
|
|
w.SetService(scriptwait.ServiceID, scriptwait.Waker(wk))
|
|
svc := NewService(s, w)
|
|
tw, _ := svc.ToFloat(h, "Probe.value", 1, 0.25, Options{})
|
|
tm, _ := svc.StartTimer(0.25, TimerOptions{Repeat: 2})
|
|
steps(svc, 16)
|
|
if len(wk.calls) != 2 || wk.calls[0] != tw.Key() || wk.types[0] != abi.HandleID(KindTween) ||
|
|
wk.calls[1] != tm.Key() || wk.types[1] != abi.HandleID(KindTimer) {
|
|
t.Fatalf("waker calls %v %v", wk.types, wk.calls)
|
|
}
|
|
steps(svc, 16)
|
|
if len(wk.calls) != 3 || !svc.TimerDone(tm) {
|
|
t.Fatalf("second fire: %v", wk.calls)
|
|
}
|
|
}
|
|
|
|
func TestTimers(t *testing.T) {
|
|
svc, _, _ := setup(t)
|
|
tm, _ := svc.StartTimer(0.5, TimerOptions{Repeat: 2})
|
|
steps(svc, 32)
|
|
if svc.Fired(tm) != 1 || svc.TimerDone(tm) {
|
|
t.Fatalf("fired %d", svc.Fired(tm))
|
|
}
|
|
svc.Pause(tm)
|
|
steps(svc, 64)
|
|
if svc.Fired(tm) != 1 {
|
|
t.Fatal("a paused timer fired")
|
|
}
|
|
svc.Resume(tm)
|
|
steps(svc, 32)
|
|
if !svc.TimerDone(tm) || svc.Timers() != 0 {
|
|
t.Fatal("a finished timer without Keep must free its slot")
|
|
}
|
|
cd, _ := svc.Cooldown(0.25, TimerOptions{})
|
|
if !svc.Ready(cd) || !svc.Use(cd) || svc.Ready(cd) || svc.Use(cd) {
|
|
t.Fatal("cooldown starts ready and is used once")
|
|
}
|
|
steps(svc, 16)
|
|
if !svc.Ready(cd) {
|
|
t.Fatal("cooldown did not recharge")
|
|
}
|
|
}
|
|
|
|
func TestOwnedTimerDiesWithNode(t *testing.T) {
|
|
svc, s, h := setup(t)
|
|
tm, _ := svc.StartTimer(1, TimerOptions{Keep: true, Owner: h})
|
|
s.Destroy(h)
|
|
svc.Step(dt, false)
|
|
if !svc.TimerDone(tm) || svc.Timers() != 0 {
|
|
t.Fatal("owner death must drop the timer")
|
|
}
|
|
}
|
|
|
|
func TestThousandTweensAllocateNothing(t *testing.T) {
|
|
s := newScene(t)
|
|
var nodes []scene.NodeHandle
|
|
for range 1000 {
|
|
h := s.Create(s.Root(), "n")
|
|
scene.Add[probe](s, h)
|
|
nodes = append(nodes, h)
|
|
}
|
|
s.Flush()
|
|
svc := NewService(s, nil)
|
|
for _, h := range nodes {
|
|
if _, err := svc.ToVec2(h, "Probe.pos", mathx.Vec2{X: 1, Y: 1}, 0.5, Options{Legs: -1, PingPong: true, Ease: curve.OutBack}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
for range 100 {
|
|
svc.StartTimer(0.3, TimerOptions{Repeat: -1})
|
|
}
|
|
steps(svc, 4)
|
|
if n := testing.AllocsPerRun(100, func() { svc.Step(dt, false) }); n != 0 {
|
|
t.Fatalf("Step allocates %v", n)
|
|
}
|
|
}
|