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
This commit is contained in:
Noah 2022-01-17 21:28:05 -08:00
parent 1cc6eee5c8
commit 9201475060
5 changed files with 189 additions and 27 deletions

View File

@ -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() {

View File

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

View File

@ -10,6 +10,8 @@ import (
"git.kirsle.net/go/render"
)
// SEE ALSO: uix/scripting.go for more global functions
// JSProxy offers a function API interface to expose to Doodad javascripts.
// These methods safely give the JS access to important attributes and functions
// without exposing unintended API surface area in the process.

View File

@ -83,6 +83,7 @@ func (w *Canvas) InstallScripts() error {
// Security: expose a selective API to the actor to the JS engine.
vm.Self = w.MakeSelfAPI(actor)
w.MakeScriptAPI(vm)
vm.Set("Self", vm.Self)
if _, err := vm.Run(actor.Doodad().Script); err != nil {

View File

@ -1,9 +1,62 @@
package uix
import "git.kirsle.net/go/render"
import (
"git.kirsle.net/apps/doodle/pkg/doodads"
"git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/scripting"
"git.kirsle.net/go/render"
)
// Functions relating to the Doodad JavaScript API for Canvas Actors.
// MakeScriptAPI makes several useful globals available to doodad
// scripts such as Actors.At()
func (w *Canvas) MakeScriptAPI(vm *scripting.VM) {
vm.Set("Actors", map[string]interface{}{
// Actors.At(Point)
"At": func(p render.Point) []*Actor {
var result = []*Actor{}
for _, actor := range w.actors {
var box = actor.Hitbox().AddPoint(actor.Position())
if actor != nil && p.Inside(box) {
result = append(result, actor)
}
}
return result
},
// Actors.FindPlayer: returns the nearest player character.
"FindPlayer": func() *Actor {
for _, actor := range w.actors {
if actor.IsPlayer() {
return actor
}
}
return nil
},
// Actors.New: create a new actor.
"New": func(filename string) *Actor {
doodad, err := doodads.LoadFile(filename)
if err != nil {
panic(err)
}
actor := NewActor("_new", &level.Actor{}, doodad)
w.AddActor(actor)
// Set up the player character's script in the VM.
if err := w.scripting.AddLevelScript(actor.ID(), filename); err != nil {
log.Error("Actors.New(%s): scripting.InstallActor(player) failed: %s", filename, err)
}
return actor
},
})
}
// MakeSelfAPI generates the `Self` object for the scripting API in
// reference to a live Canvas actor in the level.
func (w *Canvas) MakeSelfAPI(actor *Actor) map[string]interface{} {
@ -23,6 +76,7 @@ func (w *Canvas) MakeSelfAPI(actor *Actor) map[string]interface{} {
actor.MoveTo(p)
actor.SetGrounded(false)
},
"Grounded": actor.Grounded,
"SetHitbox": actor.SetHitbox,
"Hitbox": actor.Hitbox,
"SetVelocity": actor.SetVelocity,