From 0d624537efb7c54dd18050cd07a2ef0a356d749e Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Thu, 13 Feb 2025 20:12:28 -0800 Subject: [PATCH] WIP Refactor the doodad javascript API --- pkg/scripting/api/api.go | 2 + pkg/scripting/api/console.go | 32 ++++++++++++++++ pkg/scripting/api/gameplay.go | 34 +++++++++++++++++ pkg/scripting/api/time.go | 61 ++++++++++++++++++++++++++++++ pkg/scripting/js_api.go | 30 ++++++--------- pkg/scripting/supervisor_events.go | 43 +++++++++++---------- 6 files changed, 164 insertions(+), 38 deletions(-) create mode 100644 pkg/scripting/api/api.go create mode 100644 pkg/scripting/api/console.go create mode 100644 pkg/scripting/api/gameplay.go create mode 100644 pkg/scripting/api/time.go diff --git a/pkg/scripting/api/api.go b/pkg/scripting/api/api.go new file mode 100644 index 0000000..70b1774 --- /dev/null +++ b/pkg/scripting/api/api.go @@ -0,0 +1,2 @@ +// Package api defines the JavaScript API surface area for Doodad scripts. +package api diff --git a/pkg/scripting/api/console.go b/pkg/scripting/api/console.go new file mode 100644 index 0000000..6041cee --- /dev/null +++ b/pkg/scripting/api/console.go @@ -0,0 +1,32 @@ +package api + +import "github.com/dop251/goja" + +/* +Console is exported to the global scope with `console.log` and friends available. + +These functions have an API similar to that found in web browsers and node.js. +*/ +type Console struct { + Log func(string, ...interface{}) `json:"log"` + Debug func(string, ...interface{}) `json:"debug"` + Warn func(string, ...interface{}) `json:"warn"` + Error func(string, ...interface{}) `json:"error"` +} + +// ToMap converts the struct into a generic hash map. +func (g Console) ToMap() map[string]interface{} { + return map[string]interface{}{ + "log": g.Log, + "debug": g.Debug, + "warn": g.Warn, + "error": g.Error, + } +} + +// Register the global functions in your JavaScript VM. +func (g Console) Register(vm *goja.Runtime) { + for k, v := range g.ToMap() { + vm.Set(k, v) + } +} diff --git a/pkg/scripting/api/gameplay.go b/pkg/scripting/api/gameplay.go new file mode 100644 index 0000000..bcf6a9d --- /dev/null +++ b/pkg/scripting/api/gameplay.go @@ -0,0 +1,34 @@ +package api + +import ( + "git.kirsle.net/go/render" + "github.com/dop251/goja" +) + +// GameplayLevelControl +type GameplayLevelControl struct { + // EndLevel ends the game in a "win" condition. + EndLevel func() + + // FailLevel ends the game in a "fail" condition, with the message shown to the player. + FailLevel func(message string) + + // SetCheckpoint updates the player's respawn position to the given point. + SetCheckpoint func(render.Point) +} + +// ToMap converts the struct into a generic hash map. +func (g GameplayLevelControl) ToMap() map[string]interface{} { + return map[string]interface{}{ + "EndLevel": g.EndLevel, + "FailLevel": g.FailLevel, + "SetCheckpoint": g.SetCheckpoint, + } +} + +// Register the global functions in your JavaScript VM. +func (g GameplayLevelControl) Register(vm *goja.Runtime) { + for k, v := range g.ToMap() { + vm.Set(k, v) + } +} diff --git a/pkg/scripting/api/time.go b/pkg/scripting/api/time.go new file mode 100644 index 0000000..0e678a8 --- /dev/null +++ b/pkg/scripting/api/time.go @@ -0,0 +1,61 @@ +// Package api defines the JavaScript API surface area for Doodad scripts. +package api + +import ( + "time" + + "github.com/dop251/goja" +) + +// Time functions exposed on the global scope like `time.Now()`. +// +// They are mainly a direct export of useful things from the Go `time` package. +type Time struct { + Now func() time.Time + Since func(time.Time) time.Duration + Add func(t time.Time, milliseconds int64) time.Time + + // Multiples of time.Duration. + Hour time.Duration + Minute time.Duration + Second time.Duration + Millisecond time.Duration + Microsecond time.Duration +} + +// NewTime creates an instanced global `time` object for your JavaScript VM. +func NewTime() Time { + return Time{ + Now: time.Now, + Since: time.Since, + Add: func(t time.Time, ms int64) time.Time { + return t.Add(time.Duration(ms) * time.Millisecond) + }, + Hour: time.Hour, + Minute: time.Minute, + Second: time.Second, + Millisecond: time.Millisecond, + Microsecond: time.Microsecond, + } +} + +// ToMap converts the struct into a generic hash map. +func (g Time) ToMap() map[string]interface{} { + return map[string]interface{}{ + "Now": g.Now, + "Since": g.Since, + "Add": g.Add, + "Hour": g.Hour, + "Minute": g.Minute, + "Second": g.Second, + "Millisecond": g.Millisecond, + "Microsecond": g.Microsecond, + } +} + +// Register the global functions in your JavaScript VM. +func (g Time) Register(vm *goja.Runtime) { + for k, v := range g.ToMap() { + vm.Set(k, v) + } +} diff --git a/pkg/scripting/js_api.go b/pkg/scripting/js_api.go index 4e685d3..ff7116a 100644 --- a/pkg/scripting/js_api.go +++ b/pkg/scripting/js_api.go @@ -2,10 +2,10 @@ package scripting import ( "fmt" - "time" "git.kirsle.net/SketchyMaze/doodle/pkg/log" "git.kirsle.net/SketchyMaze/doodle/pkg/physics" + "git.kirsle.net/SketchyMaze/doodle/pkg/scripting/api" "git.kirsle.net/SketchyMaze/doodle/pkg/shmem" "git.kirsle.net/SketchyMaze/doodle/pkg/sound" "git.kirsle.net/go/render" @@ -29,14 +29,17 @@ func ProxyLog(vm *VM, fn func(string, ...interface{})) func(string, ...interface // NewJSProxy initializes the API structure for JavaScript binding. func NewJSProxy(vm *VM) JSProxy { + // jp := JSProxy{} + // return jp + return JSProxy{ // Console logging. - "console": map[string]interface{}{ - "log": ProxyLog(vm, log.Info), - "debug": ProxyLog(vm, log.Debug), - "warn": ProxyLog(vm, log.Warn), - "error": ProxyLog(vm, log.Error), - }, + "console": api.Console{ + Log: ProxyLog(vm, log.Info), + Debug: ProxyLog(vm, log.Debug), + Warn: ProxyLog(vm, log.Warn), + Error: ProxyLog(vm, log.Error), + }.ToMap(), // Audio API. "Sound": map[string]interface{}{ @@ -53,18 +56,7 @@ func NewJSProxy(vm *VM) JSProxy { "GetTick": func() uint64 { return shmem.Tick }, - "time": map[string]interface{}{ - "Now": time.Now, - "Since": time.Since, - "Add": func(t time.Time, ms int64) time.Time { - return t.Add(time.Duration(ms) * time.Millisecond) - }, - "Hour": time.Hour, - "Minute": time.Minute, - "Second": time.Second, - "Millisecond": time.Millisecond, - "Microsecond": time.Microsecond, - }, + "time": api.NewTime().ToMap(), // Bindings into the VM. "Events": vm.Events, diff --git a/pkg/scripting/supervisor_events.go b/pkg/scripting/supervisor_events.go index c612272..4164b33 100644 --- a/pkg/scripting/supervisor_events.go +++ b/pkg/scripting/supervisor_events.go @@ -1,6 +1,9 @@ package scripting -import "git.kirsle.net/go/render" +import ( + "git.kirsle.net/SketchyMaze/doodle/pkg/scripting/api" + "git.kirsle.net/go/render" +) /* RegisterEventHooks attaches the supervisor level event hooks into a JS VM. @@ -13,24 +16,26 @@ Names registered: - SetCheckpoint(): update the player's respawn location. */ func RegisterEventHooks(s *Supervisor, vm *VM) { - vm.Set("EndLevel", func() { - if s.onLevelFail == nil { - panic("JS FailLevel(): No OnLevelFail handler attached to script supervisor") - } - s.onLevelExit() - }) - vm.Set("FailLevel", func(message string) { - if s.onLevelFail == nil { - panic("JS FailLevel(): No OnLevelFail handler attached to script supervisor") - } - s.onLevelFail(message) - }) - vm.Set("SetCheckpoint", func(p render.Point) { - if s.onSetCheckpoint == nil { - panic("JS SetCheckpoint(): No OnSetCheckpoint handler attached to script supervisor") - } - s.onSetCheckpoint(p) - }) + api.GameplayLevelControl{ + EndLevel: func() { + if s.onLevelFail == nil { + panic("JS FailLevel(): No OnLevelFail handler attached to script supervisor") + } + s.onLevelExit() + }, + FailLevel: func(message string) { + if s.onLevelFail == nil { + panic("JS FailLevel(): No OnLevelFail handler attached to script supervisor") + } + s.onLevelFail(message) + }, + SetCheckpoint: func(p render.Point) { + if s.onSetCheckpoint == nil { + panic("JS SetCheckpoint(): No OnSetCheckpoint handler attached to script supervisor") + } + s.onSetCheckpoint(p) + }, + }.Register(vm.vm) } // OnLevelExit registers an event hook for when a Level Exit doodad is reached.