From 2913e706e2231b385cda4ecebb90f9239bfe7538 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Mon, 17 Jan 2022 21:28:05 -0800 Subject: [PATCH] Update Doodad JS API + Hostile mobs New functions are available on the JavaScript API for doodads: * Actors.At(Point) []*Actor: returns actors intersecting a point * Actors.FindPlayer() *Actor: returns the nearest player character * Actors.New(filename string): create a new actor (NOT TESTED YET!) * Self.Grounded() bool: query the grounded status of current actor With this the game's built-in doodads have been revised: * Bird: will now scan 240 pixels diagonally searching for the player character and will dive if seen. The Bird is dangerous while diving. It will return to its original altitude once it touches the ground. * Azulians: the Azulians are now dangerous to player characters but not to the Thief. Azulians will begin to follow the player when they are within the aggro range and will hop if the player is above them to try and overcome obstacles. * Blue Azulian: aggro is (250, 100) jump speed 12 movement 2 * Red Azulian: aggro is (250, 200) jump speed 14 movement 4 --- dev-assets/doodads/azulian/azulian.js | 66 ++++++++++++++----- dev-assets/doodads/bird/bird.js | 91 +++++++++++++++++++++++---- 2 files changed, 131 insertions(+), 26 deletions(-) diff --git a/dev-assets/doodads/azulian/azulian.js b/dev-assets/doodads/azulian/azulian.js index fb4dacb..2a8d3e6 100644 --- a/dev-assets/doodads/azulian/azulian.js +++ b/dev-assets/doodads/azulian/azulian.js @@ -1,5 +1,10 @@ // Azulian (Red and Blue) -var playerSpeed = 12, + +const color = Self.GetTag("color"); +var playerSpeed = color === 'blue' ? 2 : 4, + aggroX = 250, // X/Y distance sensitivity from player + aggroY = color === 'blue' ? 100 : 200, + jumpSpeed = color === 'blue' ? 12 : 14, animating = false, direction = "right", lastDirection = "right"; @@ -15,7 +20,6 @@ function setupAnimations(color) { } function main() { - const color = Self.GetTag("color"); playerSpeed = color === 'blue' ? 2 : 4; Self.SetMobile(true); @@ -36,19 +40,53 @@ function main() { let sampleRate = 5; let lastSampledX = 0; - setInterval(() => { - 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; + // 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 && e.Actor.IsPlayer() && e.Actor.Doodad().Filename !== "thief.doodad") { + FailLevel("Watch out for the Azulians!"); + return; } - sampleTick++; + }); - let Vx = parseFloat(playerSpeed * (direction === "left" ? -1 : 1)); - Self.SetVelocity(Vector(Vx, 0.0)); + 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; + if (player !== null) { + 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) { + 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; + } + sampleTick++; + } + + let Vx = parseFloat(playerSpeed * (direction === "left" ? -1 : 1)), + Vy = jump && Self.Grounded() ? parseFloat(-jumpSpeed) : Self.GetVelocity().Y; + Self.SetVelocity(Vector(Vx, Vy)); // If we changed directions, stop animating now so we can // turn around quickly without moonwalking. @@ -61,7 +99,7 @@ function main() { } lastDirection = direction; - }, 100); + }, 10); } function playerControls() { diff --git a/dev-assets/doodads/bird/bird.js b/dev-assets/doodads/bird/bird.js index 1a03356..277769f 100644 --- a/dev-assets/doodads/bird/bird.js +++ b/dev-assets/doodads/bird/bird.js @@ -1,7 +1,6 @@ // Bird -function main() { - let speed = 4, +let speed = 4, Vx = Vy = 0, altitude = Self.Position().Y; // original height in the level @@ -13,6 +12,7 @@ function main() { }; let state = states.flying; +function main() { Self.SetMobile(true); Self.SetGravity(false); Self.SetHitbox(0, 0, 46, 32); @@ -25,6 +25,12 @@ function main() { } 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.InHitbox) { return false; } @@ -33,25 +39,42 @@ function main() { // Sample our X position every few frames and detect if we've hit a solid wall. let sampleTick = 0, sampleRate = 2, - lastSampledX = 0, - lastSampledY = 0; + lastSampled = Point(0, 0); setInterval(() => { + // Sample how far we've moved to detect hitting a wall. if (sampleTick % sampleRate === 0) { - let curX = Self.Position().X; - let delta = Math.abs(curX - lastSampledX); + let curP = Self.Position() + let delta = Math.abs(curP.X - lastSampled.X); if (delta < 5) { 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++; - // If we are not flying at our original altitude, correct for that. - let curV = Self.Position(); - let Vy = 0.0; - if (curV.Y != altitude) { - Vy = curV.Y < altitude ? 1 : -1; + // 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 != altitude) { + Vy = curV.Y < altitude ? 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, @@ -59,6 +82,13 @@ function main() { 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) { @@ -73,6 +103,43 @@ function main() { }, 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. +function AI_ScanForPlayer() { + let stepY = 12, // number of pixels to skip + stepX = stepY, + limit = stepX * 20, // 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()) { + state = states.diving; + return; + } + } + } + + return; +} + // If under control of the player character. function player() { Self.SetInventory(true);