Browse Source

v0.13.0 doodads

master
Noah Petherbridge 3 years ago
parent
commit
22c189d899
  1. 5
      doodads/azulian/Makefile
  2. 43
      doodads/azulian/azulian-red.js
  3. 135
      doodads/azulian/azulian.js
  4. BIN
      doodads/azulian/white-back.png
  5. BIN
      doodads/azulian/white-front.png
  6. BIN
      doodads/azulian/white-wl1.png
  7. BIN
      doodads/azulian/white-wl2.png
  8. BIN
      doodads/azulian/white-wl3.png
  9. BIN
      doodads/azulian/white-wl4.png
  10. BIN
      doodads/azulian/white-wr1.png
  11. BIN
      doodads/azulian/white-wr2.png
  12. BIN
      doodads/azulian/white-wr3.png
  13. BIN
      doodads/azulian/white-wr4.png
  14. 12
      doodads/bird/Makefile
  15. 247
      doodads/bird/bird.js
  16. BIN
      doodads/bird/blue/dive-left.png
  17. BIN
      doodads/bird/blue/dive-right.png
  18. BIN
      doodads/bird/blue/left-1.png
  19. BIN
      doodads/bird/blue/left-2.png
  20. BIN
      doodads/bird/blue/right-1.png
  21. BIN
      doodads/bird/blue/right-2.png
  22. 0
      doodads/bird/red/dive-left.png
  23. 0
      doodads/bird/red/dive-right.png
  24. 0
      doodads/bird/red/left-1.png
  25. 0
      doodads/bird/red/left-2.png
  26. 0
      doodads/bird/red/right-1.png
  27. 0
      doodads/bird/red/right-2.png
  28. 27
      doodads/box/box.js
  29. 2
      doodads/boy/Makefile
  30. 56
      doodads/boy/boy.js
  31. BIN
      doodads/boy/idle-left-1.png
  32. BIN
      doodads/boy/idle-left-2.png
  33. BIN
      doodads/boy/idle-left-3.png
  34. BIN
      doodads/boy/idle-right-1.png
  35. BIN
      doodads/boy/idle-right-2.png
  36. BIN
      doodads/boy/idle-right-3.png
  37. 15
      doodads/build.sh
  38. 26
      doodads/buttons/button.js
  39. 6
      doodads/buttons/sticky.js
  40. 23
      doodads/crumbly-floor/crumbly-floor.js
  41. 14
      doodads/crusher/Makefile
  42. BIN
      doodads/crusher/angry.png
  43. 207
      doodads/crusher/crusher.js
  44. BIN
      doodads/crusher/ouch.png
  45. BIN
      doodads/crusher/peek-left.png
  46. BIN
      doodads/crusher/peek-right.png
  47. BIN
      doodads/crusher/sleep.png
  48. 22
      doodads/doors/colored-door.js
  49. 25
      doodads/doors/electric-door.js
  50. 16
      doodads/doors/keys.js
  51. 27
      doodads/doors/locked-door.js
  52. 58
      doodads/gems/Makefile
  53. BIN
      doodads/gems/blue-1.png
  54. BIN
      doodads/gems/blue-2.png
  55. BIN
      doodads/gems/blue-3.png
  56. BIN
      doodads/gems/blue-4.png
  57. 24
      doodads/gems/gemstone.js
  58. BIN
      doodads/gems/green-1.png
  59. BIN
      doodads/gems/green-2.png
  60. BIN
      doodads/gems/green-3.png
  61. BIN
      doodads/gems/green-4.png
  62. BIN
      doodads/gems/red-1.png
  63. BIN
      doodads/gems/red-2.png
  64. BIN
      doodads/gems/red-3.png
  65. BIN
      doodads/gems/red-4.png
  66. BIN
      doodads/gems/totem-blue-0.png
  67. BIN
      doodads/gems/totem-blue-1.png
  68. BIN
      doodads/gems/totem-blue-2.png
  69. BIN
      doodads/gems/totem-blue-3.png
  70. BIN
      doodads/gems/totem-blue-4.png
  71. BIN
      doodads/gems/totem-green-0.png
  72. BIN
      doodads/gems/totem-green-1.png
  73. BIN
      doodads/gems/totem-green-2.png
  74. BIN
      doodads/gems/totem-green-3.png
  75. BIN
      doodads/gems/totem-green-4.png
  76. BIN
      doodads/gems/totem-red-0.png
  77. BIN
      doodads/gems/totem-red-1.png
  78. BIN
      doodads/gems/totem-red-2.png
  79. BIN
      doodads/gems/totem-red-3.png
  80. BIN
      doodads/gems/totem-red-4.png
  81. BIN
      doodads/gems/totem-yellow-0.png
  82. BIN
      doodads/gems/totem-yellow-1.png
  83. BIN
      doodads/gems/totem-yellow-2.png
  84. BIN
      doodads/gems/totem-yellow-3.png
  85. BIN
      doodads/gems/totem-yellow-4.png
  86. 100
      doodads/gems/totem.js
  87. BIN
      doodads/gems/yellow-1.png
  88. BIN
      doodads/gems/yellow-2.png
  89. BIN
      doodads/gems/yellow-3.png
  90. BIN
      doodads/gems/yellow-4.png
  91. 15
      doodads/objects/anvil.js
  92. 30
      doodads/objects/checkpoint-flag.js
  93. 2
      doodads/objects/exit-flag.js
  94. 5
      doodads/objects/start-flag.js
  95. 6
      doodads/on-off/state-block-blue.js
  96. 6
      doodads/on-off/state-block-orange.js
  97. 10
      doodads/on-off/state-button.js
  98. 9
      doodads/regions/Makefile
  99. BIN
      doodads/regions/checkpoint-128.png
  100. 4
      doodads/regions/checkpoint.js

5
doodads/azulian/Makefile

@ -14,6 +14,11 @@ build:
doodad edit-doodad --tag "color=red" azu-red.doodad doodad edit-doodad --tag "color=red" azu-red.doodad
doodad install-script azulian.js azu-red.doodad doodad install-script azulian.js azu-red.doodad
doodad convert -t "White Azulian" white-front.png white-back.png \
white-wr{1,2,3,4}.png white-wl{1,2,3,4}.png azu-white.doodad
doodad edit-doodad --tag "color=white" azu-white.doodad
doodad install-script azulian.js azu-white.doodad
# Tag the category for these doodads # Tag the category for these doodads
for i in *.doodad; do\ for i in *.doodad; do\
doodad edit-doodad --tag "category=creatures" $${i};\ doodad edit-doodad --tag "category=creatures" $${i};\

43
doodads/azulian/azulian-red.js

@ -1,43 +0,0 @@
// Azulian (Red)
// DEPRECATED: they both share azulian.js now.
function main() {
var playerSpeed = 4;
var gravity = 4;
var Vx = Vy = 0;
var direction = "right";
Self.SetHitbox(0, 0, 32, 32)
Self.SetMobile(true);
Self.SetInventory(true);
Self.SetGravity(true);
Self.AddAnimation("walk-left", 100, ["red-wl1", "red-wl2", "red-wl3", "red-wl4"]);
Self.AddAnimation("walk-right", 100, ["red-wr1", "red-wr2", "red-wr3", "red-wr4"]);
// Sample our X position every few frames and detect if we've hit a solid wall.
var sampleTick = 0;
var sampleRate = 5;
var lastSampledX = 0;
setInterval(function () {
if (sampleTick % sampleRate === 0) {
var curX = Self.Position().X;
var delta = Math.abs(curX - lastSampledX);
if (delta < 5) {
direction = direction === "right" ? "left" : "right";
}
lastSampledX = curX;
}
sampleTick++;
// TODO: Vector() requires floats, pain in the butt for JS,
// the JS API should be friendlier and custom...
var Vx = parseFloat(playerSpeed * (direction === "left" ? -1 : 1));
Self.SetVelocity(Vector(Vx, 0.0));
if (!Self.IsAnimating()) {
Self.PlayAnimation("walk-" + direction, null);
}
}, 100);
}

135
doodads/azulian/azulian.js

