Ykkrosh
b8925fbbc9
Pass terrain passability data to AI scripts. Expand pathfinder passability data to 16 bits per tile, to allow more classes. Support 16-bit ints in serializer. Partially support JS typed arrays. Allow foundations to be placed on top of units (fixes #499). Stop farms and fishes blocking movement (fixes #534). Add obstruction flags to allow finer control over what they block. Associate entity IDs with obstruction shapes, to allow finding colliding entities. Support moving to the edge of a target entity with inactive obstruction. Support foundation entities in AI. Support playing as non-hele civs. This was SVN commit r8899.
380 lines
9.4 KiB
JavaScript
380 lines
9.4 KiB
JavaScript
function AnimalAI() {}
|
|
|
|
AnimalAI.prototype.Schema =
|
|
"<a:example/>" +
|
|
"<element name='NaturalBehaviour' a:help='Behaviour of the unit in the absence of player commands (intended for animals)'>" +
|
|
"<choice>" +
|
|
"<value a:help='Will actively attack any unit it encounters, even if not threatened'>violent</value>" +
|
|
"<value a:help='Will attack nearby units if it feels threatened (if they linger within LOS for too long)'>aggressive</value>" +
|
|
"<value a:help='Will attack nearby units if attacked'>defensive</value>" +
|
|
"<value a:help='Will never attack units'>passive</value>" +
|
|
"<value a:help='Will never attack units. Will typically attempt to flee for short distances when units approach'>skittish</value>" +
|
|
"</choice>" +
|
|
"</element>" +
|
|
"<element name='RoamDistance'>" +
|
|
"<ref name='positiveDecimal'/>" +
|
|
"</element>" +
|
|
"<element name='FleeDistance'>" +
|
|
"<ref name='positiveDecimal'/>" +
|
|
"</element>" +
|
|
"<element name='RoamTimeMin'>" +
|
|
"<ref name='positiveDecimal'/>" +
|
|
"</element>" +
|
|
"<element name='RoamTimeMax'>" +
|
|
"<ref name='positiveDecimal'/>" +
|
|
"</element>" +
|
|
"<element name='FeedTimeMin'>" +
|
|
"<ref name='positiveDecimal'/>" +
|
|
"</element>" +
|
|
"<element name='FeedTimeMax'>" +
|
|
"<ref name='positiveDecimal'/>" +
|
|
"</element>";
|
|
|
|
var AnimalFsmSpec = {
|
|
|
|
"MoveCompleted": function() {
|
|
// ignore spurious movement messages
|
|
// (these can happen when stopping moving at the same time
|
|
// as switching states)
|
|
},
|
|
|
|
"MoveStarted": function() {
|
|
// ignore spurious movement messages
|
|
},
|
|
|
|
"HealthChanged": function(msg) {
|
|
// If we died (got reduced to 0 hitpoints), stop the AI and act like a corpse
|
|
if (msg.to == 0)
|
|
this.SetNextState("CORPSE");
|
|
},
|
|
|
|
"CORPSE": {
|
|
"enter": function() {
|
|
this.StopMoving();
|
|
},
|
|
|
|
"Attacked": function(msg) {
|
|
// Do nothing, because we're dead already
|
|
},
|
|
},
|
|
|
|
"SKITTISH": {
|
|
|
|
"Attacked": function(msg) {
|
|
// If someone's attacking us, then run away
|
|
this.MoveAwayFrom(msg.data.attacker, +this.template.FleeDistance);
|
|
this.SetNextState("FLEEING");
|
|
this.PlaySound("panic");
|
|
},
|
|
|
|
"LeaveFoundation": function(msg) {
|
|
// Run away from the foundation
|
|
this.MoveAwayFrom(msg.target, +this.template.FleeDistance);
|
|
this.SetNextState("FLEEING");
|
|
this.PlaySound("panic");
|
|
},
|
|
|
|
"ROAMING": {
|
|
"enter": function() {
|
|
// Walk in a random direction
|
|
this.SelectAnimation("walk", false, this.GetWalkSpeed());
|
|
this.MoveRandomly(+this.template.RoamDistance);
|
|
// Set a random timer to switch to feeding state
|
|
this.StartTimer(RandomInt(+this.template.RoamTimeMin, +this.template.RoamTimeMax));
|
|
},
|
|
|
|
"leave": function() {
|
|
this.StopTimer();
|
|
},
|
|
|
|
"Timer": function(msg) {
|
|
this.SetNextState("FEEDING");
|
|
},
|
|
|
|
"MoveCompleted": function() {
|
|
this.MoveRandomly(+this.template.RoamDistance);
|
|
},
|
|
},
|
|
|
|
"FEEDING": {
|
|
"enter": function() {
|
|
// Stop and eat for a while
|
|
this.SelectAnimation("feeding");
|
|
this.StopMoving();
|
|
this.StartTimer(RandomInt(+this.template.FeedTimeMin, +this.template.FeedTimeMax));
|
|
},
|
|
|
|
"leave": function() {
|
|
this.StopTimer();
|
|
},
|
|
|
|
"MoveCompleted": function() { },
|
|
|
|
"Timer": function(msg) {
|
|
this.SetNextState("ROAMING");
|
|
},
|
|
},
|
|
|
|
"FLEEING": {
|
|
"enter": function() {
|
|
// Run quickly
|
|
var speed = this.GetRunSpeed();
|
|
this.SelectAnimation("run", false, speed);
|
|
this.SetMoveSpeed(speed);
|
|
},
|
|
|
|
"leave": function() {
|
|
// Reset normal speed
|
|
this.SetMoveSpeed(this.GetWalkSpeed());
|
|
},
|
|
|
|
"MoveCompleted": function() {
|
|
// When we've run far enough, go back to the roaming state
|
|
this.SetNextState("ROAMING");
|
|
},
|
|
},
|
|
},
|
|
|
|
"PASSIVE": {
|
|
|
|
"Attacked": function(msg) {
|
|
// Do nothing, just let them kill us
|
|
},
|
|
|
|
"LeaveFoundation": function(msg) {
|
|
// Walk away from the foundation
|
|
this.MoveAwayFrom(msg.target, 4);
|
|
this.SetNextState("FLEEING");
|
|
},
|
|
|
|
"ROAMING": {
|
|
"enter": function() {
|
|
// Walk in a random direction
|
|
this.SelectAnimation("walk", false, this.GetWalkSpeed());
|
|
this.MoveRandomly(+this.template.RoamDistance);
|
|
// Set a random timer to switch to feeding state
|
|
this.StartTimer(RandomInt(+this.template.RoamTimeMin, +this.template.RoamTimeMax));
|
|
},
|
|
|
|
"leave": function() {
|
|
this.StopTimer();
|
|
},
|
|
|
|
"Timer": function(msg) {
|
|
this.SetNextState("FEEDING");
|
|
},
|
|
|
|
"MoveCompleted": function() {
|
|
this.MoveRandomly(+this.template.RoamDistance);
|
|
},
|
|
},
|
|
|
|
"FEEDING": {
|
|
"enter": function() {
|
|
// Stop and eat for a while
|
|
this.SelectAnimation("feeding");
|
|
this.StopMoving();
|
|
this.StartTimer(RandomInt(+this.template.FeedTimeMin, +this.template.FeedTimeMax));
|
|
},
|
|
|
|
"leave": function() {
|
|
this.StopTimer();
|
|
},
|
|
|
|
"MoveCompleted": function() { },
|
|
|
|
"Timer": function(msg) {
|
|
this.SetNextState("ROAMING");
|
|
},
|
|
},
|
|
|
|
"FLEEING": {
|
|
"enter": function() {
|
|
this.SelectAnimation("walk", false, this.GetWalkSpeed());
|
|
},
|
|
|
|
"MoveCompleted": function() {
|
|
this.SetNextState("ROAMING");
|
|
},
|
|
},
|
|
},
|
|
|
|
};
|
|
|
|
var AnimalFsm = new FSM(AnimalFsmSpec);
|
|
|
|
AnimalAI.prototype.Init = function()
|
|
{
|
|
};
|
|
|
|
// FSM linkage functions:
|
|
|
|
AnimalAI.prototype.OnCreate = function()
|
|
{
|
|
var startingState = this.template.NaturalBehaviour;
|
|
startingState = startingState.toUpperCase(startingState);
|
|
|
|
if (startingState == "SKITTISH")
|
|
startingState = startingState + ".FEEDING";
|
|
else
|
|
startingState = "PASSIVE.FEEDING";
|
|
|
|
AnimalFsm.Init(this, startingState);
|
|
};
|
|
|
|
AnimalAI.prototype.SetNextState = function(state)
|
|
{
|
|
AnimalFsm.SetNextState(this, state);
|
|
};
|
|
|
|
AnimalAI.prototype.DeferMessage = function(msg)
|
|
{
|
|
AnimalFsm.DeferMessage(this, msg);
|
|
};
|
|
|
|
AnimalAI.prototype.OnMotionChanged = function(msg)
|
|
{
|
|
if (msg.starting && !msg.error)
|
|
{
|
|
AnimalFsm.ProcessMessage(this, {"type": "MoveStarted", "data": msg});
|
|
}
|
|
else if (!msg.starting || msg.error)
|
|
{
|
|
AnimalFsm.ProcessMessage(this, {"type": "MoveCompleted", "data": msg});
|
|
}
|
|
};
|
|
|
|
AnimalAI.prototype.OnAttacked = function(msg)
|
|
{
|
|
AnimalFsm.ProcessMessage(this, {"type": "Attacked", "data": msg});
|
|
};
|
|
|
|
AnimalAI.prototype.OnHealthChanged = function(msg)
|
|
{
|
|
AnimalFsm.ProcessMessage(this, {"type": "HealthChanged", "from": msg.from, "to": msg.to});
|
|
};
|
|
|
|
AnimalAI.prototype.TimerHandler = function(data, lateness)
|
|
{
|
|
AnimalFsm.ProcessMessage(this, {"type": "Timer", "data": data, "lateness": lateness});
|
|
};
|
|
|
|
AnimalAI.prototype.LeaveFoundation = function(target)
|
|
{
|
|
AnimalFsm.ProcessMessage(this, {"type": "LeaveFoundation", "target": target});
|
|
};
|
|
|
|
// Functions to be called by the FSM:
|
|
|
|
AnimalAI.prototype.GetWalkSpeed = function()
|
|
{
|
|
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
|
|
return cmpMotion.GetWalkSpeed();
|
|
};
|
|
|
|
AnimalAI.prototype.GetRunSpeed = function()
|
|
{
|
|
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
|
|
return cmpMotion.GetRunSpeed();
|
|
};
|
|
|
|
AnimalAI.prototype.PlaySound = function(name)
|
|
{
|
|
PlaySound(name, this.entity);
|
|
};
|
|
|
|
AnimalAI.prototype.SelectAnimation = function(name, once, speed, sound)
|
|
{
|
|
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
|
|
if (!cmpVisual)
|
|
return;
|
|
|
|
var soundgroup;
|
|
if (sound)
|
|
{
|
|
var cmpSound = Engine.QueryInterface(this.entity, IID_Sound);
|
|
if (cmpSound)
|
|
soundgroup = cmpSound.GetSoundGroup(sound);
|
|
}
|
|
|
|
// Set default values if unspecified
|
|
if (typeof once == "undefined")
|
|
once = false;
|
|
if (typeof speed == "undefined")
|
|
speed = 1.0;
|
|
if (typeof soundgroup == "undefined")
|
|
soundgroup = "";
|
|
|
|
cmpVisual.SelectAnimation(name, once, speed, soundgroup);
|
|
};
|
|
|
|
AnimalAI.prototype.MoveRandomly = function(distance)
|
|
{
|
|
// We want to walk in a random direction, but avoid getting stuck
|
|
// in obstacles or narrow spaces.
|
|
// So pick a circular range from approximately our current position,
|
|
// and move outwards to the nearest point on that circle, which will
|
|
// lead to us avoiding obstacles and moving towards free space.
|
|
|
|
// TODO: we probably ought to have a 'home' point, and drift towards
|
|
// that, so we don't spread out all across the whole map
|
|
|
|
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
|
|
if (!cmpPosition)
|
|
return;
|
|
|
|
if (!cmpPosition.IsInWorld())
|
|
return;
|
|
|
|
var pos = cmpPosition.GetPosition();
|
|
|
|
var jitter = 0.5;
|
|
|
|
// Randomly adjust the range's center a bit, so we tend to prefer
|
|
// moving in random directions (if there's nothing in the way)
|
|
var tx = pos.x + (2*Math.random()-1)*jitter;
|
|
var tz = pos.z + (2*Math.random()-1)*jitter;
|
|
|
|
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
|
|
cmpMotion.MoveToPointRange(tx, tz, distance, distance);
|
|
};
|
|
|
|
AnimalAI.prototype.MoveAwayFrom = function(ent, distance)
|
|
{
|
|
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
|
|
cmpMotion.MoveToTargetRange(ent, distance, distance);
|
|
};
|
|
|
|
AnimalAI.prototype.StopMoving = function()
|
|
{
|
|
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
|
|
cmpMotion.StopMoving();
|
|
};
|
|
|
|
AnimalAI.prototype.SetMoveSpeed = function(speed)
|
|
{
|
|
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
|
|
cmpMotion.SetSpeed(speed);
|
|
};
|
|
|
|
AnimalAI.prototype.StartTimer = function(interval, data)
|
|
{
|
|
if (this.timer)
|
|
error("Called StartTimer when there's already an active timer");
|
|
|
|
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
|
|
this.timer = cmpTimer.SetTimeout(this.entity, IID_AnimalAI, "TimerHandler", interval, data);
|
|
};
|
|
|
|
AnimalAI.prototype.StopTimer = function()
|
|
{
|
|
if (!this.timer)
|
|
return;
|
|
|
|
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
|
|
cmpTimer.CancelTimer(this.timer);
|
|
this.timer = undefined;
|
|
};
|
|
|
|
Engine.RegisterComponentType(IID_AnimalAI, "AnimalAI", AnimalAI);
|