Initial Doodad JavaScript System

* Add the JavaScript system for Doodads to run their scripts in levels,
  and wire initial OnCollide() handler support.
* CLI: Add a `doodad install-script` command to the doodad tool.
  * Usage: `doodad install-script <index.js> <filename.doodad>`
* Add dev-assets folder for storing source files for the official
  default doodads, sprites, levels, etc. and for now add a JavaScript
  for the first test doodad.
This commit is contained in:
Noah 2019-04-15 23:07:15 -07:00
parent b33d93599a
commit 1e80304061
12 changed files with 336 additions and 7 deletions

View File

@ -0,0 +1,61 @@
package commands
import (
"fmt"
"io/ioutil"
"git.kirsle.net/apps/doodle/pkg/doodads"
"git.kirsle.net/apps/doodle/pkg/log"
"github.com/urfave/cli"
)
// InstallScript to add the script to a doodad file.
var InstallScript cli.Command
func init() {
InstallScript = cli.Command{
Name: "install-script",
Usage: "install the JavaScript source to a doodad",
ArgsUsage: "<index.js> <filename.doodad>",
Flags: []cli.Flag{
cli.StringFlag{
Name: "key",
Usage: "chroma key color for transparency on input image files",
Value: "#ffffff",
},
},
Action: func(c *cli.Context) error {
if c.NArg() != 2 {
return cli.NewExitError(
"Usage: doodad install-script <script.js> <filename.doodad>",
1,
)
}
var (
args = c.Args()
scriptFile = args[0]
doodadFile = args[1]
)
// Read the JavaScript source.
javascript, err := ioutil.ReadFile(scriptFile)
if err != nil {
return cli.NewExitError(err.Error(), 1)
}
doodad, err := doodads.LoadJSON(doodadFile)
if err != nil {
return cli.NewExitError(
fmt.Sprintf("Failed to read doodad file: %s", err),
1,
)
}
doodad.Script = string(javascript)
doodad.WriteJSON(doodadFile)
log.Info("Installed script successfully")
return nil
},
}
}

View File

