Doodad CLI Tool Features; Write Lock and Hidden

* The `doodad` CLI tool got a lot of new commands:
  * `doodad show` to verbosely print details about Levels and Doodads.
  * `edit-level` and `edit-doodad` to update details about Levels and
    Doodads, such as their Title, Author, page type and size, etc.
* Doodads gain a `Hidden bool` that hides them from the palette in
  Editor Mode. The player character (Blue Azulian) is Hidden.
* Add some boolProps to the balance/ package and made a dynamic system
  to easily configure these with the in-game dev console.
  * Command: `boolProp list` returns available balance.boolProps
  * `boolProp <name>` returns the current value.
  * `boolProp <name> <true or false>` sets the value.
* The new boolProps are:
  * showAllDoodads: enable Hidden doodads on the palette UI (NOTE:
    reload the editor to take effect)
  * writeLockOverride: edit files that are write locked anyway
  * prettyJSON: pretty-format the JSON files saved by the game.
physics
Noah 2019-07-06 23:28:11 -07:00
parent 6476a67faf
commit fd649b7ab1
16 changed files with 706 additions and 20 deletions

View File

@ -0,0 +1,124 @@
package commands
import (
"fmt"
"git.kirsle.net/apps/doodle/pkg/doodads"
"git.kirsle.net/apps/doodle/pkg/log"
"github.com/urfave/cli"
)
// EditDoodad allows writing doodad metadata.
var EditDoodad cli.Command
func init() {
EditDoodad = cli.Command{
Name: "edit-doodad",
Usage: "update metadata for a Doodad file",
ArgsUsage: "<filename.doodad>",
Flags: []cli.Flag{
cli.StringFlag{
Name: "title",
Usage: "set the doodad title",
},
cli.StringFlag{
Name: "author",
Usage: "set the doodad author",
},
cli.BoolFlag{
Name: "hide",
Usage: "Hide the doodad from the palette",
},
cli.BoolFlag{
Name: "unhide",
Usage: "Unhide the doodad from the palette",
},
cli.BoolFlag{
Name: "lock",
Usage: "write-lock the level file",
},
cli.BoolFlag{
Name: "unlock",
Usage: "remove the write-lock on the level file",
},
},
Action: func(c *cli.Context) error {
if c.NArg() < 1 {
return cli.NewExitError(
"Usage: doodad edit-doodad <filename.doodad>",
1,
)
}
var filenames = c.Args()
for _, filename := range filenames {
if err := editDoodad(c, filename); err != nil {
log.Error("%s: %s", filename, err)
}
}
return nil
},
}
}
func editDoodad(c *cli.Context, filename string) error {
var modified bool
dd, err := doodads.LoadJSON(filename)
if err != nil {
return fmt.Errorf("Failed to load %s: %s", filename, err)
}
log.Info("File: %s", filename)
/***************************
* Update level properties *
***************************/
if c.String("title") != "" {
dd.Title = c.String("title")
log.Info("Set title: %s", dd.Title)
modified = true
}
if c.String("author") != "" {
dd.Author = c.String("author")
log.Info("Set author: %s", dd.Author)
modified = true
}
if c.Bool("hide") {
dd.Hidden = true
log.Info("Marked doodad Hidden")
modified = true
} else if c.Bool("unhide") {
dd.Hidden = false
log.Info("Doodad is no longer Hidden")
modified = true
}
if c.Bool("lock") {
dd.Locked = true
log.Info("Write lock enabled.")
modified = true
} else if c.Bool("unlock") {
dd.Locked = false
log.Info("Write lock disabled.")
modified = true
}
/******************************
* Save level changes to disk *
******************************/
if modified {
if err := dd.WriteFile(filename); err != nil {
return cli.NewExitError(fmt.Sprintf("Write error: %s", err), 1)
}
} else {
log.Warn("Note: No changes made to level")
}
return showDoodad(c, filename)
}

View File

