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{
|
||||
commands.Convert,
|
||||
commands.InstallScript,
|
||||
}
|
||||
|
||||
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.
|
||||
DebugCanvasBorder = render.Invisible
|
||||
DebugCanvasLabel = true // Tag the canvas with a label.
|
||||
DebugCanvasLabel = false // Tag the canvas with a label.
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
|
|
@ -58,8 +58,8 @@ func New(debug bool, engine render.Engine) *Doodle {
|
|||
}
|
||||
d.shell = NewShell(d)
|
||||
|
||||
if !debug {
|
||||
log.Logger.Config.Level = golog.InfoLevel
|
||||
if debug {
|
||||
log.Logger.Config.Level = golog.DebugLevel
|
||||
}
|
||||
|
||||
return d
|
||||
|
|
|
@ -8,7 +8,7 @@ var Logger *golog.Logger
|
|||
func init() {
|
||||
Logger = golog.GetLogger("doodle")
|
||||
Logger.Configure(&golog.Config{
|
||||
Level: golog.DebugLevel,
|
||||
Level: golog.InfoLevel,
|
||||
Theme: golog.DarkTheme,
|
||||
Colors: golog.ExtendedColor,
|
||||
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/level"
|
||||
"git.kirsle.net/apps/doodle/pkg/log"
|
||||
"git.kirsle.net/apps/doodle/pkg/scripting"
|
||||
"git.kirsle.net/apps/doodle/pkg/uix"
|
||||
)
|
||||
|
||||
|
@ -19,8 +20,9 @@ type PlayScene struct {
|
|||
Level *level.Level
|
||||
|
||||
// Private variables.
|
||||
d *Doodle
|
||||
drawing *uix.Canvas
|
||||
d *Doodle
|
||||
drawing *uix.Canvas
|
||||
scripting *scripting.Supervisor
|
||||
|
||||
// Custom debug labels.
|
||||
debPosition *string
|
||||
|
@ -40,6 +42,7 @@ func (s *PlayScene) Name() string {
|
|||
// Setup the play scene.
|
||||
func (s *PlayScene) Setup(d *Doodle) error {
|
||||
s.d = d
|
||||
s.scripting = scripting.NewSupervisor()
|
||||
|
||||
// Initialize debug overlay values.
|
||||
s.debPosition = new(string)
|
||||
|
@ -67,6 +70,7 @@ func (s *PlayScene) Setup(d *Doodle) error {
|
|||
s.drawing.InstallActors(s.Level.Actors)
|
||||
} else if 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)
|
||||
}
|
||||
|
||||
|
@ -77,6 +81,15 @@ func (s *PlayScene) Setup(d *Doodle) error {
|
|||
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()
|
||||
s.Player = uix.NewActor(player.ID(), &level.Actor{}, player.Doodad)
|
||||
s.Player.MoveTo(render.NewPoint(128, 128))
|
||||
|
@ -196,7 +209,7 @@ func (s *PlayScene) LoadLevel(filename string) error {
|
|||
|
||||
s.Level = level
|
||||
s.drawing.LoadLevel(s.d.Engine, s.Level)
|
||||
s.drawing.InstallActors(s.Level.Actors)
|
||||
// s.drawing.InstallActors(s.Level.Actors)
|
||||
|
||||
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/level"
|
||||
"git.kirsle.net/apps/doodle/pkg/log"
|
||||
"git.kirsle.net/apps/doodle/pkg/scripting"
|
||||
"git.kirsle.net/apps/doodle/pkg/wallpaper"
|
||||
)
|
||||
|
||||
|
@ -49,6 +50,10 @@ type Canvas struct {
|
|||
actor *Actor // if this canvas IS an actor
|
||||
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 *Wallpaper
|
||||
|
||||
|
@ -210,6 +215,13 @@ func (w *Canvas) Loop(ev *events.State) error {
|
|||
w.actors[tuple[0]].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.
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
package uix
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.kirsle.net/apps/doodle/lib/render"
|
||||
"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/userdir"
|
||||
)
|
||||
|
||||
|
@ -30,6 +32,39 @@ func (w *Canvas) InstallActors(actors level.ActorMap) error {
|
|||
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.
|
||||
func (w *Canvas) AddActor(actor *Actor) error {
|
||||
w.actors = append(w.actors, actor)
|
||||
|
|
Loading…
Reference in New Issue
Block a user