diff --git a/dev-assets/doodads/buttons/button.js b/dev-assets/doodads/buttons/button.js index 83318fb..4cb1d9e 100644 --- a/dev-assets/doodads/buttons/button.js +++ b/dev-assets/doodads/buttons/button.js @@ -3,7 +3,15 @@ function main() { var timer = 0; - Events.OnCollide( function() { + Events.OnCollide(function(e) { + // Verify they've touched the button. + if (e.Overlap.Y + e.Overlap.H < 24) { + Self.Canvas.SetBackground(RGBA(0, 255, 0, 153)); + return; + } + + Self.Canvas.SetBackground(RGBA(255, 255, 0, 153)); + if (timer > 0) { clearTimeout(timer); } @@ -13,5 +21,10 @@ function main() { Self.ShowLayer(0); timer = 0; }, 200); + }); + + Events.OnLeave(function(e) { + console.log("%s has stopped touching %s", e, Self.Doodad.Title) + Self.Canvas.SetBackground(RGBA(0, 0, 1, 0)); }) } diff --git a/dev-assets/doodads/buttons/sticky.js b/dev-assets/doodads/buttons/sticky.js index cf4fe74..a3e1373 100644 --- a/dev-assets/doodads/buttons/sticky.js +++ b/dev-assets/doodads/buttons/sticky.js @@ -1,7 +1,19 @@ function main() { console.log("%s initialized!", Self.Doodad.Title); - Events.OnCollide( function() { + var pressed = false; + + Events.OnCollide(function(e) { + if (pressed) { + return; + } + + // Verify they've touched the button. + if (e.Overlap.Y + e.Overlap.H < 24) { + return; + } + Self.ShowLayer(1); - }) + pressed = true; + }); } diff --git a/dev-assets/doodads/doors/electric-door.js b/dev-assets/doodads/doors/electric-door.js index b665d73..b9ae2b7 100644 --- a/dev-assets/doodads/doors/electric-door.js +++ b/dev-assets/doodads/doors/electric-door.js @@ -1,16 +1,29 @@ function main() { console.log("%s initialized!", Self.Doodad.Title); - var err = Self.AddAnimation("open", 100, [0, 1, 2, 3]); - console.error("door error: %s", err) + Self.AddAnimation("open", 100, [0, 1, 2, 3]); + Self.AddAnimation("close", 100, [3, 2, 1, 0]); var animating = false; + var opened = false; - Events.OnCollide(function() { - if (animating) { + Events.OnCollide(function(e) { + if (animating || opened) { return; } - animating = true; - Self.PlayAnimation("open", null); + if (e.Overlap.X + e.Overlap.W >= 16 && e.Overlap.X < 48) { + animating = true; + Self.PlayAnimation("open", function() { + opened = true; + animating = false; + }); + } }); + Events.OnLeave(function() { + if (opened) { + Self.PlayAnimation("close", function() { + opened = false; + }); + } + }) } diff --git a/dev-assets/doodads/doors/locked-door.js b/dev-assets/doodads/doors/locked-door.js index 75545f9..0c18d2f 100644 --- a/dev-assets/doodads/doors/locked-door.js +++ b/dev-assets/doodads/doors/locked-door.js @@ -3,6 +3,18 @@ function main() { var unlocked = false; Events.OnCollide(function(e) { + console.log("%s was touched by %s!", Self.Doodad.Title, e.Actor.ID()); + console.log("my box: %+v and theirs: %+v", Self.GetBoundingRect(), e.Actor.GetBoundingRect()); + console.warn("But the overlap is: %+v", e.Overlap); + console.log(Object.keys(e)); + + if (e.Overlap.X + e.Overlap.W >= 16 && e.Overlap.X < 48) { + Self.Canvas.SetBackground(RGBA(255, 0, 0, 153)); + } else { + Self.Canvas.SetBackground(RGBA(0, 255, 0, 153)); + return; + } + if (unlocked) { return; } @@ -10,4 +22,8 @@ function main() { unlocked = true; Self.PlayAnimation("open", null); }); + Events.OnLeave(function(e) { + console.log("%s has stopped touching %s", e, Self.Doodad.Title) + Self.Canvas.SetBackground(RGBA(0, 0, 1, 0)); + }) } diff --git a/dev-assets/doodads/trapdoors/down.js b/dev-assets/doodads/trapdoors/down.js index 209eedc..0764ccb 100644 --- a/dev-assets/doodads/trapdoors/down.js +++ b/dev-assets/doodads/trapdoors/down.js @@ -4,22 +4,27 @@ function main() { var timer = 0; var animationSpeed = 100; - var animating = false; + var opened = false; Self.AddAnimation("open", animationSpeed, ["down1", "down2", "down3", "down4"]); Self.AddAnimation("close", animationSpeed, ["down4", "down3", "down2", "down1"]); - Events.OnCollide( function() { - if (animating) { + Events.OnCollide( function(e) { + if (opened) { return; } - animating = true; + // Not touching the top of the door means door doesn't open. + if (e.Overlap.Y > 9) { + return; + } + + opened = true; Self.PlayAnimation("open", function() { - setTimeout(function() { - Self.PlayAnimation("close", function() { - animating = false; - }); - }, 3000); - }) + }); }); + Events.OnLeave(function() { + Self.PlayAnimation("close", function() { + opened = false; + }); + }) } diff --git a/pkg/collision/actors_test.go b/pkg/collision/actors_test.go index 63bfe10..fcac887 100644 --- a/pkg/collision/actors_test.go +++ b/pkg/collision/actors_test.go @@ -10,6 +10,9 @@ import ( func TestActorCollision(t *testing.T) { boxes := []render.Rect{ // 0: intersects with 1 + // Expected intersection rect would be + // X,Y = 90,10 + // X2,Y2 = 100,99 render.Rect{ X: 0, Y: 0, @@ -18,6 +21,9 @@ func TestActorCollision(t *testing.T) { }, // 1: intersects with 0 + // Expected intersection rect would be + // X,Y = 90,10 + // X2,Y2 = 100,99 render.Rect{ X: 90, Y: 10, @@ -34,6 +40,9 @@ func TestActorCollision(t *testing.T) { }, // 3: intersects with 4 + // Expected intersection rect would be + // X,Y = 240,200 + // X2,Y2 = 264,231 render.Rect{ X: 233, Y: 200, @@ -70,22 +79,22 @@ func TestActorCollision(t *testing.T) { }, } - assert := func(i int, result collision.IndexTuple, expectA, expectB int) { - if result[0] != expectA || result[1] != expectB { + assert := func(i int, result collision.BoxCollision, expectA, expectB int) { + if result.A != expectA || result.B != expectB { t.Errorf( "unexpected collision at index %d of BetweenBoxes() generator\n"+ "expected: (%d,%d)\n"+ " but got: (%d,%d)", i, expectA, expectB, - result[0], result[1], + result.A, result.B, ) } } var i int for overlap := range collision.BetweenBoxes(boxes) { - a, b := overlap[0], overlap[1] + a, b := overlap.A, overlap.B // Ensure expected collisions happened. switch i { diff --git a/pkg/collision/collide_actors.go b/pkg/collision/collide_actors.go index 899c57a..bb5479d 100644 --- a/pkg/collision/collide_actors.go +++ b/pkg/collision/collide_actors.go @@ -1,9 +1,21 @@ package collision import ( + "math" + "git.kirsle.net/apps/doodle/lib/render" ) +// BoxCollision holds the result of a collision BetweenBoxes. +type BoxCollision struct { + // A and B are the indexes of the boxes sent to BetweenBoxes. + A int + B int + + // Overlap is the rect of how the boxes overlap. + Overlap render.Rect +} + // IndexTuple holds two integers used as array indexes. type IndexTuple [2]int @@ -12,15 +24,30 @@ type IndexTuple [2]int // // This returns a generator that spits out indexes of the // intersecting boxes. -func BetweenBoxes(boxes []render.Rect) chan IndexTuple { - generator := make(chan IndexTuple) +func BetweenBoxes(boxes []render.Rect) chan BoxCollision { + generator := make(chan BoxCollision) go func() { // Outer loop: test each box for intersection with the others. for i, box := range boxes { for j := i + 1; j < len(boxes); j++ { - if box.Intersects(boxes[j]) { - generator <- IndexTuple{i, j} + other := boxes[j] + if box.Intersects(other) { + var ( + overlap = OverlapRelative(box, other) + topLeft = overlap.TopLeft() + bottomRight = overlap.BottomRight() + ) + generator <- BoxCollision{ + A: i, + B: j, + Overlap: render.Rect{ + X: topLeft.X, + Y: topLeft.Y, + W: bottomRight.X, + H: bottomRight.Y, + }, + } } } } @@ -30,3 +57,57 @@ func BetweenBoxes(boxes []render.Rect) chan IndexTuple { return generator } + +/* +OverlapRelative returns the Overlap box using coordinates relative +to the source rect instead of absolute coordinates. +*/ +func OverlapRelative(source, other render.Rect) CollisionBox { + var ( + // Move the source rect to 0,0 and record the distance we need + // to go to get there, so we can move the other rect the same. + deltaX = 0 - source.X + deltaY = 0 - source.Y + ) + + source.X = 0 + source.Y = 0 + other.X += deltaX + other.Y += deltaY + + return Overlap(source, other) +} + +/* +Overlap returns the overlap rectangle between two boxes. + +The two rects given have an X,Y coordinate and their W,H are their +width and heights. + +The returned CollisionBox uses absolute coordinates in the same space +as the passed-in rects. +*/ +func Overlap(a, b render.Rect) CollisionBox { + max := func(x, y int32) int32 { + return int32(math.Max(float64(x), float64(y))) + } + min := func(x, y int32) int32 { + return int32(math.Min(float64(x), float64(y))) + } + + var ( + A = GetCollisionBox(a) + B = GetCollisionBox(b) + + ATL = A.TopLeft() + ABR = A.BottomRight() + BTL = B.TopLeft() + BBR = B.BottomRight() + + // Coordinates of the intersection box. + X1, Y1 = max(ATL.X, BTL.X), max(ATL.Y, BTL.Y) + X2, Y2 = min(ABR.X, BBR.X), min(ABR.Y, BBR.Y) + ) + + return NewBox(render.NewPoint(X1, Y1), render.NewPoint(X2, Y2)) +} diff --git a/pkg/collision/collide_level.go b/pkg/collision/collide_level.go index 6e7539f..11f54df 100644 --- a/pkg/collision/collide_level.go +++ b/pkg/collision/collide_level.go @@ -206,16 +206,17 @@ func (c *Collide) ScanBoundingBox(box render.Rect, grid *level.Chunker) bool { // Check all four edges of the box in parallel on different CPU cores. type jobSide struct { - p1 render.Point - p2 render.Point + p1 render.Point // p2 is perpendicular to p1 along a straight edge + p2 render.Point // of the collision box. side Side } - jobs := []jobSide{ + jobs := []jobSide{ // We'll scan each side of the bounding box in parallel jobSide{col.Top[0], col.Top[1], Top}, jobSide{col.Bottom[0], col.Bottom[1], Bottom}, jobSide{col.Left[0], col.Left[1], Left}, jobSide{col.Right[0], col.Right[1], Right}, } + var wg sync.WaitGroup for _, job := range jobs { wg.Add(1) @@ -226,12 +227,6 @@ func (c *Collide) ScanBoundingBox(box render.Rect, grid *level.Chunker) bool { } wg.Wait() - - // TODO: the old synchronous version of the above. - // c.ScanGridLine(col.Top[0], col.Top[1], grid, Top) - // c.ScanGridLine(col.Bottom[0], col.Bottom[1], grid, Bottom) - // c.ScanGridLine(col.Left[0], col.Left[1], grid, Left) - // c.ScanGridLine(col.Right[0], col.Right[1], grid, Right) return c.IsColliding() } diff --git a/pkg/collision/debug_box.go b/pkg/collision/debug_box.go index b99aef2..8392af7 100644 --- a/pkg/collision/debug_box.go +++ b/pkg/collision/debug_box.go @@ -1,20 +1,34 @@ package collision -import "git.kirsle.net/apps/doodle/lib/render" +import ( + "fmt" + + "git.kirsle.net/apps/doodle/lib/render" +) // CollisionBox holds all of the coordinate pairs to draw the collision box // around a doodad. type CollisionBox struct { - Top []render.Point - Bottom []render.Point - Left []render.Point - Right []render.Point + Top [2]render.Point + Bottom [2]render.Point + Left [2]render.Point + Right [2]render.Point +} + +// NewBox creates a collision box from the Top Left and Bottom Right points. +func NewBox(topLeft, bottomRight render.Point) CollisionBox { + return GetCollisionBox(render.Rect{ + X: topLeft.X, + Y: topLeft.Y, + W: bottomRight.X - topLeft.X, + H: bottomRight.Y - topLeft.Y, + }) } // GetCollisionBox returns a CollisionBox with the four coordinates. func GetCollisionBox(box render.Rect) CollisionBox { return CollisionBox{ - Top: []render.Point{ + Top: [2]render.Point{ { X: box.X, Y: box.Y, @@ -24,7 +38,7 @@ func GetCollisionBox(box render.Rect) CollisionBox { Y: box.Y, }, }, - Bottom: []render.Point{ + Bottom: [2]render.Point{ { X: box.X, Y: box.Y + box.H, @@ -34,7 +48,7 @@ func GetCollisionBox(box render.Rect) CollisionBox { Y: box.Y + box.H, }, }, - Left: []render.Point{ + Left: [2]render.Point{ { X: box.X, Y: box.Y + box.H - 1, @@ -44,7 +58,7 @@ func GetCollisionBox(box render.Rect) CollisionBox { Y: box.Y + 1, }, }, - Right: []render.Point{ + Right: [2]render.Point{ { X: box.X + box.W, Y: box.Y + box.H - 1, @@ -56,3 +70,21 @@ func GetCollisionBox(box render.Rect) CollisionBox { }, } } + +// String prints the bounds of the collision box in absolute terms. +func (c CollisionBox) String() string { + return fmt.Sprintf("CollisionBox<%s:%s>", + c.TopLeft(), + c.BottomRight(), + ) +} + +// TopLeft returns the point at the top left. +func (c CollisionBox) TopLeft() render.Point { + return render.NewPoint(c.Top[0].X, c.Top[0].Y) +} + +// BottomRight returns the point at the bottom right. +func (c CollisionBox) BottomRight() render.Point { + return render.NewPoint(c.Bottom[1].X, c.Bottom[1].Y) +} diff --git a/pkg/scripting/events.go b/pkg/scripting/events.go index 8fac86b..4172010 100644 --- a/pkg/scripting/events.go +++ b/pkg/scripting/events.go @@ -1,6 +1,8 @@ package scripting import ( + "errors" + "git.kirsle.net/apps/doodle/lib/events" "github.com/robertkrimen/otto" ) @@ -15,6 +17,11 @@ const ( KeypressEvent = "OnKeypress" // i.e. arrow keys ) +// Event return errors. +var ( + ErrReturnFalse = errors.New("JS callback function returned false") +) + // Events API for Doodad scripts. type Events struct { registry map[string][]otto.Value @@ -33,8 +40,18 @@ func (e *Events) OnCollide(call otto.FunctionCall) otto.Value { } // RunCollide invokes the OnCollide handler function. -func (e *Events) RunCollide() error { - return e.run(CollideEvent) +func (e *Events) RunCollide(v interface{}) error { + return e.run(CollideEvent, v) +} + +// OnLeave fires when another actor stops colliding with yours. +func (e *Events) OnLeave(call otto.FunctionCall) otto.Value { + return e.register(LeaveEvent, call.Argument(0)) +} + +// RunLeave invokes the OnLeave handler function. +func (e *Events) RunLeave(v interface{}) error { + return e.run(LeaveEvent, v) } // OnKeypress fires when another actor collides with yours. @@ -69,10 +86,18 @@ func (e *Events) run(name string, args ...interface{}) error { } for _, callback := range e.registry[name] { - _, err := callback.Call(otto.Value{}, args...) + value, err := callback.Call(otto.Value{}, args...) if err != nil { return err } + + // If the event handler returned a boolean false, stop all other + // callbacks and return the boolean. + if value.IsBoolean() { + if b, err := value.ToBoolean(); err == nil && b == false { + return ErrReturnFalse + } + } } return nil diff --git a/pkg/uix/actor.go b/pkg/uix/actor.go index 48d11d2..99728a2 100644 --- a/pkg/uix/actor.go +++ b/pkg/uix/actor.go @@ -73,6 +73,11 @@ func (a *Actor) SetGravity(v bool) { a.hasGravity = v } +// GetBoundingRect gets the bounding box of the actor's doodad. +func (a *Actor) GetBoundingRect() render.Rect { + return doodads.GetBoundingRect(a) +} + // LayerCount returns the number of layers in this actor's drawing. func (a *Actor) LayerCount() int { return len(a.Doodad.Layers) diff --git a/pkg/uix/actor_events.go b/pkg/uix/actor_events.go new file mode 100644 index 0000000..491c2c7 --- /dev/null +++ b/pkg/uix/actor_events.go @@ -0,0 +1,9 @@ +package uix + +import "git.kirsle.net/apps/doodle/lib/render" + +// CollideEvent holds data sent to an actor's Collide handler. +type CollideEvent struct { + Actor *Actor + Overlap render.Rect +} diff --git a/pkg/uix/canvas.go b/pkg/uix/canvas.go index 3558335..30d469c 100644 --- a/pkg/uix/canvas.go +++ b/pkg/uix/canvas.go @@ -53,6 +53,9 @@ type Canvas struct { actor *Actor // if this canvas IS an actor actors []*Actor // if this canvas CONTAINS actors (i.e., is a level) + // Collision memory for the actors. + collidingActors map[string]string // mapping their IDs to each other + // Doodad scripting engine supervisor. // NOTE: initialized and managed by the play_scene. scripting *scripting.Supervisor @@ -248,24 +251,34 @@ func (w *Canvas) Loop(ev *events.State) error { } // Check collisions between actors. + var collidingActors = map[string]string{} for tuple := range collision.BetweenBoxes(boxes) { - log.Debug("Actor %s collides with %s", - w.actors[tuple[0]].ID(), - w.actors[tuple[1]].ID(), - ) - a, b := w.actors[tuple[0]], w.actors[tuple[1]] + a, b := w.actors[tuple.A], w.actors[tuple.B] + + collidingActors[a.ID()] = b.ID() // Call the OnCollide handler. if w.scripting != nil { - if err := w.scripting.To(a.ID()).Events.RunCollide(); err != nil { - log.Error(err.Error()) - } - if err := w.scripting.To(b.ID()).Events.RunCollide(); err != nil { + // Tell actor A about the collision with B. + if err := w.scripting.To(a.ID()).Events.RunCollide(&CollideEvent{ + Actor: b, + Overlap: tuple.Overlap, + }); err != nil { log.Error(err.Error()) } } } + // Check for lacks of collisions since last frame. + for sourceID, targetID := range w.collidingActors { + if _, ok := collidingActors[sourceID]; !ok { + w.scripting.To(sourceID).Events.RunLeave(targetID) + } + } + + // Store this frame's colliding actors for next frame. + w.collidingActors = collidingActors + // If the canvas is editable, only care if it's over our space. if w.Editable { cursor := render.NewPoint(ev.CursorX.Now, ev.CursorY.Now)