@ -0,0 +1,156 @@
package commands
import (
"fmt"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/apps/doodle/pkg/log"
"github.com/urfave/cli"
)
// EditLevel allows writing level metadata.
var EditLevel cli.Command
func init() {
EditLevel = cli.Command{
Name: "edit-level",
Usage: "update metadata for a Level file",
ArgsUsage: "<filename.level>",
Flags: []cli.Flag{
cli.StringFlag{
Name: "title",
Usage: "set the level title",
},
cli.StringFlag{
Name: "author",
Usage: "set the level author",
},
cli.StringFlag{
Name: "password",
Usage: "set the level password",
},
cli.StringFlag{
Name: "type",
Usage: "set the page type. One of: Unbounded, Bounded, NoNegativeSpace, Bordered",
},
cli.StringFlag{
Name: "max-size",
Usage: "set the page max size (WxH format, like 2550x3300)",
},
cli.StringFlag{
Name: "wallpaper",
Usage: "set the wallpaper filename",
},
cli.BoolFlag{
Name: "lock",
Usage: "write-lock the level file",
},
cli.BoolFlag{
Name: "unlock",
Usage: "remove the write-lock on the level file",
},
},
Action: func(c *cli.Context) error {
if c.NArg() < 1 {
return cli.NewExitError(
"Usage: doodad edit-level <filename.level>",
1,
)
}
var filenames = c.Args()
for _, filename := range filenames {
if err := editLevel(c, filename); err != nil {
log.Error("%s: %s", filename, err)
}
}
return nil
},
}
}
func editLevel(c *cli.Context, filename string) error {
var modified bool
lvl, err := level.LoadJSON(filename)
if err != nil {
return fmt.Errorf("Failed to load %s: %s", filename, err)
}
log.Info("File: %s", filename)
/***************************
* Update level properties *
***************************/
if c.String("title") != "" {
lvl.Title = c.String("title")
log.Info("Set title: %s", lvl.Title)
modified = true
}
if c.String("author") != "" {
lvl.Author = c.String("author")
log.Info("Set author: %s", lvl.Author)
modified = true
}
if c.String("password") != "" {
lvl.Password = c.String("password")
log.Info("Updated level password")
modified = true
}
if c.String("max-size") != "" {
w, h, err := render.ParseResolution(c.String("max-size"))
if err != nil {
log.Error("-max-size: %s", err)
} else {
lvl.MaxWidth = int64(w)
lvl.MaxHeight = int64(h)
modified = true
}
}
if c.Bool("lock") {
lvl.Locked = true
log.Info("Write lock enabled.")
modified = true
} else if c.Bool("unlock") {
lvl.Locked = false
log.Info("Write lock disabled.")
modified = true
}
if c.String("type") != "" {
if pageType, ok := level.PageTypeFromString(c.String("type")); ok {
lvl.PageType = pageType
log.Info("Page Type set to %s", pageType)
modified = true
} else {
log.Error("Invalid -type value. Should be like Unbounded, Bounded, NoNegativeSpace, Bordered")
}
}
if c.String("wallpaper") != "" {
lvl.Wallpaper = c.String("wallpaper")
log.Info("Set wallpaper: %s", c.String("wallpaper"))
modified = true
}
/******************************
* Save level changes to disk *
******************************/
if modified {
if err := lvl.WriteFile(filename); err != nil {
return cli.NewExitError(fmt.Sprintf("Write error: %s", err), 1)
}
} else {
log.Warn("Note: No changes made to level")
}
return showLevel(c, filename)
}

224
cmd/doodad/commands/show.go Normal file
View File

