Doodads: Use Key and Working Warp Doors

* The "Use Key" (Q or Spacebar) now activates the Warp Door instead of a
  collision event doing so.
* Warp Doors are now functional: the player opens a door, disappears,
  the door closes; player is teleported to the linked door which opens,
  appears the player and closes.
* If the player exits thru a Blue or Orange door which is disabled
  (dotted outline), the door still opens and drops the player off but
  returns to a Disabled state, acting as a one-way door.
* Clean up several debug log lines from Doodle and doodad scripts.
This commit is contained in:
Noah 2021-01-03 15:19:21 -08:00
parent 2c1185cc9f
commit 3892087932
26 changed files with 218 additions and 77 deletions

View File

@ -1,6 +1,4 @@
function main() {
console.log("Azulian '%s' initialized!", Self.Title);
var playerSpeed = 4;
var gravity = 4;
var Vx = Vy = 0;

View File

@ -4,8 +4,6 @@ function main() {
var Vx = Vy = 0;
var altitude = Self.Position().Y; // original height in the level
console.log("Bird altitude is %d", altitude);
var direction = "left";
var states = {
flying: 0,

View File

@ -1,6 +1,4 @@
function main() {
console.log("%s initialized!", Self.Title);
var timer = 0;
var pressed = false;

View File

@ -1,6 +1,4 @@
function main() {
console.log("%s initialized!", Self.Title);
var pressed = false;
// When a sticky button receives power, it pops back up.

View File

@ -1,6 +1,4 @@
function main() {
console.log("%s initialized!", Self.Title);
Self.AddAnimation("open", 100, [0, 1, 2, 3]);
Self.AddAnimation("close", 100, [3, 2, 1, 0]);
var animating = false;
@ -9,8 +7,6 @@ function main() {
Self.SetHitbox(0, 0, 34, 76);
Message.Subscribe("power", function(powered) {
console.log("%s got power=%+v", Self.Title, powered);
if (powered) {
if (animating || opened) {
return;

View File

@ -1,6 +1,5 @@
// Exit Flag.
function main() {
console.log("%s initialized!", Self.Title);
Self.SetHitbox(22+16, 16, 75-16, 86);
Events.OnCollide(function(e) {

View File

@ -4,7 +4,6 @@
var state = false;
function main() {
console.log("%s ID '%s' initialized!", Self.Title, Self.ID());
Self.SetHitbox(0, 0, 42, 42);
// When the button is activated, don't keep toggling state until we're not

View File

@ -1,5 +1,4 @@
function main() {
console.log("%s initialized!", Self.Title);
// Switch has two frames:
// 0: Off

View File

@ -1,6 +1,4 @@
function main() {
console.log("%s initialized!", Self.Title);
var timer = 0;
Self.SetHitbox(0, 0, 72, 6);

View File

@ -1,7 +1,6 @@
function main() {
// What direction is the trapdoor facing?
var direction = Self.GetTag("direction");
console.log("Trapdoor(%s) initialized", direction);
var timer = 0;

View File

@ -1,7 +1,5 @@
// Warp Doors
function main() {
console.log("Warp Door %s Initialized", Self.Title);
Self.SetHitbox(0, 0, 34, 76);
// Are we a blue or orange door? Regular warp door will be 'none'
@ -31,12 +29,18 @@ function main() {
spriteDefault = "door-1";
}
console.log("Warp %s: default=%s disabled=%+v color=%s isState=%+v state=%+v", Self.Title, spriteDefault, spriteDisabled, color, isStateDoor, state);
// Find our linked Warp Door.
var links = Self.GetLinks()
var linkedDoor = null;
for (var i = 0; i < links.length; i++) {
if (links[i].Title.indexOf("Warp Door") > -1) {
linkedDoor = links[i];
}
}
// Subscribe to the global state-change if we are a state door.
if (isStateDoor) {
Message.Subscribe("broadcast:state-change", function(newState) {
console.log("Warp %s: received state to %+v", Self.Title, newState);
state = color === 'blue' ? !newState : newState;
// Activate or deactivate the door.
@ -44,38 +48,69 @@ function main() {
});
}
// TODO: respond to a "Use" button instead of a Collide to open the door.
Events.OnCollide(function(e) {
if (!e.Settled) {
// The player Uses the door.
var flashedCooldown = false; // "Locked Door" flashed message.
Events.OnUse(function(e) {
if (animating) {
return;
}
if (animating || collide) {
// Doors without linked exits are not usable.
if (linkedDoor === null) {
if (!flashedCooldown) {
Flash("This door is locked.");
flashedCooldown = true;
setTimeout(function() {
flashedCooldown = false;
}, 1000);
}
return;
}
// Only players can use doors for now.
if (e.Actor.IsPlayer() && e.InHitbox) {
if (e.Actor.IsPlayer()) {
if (isStateDoor && !state) {
// The state door is inactive (dotted outline).
return;
}
// Freeze the player.
e.Actor.Freeze()
// Play the open and close animation.
animating = true;
collide = true;
Self.PlayAnimation("open", function() {
e.Actor.Hide()
Self.PlayAnimation("close", function() {
Self.ShowLayerNamed(isStateDoor && !state ? spriteDisabled : spriteDefault);
e.Actor.Show()
animating = false;
// Teleport the player to the linked door. Inform the target
// door of the arrival of the player so it doesn't trigger
// to send the player back here again on a loop.
if (linkedDoor !== null) {
Message.Publish("warp-door:incoming", e.Actor);
e.Actor.MoveTo(linkedDoor.Position());
}
});
});
}
});
Events.OnLeave(function(e) {
collide = false;
// Respond to incoming warp events.
Message.Subscribe("warp-door:incoming", function(player) {
animating = true;
player.Unfreeze();
Self.PlayAnimation("open", function() {
player.Show();
Self.PlayAnimation("close", function() {
animating = false;
// If the receiving door was a State Door, fix its state.
if (isStateDoor) {
Self.ShowLayerNamed(state ? spriteDefault : spriteDisabled);
}
});
});
});
}

View File

@ -109,7 +109,6 @@ func NewEditorUI(d *Doodle, s *EditorScene) *EditorUI {
// Preload pop-up windows before they're needed.
u.SetupPopups(d)
log.Error("menu size: %s", u.MenuBar.Rect())
u.screen.Pack(u.MenuBar, ui.Pack{
Side: ui.N,
FillX: true,
@ -676,7 +675,6 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar {
menu.Supervise(u.Supervisor)
menu.Compute(d.Engine)
log.Error("Setup MenuBar: %s\n", menu.Size())
return menu
}

View File

@ -106,3 +106,33 @@ func EraserTool(ev *event.State) bool {
func DoodadDropper(ev *event.State) bool {
return ev.KeyDown("d")
}
// Shift key.
func Shift(ev *event.State) bool {
return ev.Shift
}
// Left arrow.
func Left(ev *event.State) bool {
return ev.Left || ev.KeyDown("a")
}
// Right arrow.
func Right(ev *event.State) bool {
return ev.Right || ev.KeyDown("d")
}
// Up arrow.
func Up(ev *event.State) bool {
return ev.Up || ev.KeyDown("w")
}
// Down arrow.
func Down(ev *event.State) bool {
return ev.Down || ev.KeyDown("s")
}
// "Use" button.
func Use(ev *event.State) bool {
return ev.Space || ev.KeyDown("q")
}

View File

@ -57,7 +57,7 @@ func LoadFile(filename string) (*Level, error) {
// Do we have the file in bindata?
if jsonData, err := bindata.Asset(filename); err == nil {
log.Info("loaded from embedded bindata")
log.Debug("Level %s: loaded from embedded bindata", filename)
return FromJSON(filename, jsonData)
}

View File

@ -471,30 +471,30 @@ func (s *PlayScene) movePlayer(ev *event.State) {
velocity.Y = 0
// Shift to slow your roll to 1 pixel per tick.
if ev.Shift {
if keybind.Shift(ev) {
playerSpeed = 1
}
if ev.Left {
if keybind.Left(ev) {
velocity.X = -playerSpeed
} else if ev.Right {
} else if keybind.Right(ev) {
velocity.X = playerSpeed
}
if ev.Up {
if keybind.Up(ev) {
velocity.Y = -playerSpeed
} else if ev.Down {
} else if keybind.Down(ev) {
velocity.Y = playerSpeed
}
} else {
// Moving left or right.
if ev.Left {
if keybind.Left(ev) {
direction = -1
} else if ev.Right {
} else if keybind.Right(ev) {
direction = 1
}
// Up button to signal they want to jump.
if ev.Up && (s.Player.Grounded() || s.playerJumpCounter >= 0) {
if keybind.Up(ev) && (s.Player.Grounded() || s.playerJumpCounter >= 0) {
jumping = true
if s.Player.Grounded() {
@ -533,8 +533,18 @@ func (s *PlayScene) movePlayer(ev *event.State) {
}
}
// Move the player unless frozen.
// TODO: if Y=0 then gravity fails, but not doing this allows the
// player to jump while frozen. Not a HUGE deal right now as only Warp Doors
// freeze the player currently but do address this later.
if s.Player.IsFrozen() {
velocity.X = 0
}
s.Player.SetVelocity(velocity)
// If the "Use" key is pressed, set an actor flag on the player.
s.Player.SetUsing(keybind.Use(ev))
s.scripting.To(s.Player.ID()).Events.RunKeypress(ev)
}

View File

@ -13,6 +13,7 @@ const (
CollideEvent = "OnCollide" // another doodad collides with us
EnterEvent = "OnEnter" // a doodad is fully inside us
LeaveEvent = "OnLeave" // a doodad no longer collides with us
UseEvent = "OnUse" // player pressed the Use key while touching us
// Controllable (player character) doodad events
KeypressEvent = "OnKeypress" // i.e. arrow keys
@ -46,6 +47,16 @@ func (e *Events) RunCollide(v interface{}) error {
return e.run(CollideEvent, v)
}
// OnUse fires when another actor collides with yours.
func (e *Events) OnUse(call otto.FunctionCall) otto.Value {
return e.register(UseEvent, call.Argument(0))
}
// RunUse invokes the OnUse handler function.
func (e *Events) RunUse(v interface{}) error {
return e.run(UseEvent, v)
}
// OnLeave fires when another actor stops colliding with yours.
func (e *Events) OnLeave(call otto.FunctionCall) otto.Value {
return e.register(LeaveEvent, call.Argument(0))

View File

@ -93,9 +93,9 @@ func (s *Supervisor) To(name string) *VM {
// TODO: put this log back in, but add PLAYER script so it doesn't spam
// the console for missing PLAYER.
// log.Error("scripting.Supervisor.To(%s): no such VM but returning blank VM",
// name,
// )
log.Error("scripting.Supervisor.To(%s): no such VM but returning blank VM",
name,
)
return NewVM(name)
}

View File

@ -25,19 +25,20 @@ import (
// as defined in the map: its spawn coordinate and configuration.
// - A uix.Canvas that can present the actor's graphics to the screen.
type Actor struct {
id string
Drawing *doodads.Drawing
Actor *level.Actor
Canvas *Canvas
activeLayer int // active drawing frame for display
flagDestroy bool // flag the actor for destruction
flagUsing bool // flag that the (player) has pressed the Use key.
// Actor runtime variables.
hasGravity bool
isMobile bool // Mobile character, such as the player or an enemy
noclip bool // Disable collision detection
hidden bool // invisible, via Hide() and Show()
frozen bool // Frozen, via Freeze() and Unfreeze()
hitbox render.Rect
inventory map[string]int // item inventory. doodad name -> quantity, 0 for key item.
data map[string]string // arbitrary key/value store. DEPRECATED ??
@ -174,6 +175,28 @@ func (a *Actor) Show() {
a.hidden = false
}
// Freeze an actor. For the player character, this means arrow key inputs
// will stop moving the actor.
func (a *Actor) Freeze() {
a.frozen = true
}
// Unfreeze an actor.
func (a *Actor) Unfreeze() {
a.frozen = false
}
// IsFrozen returns true if the actor is frozen.
func (a *Actor) IsFrozen() bool {
return a.frozen
}
// SetUsing enables the "Use Key" flag, mainly for the player character to activate
// certain doodads in the level.
func (a *Actor) SetUsing(v bool) {
a.flagUsing = v
}
// SetNoclip sets the noclip setting for an actor. If true, the actor can
// clip through level geometry.
func (a *Actor) SetNoclip(v bool) {

View File

@ -276,6 +276,16 @@ func (w *Canvas) loopActorCollision() error {
}); err != nil && err != scripting.ErrReturnFalse {
log.Error("VM(%s).RunCollide: %s", a.ID(), err.Error())
}
// If the (player) is pressing the Use key, call the colliding
// actor's OnUse event.
if b.flagUsing {
if err := w.scripting.To(a.ID()).Events.RunUse(&UseEvent{
Actor: b,
}); err != nil {
log.Error("VM(%s).RunUse: %s", a.ID(), err.Error())
}
}
}
}
}

View File

@ -9,3 +9,8 @@ type CollideEvent struct {
InHitbox bool // If the two elected hitboxes are overlapping
Settled bool // Movement phase finished, actor script can fire actions
}
// UseEvent holds data sent to an actor's OnUse handler.
type UseEvent struct {
Actor *Actor
}

View File

@ -57,26 +57,7 @@ func (w *Canvas) InstallScripts() error {
vm := w.scripting.To(actor.ID())
// Security: expose a selective API to the actor to the JS engine.
vm.Self = map[string]interface{}{
"Filename": actor.Doodad().Filename,
"Title": actor.Doodad().Title,
// functions
"ID": actor.ID,
"GetTag": actor.Doodad().Tag,
"Position": actor.Position,
"SetHitbox": actor.SetHitbox,
"SetVelocity": actor.SetVelocity,
"SetMobile": actor.SetMobile,
"SetGravity": actor.SetGravity,
"AddAnimation": actor.AddAnimation,
"IsAnimating": actor.IsAnimating,
"PlayAnimation": actor.PlayAnimation,
"StopAnimation": actor.StopAnimation,
"ShowLayer": actor.ShowLayer,
"ShowLayerNamed": actor.ShowLayerNamed,
"Destroy": actor.Destroy,
}
vm.Self = w.MakeSelfAPI(actor)
vm.Set("Self", vm.Self)
if _, err := vm.Run(actor.Doodad().Script); err != nil {

View File

@ -2,6 +2,7 @@ package uix
import (
"errors"
"sort"
"git.kirsle.net/apps/doodle/pkg/drawtool"
"git.kirsle.net/apps/doodle/pkg/shmem"
@ -34,3 +35,34 @@ func (w *Canvas) LinkAdd(a *Actor) error {
}
return nil
}
// GetLinkedActors returns the live Actor instances (Play Mode) which are linked
// to the live actor given.
func (w *Canvas) GetLinkedActors(a *Actor) []*Actor {
// Identify the linked actor UUIDs from the level file.
linkedIDs := map[string]interface{}{}
matching := map[string]*Actor{}
for _, id := range a.Actor.Links {
linkedIDs[id] = nil
}
// Find live instances of these actors.
for _, live := range w.actors {
if _, ok := linkedIDs[live.ID()]; ok {
matching[live.ID()] = live
}
}
// Sort them deterministically and return.
keys := []string{}
for key, _ := range matching {
keys = append(keys, key)
}
sort.Strings(keys)
result := []*Actor{}
for _, key := range keys {
result = append(result, matching[key])
}
return result
}

35
pkg/uix/scripting.go Normal file
View File

@ -0,0 +1,35 @@
package uix
// Functions relating to the Doodad JavaScript API for Canvas Actors.
// MakeSelfAPI generates the `Self` object for the scripting API in
// reference to a live Canvas actor in the level.
func (w *Canvas) MakeSelfAPI(actor *Actor) map[string]interface{} {
return map[string]interface{}{
"Filename": actor.Doodad().Filename,
"Title": actor.Doodad().Title,
// functions
"ID": actor.ID,
"GetTag": actor.Doodad().Tag,
"Position": actor.Position,
"SetHitbox": actor.SetHitbox,
"SetVelocity": actor.SetVelocity,
"SetMobile": actor.SetMobile,
"SetGravity": actor.SetGravity,
"AddAnimation": actor.AddAnimation,
"IsAnimating": actor.IsAnimating,
"PlayAnimation": actor.PlayAnimation,
"StopAnimation": actor.StopAnimation,
"ShowLayer": actor.ShowLayer,
"ShowLayerNamed": actor.ShowLayerNamed,
"Destroy": actor.Destroy,
"GetLinks": func() []map[string]interface{} {
var result = []map[string]interface{}{}
for _, linked := range w.GetLinkedActors(actor) {
result = append(result, w.MakeSelfAPI(linked))
}
return result
},
}
}

View File

@ -6,7 +6,6 @@ import (
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/doodads"
"git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/shmem"
"git.kirsle.net/go/render"
"git.kirsle.net/go/ui"
@ -54,7 +53,6 @@ func NewLayerWindow(config Layers) *ui.Window {
)
config.activeLayer = fmt.Sprintf("%d", config.ActiveLayer)
log.Warn("config.activeLayer=%s", config.activeLayer)
window := ui.NewWindow(title)
window.SetButtons(ui.CloseButton)
@ -71,8 +69,6 @@ func NewLayerWindow(config Layers) *ui.Window {
Expand: true,
})
log.Info("SETUP PALETTE WINDOW")
// Draw the header row.
headers := []struct {
Name string
@ -143,7 +139,6 @@ func NewLayerWindow(config Layers) *ui.Window {
})
btnName.Handle(ui.Click, func(ed ui.EventData) error {
shmem.Prompt("New layer name ["+doodad.Layers[i].Name+"]: ", func(answer string) {
log.Warn("Answer: %s", answer)
if answer != "" {
doodad.Layers[i].Name = answer
if config.OnChange != nil {
@ -220,7 +215,6 @@ func NewLayerWindow(config Layers) *ui.Window {
Font: balance.MenuFont,
OnChange: func(newPage, perPage int) {
page = newPage
log.Info("Page: %d, %d", page, perPage)
// Re-evaluate which rows are shown/hidden for this page.
var (

View File

@ -5,7 +5,6 @@ import (
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/userdir"
"git.kirsle.net/go/render"
"git.kirsle.net/go/ui"
@ -74,7 +73,6 @@ func NewOpenLevelEditor(config OpenLevelEditor) *ui.Window {
})
for i, lvl := range levels {
func(i int, lvl string) {
log.Info("Add file %s to row %s", lvl, lvlRow.Name)
btn := ui.NewButton("Level Btn", ui.NewLabel(ui.Label{
Text: lvl,
Font: balance.MenuFont,
@ -95,7 +93,6 @@ func NewOpenLevelEditor(config OpenLevelEditor) *ui.Window {
})
if i > 0 && (i+1)%4 == 0 {
log.Warn("i=%d wrapped at mod 4", i)
lvlRow = ui.NewFrame(fmt.Sprintf("Level Row %d", i))
frame.Pack(lvlRow, ui.Pack{
Side: ui.N,

View File

@ -73,8 +73,6 @@ func NewPaletteEditor(config PaletteEditor) *ui.Window {
Expand: true,
})
log.Info("SETUP PALETTE WINDOW")
// Draw the header row.
headers := []struct {
Name string