Noah Petherbridge
fc736abd5f
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.
201 lines
5.8 KiB
JavaScript
201 lines
5.8 KiB
JavaScript
// 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");
|
|
}
|
|
});
|
|
}
|