@ -1,12 +1,29 @@
// Azulian (Red and Blue) // Azulian (Red and Blue)
var playerSpeed = 12,
const color = Self.GetTag("color");
var playerSpeed = color === 'blue' ? 2 : 4,
swimSpeed = playerSpeed * 0.4,
aggroX = 250, // X/Y distance sensitivity from player
aggroY = color === 'blue' ? 100 : 200,
jumpSpeed = color === 'blue' ? 14 : 18,
swimJumpSpeed = jumpSpeed * 0.4,
animating = false, animating = false,
direction = "right", direction = "right",
lastDirection = "right"; lastDirection = "right";
// white Azulian is faster yet than the red
if (color === 'white') {
aggroX = 1000;
aggroY = 400;
playerSpeed = 8;
swimSpeed = playerSpeed * 0.4;
jumpSpeed = 20;
swimJumpSpeed = jumpSpeed * 0.4;
}
function setupAnimations(color) { function setupAnimations(color) {
var left = color === 'blue' ? 'blu-wl' : 'red-wl',
right = color === 'blue' ? 'blu-wr' : 'red-wr',
let left = color === 'blue' ? 'blu-wl' : color + '-wl',
right = color === 'blue' ? 'blu-wr' : color + '-wr',
leftFrames = [left + '1', left + '2', left + '3', left + '4'], leftFrames = [left + '1', left + '2', left + '3', left + '4'],
rightFrames = [right + '1', right + '2', right + '3', right + '4']; rightFrames = [right + '1', right + '2', right + '3', right + '4'];
@ -15,9 +32,11 @@ function setupAnimations(color) {
} }
function main() { function main() {
var color = Self.GetTag("color");
playerSpeed = color === 'blue' ? 2 : 4; playerSpeed = color === 'blue' ? 2 : 4;
let swimJumpCooldownTick = 0, // minimum Game Tick before we can jump while swimming
swimJumpCooldown = 10; // CONFIG: delta of ticks between jumps while swimming
Self.SetMobile(true); Self.SetMobile(true);
Self.SetGravity(true); Self.SetGravity(true);
Self.SetInventory(true); Self.SetInventory(true);
@ -32,23 +51,77 @@ function main() {
// when it meets resistance. // when it meets resistance.
// Sample our X position every few frames and detect if we've hit a solid wall. // Sample our X position every few frames and detect if we've hit a solid wall.
var sampleTick = 0;
var sampleRate = 5;
var lastSampledX = 0;
setInterval(function () {
if (sampleTick % sampleRate === 0) {
var curX = Self.Position().X;
var delta = Math.abs(curX - lastSampledX);
if (delta < 5) {
direction = direction === "right" ? "left" : "right";
let sampleTick = 0;
let sampleRate = 5;
let lastSampledX = 0;
// Get the player on touch.
Events.OnCollide((e) => {
// If we're diving and we hit the player, game over!
// Azulians are friendly to Thieves though!
if (e.Settled && isPlayerFood(e.Actor)) {
FailLevel("Watch out for the Azulians!");
return;
}
});
setInterval(() => {
// If the player is nearby, walk towards them. Otherwise, default pattern
// is to walk back and forth.
let player = Actors.FindPlayer(),
followPlayer = false,
jump = false;
// Don't follow boring players.
if (player !== null && isPlayerFood(player)) {
let playerPt = player.Position(),
myPt = Self.Position();
// If the player is within aggro range, move towards.
if ((Math.abs(playerPt.X - myPt.X) < aggroX && Math.abs(playerPt.Y - myPt.Y) < aggroY)
|| (Level.Difficulty > 0)) {
direction = playerPt.X < myPt.X ? "left" : "right";
followPlayer = true;
if (playerPt.Y + player.Size().H < myPt.Y + Self.Size().H) {
jump = true;
}
}
}
// Default AI: sample position so we turn around on obstacles.
if (!followPlayer) {
if (sampleTick % sampleRate === 0) {
let curX = Self.Position().X;
let delta = Math.abs(curX - lastSampledX);
if (delta < 5) {
direction = direction === "right" ? "left" : "right";
}
lastSampledX = curX;
} }
lastSampledX = curX;
sampleTick++;
} }
sampleTick++;
var Vx = parseFloat(playerSpeed * (direction === "left" ? -1 : 1));
Self.SetVelocity(Vector(Vx, 0.0));
// Handle being underwater.
let canJump = Self.Grounded();
if (Self.IsWet()) {
let tick = GetTick();
if (tick > swimJumpCooldownTick) {
canJump = true;
swimJumpCooldownTick = tick + swimJumpCooldown;
}
}
// How speedy would our movement and jump be?
let xspeed = playerSpeed, yspeed = jumpSpeed;
if (Self.IsWet()) {
xspeed = swimSpeed;
yspeed = swimJumpSpeed;
}
let Vx = parseFloat(xspeed * (direction === "left" ? -1 : 1)),
Vy = jump && canJump ? parseFloat(-yspeed) : Self.GetVelocity().Y;
Self.SetVelocity(Vector(Vx, Vy));
// If we changed directions, stop animating now so we can // If we changed directions, stop animating now so we can
// turn around quickly without moonwalking. // turn around quickly without moonwalking.
@ -61,12 +134,12 @@ function main() {
} }
lastDirection = direction; lastDirection = direction;
}, 100);
}, 10);
} }
function playerControls() { function playerControls() {
// Note: player speed is controlled by the engine. // Note: player speed is controlled by the engine.
Events.OnKeypress(function (ev) {
Events.OnKeypress((ev) => {
if (ev.Right) { if (ev.Right) {
if (!Self.IsAnimating()) { if (!Self.IsAnimating()) {
Self.PlayAnimation("walk-right", null); Self.PlayAnimation("walk-right", null);
@ -81,3 +154,25 @@ function playerControls() {
} }
}) })
} }
// Logic to decide if the player is interesting to the Azulian (aka, if the Azulian
// will be hostile towards the player). Boring players will not be chased after and
// the Azulian will not harm them if they make contact.
function isPlayerFood(actor) {
// Not a player or is invulnerable, or Peaceful difficulty.
if (!actor.IsPlayer() || actor.Invulnerable() || Level.Difficulty < 0) {
return false;
}
// On hard mode they are hostile to any player.
if (Level.Difficulty > 0) {
return true;
}
// Azulians are friendly to Thieves and other Azulians.
if (actor.Doodad().Filename === "thief.doodad" || actor.Doodad().Title.indexOf("Azulian") > -1) {
return false;
}
return true;
}

BIN
doodads/azulian/white-back.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
doodads/azulian/white-front.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1004 B

BIN
doodads/azulian/white-wl1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
doodads/azulian/white-wl2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
doodads/azulian/white-wl3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
doodads/azulian/white-wl4.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1002 B

BIN
doodads/azulian/white-wr1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 994 B

BIN
doodads/azulian/white-wr2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
doodads/azulian/white-wr3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 985 B

BIN
doodads/azulian/white-wr4.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 999 B

12
doodads/bird/Makefile

@ -2,9 +2,17 @@ ALL: build
.PHONY: build .PHONY: build
build: build:
doodad convert -t "Bird (red)" left-1.png left-2.png right-1.png right-2.png \
dive-left.png dive-right.png bird-red.doodad
doodad convert -t "Bird (red)" red/left-1.png red/left-2.png red/right-1.png \
red/right-2.png red/dive-left.png red/dive-right.png \
bird-red.doodad
doodad install-script bird.js bird-red.doodad doodad install-script bird.js bird-red.doodad
doodad edit-doodad --tag "color=red" bird-red.doodad
doodad convert -t "Bird (blue)" blue/left-1.png blue/left-2.png blue/right-1.png \
blue/right-2.png blue/dive-left.png blue/dive-right.png \
bird-blue.doodad
doodad install-script bird.js bird-blue.doodad
doodad edit-doodad --tag "color=blue" bird-blue.doodad
# Tag the category for these doodads # Tag the category for these doodads
for i in *.doodad; do\ for i in *.doodad; do\

247
doodads/bird/bird.js

@ -1,18 +1,36 @@
// Bird
// Bird (red and blue)
function main() {
var speed = 4;
var Vx = Vy = 0;
var altitude = Self.Position().Y; // original height in the level
var direction = "left",
lastDirection = "left";
var states = {
flying: 0,
diving: 1,
};
var state = states.flying;
/*
Base A.I. behaviors (red bird) are:
- Tries to maintain original altitude in level and flies left/right.
- Divebombs the player to attack, then climbs back to its original
altitude when it hits a floor/wall.
Blue bird:
- Flies in a sine wave pattern (its target altitude fluctuates
around the bird's original placement on the level).
- Longer aggro radius to dive.
*/
let speed = 4,
Vx = Vy = 0,
color = Self.GetTag("color"), // informs our A.I. behaviors
searchStep = 12 // pixels to step while searching for a player
searchLimit = color === "blue" ? 24 : 12, // multiples of searchStep for aggro radius
altitude = Self.Position().Y, // original height in level
targetAltitude = altitude; // bird's target height to maintain
let direction = "left",
lastDirection = "left";
let states = {
flying: 0,
diving: 1,
};
let state = states.flying;
function main() {
Self.SetMobile(true); Self.SetMobile(true);
Self.SetGravity(false); Self.SetGravity(false);
Self.SetHitbox(0, 0, 46, 32); Self.SetHitbox(0, 0, 46, 32);
@ -24,33 +42,75 @@ function main() {
return player(); return player();
} }
Events.OnCollide(function (e) {
if (e.Actor.IsMobile() && e.InHitbox) {
Events.OnCollide((e) => {
// If we're diving and we hit the player, game over!
if (e.Settled && state === states.diving && e.Actor.IsPlayer()) {
FailLevel("Watch out for birds!");
return;
}
if (e.Actor.IsMobile() && e.Actor.HasGravity() && e.InHitbox) {
return false; return false;
} }
}); });
// Sample our X position every few frames and detect if we've hit a solid wall. // Sample our X position every few frames and detect if we've hit a solid wall.
var sampleTick = 0;
var sampleRate = 2;
var lastSampledX = 0;
var lastSampledY = 0;
let sampleTick = 0,
sampleRate = 2,
lastSampled = Point(0, 0);
setInterval(() => {
// Run blue bird A.I. if we are blue: moves our target altitude along a sine wave.
if (color === "blue") {
AI_BlueBird();
}
setInterval(function () {
// Sample how far we've moved to detect hitting a wall.
if (sampleTick % sampleRate === 0) { if (sampleTick % sampleRate === 0) {
var curX = Self.Position().X;
var delta = Math.abs(curX - lastSampledX);
let curP = Self.Position()
let delta = Math.abs(curP.X - lastSampled.X);
if (delta < 5) { if (delta < 5) {
direction = direction === "right" ? "left" : "right"; direction = direction === "right" ? "left" : "right";
} }
lastSampledX = curX;
// If we were diving, check Y delta too for if we hit floor
if (state === states.diving && Math.abs(curP.Y - lastSampled.Y) < 5) {
state = states.flying;
}
lastSampled = curP
} }
sampleTick++; sampleTick++;
// Are we diving?
if (state === states.diving) {
Vy = speed
} else {
// If we are not flying at our original altitude, correct for that.
let curV = Self.Position();
Vy = 0.0;
if (curV.Y != targetAltitude) {
Vy = curV.Y < targetAltitude ? 1 : -1;
}
// Scan for the player character and dive.
try {
AI_ScanForPlayer()
} catch (e) {
console.error("Error in AI_ScanForPlayer: %s", e);
}
}
// TODO: Vector() requires floats, pain in the butt for JS, // TODO: Vector() requires floats, pain in the butt for JS,
// the JS API should be friendlier and custom... // the JS API should be friendlier and custom...
var Vx = parseFloat(speed * (direction === "left" ? -1 : 1));
Self.SetVelocity(Vector(Vx, 0.0));
let Vx = parseFloat(speed * (direction === "left" ? -1 : 1));
Self.SetVelocity(Vector(Vx, Vy));
// If diving, exit - don't edit animation.
if (state === states.diving) {
Self.ShowLayerNamed("dive-" + direction);
lastDirection = direction;
return;
}
// If we changed directions, stop animating now so we can // If we changed directions, stop animating now so we can
// turn around quickly without moonwalking. // turn around quickly without moonwalking.
@ -66,34 +126,149 @@ function main() {
}, 100); }, 100);
} }
// A.I. subroutine: scan for the player character.
// The bird scans in a 45 degree angle downwards, if the
// player is seen nearby in that scan it will begin a dive.
// It's not hostile towards characters that can fly (having
// no gravity).
function AI_ScanForPlayer() {
// If Peaceful difficulty, do not attack.
if (Level.Difficulty < 0) {
return
}
let stepY = searchStep, // number of pixels to skip
stepX = stepY,
limit = stepX * searchLimit, // furthest we'll scan
scanX = scanY = 0,
size = Self.Size(),
fromPoint = Self.Position();
// From what point do we begin the scan?
if (direction === 'left') {
stepX = -stepX;
fromPoint.Y += size.H;
} else {
fromPoint.Y += size.H;
fromPoint.X += size.W;
}
scanX = fromPoint.X;
scanY = fromPoint.Y;
for (let i = 0; i < limit; i += stepY) {
scanX += stepX;
scanY += stepY;
for (let actor of Actors.At(Point(scanX, scanY))) {
if (actor.IsPlayer() && actor.HasGravity()) {
state = states.diving;
return;
}
}
}
return;
}
// Sine wave controls for the Blue bird.
var sineLimit = 32,
sineCounter = 0,
sineDirection = 1,
sineFrequency = 5, // every 500ms
sineStep = 16;
// A.I. Subroutine: sine wave pattern (Blue bird).
function AI_BlueBird() {
// The main loop runs on a 100ms interval, control how frequently
// to adjust the bird's target velocity.
if (sineCounter > 0 && (sineCounter % sineFrequency) > 1) {
sineCounter++;
return;
}
sineCounter++;
targetAltitude += sineStep*sineDirection;
// Cap the distance between our starting altitude and sine-wave target.
if (targetAltitude < altitude && altitude - targetAltitude >= sineLimit) {
targetAltitude = altitude - sineLimit;
sineDirection = 1
} else if (targetAltitude > altitude && targetAltitude - altitude >= sineLimit) {
targetAltitude = altitude + sineLimit;
sineDirection = -1
}
}
// If under control of the player character. // If under control of the player character.
function player() { function player() {
Self.SetInventory(true);
Events.OnKeypress(function (ev) {
let playerSpeed = 12,
diving = false,
falling = false;
// The player can dive by moving downwards and laterally, but
// de-cheese their ability to just sweep across the ground; if
// they aren't seen to be moving downwards, cancel the dive.
let lastPoint = Self.Position();
setInterval(() => {
let nowAt = Self.Position();
if (nowAt.Y > lastPoint.Y) {
falling = true;
} else {
falling = false;
}
lastPoint = nowAt;
}, 100);
Events.OnKeypress((ev) => {
Vx = 0; Vx = 0;
Vy = 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 && falling) {
Self.StopAnimation();
Self.ShowLayerNamed("dive-right");
diving = falling;
} else if (ev.Down && ev.Left && falling) {
Self.StopAnimation();
Self.ShowLayerNamed("dive-left");
diving = falling;
} else if (ev.Right) {
// Fly right.
if (!Self.IsAnimating()) { if (!Self.IsAnimating()) {
Self.PlayAnimation("fly-right", null); Self.PlayAnimation("fly-right", null);
} }
Vx = playerSpeed; Vx = playerSpeed;
diving = false;
} else if (ev.Left) { } else if (ev.Left) {
// Fly left.
if (!Self.IsAnimating()) { if (!Self.IsAnimating()) {
Self.PlayAnimation("fly-left", null); Self.PlayAnimation("fly-left", null);
} }
Vx = -playerSpeed; Vx = -playerSpeed;
diving = false;
} else { } else {
Self.StopAnimation();
animating = false;
// Hover in place.
if (!Self.IsAnimating()) {
Self.PlayAnimation("fly-" + direction);
}
diving = false;
} }
Self.SetVelocity(Vector(Vx, Vy));
})
// Player is invulnerable while diving.
Self.SetInvulnerable(diving);
});
Events.OnCollide((e) => {
// If the player is diving at an enemy mob, destroy it.
if (diving && e.Settled && e.Actor.IsMobile() && !e.Actor.Invulnerable()) {
Sound.Play("crumbly-break.wav");
e.Actor.Destroy();
}
});
} }

BIN
doodads/bird/blue/dive-left.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
doodads/bird/blue/dive-right.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
doodads/bird/blue/left-1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
doodads/bird/blue/left-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
doodads/bird/blue/right-1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
doodads/bird/blue/right-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

0
doodads/bird/dive-left.png → doodads/bird/red/dive-left.png

Before

Width:  |  Height:  |  Size: 959 B

After

Width:  |  Height:  |  Size: 959 B

0
doodads/bird/dive-right.png → doodads/bird/red/dive-right.png

Before

Width:  |  Height:  |  Size: 989 B

After

Width:  |  Height:  |  Size: 989 B

0
doodads/bird/left-1.png → doodads/bird/red/left-1.png

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

0
doodads/bird/left-2.png → doodads/bird/red/left-2.png

Before

Width:  |  Height:  |  Size: 1022 B

After

Width:  |  Height:  |  Size: 1022 B

0
doodads/bird/right-1.png → doodads/bird/red/right-1.png

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

0
doodads/bird/right-2.png → doodads/bird/red/right-2.png

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

27
doodads/box/box.js

@ -1,23 +1,32 @@
// Pushable Box. // Pushable Box.
var speed = 4;
var size = 75;
const speed = 4,
size = 75;
function main() { function main() {
Self.SetMobile(true); Self.SetMobile(true);
Self.SetInvulnerable(true);
Self.SetGravity(true); Self.SetGravity(true);
Self.SetHitbox(0, 0, size, size); Self.SetHitbox(0, 0, size, size);
Events.OnCollide(function (e) {
Events.OnCollide((e) => {
// Ignore events from neighboring Boxes. // Ignore events from neighboring Boxes.
if (e.Actor.Actor.Filename === "box.doodad") { if (e.Actor.Actor.Filename === "box.doodad") {
return false; return false;
} }
if (e.Actor.IsMobile() && e.InHitbox) { if (e.Actor.IsMobile() && e.InHitbox) {
var overlap = e.Overlap;
let overlap = e.Overlap;
if (overlap.Y === 0 && !(overlap.X === 0 && overlap.W < 5) && !(overlap.X === size)) { if (overlap.Y === 0 && !(overlap.X === 0 && overlap.W < 5) && !(overlap.X === size)) {
// Standing on top, ignore.
// Be sure to position them snug on top.
// TODO: this might be a nice general solution in the
// collision detector...
e.Actor.MoveTo(Point(
e.Actor.Position().X,
Self.Position().Y - e.Actor.Hitbox().Y - e.Actor.Hitbox().H - 2,
))
e.Actor.SetGrounded(true);
return false; return false;
} else if (overlap.Y === size) { } else if (overlap.Y === size) {
// From the bottom, boop it up. // From the bottom, boop it up.
@ -39,8 +48,8 @@ function main() {
}); });
// When we receive power, we reset to our original position. // When we receive power, we reset to our original position.
var origPoint = Self.Position();
Message.Subscribe("power", function (powered) {
let origPoint = Self.Position();
Message.Subscribe("power", (powered) => {
Self.MoveTo(origPoint); Self.MoveTo(origPoint);
Self.SetVelocity(Vector(0, 0)); Self.SetVelocity(Vector(0, 0));
}); });
@ -52,8 +61,8 @@ function main() {
function animate() { function animate() {
Self.AddAnimation("animate", 100, [0, 1, 2, 3, 2, 1]); Self.AddAnimation("animate", 100, [0, 1, 2, 3, 2, 1]);
var running = false;
setInterval(function () {
let running = false;
setInterval(() => {
if (!running) { if (!running) {
running = true; running = true;
Self.PlayAnimation("animate", function () { Self.PlayAnimation("animate", function () {

2
doodads/boy/Makefile

@ -5,6 +5,8 @@ build:
doodad convert -t "Boy" stand-right.png stand-left.png \ doodad convert -t "Boy" stand-right.png stand-left.png \
walk-right-1.png walk-right-2.png walk-right-3.png \ walk-right-1.png walk-right-2.png walk-right-3.png \
walk-left-1.png walk-left-2.png walk-left-3.png \ walk-left-1.png walk-left-2.png walk-left-3.png \
idle-right-1.png idle-right-2.png idle-right-3.png \
idle-left-1.png idle-left-2.png idle-left-3.png \
boy.doodad boy.doodad
doodad install-script boy.js boy.doodad doodad install-script boy.js boy.doodad

56
doodads/boy/boy.js

@ -1,38 +1,62 @@
function main() {
var playerSpeed = 12;
var gravity = 4;
var Vx = Vy = 0;
const playerSpeed = 12;
var animating = false;
var animStart = animEnd = 0;
var animFrame = animStart;
let Vx = Vy = 0,
walking = false,
direction = "right",
lastDirection = direction;
function main() {
Self.SetMobile(true); Self.SetMobile(true);
Self.SetInventory(true); Self.SetInventory(true);
Self.SetGravity(true); Self.SetGravity(true);
Self.SetHitbox(0, 0, 32, 52); Self.SetHitbox(0, 0, 32, 52);
Self.AddAnimation("walk-left", 200, ["stand-left", "walk-left-1", "walk-left-2", "walk-left-3", "walk-left-2", "walk-left-1"]); Self.AddAnimation("walk-left", 200, ["stand-left", "walk-left-1", "walk-left-2", "walk-left-3", "walk-left-2", "walk-left-1"]);
Self.AddAnimation("walk-right", 200, ["stand-right", "walk-right-1", "walk-right-2", "walk-right-3", "walk-right-2", "walk-right-1"]); Self.AddAnimation("walk-right", 200, ["stand-right", "walk-right-1", "walk-right-2", "walk-right-3", "walk-right-2", "walk-right-1"]);
Self.AddAnimation("idle-left", 200, ["idle-left-1", "idle-left-2", "idle-left-3", "idle-left-2"]);
Self.AddAnimation("idle-right", 200, ["idle-right-1", "idle-right-2", "idle-right-3", "idle-right-2"]);
// If the player suddenly changes direction, reset the animation state to quickly switch over.
let lastVelocity = Vector(0, 0);
Events.OnKeypress(function (ev) {
Events.OnKeypress((ev) => {
Vx = 0; Vx = 0;
Vy = 0; Vy = 0;
let curVelocity = Self.GetVelocity();
if ((lastVelocity.X < 0 && curVelocity.X > 0) ||
(lastVelocity.X > 0 && curVelocity.X < 0)) {
Self.StopAnimation();
}
lastVelocity = curVelocity;
lastDirection = direction;
let wasWalking = walking;
if (ev.Right) { if (ev.Right) {
if (!Self.IsAnimating()) {
Self.PlayAnimation("walk-right", null);
}
direction = "right";
Vx = playerSpeed; Vx = playerSpeed;
walking = true;
} else if (ev.Left) { } else if (ev.Left) {
if (!Self.IsAnimating()) {
Self.PlayAnimation("walk-left", null);
}
direction = "left";
Vx = -playerSpeed; Vx = -playerSpeed;
walking = true;
} else { } else {
// Has stopped walking!
walking = false;
stoppedWalking = true;
}
// Should we stop animating? (changed state)
if (direction !== lastDirection || wasWalking !== walking) {
Self.StopAnimation(); Self.StopAnimation();
animating = false;
} }
// Self.SetVelocity(Point(Vx, Vy));
// And play what animation?
if (!Self.IsAnimating()) {
if (walking) {
Self.PlayAnimation("walk-"+direction, null);
} else {
Self.PlayAnimation("idle-"+direction, null);
}
}
}) })
} }

BIN
doodads/boy/idle-left-1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
doodads/boy/idle-left-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
doodads/boy/idle-left-3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
doodads/boy/idle-right-1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
doodads/boy/idle-right-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
doodads/boy/idle-right-3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

15
doodads/build.sh

@ -34,6 +34,10 @@ doors() {
cd doors/ cd doors/
make make
cd .. cd ..
cd gems/
make
cd ..
} }
trapdoors() { trapdoors() {
@ -84,6 +88,16 @@ warpdoor() {
cd .. cd ..
} }
creatures() {
cd snake/
make
cd ..
cd crusher/
make
cd ..
}
boy boy
buttons buttons
switches switches
@ -94,6 +108,7 @@ mobs
objects objects
onoff onoff
warpdoor warpdoor
creatures
doodad edit-doodad -quiet -lock -author "Noah" ../../assets/doodads/*.doodad doodad edit-doodad -quiet -lock -author "Noah" ../../assets/doodads/*.doodad
doodad edit-doodad ../../assets/doodads/azu-blu.doodad doodad edit-doodad ../../assets/doodads/azu-blu.doodad
doodad edit-doodad -hide ../../assets/doodads/boy.doodad doodad edit-doodad -hide ../../assets/doodads/boy.doodad

26
doodads/buttons/button.js

@ -1,19 +1,26 @@
function main() { function main() {
var timer = 0;
var pressed = false;
let timer = 0;
let pressed = false;
// Has a linked Sticky Button been pressed permanently down? // Has a linked Sticky Button been pressed permanently down?
var stickyDown = false;
Message.Subscribe("sticky:down", function(down) {
let stickyDown = false;
Message.Subscribe("sticky:down", (down) => {
stickyDown = down; stickyDown = down;
Self.ShowLayer(stickyDown ? 1 : 0); Self.ShowLayer(stickyDown ? 1 : 0);
}); });
Events.OnCollide(function(e) {
// Track who all is colliding with us.
let colliders = {};
Events.OnCollide((e) => {
if (!e.Settled) { if (!e.Settled) {
return; return;
} }
if (colliders[e.Actor.ID()] == undefined) {
colliders[e.Actor.ID()] = true;
}
// If a linked Sticky Button is pressed, button stays down too and // If a linked Sticky Button is pressed, button stays down too and
// doesn't interact. // doesn't interact.
if (stickyDown) { if (stickyDown) {
@ -37,12 +44,17 @@ function main() {
} }
Self.ShowLayer(1); Self.ShowLayer(1);
timer = setTimeout(function() {
});
Events.OnLeave((e) => {
delete colliders[e.Actor.ID()];
if (Object.keys(colliders).length === 0 && !stickyDown) {
Sound.Play("button-up.wav") Sound.Play("button-up.wav")
Self.ShowLayer(0); Self.ShowLayer(0);
Message.Publish("power", false); Message.Publish("power", false);
timer = 0; timer = 0;
pressed = false; pressed = false;
}, 200);
}
}); });
} }

6
doodads/buttons/sticky.js

@ -1,8 +1,8 @@
function main() { function main() {
var pressed = false;
let pressed = false;
// When a sticky button receives power, it pops back up. // When a sticky button receives power, it pops back up.
Message.Subscribe("power", function (powered) {
Message.Subscribe("power", (powered) => {
if (powered && pressed) { if (powered && pressed) {
Self.ShowLayer(0); Self.ShowLayer(0);
pressed = false; pressed = false;
@ -12,7 +12,7 @@ function main() {
} }
}) })
Events.OnCollide(function (e) {
Events.OnCollide((e) => {
if (!e.Settled) { if (!e.Settled) {
return; return;
} }

23
doodads/crumbly-floor/crumbly-floor.js

@ -6,19 +6,16 @@ function main() {
Self.AddAnimation("fall", 100, ["fall1", "fall2", "fall3", "fall4"]); Self.AddAnimation("fall", 100, ["fall1", "fall2", "fall3", "fall4"]);
// Recover time for the floor to respawn. // Recover time for the floor to respawn.
var recover = 5000;
let recover = 5000;
// States of the floor. // States of the floor.
var stateSolid = 0;
var stateShaking = 1;
var stateFalling = 2;
var stateFallen = 3;
var state = stateSolid;
let stateSolid = 0;
let stateShaking = 1;
let stateFalling = 2;
let stateFallen = 3;
let state = stateSolid;
// Started the animation?
var startedAnimation = false;
Events.OnCollide(function(e) {
Events.OnCollide((e) => {
// If the floor is falling, the player passes right thru. // If the floor is falling, the player passes right thru.
if (state === stateFalling || state === stateFallen) { if (state === stateFalling || state === stateFallen) {
@ -40,15 +37,15 @@ function main() {
// Begin the animation sequence if we're in the solid state. // Begin the animation sequence if we're in the solid state.
if (state === stateSolid) { if (state === stateSolid) {
state = stateShaking; state = stateShaking;
Self.PlayAnimation("shake", function() {
Self.PlayAnimation("shake", () => {
state = stateFalling; state = stateFalling;
Self.PlayAnimation("fall", function() {
Self.PlayAnimation("fall", () => {
Sound.Play("crumbly-break.wav") Sound.Play("crumbly-break.wav")
state = stateFallen; state = stateFallen;
Self.ShowLayerNamed("fallen"); Self.ShowLayerNamed("fallen");
// Recover after a while. // Recover after a while.
setTimeout(function() {
setTimeout(() => {
Self.ShowLayer(0); Self.ShowLayer(0);
state = stateSolid; state = stateSolid;
}, recover); }, recover);

14
doodads/crusher/Makefile

@ -0,0 +1,14 @@
ALL: build
.PHONY: build
build:
doodad convert -t "Crusher" sleep.png peek-left.png peek-right.png \
angry.png ouch.png crusher.doodad
doodad install-script crusher.js crusher.doodad
# Tag the category for these doodads
for i in *.doodad; do\
doodad edit-doodad --tag "category=creatures" $${i};\
done
cp *.doodad ../../../assets/doodads/

BIN
doodads/crusher/angry.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

207
doodads/crusher/crusher.js

@ -0,0 +1,207 @@
// Crusher
/*
A.I. Behaviors:
- Sleeps and hangs in the air in a high place.
- When a player gets nearby, it begins "peeking" in their direction.
- When the player is below, tries to drop and crush them or any
other mobile doodad.
- The top edge is safe to walk on and ride back up like an elevator.
*/
let direction = "left",
dropSpeed = 12,
riseSpeed = 4,
watchRadius = 300, // player nearby distance to start peeking
fallRadius = 120, // player distance before it drops
helmetThickness = 48, // safe solid hitbox height
fireThickness = 12, // dangerous bottom thickness
targetAltitude = Self.Position()
lastAltitude = targetAltitude.Y
size = Self.Size();
const states = {
idle: 0,
peeking: 1,
drop: 2,
falling: 3,
hit: 4,
rising: 5,
};
let state = states.idle;
function main() {
Self.SetMobile(true);
Self.SetGravity(false);
Self.SetInvulnerable(true);
Self.SetHitbox(5, 2, 90, 73);
Self.AddAnimation("hit", 50,
["angry", "ouch", "angry", "angry", "angry", "angry",
"sleep", "sleep", "sleep", "sleep", "sleep", "sleep",
"sleep", "sleep", "sleep", "sleep", "sleep", "sleep",
"sleep", "sleep", "sleep", "sleep", "sleep", "sleep"],
)
// Player Character controls?
if (Self.IsPlayer()) {
return player();
}
let hitbox = Self.Hitbox();
Events.OnCollide((e) => {
// The bottom is deadly if falling.
if (state === states.falling || state === states.hit && e.Settled) {
if (e.Actor.IsMobile() && e.InHitbox && !e.Actor.Invulnerable()) {
if (e.Overlap.H > 72) {
if (e.Actor.IsPlayer()) {
FailLevel("Don't get crushed!");
return;
} else {
e.Actor.Destroy();
}
}
}
}
// Our top edge is always solid.
if (e.Actor.IsPlayer() && e.InHitbox) {
if (e.Overlap.Y < helmetThickness) {
// Be sure to position them snug on top.
// TODO: this might be a nice general solution in the
// collision detector...
e.Actor.MoveTo(Point(
e.Actor.Position().X,
Self.Position().Y - e.Actor.Hitbox().Y - e.Actor.Hitbox().H,
))
e.Actor.SetGrounded(true);
}
}
// The whole hitbox is ordinarily solid.
if (state !== state.falling) {
if (e.Actor.IsMobile() && e.InHitbox) {
return false;
}
}
});
setInterval(() => {
// Find the player.
let player = Actors.FindPlayer(),
playerPoint = player.Position(),
point = Self.Position(),
delta = 0,
nearby = false,
below = false;
// Face the player.
if (playerPoint.X < point.X + (size.W / 2)) {
direction = "left";
delta = Math.abs(playerPoint.X - (point.X + (size.W/2)));
}
else if (playerPoint.X > point.X + (size.W / 2)) {
direction = "right";
delta = Math.abs(playerPoint.X - (point.X + (size.W/2)));
}
if (delta < watchRadius) {
nearby = true;
}
if (delta < fallRadius) {
// Check if the player is below us.
if (playerPoint.Y > point.Y + size.H) {
below = true;
}
}
switch (state) {
case states.idle:
if (nearby) {
Self.ShowLayerNamed("peek-"+direction);
} else {
Self.ShowLayerNamed("sleep");
}
if (below) {
state = states.drop;
} else if (nearby) {
state = states.peeking;
}
break;
case states.peeking:
if (nearby) {
Self.ShowLayerNamed("peek-"+direction);
} else {
state = states.idle;
break;
}
if (below) {
state = states.drop;
}
break;
case states.drop:
// Begin the fall.
Self.ShowLayerNamed("angry");
Self.SetVelocity(Vector(0.0, dropSpeed));
lastAltitude = -point.Y;
state = states.falling;
case states.falling:
Self.ShowLayerNamed("angry");
Self.SetVelocity(Vector(0.0, dropSpeed));
// Landed?
if (point.Y === lastAltitude) {
Sound.Play("crumbly-break.wav")
state = states.hit;
Self.PlayAnimation("hit", () => {
state = states.rising;
});
}
break;
case states.hit:
// A transitory state while the hit animation
// plays out.
break;
case states.rising:
Self.ShowLayerNamed("sleep");
Self.SetVelocity(Vector(0, -riseSpeed));
point = Self.Position();
if (point.Y <= targetAltitude.Y+4 || point.Y === lastAltitude.Y) {
Self.MoveTo(targetAltitude);
Self.SetVelocity(Vector(0, 0))
state = states.idle;
}
}
lastAltitude = point.Y;
}, 100);
}
// If under control of the player character.
function player() {
Events.OnKeypress((ev) => {
if (ev.Right) {
direction = "right";
} else if (ev.Left) {
direction = "left";
}
// Jump!
if (ev.Down) {
Self.ShowLayerNamed("angry");
return;
} else if (ev.Right && ev.Left) {
Self.ShowLayerNamed("ouch");
} else if (ev.Right || ev.Left) {
Self.ShowLayerNamed("peek-"+direction);
} else {
Self.ShowLayerNamed("sleep");
}
});
}

BIN
doodads/crusher/ouch.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
doodads/crusher/peek-left.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
doodads/crusher/peek-right.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
doodads/crusher/sleep.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

22
doodads/doors/colored-door.js

@ -1,9 +1,11 @@
function main() {
var color = Self.GetTag("color");
var keyname = color === "small" ? "small-key.doodad" : "key-" + color + ".doodad";
// Colored Locked Doors.
const color = Self.GetTag("color"),
keyname = color === "small" ? "small-key.doodad" : "key-" + color + ".doodad";
function main() {
// Layers in the doodad image. // Layers in the doodad image.
var layer = {
let layer = {
closed: 0, closed: 0,
unlocked: 1, unlocked: 1,
right: 2, right: 2,
@ -11,13 +13,13 @@ function main() {
}; };
// Variables that change in event handler. // Variables that change in event handler.
var unlocked = false; // Key has been used to unlock the door (one time).
var opened = false; // If door is currently showing its opened state.
var enterSide = 0; // Side of player entering the door, -1 or 1, left or right.
let unlocked = false; // Key has been used to unlock the door (one time).
let opened = false; // If door is currently showing its opened state.
let enterSide = 0; // Side of player entering the door, -1 or 1, left or right.
Self.SetHitbox(34, 0, 13, 76); Self.SetHitbox(34, 0, 13, 76);
Events.OnCollide(function(e) {
Events.OnCollide((e) => {
// Record the side that this actor has touched us, in case the door // Record the side that this actor has touched us, in case the door
// needs to open. // needs to open.
if (enterSide === 0) { if (enterSide === 0) {
@ -37,7 +39,7 @@ function main() {
} }
// Do they have our key? // Do they have our key?
var hasKey = e.Actor.HasItem(keyname) >= 0;
let hasKey = e.Actor.HasItem(keyname) >= 0;
if (!hasKey) { if (!hasKey) {
// Door is locked. // Door is locked.
return false; return false;
@ -55,7 +57,7 @@ function main() {
} }
} }
}); });
Events.OnLeave(function(e) {
Events.OnLeave((e) => {
Self.ShowLayer(unlocked ? layer.unlocked : layer.closed); Self.ShowLayer(unlocked ? layer.unlocked : layer.closed);
// Sound.Play("door-close.wav") // Sound.Play("door-close.wav")

25
doodads/doors/electric-door.js

@ -1,27 +1,28 @@
var animating = false;
var opened = false;
var powerState = false;
// Electric Door
let animating = false;
let opened = false;
let powerState = false;
// Function to handle the door opening or closing. // Function to handle the door opening or closing.
function setPoweredState(powered) { function setPoweredState(powered) {
powerState = powered; powerState = powered;
console.log("setPoweredState: %+v", powered)
if (powered) { if (powered) {
if (animating || opened) { if (animating || opened) {
return; return;
} }
animating = true; animating = true;
opened = true;
Sound.Play("electric-door.wav") Sound.Play("electric-door.wav")
Self.PlayAnimation("open", function() {
opened = true;
Self.PlayAnimation("open", () => {
animating = false; animating = false;
}); });
} else { } else {
animating = true; animating = true;
Sound.Play("electric-door.wav") Sound.Play("electric-door.wav")
Self.PlayAnimation("close", function() {
Self.PlayAnimation("close", () => {
opened = false; opened = false;
animating = false; animating = false;
}) })
@ -32,7 +33,6 @@ function main() {
Self.AddAnimation("open", 100, [0, 1, 2, 3]); Self.AddAnimation("open", 100, [0, 1, 2, 3]);
Self.AddAnimation("close", 100, [3, 2, 1, 0]); Self.AddAnimation("close", 100, [3, 2, 1, 0]);
Self.SetHitbox(0, 0, 34, 76); Self.SetHitbox(0, 0, 34, 76);
// A linked Switch that activates the door will send the Toggle signal // A linked Switch that activates the door will send the Toggle signal
@ -40,14 +40,13 @@ function main() {
// state on this signal, and ignore the very next Power signal. Ordinary // state on this signal, and ignore the very next Power signal. Ordinary
// power sources like Buttons will work as normal, as they emit only a power // power sources like Buttons will work as normal, as they emit only a power
// signal. // signal.
var ignoreNextPower = false;
Message.Subscribe("switch:toggle", function(powered) {
console.log("A switch powered %+v, setPoweredState(%+v) to opposite", powered, powerState);
let ignoreNextPower = false;
Message.Subscribe("switch:toggle", (powered) => {
ignoreNextPower = true; ignoreNextPower = true;
setPoweredState(!powerState); setPoweredState(!powerState);
}) })
Message.Subscribe("power", function(powered) {
Message.Subscribe("power", (powered) => {
if (ignoreNextPower) { if (ignoreNextPower) {
ignoreNextPower = false; ignoreNextPower = false;
return; return;
@ -56,7 +55,7 @@ function main() {
setPoweredState(powered); setPoweredState(powered);
}); });
Events.OnCollide(function(e) {
Events.OnCollide((e) => {
if (e.InHitbox) { if (e.InHitbox) {
if (!opened) { if (!opened) {
return false; return false;

16
doodads/doors/keys.js

@ -1,10 +1,18 @@
function main() {
var color = Self.GetTag("color");
var quantity = color === "small" ? 1 : 0;
// Colored Keys and Small Key
const color = Self.GetTag("color"),
quantity = color === "small" ? 1 : 0;
Events.OnCollide(function (e) {
function main() {
Events.OnCollide((e) => {
if (e.Settled) { if (e.Settled) {
if (e.Actor.HasInventory()) { if (e.Actor.HasInventory()) {
// If we don't have a quantity, and the actor already has
// one of us, don't pick it up so the player can get it.
if (quantity === 0 && e.Actor.HasItem(Self.Filename) === 0 && !e.Actor.IsPlayer()) {
return;
}
Sound.Play("item-get.wav") Sound.Play("item-get.wav")
e.Actor.AddItem(Self.Filename, quantity); e.Actor.AddItem(Self.Filename, quantity);
Self.Destroy(); Self.Destroy();

27
doodads/doors/locked-door.js

@ -1,27 +0,0 @@
// DEPRECATED: old locked door script. Superceded by colored-door.js.
function main() {
Self.AddAnimation("open", 0, [1]);
var unlocked = false;
var color = Self.GetTag("color");
Self.SetHitbox(16, 0, 32, 64);
Events.OnCollide(function(e) {
if (unlocked) {
return;
}
if (e.InHitbox) {
var data = e.Actor.GetData("key:" + color);
if (data === "") {
// Door is locked.
return false;
}
if (e.Settled) {
unlocked = true;
Self.PlayAnimation("open", null);
}
}
});
}

58
doodads/gems/Makefile

@ -0,0 +1,58 @@
ALL: build
.PHONY: build
build:
###
# Gemstones
###
doodad convert -t "Gemstone (Green)" green-1.png green-2.png green-3.png green-4.png \
gem-green.doodad
doodad install-script gemstone.js gem-green.doodad
doodad edit-doodad --tag "color=green" gem-green.doodad
doodad convert -t "Gemstone (Red)" red-1.png red-2.png red-3.png red-4.png \
gem-red.doodad
doodad install-script gemstone.js gem-red.doodad
doodad edit-doodad --tag "color=red" gem-red.doodad
doodad convert -t "Gemstone (Blue)" blue-1.png blue-2.png blue-3.png blue-4.png \
gem-blue.doodad
doodad install-script gemstone.js gem-blue.doodad
doodad edit-doodad --tag "color=blue" gem-blue.doodad
doodad convert -t "Gemstone (Yellow)" yellow-1.png yellow-2.png yellow-3.png yellow-4.png \
gem-yellow.doodad
doodad install-script gemstone.js gem-yellow.doodad
doodad edit-doodad --tag "color=yellow" gem-yellow.doodad
###
# Totems
###
doodad convert -t "Gemstone Totem (Green)" totem-green-1.png totem-green-2.png totem-green-3.png \
totem-green-4.png totem-green-0.png gem-totem-green.doodad
doodad install-script totem.js gem-totem-green.doodad
doodad edit-doodad --tag "color=green" gem-totem-green.doodad
doodad convert -t "Gemstone Totem (Yellow)" totem-yellow-1.png totem-yellow-2.png totem-yellow-3.png \
totem-yellow-4.png totem-yellow-0.png gem-totem-yellow.doodad
doodad install-script totem.js gem-totem-yellow.doodad
doodad edit-doodad --tag "color=yellow" gem-totem-yellow.doodad
doodad convert -t "Gemstone Totem (Blue)" totem-blue-1.png totem-blue-2.png totem-blue-3.png \
totem-blue-4.png totem-blue-0.png gem-totem-blue.doodad
doodad install-script totem.js gem-totem-blue.doodad
doodad edit-doodad --tag "color=blue" gem-totem-blue.doodad
doodad convert -t "Gemstone Totem (Red)" totem-red-1.png totem-red-2.png totem-red-3.png \
totem-red-4.png totem-red-0.png gem-totem-red.doodad
doodad install-script totem.js gem-totem-red.doodad
doodad edit-doodad --tag "color=red" gem-totem-red.doodad
# Tag the category for these doodads
for i in *.doodad; do\
doodad edit-doodad --tag "category=doors" $${i};\
done
cp *.doodad ../../../assets/doodads/

BIN
doodads/gems/blue-1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 891 B

BIN
doodads/gems/blue-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 927 B

BIN
doodads/gems/blue-3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

BIN
doodads/gems/blue-4.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

24
doodads/gems/gemstone.js

@ -0,0 +1,24 @@
// Gem stone collectibles/keys.
const color = Self.GetTag("color"),
shimmerFreq = 1000;
function main() {
Self.SetMobile(true);
Self.SetGravity(true);
Self.AddAnimation("shimmer", 100, [0, 1, 2, 3, 0]);
Events.OnCollide((e) => {
if (e.Settled) {
if (e.Actor.HasInventory()) {
Sound.Play("item-get.wav")
e.Actor.AddItem(Self.Filename, 1);
Self.Destroy();
}
}
});
setInterval(() => {
Self.PlayAnimation("shimmer", null);
}, shimmerFreq);
}

BIN
doodads/gems/green-1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 B

BIN
doodads/gems/green-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 B

BIN
doodads/gems/green-3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 B

BIN
doodads/gems/green-4.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

BIN
doodads/gems/red-1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 B

BIN
doodads/gems/red-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 B

BIN
doodads/gems/red-3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 B

BIN
doodads/gems/red-4.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 B

BIN
doodads/gems/totem-blue-0.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 B

BIN
doodads/gems/totem-blue-1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 958 B

BIN
doodads/gems/totem-blue-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1005 B

BIN
doodads/gems/totem-blue-3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 987 B

BIN
doodads/gems/totem-blue-4.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 921 B

BIN
doodads/gems/totem-green-0.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 B

BIN
doodads/gems/totem-green-1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 887 B

BIN
doodads/gems/totem-green-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 938 B

BIN
doodads/gems/totem-green-3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 978 B

BIN
doodads/gems/totem-green-4.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 956 B

BIN
doodads/gems/totem-red-0.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 B

BIN
doodads/gems/totem-red-1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 B

BIN
doodads/gems/totem-red-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 B

BIN
doodads/gems/totem-red-3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 B

BIN
doodads/gems/totem-red-4.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 817 B

BIN
doodads/gems/totem-yellow-0.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 837 B

BIN
doodads/gems/totem-yellow-1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
doodads/gems/totem-yellow-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
doodads/gems/totem-yellow-3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
doodads/gems/totem-yellow-4.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

100
doodads/gems/totem.js

@ -0,0 +1,100 @@
// Gem stone totem socket.
/*
The Totem is a type of key-door that holds onto its corresponding
Gemstone. When a doodad holding the right Gemstone touches the
totem, the totem takes the gemstone and activates.
If the Totem is not linked to any other Totems, it immediately
sends a power(true) signal upon activation.
If the Totem is linked to other Totems, it waits until all totems
have been activated before it will emit a power signal. Only one
such totem needs to be linked to e.g. an Electric Door - no matter
which totem is solved last, they'll all emit a power signal when
all of their linked totems are activated.
*/
let color = Self.GetTag("color"),
keyname = "gem-"+color+".doodad",
activated = false,
linkedReceiver = false, // is linked to a non-totem which might want power
totems = {}, // linked totems
shimmerFreq = 1000;
function main() {
// Show the hollow socket on level load (last layer)
Self.ShowLayer(4);
// Find any linked totems.
for (let link of Self.GetLinks()) {
if (link.Filename.indexOf("gem-totem") > -1) {
totems[link.ID()] = false;
} else {
linkedReceiver = true;
}
}
// Shimmer animation is just like the gemstones: first 4 frames
// are the filled socket sprites.
Self.AddAnimation("shimmer", 100, [0, 1, 2, 3, 0]);
Events.OnCollide((e) => {
if (activated) return;
if (e.Actor.IsMobile() && e.Settled) {
// Do they have our gemstone?
let hasKey = e.Actor.HasItem(keyname) >= 0;
if (!hasKey) {
return;
}
// Take the gemstone.
e.Actor.RemoveItem(keyname, 1);
Self.ShowLayer(0);
// Emit to our linked totem neighbors.
activated = true;
Message.Publish("gem-totem:activated", Self.ID());
tryPower();
}
});
Message.Subscribe("gem-totem:activated", (totemId) => {
totems[totemId] = true;
tryPower();
})
setInterval(() => {
if (activated) {
Self.PlayAnimation("shimmer", null);
}
}, shimmerFreq);
}
// Try to send a power signal for an activated totem.
function tryPower() {
// Only emit power if we are linked to something other than a totem.
if (!linkedReceiver) {
return;
}
// Can't if any of our linked totems aren't activated.
try {
for (let totemId of Object.keys(totems)) {
if (totems[totemId] === false) {
return;
}
}
} catch(e) {
console.error("Caught: %s", e);
}
// Can't if we aren't powered.
if (activated === false) {
return;
}
// Emit power!
Message.Publish("power", true);
}

BIN
doodads/gems/yellow-1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 B

BIN
doodads/gems/yellow-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 921 B

BIN
doodads/gems/yellow-3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 898 B

BIN
doodads/gems/yellow-4.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 879 B

15
doodads/objects/anvil.js

@ -6,11 +6,12 @@ function main() {
Self.SetHitbox(0, 0, 48, 25); Self.SetHitbox(0, 0, 48, 25);
Self.SetMobile(true); Self.SetMobile(true);
Self.SetGravity(true); Self.SetGravity(true);
Self.SetInvulnerable(true);
// Monitor our Y position to tell if we've been falling. // Monitor our Y position to tell if we've been falling.
var lastPoint = Self.Position();
setInterval(function () {
var nowAt = Self.Position();
let lastPoint = Self.Position();
setInterval(() => {
let nowAt = Self.Position();
if (nowAt.Y > lastPoint.Y) { if (nowAt.Y > lastPoint.Y) {
falling = true; falling = true;
} else { } else {
@ -19,7 +20,7 @@ function main() {
lastPoint = nowAt; lastPoint = nowAt;
}, 100); }, 100);
Events.OnCollide(function (e) {
Events.OnCollide((e) => {
if (!e.Settled) { if (!e.Settled) {
return; return;
} }
@ -33,7 +34,7 @@ function main() {
FailLevel("Watch out for anvils!"); FailLevel("Watch out for anvils!");
return; return;
} }
else if (e.Actor.IsMobile()) {
else if (e.Actor.IsMobile() && !e.Actor.Invulnerable()) {
// Destroy mobile doodads. // Destroy mobile doodads.
Sound.Play("crumbly-break.wav"); Sound.Play("crumbly-break.wav");
e.Actor.Destroy(); e.Actor.Destroy();
@ -43,8 +44,8 @@ function main() {
}); });
// When we receive power, we reset to our original position. // When we receive power, we reset to our original position.
var origPoint = Self.Position();
Message.Subscribe("power", function (powered) {
let origPoint = Self.Position();
Message.Subscribe("power", (powered) => {
Self.MoveTo(origPoint); Self.MoveTo(origPoint);
Self.SetVelocity(Vector(0, 0)); Self.SetVelocity(Vector(0, 0));
}); });

30
doodads/objects/checkpoint-flag.js

@ -1,17 +1,28 @@
// Checkpoint Flag. // Checkpoint Flag.
var isCurrentCheckpoint = false;
var isCurrentCheckpoint = false,
playerEntered = false
broadcastCooldown = time.Now();
function main() { function main() {
Self.SetHitbox(22 + 16, 16, 75 - 16, 86); Self.SetHitbox(22 + 16, 16, 75 - 16, 86);
setActive(false); 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 // Checkpoints broadcast to all of their peers so they all
// know which one is the most recently activated. // know which one is the most recently activated.
Message.Subscribe("broadcast:checkpoint", function (currentID) {
Message.Subscribe("broadcast:checkpoint", (currentID) => {
setActive(false); setActive(false);
return "a ok";
}); });
Events.OnCollide(function (e) {
Events.OnCollide((e) => {
if (!e.Settled) { if (!e.Settled) {
return; return;
} }
@ -21,10 +32,19 @@ function main() {
return; return;
} }
// Set the player checkpoint.
SetCheckpoint(Self.Position()); SetCheckpoint(Self.Position());
setActive(true); 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);
}
}); });
} }

2
doodads/objects/exit-flag.js

@ -2,7 +2,7 @@
function main() { function main() {
Self.SetHitbox(22 + 16, 16, 75 - 16, 86); Self.SetHitbox(22 + 16, 16, 75 - 16, 86);
Events.OnCollide(function (e) {
Events.OnCollide((e) => {
if (!e.Settled) { if (!e.Settled) {
return; return;
} }

5
doodads/objects/start-flag.js

@ -4,8 +4,7 @@ function main() {
// Linking a doodad to the Start Flag sets the // Linking a doodad to the Start Flag sets the
// player character. Destroy the original doodads. // player character. Destroy the original doodads.
var links = Self.GetLinks();
for (var i = 0; i < links.length; i++) {
links[i].Destroy();
for (var link of Self.GetLinks()) {
link.Destroy();
} }
} }

6
doodads/on-off/state-block-blue.js

@ -3,9 +3,9 @@ function main() {
Self.SetHitbox(0, 0, 42, 42); Self.SetHitbox(0, 0, 42, 42);
// Blue block is ON by default. // Blue block is ON by default.
var state = true;
let state = true;
Message.Subscribe("broadcast:state-change", function(newState) {
Message.Subscribe("broadcast:state-change", (newState) => {
state = !newState; state = !newState;
// Layer 0: ON // Layer 0: ON
@ -13,7 +13,7 @@ function main() {
Self.ShowLayer(state ? 0 : 1); Self.ShowLayer(state ? 0 : 1);
}); });
Events.OnCollide(function(e) {
Events.OnCollide((e) => {
if (e.Actor.IsMobile() && e.InHitbox) { if (e.Actor.IsMobile() && e.InHitbox) {
if (state) { if (state) {
return false; return false;

6
doodads/on-off/state-block-orange.js

@ -3,9 +3,9 @@ function main() {
Self.SetHitbox(0, 0, 42, 42); Self.SetHitbox(0, 0, 42, 42);
// Orange block is OFF by default. // Orange block is OFF by default.
var state = false;
let state = false;
Message.Subscribe("broadcast:state-change", function(newState) {
Message.Subscribe("broadcast:state-change", (newState) => {
state = newState; state = newState;
// Layer 0: OFF // Layer 0: OFF
@ -13,7 +13,7 @@ function main() {
Self.ShowLayer(state ? 1 : 0); Self.ShowLayer(state ? 1 : 0);
}); });
Events.OnCollide(function(e) {
Events.OnCollide((e) => {
if (e.Actor.IsMobile() && e.InHitbox) { if (e.Actor.IsMobile() && e.InHitbox) {
if (state) { if (state) {
return false; return false;

10
doodads/on-off/state-button.js

@ -1,23 +1,23 @@
// State Block Control Button // State Block Control Button
// Button is "OFF" by default. // Button is "OFF" by default.
var state = false;
let state = false;
function main() { function main() {
Self.SetHitbox(0, 0, 42, 42); Self.SetHitbox(0, 0, 42, 42);
// When the button is activated, don't keep toggling state until we're not // When the button is activated, don't keep toggling state until we're not
// being touched again. // being touched again.
var colliding = false;
let colliding = false;
// If we receive a state change event from a DIFFERENT on/off button, update // If we receive a state change event from a DIFFERENT on/off button, update
// ourself to match the state received. // ourself to match the state received.
Message.Subscribe("broadcast:state-change", function(value) {
Message.Subscribe("broadcast:state-change", (value) => {
state = value; state = value;
showSprite(); showSprite();
}); });
Events.OnCollide(function(e) {
Events.OnCollide((e) => {
if (colliding) { if (colliding) {
return false; return false;
} }
@ -40,7 +40,7 @@ function main() {
return false; return false;
}); });
Events.OnLeave(function(e) {
Events.OnLeave((e) => {
colliding = false; colliding = false;
}) })
} }

9
doodads/regions/Makefile

@ -23,6 +23,15 @@ build:
doodad convert -t "Power Source" power-64.png power-source.doodad doodad convert -t "Power Source" power-64.png power-source.doodad
doodad install-script power.js power-source.doodad doodad install-script power.js power-source.doodad
# Warp Door
doodad convert -t "Invisible Warp Door" warp-door-64.png reg-warp-door.doodad
doodad edit-doodad --tag "color=invisible" reg-warp-door.doodad
doodad install-script ../warp-door/warp-door.js reg-warp-door.doodad
# Reset Level Timer
doodad convert -t "Reset Level Timer" timer-64.png reg-reset-timer.doodad
doodad install-script reset-timer.js reg-reset-timer.doodad
for i in *.doodad; do\ for i in *.doodad; do\
doodad edit-doodad --tag "category=technical" $${i};\ doodad edit-doodad --tag "category=technical" $${i};\
done done

BIN
doodads/regions/checkpoint-128.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 957 B

After

Width:  |  Height:  |  Size: 956 B

4
doodads/regions/checkpoint.js

@ -8,11 +8,11 @@ function main() {
// Checkpoints broadcast to all of their peers so they all // Checkpoints broadcast to all of their peers so they all
// know which one is the most recently activated. // know which one is the most recently activated.
Message.Subscribe("broadcast:checkpoint", function (currentID) {
Message.Subscribe("broadcast:checkpoint", (currentID) => {
setActive(false); setActive(false);
}); });
Events.OnCollide(function (e) {
Events.OnCollide((e) => {
if (isCurrentCheckpoint || !e.Settled) { if (isCurrentCheckpoint || !e.Settled) {
return; return;
} }

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save