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