@ -0,0 +1,224 @@
package commands
import (
"fmt"
"path/filepath"
"strings"
"git.kirsle.net/apps/doodle/pkg/doodads"
"git.kirsle.net/apps/doodle/pkg/enum"
"git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/apps/doodle/pkg/log"
"github.com/urfave/cli"
)
// Show information about a Level or Doodad file.
var Show cli.Command
func init() {
Show = cli.Command{
Name: "show",
Usage: "show information about a level or doodad file",
ArgsUsage: "<.level or .doodad>",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "actors",
Usage: "print verbose actor data in Level files",
},
cli.BoolFlag{
Name: "chunks",
Usage: "print verbose data about all the pixel chunks in a file",
},
cli.BoolFlag{
Name: "script",
Usage: "print the script from a doodad file and exit",
},
cli.BoolFlag{
Name: "verbose, v",
Usage: "print verbose output (all verbose flags enabled)",
},
},
Action: func(c *cli.Context) error {
if c.NArg() < 1 {
return cli.NewExitError(
"Usage: doodad show <.level .doodad ...>",
1,
)
}
filenames := c.Args()
for _, filename := range filenames {
switch strings.ToLower(filepath.Ext(filename)) {
case enum.LevelExt:
if err := showLevel(c, filename); err != nil {
log.Error(err.Error())
return cli.NewExitError("Error", 1)
}
case enum.DoodadExt:
if err := showDoodad(c, filename); err != nil {
log.Error(err.Error())
return cli.NewExitError("Error", 1)
}
default:
log.Error("File %s: not a level or doodad", filename)
}
}
return nil
},
}
}
// showLevel shows data about a level file.
func showLevel(c *cli.Context, filename string) error {
lvl, err := level.LoadJSON(filename)
if err != nil {
return err
}
fmt.Printf("===== Level: %s =====\n", filename)
fmt.Println("Headers:")
fmt.Printf(" File version: %d\n", lvl.Version)
fmt.Printf(" Game version: %s\n", lvl.GameVersion)
fmt.Printf(" Level title: %s\n", lvl.Title)
fmt.Printf(" Author: %s\n", lvl.Author)
fmt.Printf(" Password: %s\n", lvl.Password)
fmt.Printf(" Locked: %+v\n", lvl.Locked)
fmt.Println("")
showPalette(lvl.Palette)
fmt.Println("Level Settings:")
fmt.Printf(" Page type: %s\n", lvl.PageType.String())
fmt.Printf(" Max size: %dx%d\n", lvl.MaxWidth, lvl.MaxHeight)
fmt.Printf(" Wallpaper: %s\n", lvl.Wallpaper)
fmt.Println("")
// Print the actor information.
fmt.Println("Actors:")
fmt.Printf(" Level contains %d actors\n", len(lvl.Actors))
if c.Bool("actors") || c.Bool("verbose") {
fmt.Println(" List of Actors:")
for id, actor := range lvl.Actors {
fmt.Printf(" - Name: %s\n", actor.Filename)
fmt.Printf(" UUID: %s\n", id)
fmt.Printf(" At: %s\n", actor.Point)
if c.Bool("links") {
for _, link := range actor.Links {
if other, ok := lvl.Actors[link]; ok {
fmt.Printf(" Link: %s (%s)\n", link, other.Filename)
} else {
fmt.Printf(" Link: %s (**UNRESOLVED**)", link)
}
}
}
}
fmt.Println("")
} else {
fmt.Print(" Use -actors or -verbose to serialize Actors\n\n")
}
// Serialize chunk information.
showChunker(c, lvl.Chunker)
fmt.Println("")
return nil
}
func showDoodad(c *cli.Context, filename string) error {
dd, err := doodads.LoadJSON(filename)
if err != nil {
return err
}
if c.Bool("script") {
fmt.Printf("// %s.js\n", filename)
fmt.Println(strings.TrimSpace(dd.Script))
return nil
}
fmt.Printf("===== Doodad: %s =====\n", filename)
fmt.Println("Headers:")
fmt.Printf(" File version: %d\n", dd.Version)
fmt.Printf(" Game version: %s\n", dd.GameVersion)
fmt.Printf(" Doodad title: %s\n", dd.Title)
fmt.Printf(" Author: %s\n", dd.Author)
fmt.Printf(" Locked: %+v\n", dd.Locked)
fmt.Printf(" Hidden: %+v\n", dd.Hidden)
fmt.Printf(" Script size: %d bytes\n", len(dd.Script))
fmt.Println("")
if len(dd.Tags) > 0 {
fmt.Println("Tags:")
for k, v := range dd.Tags {
fmt.Printf(" %s: %s\n", k, v)
}
fmt.Println("")
}
showPalette(dd.Palette)
for i, layer := range dd.Layers {
fmt.Printf("Layer %d: %s\n", i, layer.Name)
showChunker(c, layer.Chunker)
}
fmt.Println("")
return nil
}
func showPalette(pal *level.Palette) {
fmt.Println("Palette:")
for _, sw := range pal.Swatches {
fmt.Printf(" - Swatch name: %s\n", sw.Name)
fmt.Printf(" Attributes: %s\n", sw.Attributes())
fmt.Printf(" Color: %s\n", sw.Color.ToHex())
}
fmt.Println("")
}
func showChunker(c *cli.Context, ch *level.Chunker) {
var worldSize = ch.WorldSize()
var width = worldSize.W - worldSize.X
var height = worldSize.H - worldSize.Y
fmt.Println("Chunks:")
fmt.Printf(" Pixels Per Chunk: %d^2\n", ch.Size)
fmt.Printf(" Number Generated: %d\n", len(ch.Chunks))
fmt.Printf(" Coordinate Range: (%d,%d) ... (%d,%d)\n",
worldSize.X,
worldSize.Y,
worldSize.W,
worldSize.H,
)
fmt.Printf(" World Dimensions: %dx%d\n", width, height)
// Verbose chunk information.
if c.Bool("chunks") || c.Bool("verbose") {
fmt.Println(" Chunk Details:")
for point, chunk := range ch.Chunks {
fmt.Printf(" - Coord: %s\n", point)
fmt.Printf(" Type: %s\n", chunkTypeToName(chunk.Type))
fmt.Printf(" Range: (%d,%d) ... (%d,%d)\n",
int(point.X)*ch.Size,
int(point.Y)*ch.Size,
(int(point.X)*ch.Size)+ch.Size,
(int(point.Y)*ch.Size)+ch.Size,
)
}
} else {
fmt.Println(" Use -chunks or -verbose to serialize Chunks")
}
fmt.Println("")
}
func chunkTypeToName(v int) string {
switch v {
case level.MapType:
return "map"
case level.GridType:
return "grid"
default:
return fmt.Sprintf("type %d", v)
}
}

