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.
This commit is contained in:
parent
44aba8f1b4
commit
626fd53a84
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
********/
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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{} {
|
||||
|
|
Loading…
Reference in New Issue
Block a user