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:
parent
b33d93599a
commit
1e80304061
61
cmd/doodad/commands/install_script.go
Normal file
61
cmd/doodad/commands/install_script.go
Normal 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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -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))
|
||||||
|
|
13
dev-assets/doodads/test/index.js
Normal file
13
dev-assets/doodads/test/index.js
Normal 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));
|
||||||
|
});
|
||||||
|
}
|
|
@ -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() {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
53
pkg/scripting/events.go
Normal 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"
|
||||||
|
)
|
62
pkg/scripting/scripting.go
Normal file
62
pkg/scripting/scripting.go
Normal 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
79
pkg/scripting/vm.go
Normal 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
|
||||||
|
}
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user