251 lines
6.4 KiB
Go
251 lines
6.4 KiB
Go
package integration
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"aego/contracts/scriptevents"
|
|
"aego/core/log"
|
|
"aego/core/mathx"
|
|
"aego/engine"
|
|
"aego/engine/module"
|
|
"aego/modules/physics"
|
|
"aego/modules/physics/components"
|
|
"aego/modules/physics/stdlib"
|
|
"aego/modules/physics/systems"
|
|
"aego/modules/physics/world"
|
|
"aego/scene"
|
|
"aego/schema"
|
|
"aego/script/abi"
|
|
"aego/script/bind"
|
|
"aego/script/bind/manifest"
|
|
"aego/script/vm"
|
|
)
|
|
|
|
type physEvent struct {
|
|
node scene.NodeHandle
|
|
id scriptevents.EventID
|
|
args []vm.Value
|
|
}
|
|
|
|
type fakeSink struct {
|
|
got []physEvent
|
|
listen map[scriptevents.EventID]bool
|
|
}
|
|
|
|
func (s *fakeSink) Emit(node scene.NodeHandle, id scriptevents.EventID, args []vm.Value) {
|
|
s.got = append(s.got, physEvent{node: node, id: id, args: append([]vm.Value(nil), args...)})
|
|
}
|
|
|
|
func (s *fakeSink) Flush() {}
|
|
|
|
func (s *fakeSink) ListensOn(scene.NodeHandle, scriptevents.EventID) bool {
|
|
return false
|
|
}
|
|
|
|
type listeningSink struct {
|
|
fakeSink
|
|
}
|
|
|
|
func (s *listeningSink) ListensOn(_ scene.NodeHandle, id scriptevents.EventID) bool {
|
|
return s.listen[id]
|
|
}
|
|
|
|
func physicsScene(t *testing.T) *scene.Scene {
|
|
t.Helper()
|
|
types := scene.NewTypes(schema.NewRegistry())
|
|
if _, err := module.Load(types, nil, physics.New()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := types.Schema().Freeze(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return scene.New(types, nil, scene.Options{})
|
|
}
|
|
|
|
func physBox(s *scene.Scene, name string, at, size mathx.Vec2, typ components.BodyType, body bool) scene.NodeHandle {
|
|
h := s.Create(s.Root(), name)
|
|
s.SetPosition(h, at)
|
|
if body {
|
|
b := scene.Add[components.Body2D](s, h)
|
|
*b = components.DefaultBody2D()
|
|
b.Type = typ
|
|
}
|
|
c := scene.Add[components.BoxCollider2D](s, h)
|
|
*c = components.DefaultBox()
|
|
c.Size = size
|
|
return h
|
|
}
|
|
|
|
func TestPhysicsCollisionEvents(t *testing.T) {
|
|
s := physicsScene(t)
|
|
ground := physBox(s, "Ground", mathx.Vec2{Y: 5}, mathx.Vec2{X: 20, Y: 1}, components.Static, false)
|
|
crate := physBox(s, "Crate", mathx.Vec2{Y: 3}, mathx.Vec2{X: 1, Y: 1}, components.Dynamic, true)
|
|
var mat [32]uint32
|
|
for i := range mat {
|
|
mat[i] = ^uint32(0)
|
|
}
|
|
sim, err := systems.New(s, world.DefaultConfig(), mat, log.Nop())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer sim.Close()
|
|
lib := stdlib.Install(bind.New())
|
|
if lib == nil {
|
|
t.Fatal("stdlib did not install")
|
|
}
|
|
ids := stdlib.EventIDs{Enter: 0, Stay: 1, Exit: 2, TriggerEnter: 3, TriggerExit: 4}
|
|
em := &stdlib.Emitter{IDs: ids, Lib: lib}
|
|
sink := &listeningSink{fakeSink{listen: map[scriptevents.EventID]bool{}}}
|
|
w := engine.NewWorld("play")
|
|
w.SetService(systems.ServiceID, sim)
|
|
w.SetService(scriptevents.ServiceID, scriptevents.Sink(sink))
|
|
ctx := &engine.Context{World: w}
|
|
tick := func() {
|
|
sink.got = sink.got[:0]
|
|
s.Flush()
|
|
sim.Sync()
|
|
sim.Step(1.0 / 60)
|
|
sim.Writeback()
|
|
em.Run(ctx)
|
|
}
|
|
count := func(id scriptevents.EventID) int {
|
|
n := 0
|
|
for _, e := range sink.got {
|
|
if e.id == id {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
entered := false
|
|
for i := 0; i < 120 && !entered; i++ {
|
|
tick()
|
|
if count(ids.Enter) == 0 {
|
|
continue
|
|
}
|
|
entered = true
|
|
for _, e := range sink.got {
|
|
if e.id != ids.Enter {
|
|
continue
|
|
}
|
|
c := lib.Contact(e.args[1:])
|
|
other := bind.NodeOf(e.args[0])
|
|
switch e.node {
|
|
case crate:
|
|
if other != ground || c.Normal.Y >= 0 || c.SelfCollider != crate || c.OtherCollider != ground {
|
|
t.Fatalf("crate contact %+v other %v", c, other)
|
|
}
|
|
case ground:
|
|
if other != crate || c.Normal.Y <= 0 {
|
|
t.Fatalf("ground contact %+v", c)
|
|
}
|
|
default:
|
|
t.Fatalf("event for an unexpected node %v", e.node)
|
|
}
|
|
}
|
|
if count(ids.Enter) != 2 {
|
|
t.Fatalf("enter events %d", count(ids.Enter))
|
|
}
|
|
}
|
|
if !entered {
|
|
t.Fatal("crate never touched the ground")
|
|
}
|
|
tick()
|
|
if count(ids.Stay) != 0 {
|
|
t.Fatal("stay emitted without a listener")
|
|
}
|
|
sink.listen[ids.Stay] = true
|
|
tick()
|
|
if count(ids.Stay) != 2 {
|
|
t.Fatalf("stay events %d", count(ids.Stay))
|
|
}
|
|
s.Destroy(crate)
|
|
tick()
|
|
if count(ids.Exit) != 2 {
|
|
t.Fatalf("exit events %d", count(ids.Exit))
|
|
}
|
|
}
|
|
|
|
func TestPhysicsTriggerEvents(t *testing.T) {
|
|
s := physicsScene(t)
|
|
zone := s.Create(s.Root(), "Zone")
|
|
s.SetPosition(zone, mathx.Vec2{Y: 3})
|
|
z := scene.Add[components.BoxCollider2D](s, zone)
|
|
*z = components.DefaultBox()
|
|
z.IsTrigger, z.Size = true, mathx.Vec2{X: 4, Y: 1}
|
|
ball := physBox(s, "Ball", mathx.Vec2{}, mathx.Vec2{X: 0.5, Y: 0.5}, components.Dynamic, true)
|
|
var mat [32]uint32
|
|
for i := range mat {
|
|
mat[i] = ^uint32(0)
|
|
}
|
|
sim, _ := systems.New(s, world.DefaultConfig(), mat, log.Nop())
|
|
defer sim.Close()
|
|
lib := stdlib.Install(bind.New())
|
|
ids := stdlib.EventIDs{Enter: 0, Stay: 1, Exit: 2, TriggerEnter: 3, TriggerExit: 4}
|
|
em := &stdlib.Emitter{IDs: ids, Lib: lib}
|
|
sink := &fakeSink{}
|
|
w := engine.NewWorld("play")
|
|
w.SetService(systems.ServiceID, sim)
|
|
w.SetService(scriptevents.ServiceID, scriptevents.Sink(sink))
|
|
enter, exit := 0, 0
|
|
for i := 0; i < 180; i++ {
|
|
s.Flush()
|
|
sim.Sync()
|
|
sim.Step(1.0 / 60)
|
|
sim.Writeback()
|
|
sink.got = sink.got[:0]
|
|
em.Run(&engine.Context{World: w})
|
|
for _, e := range sink.got {
|
|
switch e.id {
|
|
case ids.TriggerEnter:
|
|
enter++
|
|
if len(e.args) != 1 {
|
|
t.Fatalf("trigger args %d", len(e.args))
|
|
}
|
|
case ids.TriggerExit:
|
|
exit++
|
|
case ids.Enter:
|
|
t.Fatal("trigger produced a collision event")
|
|
}
|
|
}
|
|
}
|
|
if enter != 2 || exit != 2 {
|
|
t.Fatalf("trigger enter %d exit %d", enter, exit)
|
|
}
|
|
_ = ball
|
|
}
|
|
|
|
func TestPhysicsScriptSurface(t *testing.T) {
|
|
types := scene.NewTypes(schema.NewRegistry())
|
|
b := bind.New()
|
|
if _, err := module.Load(types, b, physics.New()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m, err := manifest.FromRegistry(b, types.Schema())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, fn := range []string{"Raycast", "Linecast", "RaycastAll", "OverlapPoint", "OverlapCircle", "OverlapBox", "Gravity", "SetGravity", "Layer", "LayerMask"} {
|
|
if _, ok := m.FuncDoc(stdlib.ModPhysics, fn); !ok {
|
|
t.Errorf("aego/physics.%s is missing", fn)
|
|
}
|
|
}
|
|
for _, st := range []string{"Contact", "RayHit"} {
|
|
if _, ok := m.StructDoc(stdlib.ModPhysics, st); !ok {
|
|
t.Errorf("struct %s is missing", st)
|
|
}
|
|
}
|
|
poly, ok := m.Component(components.NamePolygon)
|
|
if !ok {
|
|
t.Fatal("polygon collider is not visible to scripts")
|
|
}
|
|
f, ok := poly.Field("Points")
|
|
if !ok || !f.ReadOnly || f.Type.Kind != abi.Slice {
|
|
t.Fatalf("Points field %+v %v", f, ok)
|
|
}
|
|
body, _ := m.Component(components.NameBody)
|
|
if v, ok := body.Field("Velocity"); !ok || !v.ReadOnly {
|
|
t.Fatalf("Velocity field %+v %v", v, ok)
|
|
}
|
|
}
|