diff --git a/dev-assets/doodads/azulian/azulian.js b/dev-assets/doodads/azulian/azulian.js index d34cefa..221bc16 100644 --- a/dev-assets/doodads/azulian/azulian.js +++ b/dev-assets/doodads/azulian/azulian.js @@ -52,7 +52,7 @@ function main() { Events.OnCollide((e) => { // If we're diving and we hit the player, game over! // Azulians are friendly to Thieves though! - if (e.Settled && e.Actor.IsPlayer() && e.Actor.Doodad().Filename !== "thief.doodad") { + if (e.Settled && e.Actor.IsPlayer() && e.Actor.Doodad().Filename !== "thief.doodad" && e.Actor.Doodad().Title.indexOf("Azulian") < 0) { FailLevel("Watch out for the Azulians!"); return; } diff --git a/dev-assets/doodads/bird/bird.js b/dev-assets/doodads/bird/bird.js index 8d24276..7a11162 100644 --- a/dev-assets/doodads/bird/bird.js +++ b/dev-assets/doodads/bird/bird.js @@ -144,32 +144,45 @@ function AI_ScanForPlayer() { // If under control of the player character. function player() { + var playerSpeed = 12; + Self.SetInventory(true); Events.OnKeypress((ev) => { Vx = 0; Vy = 0; - if (ev.Up) { - Vy = -playerSpeed; - } else if (ev.Down) { - Vy = playerSpeed; + if (ev.Right) { + direction = "right"; + } else if (ev.Left) { + direction = "left"; } - if (ev.Right) { + // Dive! + if (ev.Down && ev.Right) { + Self.StopAnimation(); + Self.ShowLayerNamed("dive-right"); + } else if (ev.Down && ev.Left) { + Self.StopAnimation(); + Self.ShowLayerNamed("dive-left"); + } else if (ev.Right) { + // Fly right. if (!Self.IsAnimating()) { Self.PlayAnimation("fly-right", null); } Vx = playerSpeed; } else if (ev.Left) { + // Fly left. if (!Self.IsAnimating()) { Self.PlayAnimation("fly-left", null); } Vx = -playerSpeed; } else { - Self.StopAnimation(); - animating = false; + // Hover in place. + if (!Self.IsAnimating()) { + Self.PlayAnimation("fly-"+direction); + } } - Self.SetVelocity(Vector(Vx, Vy)); + // Self.SetVelocity(Vector(Vx, Vy)); }) } diff --git a/dev-assets/doodads/objects/checkpoint-flag.js b/dev-assets/doodads/objects/checkpoint-flag.js index 4e74741..5b28815 100644 --- a/dev-assets/doodads/objects/checkpoint-flag.js +++ b/dev-assets/doodads/objects/checkpoint-flag.js @@ -1,14 +1,25 @@ // Checkpoint Flag. -var isCurrentCheckpoint = false; +var isCurrentCheckpoint = false, + playerEntered = false + broadcastCooldown = time.Now(); function main() { Self.SetHitbox(22 + 16, 16, 75 - 16, 86); setActive(false); + // If the checkpoint is linked to any doodad, the player character will + // become that doodad when they cross this checkpoint. + let skin = null; + for (let actor of Self.GetLinks()) { + skin = actor.Filename; + actor.Destroy(); + } + // Checkpoints broadcast to all of their peers so they all // know which one is the most recently activated. Message.Subscribe("broadcast:checkpoint", (currentID) => { setActive(false); + return "a ok"; }); Events.OnCollide((e) => { @@ -21,10 +32,19 @@ function main() { return; } - // Set the player checkpoint. SetCheckpoint(Self.Position()); setActive(true); - Message.Broadcast("broadcast:checkpoint", Self.ID()) + + // Don't spam the PubSub queue or we get races and deadlocks. + if (time.Now().After(broadcastCooldown)) { + Message.Broadcast("broadcast:checkpoint", Self.ID()); + broadcastCooldown = time.Now().Add(5 * time.Second) + } + + // Are we setting a new player skin? + if (skin && e.Actor.Doodad().Filename !== skin) { + Actors.SetPlayerCharacter(skin); + } }); } diff --git a/dev-assets/doodads/thief/thief.js b/dev-assets/doodads/thief/thief.js index 91ee6a7..77c2a70 100644 --- a/dev-assets/doodads/thief/thief.js +++ b/dev-assets/doodads/thief/thief.js @@ -107,24 +107,17 @@ function ai() { // If under control of the player character. function playable() { Events.OnKeypress((ev) => { - Vx = 0; - Vy = 0; - if (ev.Right) { if (!Self.IsAnimating()) { Self.PlayAnimation("walk-right", null); } - Vx = playerSpeed; } else if (ev.Left) { if (!Self.IsAnimating()) { Self.PlayAnimation("walk-left", null); } - Vx = -playerSpeed; } else { Self.StopAnimation(); animating = false; } - - // Self.SetVelocity(Point(Vx, Vy)); }) } diff --git a/pkg/play_scene.go b/pkg/play_scene.go index ecb7230..0c5405c 100644 --- a/pkg/play_scene.go +++ b/pkg/play_scene.go @@ -221,6 +221,9 @@ func (s *PlayScene) setupAsync(d *Doodle) error { } } + // Handle a doodad changing the player character. + s.drawing.OnSetPlayerCharacter = s.SetPlayerCharacter + // Given a filename or map data to play? if s.Level != nil { log.Debug("PlayScene.Setup: received level from scene caller") @@ -259,7 +262,7 @@ func (s *PlayScene) setupAsync(d *Doodle) error { } // Load in the player character. - s.setupPlayer() + s.setupPlayer(balance.PlayerCharacterDoodad) // Run all the actor scripts' main() functions. if err := s.drawing.InstallScripts(); err != nil { @@ -286,15 +289,29 @@ func (s *PlayScene) setupAsync(d *Doodle) error { return nil } +// SetPlayerCharacter changes the doodad used for the player, by destroying the +// current player character and making it from scratch. +func (s *PlayScene) SetPlayerCharacter(filename string) { + spawn := s.Player.Position() + s.Player.Destroy() + s.drawing.RemoveActor(s.Player) + + log.Info("SetPlayerCharacter: %s", filename) + s.installPlayerDoodad(filename, spawn, render.Rect{}) + if err := s.drawing.InstallScripts(); err != nil { + log.Error("SetPlayerCharacter: InstallScripts: %s", err) + } +} + // setupPlayer creates and configures the Player Character in the level. -func (s *PlayScene) setupPlayer() { +func (s *PlayScene) setupPlayer(playerCharacterFilename string) { // Find the spawn point of the player. Search the level for the // "start-flag.doodad" var ( - playerCharacterFilename = balance.PlayerCharacterDoodad - isStartFlagCharacter bool + isStartFlagCharacter bool spawn render.Point + centerIn render.Rect flag = &level.Actor{} flagSize = render.NewRect(86, 86) // TODO: start-flag.doodad is 86x86 px flagCount int @@ -333,23 +350,14 @@ func (s *PlayScene) setupPlayer() { // The Start Flag becomes the player's initial checkpoint. s.lastCheckpoint = flag.Point - // Load in the player character. - player, err := doodads.LoadFile(playerCharacterFilename) - if err != nil { - log.Error("PlayScene.Setup: failed to load player doodad: %s", err) - player = doodads.NewDummy(32) - } - if !s.SpawnPoint.IsZero() { spawn = s.SpawnPoint } else { - spawn = render.NewPoint( - // X: centered inside the flag. - flag.Point.X+(flagSize.W/2)-(player.Layers[0].Chunker.Size/2), - - // Y: the bottom of the flag, 4 pixels from the floor. - flag.Point.Y+flagSize.H-4-(player.Layers[0].Chunker.Size), - ) + spawn = flag.Point + centerIn = render.Rect{ + W: flagSize.W, + H: flagSize.H, + } } // Surface warnings around the spawn flag. @@ -359,6 +367,33 @@ func (s *PlayScene) setupPlayer() { s.d.FlashError("Warning: this level contains multiple Start Flags. Player spawn point is ambiguous.") } + s.installPlayerDoodad(playerCharacterFilename, spawn, centerIn) +} + +// Load and install the player doodad onto the level. +// Make sure the previous PLAYER was removed. +// If spawn is zero, uses the player's last spawn point. +// centerIn is optional, ignored if zero. +func (s *PlayScene) installPlayerDoodad(filename string, spawn render.Point, centerIn render.Rect) { + // Load in the player character. + player, err := doodads.LoadFile(filename) + if err != nil { + log.Error("PlayScene.Setup: failed to load player doodad: %s", err) + player = doodads.NewDummy(32) + } + + // Center the player within the box of the doodad, for the Start Flag especially. + if !centerIn.IsZero() { + spawn = render.NewPoint( + spawn.X+(centerIn.W/2)-(player.Layers[0].Chunker.Size/2), + + // Y: the bottom of the flag, 4 pixels from the floor. + spawn.Y+centerIn.H-4-(player.Layers[0].Chunker.Size), + ) + } else if spawn.IsZero() && !s.SpawnPoint.IsZero() { + spawn = s.SpawnPoint + } + s.Player = uix.NewActor("PLAYER", &level.Actor{}, player) s.Player.MoveTo(spawn) s.drawing.AddActor(s.Player) diff --git a/pkg/scripting/events.go b/pkg/scripting/events.go index 664cba6..b22e516 100644 --- a/pkg/scripting/events.go +++ b/pkg/scripting/events.go @@ -6,6 +6,7 @@ import ( "sync" "git.kirsle.net/apps/doodle/pkg/keybind" + "git.kirsle.net/apps/doodle/pkg/log" "github.com/dop251/goja" ) @@ -99,6 +100,15 @@ func (e *Events) run(name string, args ...interface{}) error { e.lock.RLock() defer e.lock.RUnlock() + defer func() { + if err := recover(); err != nil { + // TODO EXCEPTIONS: I once saw a "runtime error: index out of range [-1]" + // from an OnCollide handler between azu-white and thief that was crashing + // the app, report this upstream nicely to the user. + log.Error("PANIC: JS %s handler: %s", name, err) + } + }() + if _, ok := e.registry[name]; !ok { return nil } @@ -116,6 +126,10 @@ func (e *Events) run(name string, args ...interface{}) error { value, err := function(goja.Undefined(), params...) if err != nil { + // TODO EXCEPTIONS: this err is useful like + // `ReferenceError: playerSpeed is not defined at :173:9(93)` + // but wherever we're returning the err to isn't handling it! + log.Error("Scripting error on %s: %s", name, err) return err } diff --git a/pkg/scripting/pubsub.go b/pkg/scripting/pubsub.go index 6bff350..93cbda3 100644 --- a/pkg/scripting/pubsub.go +++ b/pkg/scripting/pubsub.go @@ -23,6 +23,14 @@ func RegisterPublishHooks(s *Supervisor, vm *VM) { // Goroutine to watch the VM's inbound channel and invoke Subscribe handlers // for any matching messages received. go func() { + // Catch any exceptions raised by the JavaScript VM. + defer func() { + if err := recover(); err != nil { + // TODO EXCEPTIONS + log.Error("RegisterPublishHooks(%s): %s", vm.Name, err) + } + }() + for msg := range vm.Inbound { vm.muSubscribe.Lock() @@ -30,8 +38,7 @@ func RegisterPublishHooks(s *Supervisor, vm *VM) { for _, callback := range vm.subscribe[msg.Name] { log.Debug("PubSub: %s receives from %s: %s", vm.Name, msg.SenderID, msg.Name) if function, ok := goja.AssertFunction(callback); ok { - result, err := function(goja.Undefined(), msg.Args...) - log.Debug("Result: %s, %s", result, err) + function(goja.Undefined(), msg.Args...) } } } @@ -70,6 +77,10 @@ func RegisterPublishHooks(s *Supervisor, vm *VM) { "Broadcast": func(name string, v ...goja.Value) { // Send the message to all actor VMs. for _, toVM := range s.scripts { + if toVM == nil { + continue + } + if vm.Name == toVM.Name { log.Debug("Broadcast(%s): skip to vm '%s' cuz it is the sender", name, toVM.Name) continue diff --git a/pkg/scripting/scripting.go b/pkg/scripting/scripting.go index 5e7d50c..03163a8 100644 --- a/pkg/scripting/scripting.go +++ b/pkg/scripting/scripting.go @@ -109,3 +109,12 @@ func (s *Supervisor) GetVM(name string) (*VM, error) { } return nil, errors.New("not found") } + +// RemoveVM removes a script from the supervisor, stopping it. +func (s *Supervisor) RemoveVM(name string) error { + if _, ok := s.scripts[name]; ok { + delete(s.scripts, name) + return nil + } + return errors.New("not found") +} diff --git a/pkg/uix/actor_collision.go b/pkg/uix/actor_collision.go index 5fe139f..5348f67 100644 --- a/pkg/uix/actor_collision.go +++ b/pkg/uix/actor_collision.go @@ -39,6 +39,10 @@ func (w *Canvas) loopActorCollision() error { // trying to take your inventory. // var wg sync.WaitGroup for i, a := range w.actors { + if a.IsFrozen() { + continue + } + // wg.Add(1) //go func(i int, a *Actor) { @@ -236,6 +240,9 @@ func (w *Canvas) loopActorCollision() error { lastGoodBox.X = lockX } } else { + if err != nil { + log.Error("RunCollide on %s (%s) errored: %s", a.ID(), a.Actor.Filename, err) + } // Move them back to the last good box. lastGoodBox = test } diff --git a/pkg/uix/canvas.go b/pkg/uix/canvas.go index ee03994..06cd283 100644 --- a/pkg/uix/canvas.go +++ b/pkg/uix/canvas.go @@ -91,6 +91,10 @@ type Canvas struct { // Collision handlers for level geometry. OnLevelCollision func(*Actor, *collision.Collide) + // Handler when a doodad script called Actors.SetPlayerCharacter. + // The filename.doodad is given. + OnSetPlayerCharacter func(filename string) + /******** * Editable canvas private variables. ********/ diff --git a/pkg/uix/canvas_actors.go b/pkg/uix/canvas_actors.go index bab7834..8b4d87a 100644 --- a/pkg/uix/canvas_actors.go +++ b/pkg/uix/canvas_actors.go @@ -81,6 +81,11 @@ func (w *Canvas) InstallScripts() error { for _, actor := range w.actors { vm := w.scripting.To(actor.ID()) + if vm.Self != nil { + // Already initialized! + continue + } + // Security: expose a selective API to the actor to the JS engine. vm.Self = w.MakeSelfAPI(actor) w.MakeScriptAPI(vm) @@ -114,6 +119,19 @@ func (w *Canvas) AddActor(actor *Actor) error { return nil } +// RemoveActor removes the actor from the canvas. +func (w *Canvas) RemoveActor(actor *Actor) { + var actors = []*Actor{} + for _, exist := range w.actors { + if actor == exist { + w.scripting.RemoveVM(actor.ID()) + continue + } + actors = append(actors, exist) + } + w.actors = actors +} + // drawActors is a subroutine of Present() that superimposes the actors on top // of the level drawing. func (w *Canvas) drawActors(e render.Engine, p render.Point) { diff --git a/pkg/uix/scripting.go b/pkg/uix/scripting.go index d01fc7c..207dc2c 100644 --- a/pkg/uix/scripting.go +++ b/pkg/uix/scripting.go @@ -54,6 +54,15 @@ func (w *Canvas) MakeScriptAPI(vm *scripting.VM) { return actor }, + + // Actors.SetPlayerCharacter: remake the player character. + "SetPlayerCharacter": func(filename string) { + if w.OnSetPlayerCharacter != nil { + w.OnSetPlayerCharacter(filename) + } else { + log.Error("Actors.SetPlayerCharacter: caller was not ready") + } + }, }) } @@ -98,6 +107,8 @@ func (w *Canvas) MakeSelfAPI(actor *Actor) map[string]interface{} { "HasItem": actor.HasItem, "ClearInventory": actor.ClearInventory, "Destroy": actor.Destroy, + "Freeze": actor.Freeze, + "Unfreeze": actor.Unfreeze, "Hide": actor.Hide, "Show": actor.Show, "GetLinks": func() []map[string]interface{} {