@ -44,6 +44,7 @@ func main() {
app.Commands = []cli.Command{ app.Commands = []cli.Command{
commands.Convert, commands.Convert,
commands.InstallScript,
} }
sort.Sort(cli.FlagsByName(app.Flags)) sort.Sort(cli.FlagsByName(app.Flags))

View File

@ -0,0 +1,13 @@
// Test Doodad Script
function main() {
console.log("I am actor ID " + Self.ID());
// Set our doodad's background color to pink. It will be turned
// red whenever something collides with us.
Self.Canvas.SetBackground(RGBA(255, 153, 255, 153));
Events.OnCollide( function(e) {
console.log("Collided with something!");
Self.Canvas.SetBackground(RGBA(255, 0, 0, 153));
});
}

View File

@ -29,7 +29,7 @@ var (
// Put a border around all Canvas widgets. // Put a border around all Canvas widgets.
DebugCanvasBorder = render.Invisible DebugCanvasBorder = render.Invisible
DebugCanvasLabel = true // Tag the canvas with a label. DebugCanvasLabel = false // Tag the canvas with a label.
) )
func init() { func init() {

View File

@ -58,8 +58,8 @@ func New(debug bool, engine render.Engine) *Doodle {
} }
d.shell = NewShell(d) d.shell = NewShell(d)
if !debug { if debug {
log.Logger.Config.Level = golog.InfoLevel log.Logger.Config.Level = golog.DebugLevel
} }
return d return d

View File

@ -8,7 +8,7 @@ var Logger *golog.Logger
func init() { func init() {
Logger = golog.GetLogger("doodle") Logger = golog.GetLogger("doodle")
Logger.Configure(&golog.Config{ Logger.Configure(&golog.Config{
Level: golog.DebugLevel, Level: golog.InfoLevel,
Theme: golog.DarkTheme, Theme: golog.DarkTheme,
Colors: golog.ExtendedColor, Colors: golog.ExtendedColor,
TimeFormat: "2006-01-02 15:04:05.000000", TimeFormat: "2006-01-02 15:04:05.000000",

View File

@ -9,6 +9,7 @@ import (
"git.kirsle.net/apps/doodle/pkg/doodads/dummy" "git.kirsle.net/apps/doodle/pkg/doodads/dummy"
"git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/scripting"
"git.kirsle.net/apps/doodle/pkg/uix" "git.kirsle.net/apps/doodle/pkg/uix"
) )
@ -21,6 +22,7 @@ type PlayScene struct {
// Private variables. // Private variables.
d *Doodle d *Doodle
drawing *uix.Canvas drawing *uix.Canvas
scripting *scripting.Supervisor
// Custom debug labels. // Custom debug labels.
debPosition *string debPosition *string
@ -40,6 +42,7 @@ func (s *PlayScene) Name() string {
// Setup the play scene. // Setup the play scene.
func (s *PlayScene) Setup(d *Doodle) error { func (s *PlayScene) Setup(d *Doodle) error {
s.d = d s.d = d
s.scripting = scripting.NewSupervisor()
// Initialize debug overlay values. // Initialize debug overlay values.
s.debPosition = new(string) s.debPosition = new(string)
@ -67,6 +70,7 @@ func (s *PlayScene) Setup(d *Doodle) error {
s.drawing.InstallActors(s.Level.Actors) s.drawing.InstallActors(s.Level.Actors)
} else if s.Filename != "" { } else if s.Filename != "" {
log.Debug("PlayScene.Setup: loading map from file %s", s.Filename) log.Debug("PlayScene.Setup: loading map from file %s", s.Filename)
// NOTE: s.LoadLevel also calls s.drawing.InstallActors
s.LoadLevel(s.Filename) s.LoadLevel(s.Filename)
} }
@ -77,6 +81,15 @@ func (s *PlayScene) Setup(d *Doodle) error {
s.drawing.InstallActors(s.Level.Actors) s.drawing.InstallActors(s.Level.Actors)
} }
// Load all actor scripts.
s.drawing.SetScriptSupervisor(s.scripting)
if err := s.scripting.InstallScripts(s.Level); err != nil {
log.Error("PlayScene.Setup: failed to InstallScripts: %s", err)
}
if err := s.drawing.InstallScripts(); err != nil {
log.Error("PlayScene.Setup: failed to drawing.InstallScripts: %s", err)
}
player := dummy.NewPlayer() player := dummy.NewPlayer()
s.Player = uix.NewActor(player.ID(), &level.Actor{}, player.Doodad) s.Player = uix.NewActor(player.ID(), &level.Actor{}, player.Doodad)
s.Player.MoveTo(render.NewPoint(128, 128)) s.Player.MoveTo(render.NewPoint(128, 128))
@ -196,7 +209,7 @@ func (s *PlayScene) LoadLevel(filename string) error {
s.Level = level s.Level = level
s.drawing.LoadLevel(s.d.Engine, s.Level) s.drawing.LoadLevel(s.d.Engine, s.Level)
s.drawing.InstallActors(s.Level.Actors) // s.drawing.InstallActors(s.Level.Actors)
return nil return nil
} }

53
pkg/scripting/events.go Normal file
View File

@ -0,0 +1,53 @@
package scripting
import (
"github.com/robertkrimen/otto"
)
// Events API for Doodad scripts.
type Events struct {
registry map[string][]otto.Value
}
// NewEvents initializes the Events API.
func NewEvents() *Events {
return &Events{
registry: map[string][]otto.Value{},
}
}
// OnCollide fires when another actor collides with yours.
func (e *Events) OnCollide(call otto.FunctionCall) otto.Value {
callback := call.Argument(0)
if !callback.IsFunction() {
return otto.Value{} // TODO
}
if _, ok := e.registry[CollideEvent]; !ok {
e.registry[CollideEvent] = []otto.Value{}
}
e.registry[CollideEvent] = append(e.registry[CollideEvent], callback)
return otto.Value{}
}
// RunCollide invokes the OnCollide handler function.
func (e *Events) RunCollide() error {
if _, ok := e.registry[CollideEvent]; !ok {
return nil
}
for _, callback := range e.registry[CollideEvent] {
_, err := callback.Call(otto.Value{}, "test argument")
if err != nil {
return err
}
}
return nil
}
// Event name constants.
const (
CollideEvent = "collide"
)

View File

@ -0,0 +1,62 @@
// Package scripting manages the JavaScript VMs for Doodad
// scripts.
package scripting
import (
"errors"
"fmt"
"git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/apps/doodle/pkg/log"
)
// Supervisor manages the JavaScript VMs for each doodad by its
// unique ID.
type Supervisor struct {
scripts map[string]*VM
}
// NewSupervisor creates a new JavaScript Supervior.
func NewSupervisor() *Supervisor {
return &Supervisor{
scripts: map[string]*VM{},
}
}
// InstallScripts loads scripts for all actors in the level.
func (s *Supervisor) InstallScripts(level *level.Level) error {
for _, actor := range level.Actors {
id := actor.ID()
log.Debug("InstallScripts: load script from Actor %s", id)
if _, ok := s.scripts[id]; ok {
return fmt.Errorf("duplicate actor ID %s in level", id)
}
s.scripts[id] = NewVM(id)
if err := s.scripts[id].RegisterLevelHooks(); err != nil {
return err
}
}
return nil
}
// To returns the VM for a named script.
func (s *Supervisor) To(name string) *VM {
if vm, ok := s.scripts[name]; ok {
return vm
}
log.Error("scripting.Supervisor.To(%s): no such VM but returning blank VM",
name,
)
return NewVM(name)
}
// GetVM returns a script VM from the supervisor.
func (s *Supervisor) GetVM(name string) (*VM, error) {
if vm, ok := s.scripts[name]; ok {
return vm, nil
}
return nil, errors.New("not found")
}

79
pkg/scripting/vm.go Normal file
View File

@ -0,0 +1,79 @@
package scripting
import (
"errors"
"fmt"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/pkg/log"
"github.com/robertkrimen/otto"
)
// VM manages a single isolated JavaScript VM.
type VM struct {
Name string
// Globals available to the scripts.
Events *Events
Self interface{}
vm *otto.Otto
}
// NewVM creates a new JavaScript VM.
func NewVM(name string) *VM {
vm := &VM{
Name: name,
Events: NewEvents(),
vm: otto.New(),
}
return vm
}
// Run code in the VM.
func (vm *VM) Run(src interface{}) (otto.Value, error) {
v, err := vm.vm.Run(src)
return v, err
}
// Set a value in the VM.
func (vm *VM) Set(name string, v interface{}) error {
return vm.vm.Set(name, v)
}
// RegisterLevelHooks registers accessors to the level hooks
// and Doodad API for Play Mode.
func (vm *VM) RegisterLevelHooks() error {
bindings := map[string]interface{}{
"log": log.Logger,
"RGBA": render.RGBA,
"Point": render.NewPoint,
"Self": vm.Self,
"Events": vm.Events,
}
for name, v := range bindings {
err := vm.vm.Set(name, v)
if err != nil {
return fmt.Errorf("RegisterLevelHooks(%s): %s",
name, err,
)
}
}
vm.vm.Run(`console = {}; console.log = log.Info;`)
return nil
}
// Main calls the main function of the script.
func (vm *VM) Main() error {
function, err := vm.vm.Get("main")
if err != nil {
return err
}
if !function.IsFunction() {
return errors.New("main is not a function")
}
_, err = function.Call(otto.Value{})
return err
}

View File

@ -13,6 +13,7 @@ import (
"git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/doodads"
"git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/scripting"
"git.kirsle.net/apps/doodle/pkg/wallpaper" "git.kirsle.net/apps/doodle/pkg/wallpaper"
) )
@ -49,6 +50,10 @@ type Canvas struct {
actor *Actor // if this canvas IS an actor actor *Actor // if this canvas IS an actor
actors []*Actor // if this canvas CONTAINS actors (i.e., is a level) actors []*Actor // if this canvas CONTAINS actors (i.e., is a level)
// Doodad scripting engine supervisor.
// NOTE: initialized and managed by the play_scene.
scripting *scripting.Supervisor
// Wallpaper settings. // Wallpaper settings.
wallpaper *Wallpaper wallpaper *Wallpaper
@ -210,6 +215,13 @@ func (w *Canvas) Loop(ev *events.State) error {
w.actors[tuple[0]].ID(), w.actors[tuple[0]].ID(),
w.actors[tuple[1]].ID(), w.actors[tuple[1]].ID(),
) )
a, b := w.actors[tuple[0]], w.actors[tuple[1]]
// Call the OnCollide handler.
if w.scripting != nil {
w.scripting.To(a.ID()).Events.RunCollide()
w.scripting.To(b.ID()).Events.RunCollide()
}
} }
// If the canvas is editable, only care if it's over our space. // If the canvas is editable, only care if it's over our space.

View File

@ -1,12 +1,14 @@
package uix package uix
import ( import (
"errors"
"fmt" "fmt"
"git.kirsle.net/apps/doodle/lib/render" "git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/doodads"
"git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/scripting"
"git.kirsle.net/apps/doodle/pkg/userdir" "git.kirsle.net/apps/doodle/pkg/userdir"
) )
@ -30,6 +32,39 @@ func (w *Canvas) InstallActors(actors level.ActorMap) error {
return nil return nil
} }
// SetScriptSupervisor assigns the Canvas scripting supervisor to enable
// interaction with actor scripts.
func (w *Canvas) SetScriptSupervisor(s *scripting.Supervisor) {
w.scripting = s
}
// InstallScripts loads all the current actors' scripts into the scripting
// engine supervisor.
func (w *Canvas) InstallScripts() error {
if w.scripting == nil {
return errors.New("no script supervisor is configured for this canvas")
}
if len(w.actors) == 0 {
return errors.New("no actors exist in this canvas to install scripts for")
}
for _, actor := range w.actors {
vm := w.scripting.To(actor.ID())
vm.Self = actor
vm.Set("Self", vm.Self)
vm.Run(actor.Drawing.Doodad.Script)
// Call the main() function.
log.Error("Calling Main() for %s", actor.ID())
if err := vm.Main(); err != nil {
log.Error("main() for actor %s errored: %s", actor.ID(), err)
}
}
return nil
}
// AddActor injects additional actors into the canvas, such as a Player doodad. // AddActor injects additional actors into the canvas, such as a Player doodad.
func (w *Canvas) AddActor(actor *Actor) error { func (w *Canvas) AddActor(actor *Actor) error {
w.actors = append(w.actors, actor) w.actors = append(w.actors, actor)