Checkpoint Flag can Re-assign Player Character

Link a Doodad to a Checkpoint Flag (like you would a Start Flag) and
crossing the flag will replace the player with that doodad. Multiple
checkpoint flags like this can toggle you between characters.

* Azulians are now friendly to player characters who have the word
  "Azulian" in their title.
* Improve Bird as the playable character:
  * Dive animation if the player flies diagonally downwards
  * Animation loop while hovering in the air instead of pausing
* Checkpoint flags don't spam each other on PubSub so much which could
  sometimes lead to deadlocks!

SetPlayerCharacter added to the JavaScript API. The Checkpoint Flag
(not the region) can link to a doodad and replace the player character
with that linked doodad when you activate the checkpoint:

    Actors.SetPlayerCharacter(filename string): like "boy.doodad"

Add various panic catchers to make JavaScript safer and log issues
to console.
pull/84/head
Noah 2022-01-18 21:24:36 -08:00
parent 44aba8f1b4
commit 626fd53a84
12 changed files with 174 additions and 39 deletions

View File

@ -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;
}

View File

@ -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));
})
}

View File

@ -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);
}
});
}

View File

@ -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));
})
}

View File

@ -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)

View File

@ -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 <eval>: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
}

View File

@ -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

View File

@ -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")
}

View File

@ -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
}

View File

@ -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.
********/

View File

@ -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) {

View File

@ -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{} {