View File

@ -52,6 +52,9 @@ func main() {
app.Commands = []cli.Command{
commands.Convert,
commands.Show,
commands.EditLevel,
commands.EditDoodad,
commands.InstallScript,
}

View File

@ -1,5 +1,36 @@
package render
import (
"fmt"
"regexp"
"strconv"
)
var regexpResolution = regexp.MustCompile(`^(\d+)x(\d+)$`)
// ParseResolution turns a resolution string like "1024x768" and returns the
// width and height values.
func ParseResolution(resi string) (int, int, error) {
m := regexpResolution.FindStringSubmatch(resi)
if m == nil {
return 0, 0, fmt.Errorf("invalid resolution format, should be %s",
regexpResolution.String(),
)
}
width, err := strconv.Atoi(m[1])
if err != nil {
return 0, 0, err
}
height, err := strconv.Atoi(m[2])
if err != nil {
return 0, 0, err
}
return width, height, nil
}
// TrimBox helps with Engine.Copy() to trim a destination box so that it
// won't overflow with the parent container.
func TrimBox(src, dst *Rect, p Point, S Rect, thickness int32) {

60
pkg/balance/boolprops.go Normal file
View File

@ -0,0 +1,60 @@
package balance
import (
"errors"
"fmt"
"sort"
"strings"
)
// Fun bool props to wreak havoc in the game.
var (
// Force show hidden doodads in the palette in Editor Mode.
ShowHiddenDoodads bool
// Force ability to edit Locked levels and doodads.
WriteLockOverride bool
// Pretty-print JSON files when writing.
JSONIndent bool
)
// Human friendly names for the boolProps. Not necessarily the long descriptive
// variable names above.
var props = map[string]*bool{
"showAllDoodads": &ShowHiddenDoodads,
"writeLockOverride": &WriteLockOverride,
"prettyJSON": &JSONIndent,
// WARNING: SLOW!
"disableChunkTextureCache": &DisableChunkTextureCache,
}
// GetBoolProp reads the current value of a boolProp.
// Special value "list" will error out with a list of available props.
func GetBoolProp(name string) (bool, error) {
if name == "list" {
var keys []string
for k := range props {
keys = append(keys, k)
}
sort.Strings(keys)
return false, fmt.Errorf(
"Boolprops: %s",
strings.Join(keys, ", "),
)
}
if prop, ok := props[name]; ok {
return *prop, nil
}
return false, errors.New("no such boolProp")
}
// BoolProp allows easily setting a boolProp by name.
func BoolProp(name string, v bool) error {
if prop, ok := props[name]; ok {
*prop = v
return nil
}
return errors.New("no such boolProp")
}

View File

@ -34,9 +34,6 @@ var (
// Set to a color other than Invisible to force the uix.Canvas to color ALL
// Stroke pixels in this color.
DebugCanvasStrokeColor = render.Invisible
// Pretty-print JSON files when writing.
JSONIndent = true
)
func init() {

View File

@ -5,6 +5,7 @@ import (
"fmt"
"strconv"
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/enum"
"github.com/robertkrimen/otto"
)
@ -201,8 +202,19 @@ func (c Command) Quit() error {
// BoolProp command sets available boolean variables.
func (c Command) BoolProp(d *Doodle) error {
if len(c.Args) == 1 {
// Showing the value of a boolProp. Only supported for those defined
// in balance/boolprops.go
value, err := balance.GetBoolProp(c.Args[0])
if err != nil {
return err
}
d.Flash("%s: %+v", c.Args[0], value)
return nil
}
if len(c.Args) != 2 {
return errors.New("Usage: boolProp <name> <true or false>")
return errors.New("Usage: boolProp <name> [true or false]")
}
var (
@ -229,7 +241,12 @@ func (c Command) BoolProp(d *Doodle) error {
if ok {
d.Flash("Set boolProp %s=%s", name, strconv.FormatBool(truthy))
} else {
d.Flash("Unknown boolProp name %s", name)
// Try the global boolProps in balance package.
if err := balance.BoolProp(name, truthy); err != nil {
d.Flash("%s", err)
} else {
d.Flash("%s: %+v", name, truthy)
}
}
return nil

View File

@ -10,6 +10,7 @@ import (
type Doodad struct {
level.Base
Filename string `json:"-"` // used internally, not saved in json
Hidden bool `json:"hidden,omitempty"`
Palette *level.Palette `json:"palette"`
Script string `json:"script"`
Layers []Layer `json:"layers"`

View File

@ -7,13 +7,17 @@ import (
"io/ioutil"
"os"
"path/filepath"
"git.kirsle.net/apps/doodle/pkg/balance"
)
// ToJSON serializes the doodad as JSON.
func (d *Doodad) ToJSON() ([]byte, error) {
out := bytes.NewBuffer([]byte{})
encoder := json.NewEncoder(out)
encoder.SetIndent("", "\t")
if balance.JSONIndent {
encoder.SetIndent("", "\t")
}
err := encoder.Encode(d)
return out.Bytes(), err
}

View File

@ -8,6 +8,7 @@ import (
"git.kirsle.net/apps/doodle/lib/events"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/doodads"
"git.kirsle.net/apps/doodle/pkg/drawtool"
"git.kirsle.net/apps/doodle/pkg/enum"
@ -86,6 +87,18 @@ func (s *EditorScene) Setup(d *Doodle) error {
}
}
// Write locked level?
if s.Level != nil && s.Level.Locked {
if balance.WriteLockOverride {
d.Flash("Note: write lock has been overridden")
} else {
d.Flash("That level is write-protected and cannot be viewed in the editor.")
s.Level = nil
s.UI.Canvas.ClearActors()
s.filename = ""
}
}
// No level?
if s.Level == nil {
log.Debug("EditorScene.Setup: initializing a new Level")
@ -96,7 +109,7 @@ func (s *EditorScene) Setup(d *Doodle) error {
s.UI.Canvas.Scrollable = true
}
case enum.DoodadDrawing:
// No Doodad?
// Getting a doodad from file?
if s.filename != "" && s.OpenFile {
log.Debug("EditorScene.Setup: Loading doodad from filename at %s", s.filename)
if err := s.LoadDoodad(s.filename); err != nil {
@ -104,6 +117,17 @@ func (s *EditorScene) Setup(d *Doodle) error {
}
}
// Write locked doodad?
if s.Doodad != nil && s.Doodad.Locked {
if balance.WriteLockOverride {
d.Flash("Note: write lock has been overridden")
} else {
d.Flash("That doodad is write-protected and cannot be viewed in the editor.")
s.Doodad = nil
s.filename = ""
}
}
// No Doodad?
if s.Doodad == nil {
log.Debug("EditorScene.Setup: initializing a new Doodad")

View File

@ -243,7 +243,7 @@ func (u *EditorUI) Loop(ev *events.State) error {
)
// Statusbar filename label.
filename := "untitled.map"
filename := "untitled.level"
fileType := "Level"
if u.Scene.filename != "" {
filename = u.Scene.filename

View File

@ -126,14 +126,33 @@ func (u *EditorUI) setupDoodadFrame(e render.Engine, window *ui.Window) (*ui.Fra
var buttonSize = (paletteWidth - window.BoxThickness(2)) / int32(perRow)
u.doodadButtonSize = buttonSize
// Load all the doodads, skip hidden ones.
var items []*doodads.Doodad
for _, filename := range doodadsAvailable {
doodad, err := doodads.LoadFile(filename)
if err != nil {
log.Error(err.Error())
doodad = doodads.New(balance.DoodadSize)
}
// Skip hidden doodads.
if doodad.Hidden && !balance.ShowHiddenDoodads {
log.Info("skip %s: hidden doodad", filename)
continue
}
doodad.Filename = filename
items = append(items, doodad)
}
// Draw the doodad buttons in a grid `perRow` buttons wide.
var (
row *ui.Frame
rowCount int // for labeling the ui.Frame for each row
btnRows = []*ui.Frame{} // Collect the row frames for the buttons.
)
for i, filename := range doodadsAvailable {
filename := filename
for i, doodad := range items {
doodad := doodad
if row == nil || i%perRow == 0 {
rowCount++
@ -146,18 +165,12 @@ func (u *EditorUI) setupDoodadFrame(e render.Engine, window *ui.Window) (*ui.Fra
})
}
doodad, err := doodads.LoadFile(filename)
if err != nil {
log.Error(err.Error())
doodad = doodads.New(balance.DoodadSize)
}
can := uix.NewCanvas(int(buttonSize), true)
can.Name = filename
can.Name = doodad.Title
can.SetBackground(render.White)
can.LoadDoodad(doodad)
btn := ui.NewButton(filename, can)
btn := ui.NewButton(doodad.Title, can)
btn.Resize(render.NewRect(
buttonSize-2, // TODO: without the -2 the button border
buttonSize-2, // rests on top of the window border.

View File

@ -2,6 +2,7 @@ package level
import (
"fmt"
"strings"
"git.kirsle.net/apps/doodle/lib/render"
)
@ -44,6 +45,32 @@ func (s Swatch) String() string {
return s.Name
}
// Attributes returns a comma-separated list of attributes as a string on
// this swatch. This is for debugging and the `doodad show` CLI command to
// summarize the swatch and shouldn't be used for game logic.
func (s *Swatch) Attributes() string {
var result string
if s.Solid {
result += "solid,"
}
if s.Fire {
result += "fire,"
}
if s.Water {
result += "water,"
}
if s.isSparse {
result += "sparse,"
}
if result == "" {
result = "none,"
}
return strings.TrimSuffix(result, ",")
}
// IsSparse returns whether this Swatch is sparse (has only a palette index) and
// requires inflation.
func (s *Swatch) IsSparse() bool {

View File

@ -21,16 +21,16 @@ type Base struct {
GameVersion string `json:"gameVersion" msgpack:"1"` // Game version that created the level.
Title string `json:"title" msgpack:"2"`
Author string `json:"author" msgpack:"3"`
Locked bool `json:"locked" msgpack:"4"`
// Every drawing type is able to embed other files inside of itself.
Files FileSystem `json:"files" msgpack:"4"`
Files FileSystem `json:"files" msgpack:"5"`
}
// Level is the container format for Doodle map drawings.
type Level struct {
Base
Password string `json:"passwd" msgpack:"10"`
Locked bool `json:"locked" msgpack:"11"`
// Chunked pixel data.
Chunker *Chunker `json:"chunks" msgpack:"12"`

View File

@ -31,6 +31,11 @@ func (w *Canvas) InstallActors(actors level.ActorMap) error {
return nil
}
// ClearActors removes all the actors from the Canvas.
func (w *Canvas) ClearActors() {
w.actors = []*Actor{}
}
// SetScriptSupervisor assigns the Canvas scripting supervisor to enable
// interaction with actor scripts.
func (w *Canvas) SetScriptSupervisor(s *scripting.Supervisor) {