diff --git a/dev-assets/doodads/azulian/azulian.js b/dev-assets/doodads/azulian/azulian.js index 69bf9c8..5178cc7 100644 --- a/dev-assets/doodads/azulian/azulian.js +++ b/dev-assets/doodads/azulian/azulian.js @@ -10,6 +10,7 @@ function main() { var animFrame = animStart; Self.SetGravity(true); + Self.SetHitbox(7, 4, 17, 28); Self.AddAnimation("walk-left", 100, ["blu-wl1", "blu-wl2", "blu-wl3", "blu-wl4"]); Self.AddAnimation("walk-right", 100, ["blu-wr1", "blu-wr2", "blu-wr3", "blu-wr4"]); diff --git a/dev-assets/doodads/doors/keys.js b/dev-assets/doodads/doors/keys.js index ab81605..7c9a495 100644 --- a/dev-assets/doodads/doors/keys.js +++ b/dev-assets/doodads/doors/keys.js @@ -1,5 +1,7 @@ function main() { Events.OnCollide(function(e) { + console.log("%s picked up by %s", Self.Doodad.Title, e.Actor.Title); + e.Actor.SetData("key:" + Self.Doodad.Title, "true"); Self.Destroy(); }) } diff --git a/dev-assets/doodads/doors/locked-door.js b/dev-assets/doodads/doors/locked-door.js index 0c18d2f..cabbce2 100644 --- a/dev-assets/doodads/doors/locked-door.js +++ b/dev-assets/doodads/doors/locked-door.js @@ -2,25 +2,33 @@ function main() { Self.AddAnimation("open", 0, [1]); var unlocked = false; + // Map our door names to key names. + var KeyMap = { + "Blue Door": "Blue Key", + "Red Door": "Red Key", + "Green Door": "Green Key", + "Yellow Door": "Yellow Key" + } + + log.Warn("%s loaded!", Self.Doodad.Title); + console.log("%s Setting hitbox", Self.Doodad.Title); + Self.SetHitbox(16, 0, 32, 64); + 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; } - unlocked = true; - Self.PlayAnimation("open", null); + if (e.InHitbox) { + var data = e.Actor.GetData("key:" + KeyMap[Self.Doodad.Title]); + if (data === "") { + // Door is locked. + return false; + } + + unlocked = true; + Self.PlayAnimation("open", null); + } }); Events.OnLeave(function(e) { console.log("%s has stopped touching %s", e, Self.Doodad.Title) diff --git a/dev-assets/doodads/trapdoors/down.js b/dev-assets/doodads/trapdoors/down.js index 0764ccb..e861082 100644 --- a/dev-assets/doodads/trapdoors/down.js +++ b/dev-assets/doodads/trapdoors/down.js @@ -3,6 +3,8 @@ function main() { var timer = 0; + Self.SetHitbox(0, 0, 72, 9); + var animationSpeed = 100; var opened = false; Self.AddAnimation("open", animationSpeed, ["down1", "down2", "down3", "down4"]); @@ -13,18 +15,24 @@ function main() { return; } - // Not touching the top of the door means door doesn't open. - if (e.Overlap.Y > 9) { - return; + // Is the actor colliding our solid part? + if (e.InHitbox) { + // Touching the top or the bottom? + if (e.Overlap.Y > 0) { + return false; // solid wall when touched from below + } else { + opened = true; + Self.PlayAnimation("open", function() { + }); + } } - - opened = true; - Self.PlayAnimation("open", function() { - }); }); + Events.OnLeave(function() { - Self.PlayAnimation("close", function() { - opened = false; - }); + if (opened) { + Self.PlayAnimation("close", function() { + opened = false; + }); + } }) } diff --git a/lib/debugging/debugging.go b/lib/debugging/debugging.go index d20ca85..04bfd1c 100644 --- a/lib/debugging/debugging.go +++ b/lib/debugging/debugging.go @@ -102,3 +102,10 @@ func PrintCallers() { fmt.Printf("%d: %s\n", i, caller) } } + +// Pause until the user hits enter in the console. +func Pause() { + var x string + fmt.Print("Press enter to continue . . .") + fmt.Scanf("%s", &x) +} diff --git a/lib/render/rect_test.go b/lib/render/rect_test.go index c6d8bb4..f0528c1 100644 --- a/lib/render/rect_test.go +++ b/lib/render/rect_test.go @@ -54,6 +54,16 @@ func TestIntersection(t *testing.T) { B: newRect(0, -240, 874, 490), Expect: false, // XXX: must be true }, + { + A: newRect(0, 30, 9, 62), + B: newRect(16, 0, 32, 64), + Expect: false, + }, + { + A: newRect(0, 30, 11, 62), + B: newRect(7, 4, 17, 28), + Expect: false, + }, } for _, test := range tests { diff --git a/pkg/collision/collide_actors.go b/pkg/collision/collide_actors.go index bb5479d..78e0725 100644 --- a/pkg/collision/collide_actors.go +++ b/pkg/collision/collide_actors.go @@ -1,6 +1,7 @@ package collision import ( + "errors" "math" "git.kirsle.net/apps/doodle/lib/render" @@ -32,22 +33,11 @@ func BetweenBoxes(boxes []render.Rect) chan BoxCollision { for i, box := range boxes { for j := i + 1; j < len(boxes); 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, - }, - } + collision, err := CompareBoxes(box, other) + if err == nil { + collision.A = i + collision.B = j + generator <- collision } } } @@ -58,6 +48,28 @@ func BetweenBoxes(boxes []render.Rect) chan BoxCollision { return generator } +// CompareBoxes checks if two boxes overlaps and returns information about +// the overlap. The boxes are bounding rectangles like those given to +// BetweenBoxes(). +func CompareBoxes(box, other render.Rect) (BoxCollision, error) { + if box.Intersects(other) { + var ( + overlap = OverlapRelative(box, other) + topLeft = overlap.TopLeft() + bottomRight = overlap.BottomRight() + ) + return BoxCollision{ + Overlap: render.Rect{ + X: topLeft.X, + Y: topLeft.Y, + W: bottomRight.X, + H: bottomRight.Y, + }, + }, nil + } + return BoxCollision{}, errors.New("boxes do not intersect") +} + /* OverlapRelative returns the Overlap box using coordinates relative to the source rect instead of absolute coordinates. diff --git a/pkg/doodads/actor.go b/pkg/doodads/actor.go index 09f65e4..0b1f173 100644 --- a/pkg/doodads/actor.go +++ b/pkg/doodads/actor.go @@ -16,6 +16,10 @@ type Actor interface { Grounded() bool SetGrounded(bool) + // Actor's elected hitbox set by their script. + SetHitbox(x, y, w, h int) + Hitbox() render.Rect + // Movement commands. MoveBy(render.Point) // Add {X,Y} to current Position. MoveTo(render.Point) // Set current Position to {X,Y}. diff --git a/pkg/doodads/doodad.go b/pkg/doodads/doodad.go index 3b3cfc2..5c1e0e7 100644 --- a/pkg/doodads/doodad.go +++ b/pkg/doodads/doodad.go @@ -9,10 +9,11 @@ import ( // Doodad is a reusable component for Levels that have scripts and graphics. type Doodad struct { level.Base - Filename string `json:"-"` // used internally, not saved in json - Palette *level.Palette `json:"palette"` - Script string `json:"script"` - Layers []Layer `json:"layers"` + Filename string `json:"-"` // used internally, not saved in json + Palette *level.Palette `json:"palette"` + Script string `json:"script"` + Layers []Layer `json:"layers"` + Tags map[string]string `json:"data"` // arbitrary key/value data storage } // Layer holds a layer of drawing data for a Doodad. @@ -38,6 +39,7 @@ func New(size int) *Doodad { Chunker: level.NewChunker(size), }, }, + Tags: map[string]string{}, } } diff --git a/pkg/doodads/drawing.go b/pkg/doodads/drawing.go index fd74a2f..dbe404c 100644 --- a/pkg/doodads/drawing.go +++ b/pkg/doodads/drawing.go @@ -14,6 +14,7 @@ type Drawing struct { velocity render.Point accel int size render.Rect + hitbox render.Rect grounded bool } @@ -75,6 +76,21 @@ func (d *Drawing) SetGrounded(v bool) { d.grounded = v } +// SetHitbox sets the actor's elected hitbox. +func (d *Drawing) SetHitbox(x, y, w, h int) { + d.hitbox = render.Rect{ + X: int32(x), + Y: int32(y), + W: int32(w), + H: int32(h), + } +} + +// Hitbox returns the actor's elected hitbox. +func (d *Drawing) Hitbox() render.Rect { + return d.hitbox +} + // MoveBy a relative value. func (d *Drawing) MoveBy(by render.Point) { d.point.Add(by) diff --git a/pkg/uix/actor.go b/pkg/uix/actor.go index 99728a2..ddb3ab3 100644 --- a/pkg/uix/actor.go +++ b/pkg/uix/actor.go @@ -29,6 +29,8 @@ type Actor struct { // Actor runtime variables. hasGravity bool + hitbox render.Rect + data map[string]string // Animation variables. animations map[string]*Animation @@ -78,6 +80,41 @@ func (a *Actor) GetBoundingRect() render.Rect { return doodads.GetBoundingRect(a) } +// SetHitbox sets the actor's elected hitbox. +func (a *Actor) SetHitbox(x, y, w, h int) { + a.hitbox = render.Rect{ + X: int32(x), + Y: int32(y), + W: int32(w), + H: int32(h), + } +} + +// Hitbox returns the actor's elected hitbox. +func (a *Actor) Hitbox() render.Rect { + return a.hitbox +} + +// SetData sets an arbitrary field in the actor's K/V storage. +func (a *Actor) SetData(key, value string) { + if a.data == nil { + a.data = map[string]string{} + } + a.data[key] = value +} + +// GetData gets an arbitrary field from the actor's K/V storage. +// Missing keys just return a blank string (friendly to the JavaScript +// environment). +func (a *Actor) GetData(key string) string { + if a.data == nil { + return "" + } + + v, _ := a.data[key] + return v +} + // 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_collision.go b/pkg/uix/actor_collision.go new file mode 100644 index 0000000..99034cb --- /dev/null +++ b/pkg/uix/actor_collision.go @@ -0,0 +1,157 @@ +package uix + +import ( + "sync" + "time" + + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/pkg/balance" + "git.kirsle.net/apps/doodle/pkg/collision" + "git.kirsle.net/apps/doodle/pkg/doodads" + "git.kirsle.net/apps/doodle/pkg/scripting" + "github.com/kirsle/blog/src/log" + "github.com/robertkrimen/otto" +) + +// loopActorCollision is the Loop function that checks if pairs of +// actors are colliding with each other, and handles their scripting +// responses to such collisions. +// +// boxes: array of Actor bounding box rects. +func (w *Canvas) loopActorCollision() error { + var ( + // Current time of this tick so we can advance animations. + now = time.Now() + + // As we iterate over all actors below to process their movement, track + // their bounding rectangles so we can later see if any pair of actors + // intersect each other. Also, in case of actor scripts protesting a + // collision later, store each actor's original position before the move. + boxes = make([]render.Rect, len(w.actors)) + originalPositions = map[string]render.Point{} + ) + + // Loop over all the actors in parallel, processing their movement and + // checking collision data against the level geometry. + var wg sync.WaitGroup + for i, a := range w.actors { + wg.Add(1) + go func(i int, a *Actor) { + defer wg.Done() + originalPositions[a.ID()] = a.Position() + + // Advance any animations for this actor. + if a.activeAnimation != nil && a.activeAnimation.nextFrameAt.Before(now) { + if done := a.TickAnimation(a.activeAnimation); done { + // Animation has finished, run the callback script. + if a.animationCallback.IsFunction() { + a.animationCallback.Call(otto.NullValue()) + } + + // Clean up the animation state. + a.StopAnimation() + } + } + + // Get the actor's velocity to see if it's moving this tick. + v := a.Velocity() + if a.hasGravity { + v.Y += int32(balance.Gravity) + } + + // If not moving, grab the bounding box right now. + if v == render.Origin { + boxes[i] = doodads.GetBoundingRect(a) + return + } + + // Create a delta point from their current location to where they + // want to move to this tick. + delta := a.Position() + delta.Add(v) + + // Check collision with level geometry. + info, ok := collision.CollidesWithGrid(a, w.chunks, delta) + if ok { + // Collision happened with world. + } + delta = info.MoveTo // Move us back where the collision check put us + + // Move the actor's World Position to the new location. + a.MoveTo(delta) + + // Keep the actor from leaving the world borders of bounded maps. + w.loopContainActorsInsideLevel(a) + + // Store this actor's bounding box after they've moved. + boxes[i] = doodads.GetBoundingRect(a) + }(i, a) + wg.Wait() + } + + var collidingActors = map[string]string{} + for tuple := range collision.BetweenBoxes(boxes) { + a, b := w.actors[tuple.A], w.actors[tuple.B] + collidingActors[a.ID()] = b.ID() + + // Call the OnCollide handler. + if w.scripting != nil { + // Tell actor A about the collision with B. + if err := w.scripting.To(a.ID()).Events.RunCollide(&CollideEvent{ + Actor: b, + Overlap: tuple.Overlap, + InHitbox: tuple.Overlap.Intersects(a.Hitbox()), + }); err != nil { + if err == scripting.ErrReturnFalse { + if origPoint, ok := originalPositions[b.ID()]; ok { + // Trace a vector back from the actor's current position + // to where they originated from and find the earliest + // point where they are not violating the hitbox. + var ( + rect = doodads.GetBoundingRect(b) + hitbox = a.Hitbox() + ) + for point := range render.IterLine2( + b.Position(), + origPoint, + ) { + test := render.Rect{ + X: point.X, + Y: point.Y, + W: rect.W, + H: rect.H, + } + info, err := collision.CompareBoxes( + boxes[tuple.A], + test, + ) + if err != nil || !info.Overlap.Intersects(hitbox) { + b.MoveTo(point) + break + } + } + } else { + log.Error( + "ERROR: Actors %s and %s overlap and the script returned false,"+ + "but I didn't store %s original position earlier??", + a.Doodad.Title, b.Doodad.Title, b.Doodad.Title, + ) + } + } else { + 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 + return nil +} diff --git a/pkg/uix/actor_events.go b/pkg/uix/actor_events.go index 491c2c7..e050ec4 100644 --- a/pkg/uix/actor_events.go +++ b/pkg/uix/actor_events.go @@ -4,6 +4,7 @@ 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 + Actor *Actor + Overlap render.Rect + InHitbox bool // If the two elected hitboxes are overlapping } diff --git a/pkg/uix/canvas.go b/pkg/uix/canvas.go index 30d469c..b68fb0c 100644 --- a/pkg/uix/canvas.go +++ b/pkg/uix/canvas.go @@ -4,20 +4,16 @@ import ( "fmt" "os" "strings" - "sync" - "time" "git.kirsle.net/apps/doodle/lib/events" "git.kirsle.net/apps/doodle/lib/render" "git.kirsle.net/apps/doodle/lib/ui" "git.kirsle.net/apps/doodle/pkg/balance" - "git.kirsle.net/apps/doodle/pkg/collision" "git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/scripting" "git.kirsle.net/apps/doodle/pkg/wallpaper" - "github.com/robertkrimen/otto" ) // Canvas is a custom ui.Widget that manages a single drawing. @@ -178,7 +174,7 @@ func (w *Canvas) Loop(ev *events.State) error { _ = w.loopConstrainScroll() // Current time of this loop so we can advance animations. - now := time.Now() + // now := time.Now() // Remove any actors that were destroyed the previous tick. var newActors []*Actor @@ -192,93 +188,11 @@ func (w *Canvas) Loop(ev *events.State) error { w.actors = newActors } - // Move any actors. As we iterate over all actors, track their bounding - // rectangles so we can later see if any pair of actors intersect each other. - boxes := make([]render.Rect, len(w.actors)) - var wg sync.WaitGroup - for i, a := range w.actors { - wg.Add(1) - go func(i int, a *Actor) { - defer wg.Done() - - // Advance any animations for this actor. - if a.activeAnimation != nil && a.activeAnimation.nextFrameAt.Before(now) { - if done := a.TickAnimation(a.activeAnimation); done { - // Animation has finished, run the callback script. - if a.animationCallback.IsFunction() { - a.animationCallback.Call(otto.NullValue()) - } - - // Clean up the animation state. - a.StopAnimation() - } - } - - // Get the actor's velocity to see if it's moving this tick. - v := a.Velocity() - if a.hasGravity { - v.Y += int32(balance.Gravity) - } - - // If not moving, grab the bounding box right now. - if v == render.Origin { - boxes[i] = doodads.GetBoundingRect(a) - return - } - - // Create a delta point from their current location to where they - // want to move to this tick. - delta := a.Position() - delta.Add(v) - - // Check collision with level geometry. - info, ok := collision.CollidesWithGrid(a, w.chunks, delta) - if ok { - // Collision happened with world. - } - delta = info.MoveTo // Move us back where the collision check put us - - // Move the actor's World Position to the new location. - a.MoveTo(delta) - - // Keep the actor from leaving the world borders of bounded maps. - w.loopContainActorsInsideLevel(a) - - // Store this actor's bounding box after they've moved. - boxes[i] = doodads.GetBoundingRect(a) - }(i, a) - wg.Wait() - } - // Check collisions between actors. - var collidingActors = map[string]string{} - for tuple := range collision.BetweenBoxes(boxes) { - a, b := w.actors[tuple.A], w.actors[tuple.B] - - collidingActors[a.ID()] = b.ID() - - // Call the OnCollide handler. - if w.scripting != 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()) - } - } + if err := w.loopActorCollision(); err != nil { + log.Error("loopActorCollision: %s", err) } - // 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)