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:
parent
1cc6eee5c8
commit
9201475060
|
@ -1,5 +1,10 @@
|
||||||
// Azulian (Red and Blue)
|
// 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,
|
animating = false,
|
||||||
direction = "right",
|
direction = "right",
|
||||||
lastDirection = "right";
|
lastDirection = "right";
|
||||||
|
@ -15,7 +20,6 @@ function setupAnimations(color) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
const color = Self.GetTag("color");
|
|
||||||
playerSpeed = color === 'blue' ? 2 : 4;
|
playerSpeed = color === 'blue' ? 2 : 4;
|
||||||
|
|
||||||
Self.SetMobile(true);
|
Self.SetMobile(true);
|
||||||
|
@ -36,7 +40,39 @@ function main() {
|
||||||
let sampleRate = 5;
|
let sampleRate = 5;
|
||||||
let lastSampledX = 0;
|
let lastSampledX = 0;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
setInterval(() => {
|
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) {
|
if (sampleTick % sampleRate === 0) {
|
||||||
let curX = Self.Position().X;
|
let curX = Self.Position().X;
|
||||||
let delta = Math.abs(curX - lastSampledX);
|
let delta = Math.abs(curX - lastSampledX);
|
||||||
|
@ -46,9 +82,11 @@ function main() {
|
||||||
lastSampledX = curX;
|
lastSampledX = curX;
|
||||||
}
|
}
|
||||||
sampleTick++;
|
sampleTick++;
|
||||||
|
}
|
||||||
|
|
||||||
let Vx = parseFloat(playerSpeed * (direction === "left" ? -1 : 1));
|
let Vx = parseFloat(playerSpeed * (direction === "left" ? -1 : 1)),
|
||||||
Self.SetVelocity(Vector(Vx, 0.0));
|
Vy = jump && Self.Grounded() ? parseFloat(-jumpSpeed) : Self.GetVelocity().Y;
|
||||||
|
Self.SetVelocity(Vector(Vx, Vy));
|
||||||
|
|
||||||
// If we changed directions, stop animating now so we can
|
// If we changed directions, stop animating now so we can
|
||||||
// turn around quickly without moonwalking.
|
// turn around quickly without moonwalking.
|
||||||
|
@ -61,7 +99,7 @@ function main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
lastDirection = direction;
|
lastDirection = direction;
|
||||||
}, 100);
|
}, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
function playerControls() {
|
function playerControls() {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
// Bird
|
// Bird
|
||||||
|
|
||||||
function main() {
|
|
||||||
let speed = 4,
|
let speed = 4,
|
||||||
Vx = Vy = 0,
|
Vx = Vy = 0,
|
||||||
altitude = Self.Position().Y; // original height in the level
|
altitude = Self.Position().Y; // original height in the level
|
||||||
|
@ -13,6 +12,7 @@ function main() {
|
||||||
};
|
};
|
||||||
let state = states.flying;
|
let state = states.flying;
|
||||||
|
|
||||||
|
function main() {
|
||||||
Self.SetMobile(true);
|
Self.SetMobile(true);
|
||||||
Self.SetGravity(false);
|
Self.SetGravity(false);
|
||||||
Self.SetHitbox(0, 0, 46, 32);
|
Self.SetHitbox(0, 0, 46, 32);
|
||||||
|
@ -25,6 +25,12 @@ function main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
Events.OnCollide((e) => {
|
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) {
|
if (e.Actor.IsMobile() && e.InHitbox) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -33,32 +39,56 @@ function main() {
|
||||||
// Sample our X position every few frames and detect if we've hit a solid wall.
|
// Sample our X position every few frames and detect if we've hit a solid wall.
|
||||||
let sampleTick = 0,
|
let sampleTick = 0,
|
||||||
sampleRate = 2,
|
sampleRate = 2,
|
||||||
lastSampledX = 0,
|
lastSampled = Point(0, 0);
|
||||||
lastSampledY = 0;
|
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
|
// Sample how far we've moved to detect hitting a wall.
|
||||||
if (sampleTick % sampleRate === 0) {
|
if (sampleTick % sampleRate === 0) {
|
||||||
let curX = Self.Position().X;
|
let curP = Self.Position()
|
||||||
let delta = Math.abs(curX - lastSampledX);
|
let delta = Math.abs(curP.X - lastSampled.X);
|
||||||
if (delta < 5) {
|
if (delta < 5) {
|
||||||
direction = direction === "right" ? "left" : "right";
|
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++;
|
sampleTick++;
|
||||||
|
|
||||||
|
// Are we diving?
|
||||||
|
if (state === states.diving) {
|
||||||
|
Vy = speed
|
||||||
|
} else {
|
||||||
// If we are not flying at our original altitude, correct for that.
|
// If we are not flying at our original altitude, correct for that.
|
||||||
let curV = Self.Position();
|
let curV = Self.Position();
|
||||||
let Vy = 0.0;
|
Vy = 0.0;
|
||||||
if (curV.Y != altitude) {
|
if (curV.Y != altitude) {
|
||||||
Vy = curV.Y < altitude ? 1 : -1;
|
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,
|
// TODO: Vector() requires floats, pain in the butt for JS,
|
||||||
// the JS API should be friendlier and custom...
|
// the JS API should be friendlier and custom...
|
||||||
let Vx = parseFloat(speed * (direction === "left" ? -1 : 1));
|
let Vx = parseFloat(speed * (direction === "left" ? -1 : 1));
|
||||||
Self.SetVelocity(Vector(Vx, Vy));
|
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
|
// If we changed directions, stop animating now so we can
|
||||||
// turn around quickly without moonwalking.
|
// turn around quickly without moonwalking.
|
||||||
if (direction !== lastDirection) {
|
if (direction !== lastDirection) {
|
||||||
|
@ -73,6 +103,43 @@ function main() {
|
||||||
}, 100);
|
}, 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.
|
// If under control of the player character.
|
||||||
function player() {
|
function player() {
|
||||||
Self.SetInventory(true);
|
Self.SetInventory(true);
|
||||||
|
|
|
@ -10,6 +10,8 @@ import (
|
||||||
"git.kirsle.net/go/render"
|
"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.
|
// JSProxy offers a function API interface to expose to Doodad javascripts.
|
||||||
// These methods safely give the JS access to important attributes and functions
|
// These methods safely give the JS access to important attributes and functions
|
||||||
// without exposing unintended API surface area in the process.
|
// without exposing unintended API surface area in the process.
|
||||||
|
|
|
@ -83,6 +83,7 @@ func (w *Canvas) InstallScripts() error {
|
||||||
|
|
||||||
// Security: expose a selective API to the actor to the JS engine.
|
// Security: expose a selective API to the actor to the JS engine.
|
||||||
vm.Self = w.MakeSelfAPI(actor)
|
vm.Self = w.MakeSelfAPI(actor)
|
||||||
|
w.MakeScriptAPI(vm)
|
||||||
vm.Set("Self", vm.Self)
|
vm.Set("Self", vm.Self)
|
||||||
|
|
||||||
if _, err := vm.Run(actor.Doodad().Script); err != nil {
|
if _, err := vm.Run(actor.Doodad().Script); err != nil {
|
||||||
|
|
|
@ -1,9 +1,62 @@
|
||||||
package uix
|
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.
|
// 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
|
// MakeSelfAPI generates the `Self` object for the scripting API in
|
||||||
// reference to a live Canvas actor in the level.
|
// reference to a live Canvas actor in the level.
|
||||||
func (w *Canvas) MakeSelfAPI(actor *Actor) map[string]interface{} {
|
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.MoveTo(p)
|
||||||
actor.SetGrounded(false)
|
actor.SetGrounded(false)
|
||||||
},
|
},
|
||||||
|
"Grounded": actor.Grounded,
|
||||||
"SetHitbox": actor.SetHitbox,
|
"SetHitbox": actor.SetHitbox,
|
||||||
"Hitbox": actor.Hitbox,
|
"Hitbox": actor.Hitbox,
|
||||||
"SetVelocity": actor.SetVelocity,
|
"SetVelocity": actor.SetVelocity,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user