Doodads: Gems, Snake and Crusher

Adds several new doodads to the game and 5 new wallpapers (parchment
paper in blue, green, red, white and yellow).

New doodads:

* Crusher: A purple block-headed mob wearing an iron helmet. It tries
  to crush the player when you get underneath. Its flat helmet can be
  ridden on like an elevator back up.
* Snake: A green stationary mob that always faces toward the player.
  If the player is nearby and jumps, the Snake will jump too and hope
  to catch the player in mid-air.
* Gems and Totems: A new key & lock collectible. Gems have quantity so
  you can collect multiple, and place them into matching Totems. A
  Totem gives off a power signal when its gem is placed and all other
  Totems it is linked to have also been activated. A single Totem may
  link to an Electric Door and require only one gem to open it, or it
  can link to other Totems and they all require gems before the power
  signal is sent out.
This commit is contained in:
Noah 2022-05-01 15:18:23 -07:00
parent 0ed5d6ae0c
commit 50980caebb
62 changed files with 578 additions and 1 deletions

View File

@ -19,7 +19,15 @@ function main() {
let 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...
console.log("new box code");
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.

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,200 @@
// 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,
falling: 2,
hit: 3,
rising: 4,
};
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.falling;
} 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.falling;
}
break;
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");
}
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 891 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 927 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 958 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1005 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 987 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 921 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 887 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 938 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 978 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 956 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 817 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 837 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,108 @@
// 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;
}
}
console.log("Totem %s is linked to %d neighbors", Self.ID(), Object.keys(totems).length);
// 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;
}
console.log("Totem %s (%s) tries power", Self.ID(), Self.Filename);
// Can't if any of our linked totems aren't activated.
try {
for (let totemId of Object.keys(totems)) {
console.log("Totem %s (%s) sees linked totem %s", Self.ID(), Self.Filename, totemId);
if (totems[totemId] === false) {
console.log("Can't, a linked totem not active!");
return;
}
}
} catch(e) {
console.error("Caught: %s", e);
}
// Can't if we aren't powered.
if (activated === false) {
console.log("Can't, we are not active!");
return;
}
// Emit power!
console.log("POWER!");
Message.Publish("power", true);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 921 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 898 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 879 B

View File

@ -0,0 +1,15 @@
ALL: build
.PHONY: build
build:
doodad convert -t "Snake" left-1.png left-2.png left-3.png right-1.png right-2.png right-3.png \
attack-left-1.png attack-left-2.png attack-left-3.png attack-right-1.png attack-right-2.png \
attack-right-3.png snake.doodad
doodad install-script snake.js snake.doodad
# Tag the category for these doodads
for i in *.doodad; do\
doodad edit-doodad --tag "category=creatures" $${i};\
done
cp *.doodad ../../../assets/doodads/

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 935 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 916 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,135 @@
// Snake
/*
A.I. Behaviors:
- Always turns to face the nearest player character
- Jumps up when the player tries to jump over them,
aiming to attack the player.
*/
let direction = "left",
jumpSpeed = 12,
watchRadius = 300, // player nearby distance for snake to jump
jumpCooldownStart = time.Now(),
size = Self.Size();
const states = {
idle: 0,
attacking: 1,
};
let state = states.idle;
function main() {
Self.SetMobile(true);
Self.SetGravity(true);
Self.SetHitbox(20, 0, 28, 58);
Self.AddAnimation("idle-left", 100, ["left-1", "left-2", "left-3", "left-2"]);
Self.AddAnimation("idle-right", 100, ["right-1", "right-2", "right-3", "right-2"]);
Self.AddAnimation("attack-left", 100, ["attack-left-1", "attack-left-2", "attack-left-3"])
Self.AddAnimation("attack-right", 100, ["attack-right-1", "attack-right-2", "attack-right-3"])
// Player Character controls?
if (Self.IsPlayer()) {
return player();
}
Events.OnCollide((e) => {
// The snake is deadly to the touch.
if (e.Settled && e.Actor.IsPlayer() && e.InHitbox) {
// Friendly to fellow snakes.
if (e.Actor.Doodad().Filename.indexOf("snake") > -1) {
return;
}
FailLevel("Watch out for snakes!");
return;
}
});
setInterval(() => {
// Find the player.
let player = Actors.FindPlayer(),
playerPoint = player.Position(),
point = Self.Position(),
delta = 0,
nearby = 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) {
console.log("Player is nearby snake! %d", delta);
nearby = true;
}
// If we are idle and the player is jumping nearby...
if (state == states.idle && nearby && Self.Grounded()) {
if (playerPoint.Y - point.Y+(size.H/2) < 20) {
console.warn("Player is jumping near us!")
// Enter attack state.
if (time.Since(jumpCooldownStart) > 500 * time.Millisecond) {
state = states.attacking;
Self.SetVelocity(Vector(0, -jumpSpeed));
Self.StopAnimation();
Self.PlayAnimation("attack-"+direction, null);
return;
}
}
}
// If we are attacking and gravity has claimed us back.
if (state === states.attacking && Self.Grounded()) {
console.log("Landed again after jump!");
state = states.idle;
jumpCooldownStart = time.Now();
Self.StopAnimation();
}
// Ensure that the animations are always rolling.
if (state === states.idle && !Self.IsAnimating()) {
Self.PlayAnimation("idle-"+direction, null);
}
}, 100);
}
// If under control of the player character.
function player() {
let jumping = false;
Events.OnKeypress((ev) => {
Vx = 0;
Vy = 0;
if (ev.Right) {
direction = "right";
} else if (ev.Left) {
direction = "left";
}
// Jump!
if (ev.Up && !jumping) {
Self.StopAnimation();
Self.PlayAnimation("attack-"+direction, null);
jumping = true;
return;
}
if (jumping && Self.Grounded()) {
Self.StopAnimation();
jumping = false;
}
if (!jumping && !Self.IsAnimating()) {
Self.PlayAnimation("idle-"+direction, null);
}
});
}