doodads/bird/bird.js

292 lines
7.1 KiB
JavaScript

// Bird (red and blue)
/*
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.SetGravity(false);
Self.SetHitbox(0, 0, 46, 32);
Self.AddAnimation("fly-left", 100, ["left-1", "left-2"]);
Self.AddAnimation("fly-right", 100, ["right-1", "right-2"]);
// Player Character controls?
if (Self.IsPlayer()) {
return player();
}
// No A.I.?
if (Self.GetOption("No A.I.") === true) {
return;
}
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;
}
});
// Sample our X position every few frames and detect if we've hit a solid wall.
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();
}
// Sample how far we've moved to detect hitting a wall.
if (sampleTick % sampleRate === 0) {
let curP = Self.Position()
let delta = Math.abs(curP.X - lastSampled.X);
if (delta < 5) {
direction = direction === "right" ? "left" : "right";
}
// 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++;
// Play the bird flap sound if we are on screen.
if (Self.IsOnScreen()) {
Sound.Play("bird-fly.mp3");
}
// 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,
// the JS API should be friendlier and custom...
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
// turn around quickly without moonwalking.
if (direction !== lastDirection) {
Self.StopAnimation();
}
if (!Self.IsAnimating()) {
Self.PlayAnimation("fly-" + direction, null);
}
lastDirection = direction;
}, 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()) {
Sound.Play("bird-dive.mp3");
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.
function player() {
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;
Vy = 0;
if (ev.Right) {
direction = "right";
} else if (ev.Left) {
direction = "left";
}
// Dive!
if (ev.Down && ev.Right && falling) {
Self.StopAnimation();
Self.ShowLayerNamed("dive-right");
Sound.Play("bird-dive.mp3");
diving = falling;
} else if (ev.Down && ev.Left && falling) {
Self.StopAnimation();
Self.ShowLayerNamed("dive-left");
Sound.Play("bird-dive.mp3");
diving = falling;
} else if (ev.Right) {
// Fly right.
if (!Self.IsAnimating()) {
Self.PlayAnimation("fly-right", null);
}
Vx = playerSpeed;
diving = false;
} else if (ev.Left) {
// Fly left.
if (!Self.IsAnimating()) {
Self.PlayAnimation("fly-left", null);
}
Vx = -playerSpeed;
diving = false;
} else {
// Hover in place.
if (!Self.IsAnimating()) {
Self.PlayAnimation("fly-" + direction);
}
diving = false;
}
if (!diving) {
Sound.Play("bird-fly.mp3");
}
// 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.mp3");
e.Actor.Destroy();
}
});
}