doodle/pkg/scripting/events.go
Noah Petherbridge 626fd53a84 Checkpoint Flag can Re-assign Player Character
Link a Doodad to a Checkpoint Flag (like you would a Start Flag) and
crossing the flag will replace the player with that doodad. Multiple
checkpoint flags like this can toggle you between characters.

* Azulians are now friendly to player characters who have the word
  "Azulian" in their title.
* Improve Bird as the playable character:
  * Dive animation if the player flies diagonally downwards
  * Animation loop while hovering in the air instead of pausing
* Checkpoint flags don't spam each other on PubSub so much which could
  sometimes lead to deadlocks!

SetPlayerCharacter added to the JavaScript API. The Checkpoint Flag
(not the region) can link to a doodad and replace the player character
with that linked doodad when you activate the checkpoint:

    Actors.SetPlayerCharacter(filename string): like "boy.doodad"

Add various panic catchers to make JavaScript safer and log issues
to console.
2022-01-18 21:24:36 -08:00

145 lines
3.9 KiB
Go

package scripting
import (
"errors"
"fmt"
"sync"
"git.kirsle.net/apps/doodle/pkg/keybind"
"git.kirsle.net/apps/doodle/pkg/log"
"github.com/dop251/goja"
)
// Event name constants.
const (
CollideEvent = "OnCollide" // another doodad collides with us
EnterEvent = "OnEnter" // a doodad is fully inside us
LeaveEvent = "OnLeave" // a doodad no longer collides with us
UseEvent = "OnUse" // player pressed the Use key while touching us
// Controllable (player character) doodad events
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 {
runtime *goja.Runtime
registry map[string][]goja.Value
lock sync.RWMutex
}
// NewEvents initializes the Events API.
func NewEvents(runtime *goja.Runtime) *Events {
return &Events{
runtime: runtime,
registry: map[string][]goja.Value{},
}
}
// OnCollide fires when another actor collides with yours.
func (e *Events) OnCollide(call goja.FunctionCall) goja.Value {
return e.register(CollideEvent, call.Argument(0))
}
// RunCollide invokes the OnCollide handler function.
func (e *Events) RunCollide(v interface{}) error {
return e.run(CollideEvent, v)
}
// OnUse fires when another actor collides with yours.
func (e *Events) OnUse(call goja.FunctionCall) goja.Value {
return e.register(UseEvent, call.Argument(0))
}
// RunUse invokes the OnUse handler function.
func (e *Events) RunUse(v interface{}) error {
return e.run(UseEvent, v)
}
// OnLeave fires when another actor stops colliding with yours.
func (e *Events) OnLeave(call goja.FunctionCall) goja.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.
func (e *Events) OnKeypress(call goja.FunctionCall) goja.Value {
return e.register(KeypressEvent, call.Argument(0))
}
// RunKeypress invokes the OnCollide handler function.
func (e *Events) RunKeypress(ev keybind.State) error {
return e.run(KeypressEvent, e.runtime.ToValue(ev))
}
// register a named event.
func (e *Events) register(name string, callback goja.Value) goja.Value {
e.lock.Lock()
defer e.lock.Unlock()
if _, ok := e.registry[name]; !ok {
e.registry[name] = []goja.Value{}
}
e.registry[name] = append(e.registry[name], callback)
return goja.Undefined()
}
// Run an event handler. Returns an error only if there was a JavaScript error
// inside the function. If there are no event handlers, just returns nil.
func (e *Events) run(name string, args ...interface{}) error {
e.lock.RLock()
defer e.lock.RUnlock()
defer func() {
if err := recover(); err != nil {
// TODO EXCEPTIONS: I once saw a "runtime error: index out of range [-1]"
// from an OnCollide handler between azu-white and thief that was crashing
// the app, report this upstream nicely to the user.
log.Error("PANIC: JS %s handler: %s", name, err)
}
}()
if _, ok := e.registry[name]; !ok {
return nil
}
var params = make([]goja.Value, len(args))
for i, v := range args {
params[i] = e.runtime.ToValue(v)
}
for _, callback := range e.registry[name] {
function, ok := goja.AssertFunction(callback)
if !ok {
return fmt.Errorf("failed to callback %s: %s", name, callback)
}
value, err := function(goja.Undefined(), params...)
if err != nil {
// TODO EXCEPTIONS: this err is useful like
// `ReferenceError: playerSpeed is not defined at <eval>:173:9(93)`
// but wherever we're returning the err to isn't handling it!
log.Error("Scripting error on %s: %s", name, err)
return err
}
// If the event handler returned a boolean false, stop all other
// callbacks and return the boolean.
if b, ok := value.Export().(bool); ok && !b {
return ErrReturnFalse
}
}
return nil
}