forked from 0ad/0ad
# Improve animal AI, based on patch from Badmadblacksad.
Fixes #563. This was SVN commit r8995.
This commit is contained in:
parent
83d0492fca
commit
18b317bc19
@ -1,387 +0,0 @@
|
||||
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
|
||||
},
|
||||
|
||||
"LeaveFoundation": function(msg) {
|
||||
// We can't walk away from the foundation (since we're dead),
|
||||
// but we mustn't block its construction (since the builders would get stuck),
|
||||
// and we don't want to trick gatherers into trying to reach us when
|
||||
// we're stuck in the middle of a building, so just delete our corpse.
|
||||
Engine.DestroyEntity(this.entity);
|
||||
},
|
||||
},
|
||||
|
||||
"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);
|
@ -90,11 +90,7 @@ Foundation.prototype.Build = function(builderEnt, work)
|
||||
if (cmpUnitAI)
|
||||
cmpUnitAI.LeaveFoundation(this.entity);
|
||||
|
||||
var cmpAnimalAI = Engine.QueryInterface(ent, IID_AnimalAI);
|
||||
if (cmpAnimalAI)
|
||||
cmpAnimalAI.LeaveFoundation(this.entity);
|
||||
|
||||
// TODO: What if an obstruction has no UnitAI/AnimalAI?
|
||||
// TODO: What if an obstruction has no UnitAI?
|
||||
}
|
||||
|
||||
// TODO: maybe we should tell the builder to use a special
|
||||
|
@ -5,7 +5,38 @@ UnitAI.prototype.Schema =
|
||||
"<a:example/>" +
|
||||
"<element name='FormationController'>" +
|
||||
"<data type='boolean'/>" +
|
||||
"</element>";
|
||||
"</element>" +
|
||||
"<optional>" +
|
||||
"<interleave>" +
|
||||
"<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>"+
|
||||
"</interleave>" +
|
||||
"</optional>";
|
||||
|
||||
// Very basic stance support (currently just for test maps where we don't want
|
||||
// everyone killing each other immediately after loading)
|
||||
@ -18,6 +49,7 @@ var g_Stances = {
|
||||
},
|
||||
};
|
||||
|
||||
// See ../helpers/FSM.js for some documentation of this FSM specification syntax
|
||||
var UnitFsmSpec = {
|
||||
|
||||
// Default event handlers:
|
||||
@ -44,6 +76,9 @@ var UnitFsmSpec = {
|
||||
// ignore attacker
|
||||
},
|
||||
|
||||
"HealthChanged": function(msg) {
|
||||
// ignore
|
||||
},
|
||||
|
||||
// Formation handlers:
|
||||
|
||||
@ -53,6 +88,13 @@ var UnitFsmSpec = {
|
||||
|
||||
// Called when being told to walk as part of a formation
|
||||
"Order.FormationWalk": function(msg) {
|
||||
if (this.IsAnimal())
|
||||
{
|
||||
// TODO: let players move captured animals around
|
||||
this.FinishOrder();
|
||||
return;
|
||||
}
|
||||
|
||||
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
|
||||
cmpUnitMotion.MoveToFormationOffset(msg.data.target, msg.data.x, msg.data.z);
|
||||
|
||||
@ -71,11 +113,25 @@ var UnitFsmSpec = {
|
||||
// (these will switch the unit out of formation mode)
|
||||
|
||||
"Order.Walk": function(msg) {
|
||||
if (this.IsAnimal())
|
||||
{
|
||||
// TODO: let players move captured animals around
|
||||
this.FinishOrder();
|
||||
return;
|
||||
}
|
||||
|
||||
this.MoveToPoint(this.order.data.x, this.order.data.z);
|
||||
this.SetNextState("INDIVIDUAL.WALKING");
|
||||
},
|
||||
|
||||
"Order.WalkToTarget": function(msg) {
|
||||
if (this.IsAnimal())
|
||||
{
|
||||
// TODO: let players move captured animals around
|
||||
this.FinishOrder();
|
||||
return;
|
||||
}
|
||||
|
||||
var ok = this.MoveToTarget(this.order.data.target);
|
||||
if (ok)
|
||||
{
|
||||
@ -111,6 +167,9 @@ var UnitFsmSpec = {
|
||||
if (this.MoveToTargetRange(this.order.data.target, IID_Attack, this.attackType))
|
||||
{
|
||||
// We've started walking to the given point
|
||||
if (this.IsAnimal())
|
||||
this.SetNextState("ANIMAL.COMBAT.APPROACHING");
|
||||
else
|
||||
this.SetNextState("INDIVIDUAL.COMBAT.APPROACHING");
|
||||
}
|
||||
else
|
||||
@ -118,6 +177,9 @@ var UnitFsmSpec = {
|
||||
// We are already at the target, or can't move at all,
|
||||
// so try attacking it from here.
|
||||
// TODO: need better handling of the can't-reach-target case
|
||||
if (this.IsAnimal())
|
||||
this.SetNextState("ANIMAL.COMBAT.ATTACKING");
|
||||
else
|
||||
this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING");
|
||||
}
|
||||
},
|
||||
@ -299,6 +361,12 @@ var UnitFsmSpec = {
|
||||
// States for entities not part of a formation:
|
||||
"INDIVIDUAL": {
|
||||
|
||||
"enter": function() {
|
||||
// Sanity-checking
|
||||
if (this.IsAnimal())
|
||||
error("Animal got moved into INDIVIDUAL.* state");
|
||||
},
|
||||
|
||||
"Attacked": function(msg) {
|
||||
// Default behaviour: attack back at our attacker
|
||||
if (this.CanAttack(msg.data.attacker))
|
||||
@ -841,6 +909,178 @@ var UnitFsmSpec = {
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
"ANIMAL": {
|
||||
|
||||
"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");
|
||||
},
|
||||
|
||||
"Attacked": function(msg) {
|
||||
if (this.template.NaturalBehaviour == "skittish" ||
|
||||
this.template.NaturalBehaviour == "passive")
|
||||
{
|
||||
this.MoveToTargetRangeExplicit(msg.data.attacker, +this.template.FleeDistance, +this.template.FleeDistance);
|
||||
this.SetNextState("FLEEING");
|
||||
this.PlaySound("panic");
|
||||
}
|
||||
else if (this.template.NaturalBehaviour == "violent" ||
|
||||
this.template.NaturalBehaviour == "aggressive" ||
|
||||
this.template.NaturalBehaviour == "defensive")
|
||||
{
|
||||
if (this.CanAttack(msg.data.attacker))
|
||||
this.ReplaceOrder("Attack", { "target": msg.data.attacker });
|
||||
}
|
||||
},
|
||||
|
||||
"Order.LeaveFoundation": function(msg) {
|
||||
// Run away from the foundation
|
||||
this.MoveToTargetRangeExplicit(msg.data.target, +this.template.FleeDistance, +this.template.FleeDistance);
|
||||
this.SetNextState("FLEEING");
|
||||
this.PlaySound("panic");
|
||||
},
|
||||
|
||||
"IDLE": {
|
||||
// (We need an IDLE state so that FinishOrder works)
|
||||
|
||||
"enter": function() {
|
||||
// Start feeding immediately
|
||||
this.SetNextState("FEEDING");
|
||||
return true;
|
||||
},
|
||||
},
|
||||
|
||||
"CORPSE": {
|
||||
"enter": function() {
|
||||
this.StopMoving();
|
||||
},
|
||||
|
||||
// Ignore all orders that animals might otherwise respond to
|
||||
"Order.FormationWalk": function() { },
|
||||
"Order.Walk": function() { },
|
||||
"Order.WalkToTarget": function() { },
|
||||
"Order.Attack": function() { },
|
||||
|
||||
"Attacked": function(msg) {
|
||||
// Do nothing, because we're dead already
|
||||
},
|
||||
|
||||
"Order.LeaveFoundation": function(msg) {
|
||||
// We can't walk away from the foundation (since we're dead),
|
||||
// but we mustn't block its construction (since the builders would get stuck),
|
||||
// and we don't want to trick gatherers into trying to reach us when
|
||||
// we're stuck in the middle of a building, so just delete our corpse.
|
||||
Engine.DestroyEntity(this.entity);
|
||||
},
|
||||
},
|
||||
|
||||
"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();
|
||||
},
|
||||
|
||||
"LosRangeUpdate": function(msg) {
|
||||
if (this.template.NaturalBehaviour == "skittish")
|
||||
{
|
||||
if (msg.data.added.length > 0)
|
||||
{
|
||||
this.MoveToTargetRangeExplicit(msg.data.added[0], +this.template.FleeDistance, +this.template.FleeDistance);
|
||||
this.SetNextState("FLEEING");
|
||||
this.PlaySound("panic");
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Start attacking one of the newly-seen enemy (if any)
|
||||
else if (this.template.NaturalBehaviour == "violent" ||
|
||||
this.template.NaturalBehaviour == "aggressive")
|
||||
{
|
||||
this.AttackVisibleEntity(msg.data.added);
|
||||
}
|
||||
|
||||
// TODO: if two units enter our range together, we'll attack the
|
||||
// first and then the second won't trigger another LosRangeUpdate
|
||||
// so we won't notice it. Probably we should do something with
|
||||
// ResetActiveQuery in ROAMING.enter/FEEDING.enter in order to
|
||||
// find any units that are already in range.
|
||||
},
|
||||
|
||||
"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();
|
||||
},
|
||||
|
||||
"LosRangeUpdate": function(msg) {
|
||||
if (this.template.NaturalBehaviour == "skittish")
|
||||
{
|
||||
if (msg.data.added.length > 0)
|
||||
{
|
||||
this.MoveToTargetRangeExplicit(msg.data.added[0], +this.template.FleeDistance, +this.template.FleeDistance);
|
||||
this.SetNextState("FLEEING");
|
||||
this.PlaySound("panic");
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Start attacking one of the newly-seen enemy (if any)
|
||||
else if (this.template.NaturalBehaviour == "violent")
|
||||
{
|
||||
this.AttackVisibleEntity(msg.data.added);
|
||||
}
|
||||
},
|
||||
|
||||
"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");
|
||||
},
|
||||
},
|
||||
|
||||
"COMBAT": "INDIVIDUAL.COMBAT", // reuse the same combat behaviour for animals
|
||||
},
|
||||
};
|
||||
|
||||
var UnitFsm = new FSM(UnitFsmSpec);
|
||||
@ -860,6 +1100,11 @@ UnitAI.prototype.IsFormationController = function()
|
||||
return (this.template.FormationController == "true");
|
||||
};
|
||||
|
||||
UnitAI.prototype.IsAnimal = function()
|
||||
{
|
||||
return (this.template.NaturalBehaviour ? true : false);
|
||||
};
|
||||
|
||||
UnitAI.prototype.IsIdle = function()
|
||||
{
|
||||
return this.isIdle;
|
||||
@ -867,7 +1112,9 @@ UnitAI.prototype.IsIdle = function()
|
||||
|
||||
UnitAI.prototype.OnCreate = function()
|
||||
{
|
||||
if (this.IsFormationController())
|
||||
if (this.IsAnimal())
|
||||
UnitFsm.Init(this, "ANIMAL.FEEDING");
|
||||
else if (this.IsFormationController())
|
||||
UnitFsm.Init(this, "FORMATIONCONTROLLER.IDLE");
|
||||
else
|
||||
UnitFsm.Init(this, "INDIVIDUAL.IDLE");
|
||||
@ -1076,6 +1323,11 @@ UnitAI.prototype.OnAttacked = function(msg)
|
||||
UnitFsm.ProcessMessage(this, {"type": "Attacked", "data": msg});
|
||||
};
|
||||
|
||||
UnitAI.prototype.OnHealthChanged = function(msg)
|
||||
{
|
||||
UnitFsm.ProcessMessage(this, {"type": "HealthChanged", "from": msg.from, "to": msg.to});
|
||||
};
|
||||
|
||||
UnitAI.prototype.OnRangeUpdate = function(msg)
|
||||
{
|
||||
if (msg.tag == this.losRangeQuery)
|
||||
@ -1545,6 +1797,12 @@ UnitAI.prototype.CanGarrison = function(target)
|
||||
if (!cmpGarrisonHolder)
|
||||
return false;
|
||||
|
||||
// Don't let animals garrison for now
|
||||
// (If we want to support that, we'll need to change Order.Garrison so it
|
||||
// doesn't move the animal into an INVIDIDUAL.* state)
|
||||
if (this.IsAnimal())
|
||||
return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@ -1614,5 +1872,43 @@ UnitAI.prototype.CanRepair = function(target)
|
||||
return true;
|
||||
};
|
||||
|
||||
//// Animal specific functions ////
|
||||
|
||||
UnitAI.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);
|
||||
};
|
||||
|
||||
UnitAI.prototype.SetMoveSpeed = function(speed)
|
||||
{
|
||||
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
|
||||
cmpMotion.SetSpeed(speed);
|
||||
};
|
||||
|
||||
Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI);
|
||||
|
@ -1 +0,0 @@
|
||||
Engine.RegisterInterface("AnimalAI");
|
@ -70,6 +70,9 @@ var FsmSpec = {
|
||||
},
|
||||
},
|
||||
|
||||
// Define a new state which is an exact copy of another
|
||||
// state that is defined elsewhere in this FSM:
|
||||
"OTHERSUBSTATENAME": "STATENAME.SUBSTATENAME",
|
||||
}
|
||||
|
||||
}
|
||||
@ -143,6 +146,23 @@ function FSM(spec)
|
||||
|
||||
function process(fsm, node, path, handlers)
|
||||
{
|
||||
// Handle string references to nodes defined elsewhere in the FSM spec
|
||||
if (typeof node === "string")
|
||||
{
|
||||
var refpath = node.split(".");
|
||||
var refd = spec;
|
||||
for each (var p in refpath)
|
||||
{
|
||||
refd = refd[p];
|
||||
if (!refd)
|
||||
{
|
||||
error("FSM node "+path.join(".")+" referred to non-defined node "+node);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
node = refd;
|
||||
}
|
||||
|
||||
var state = {};
|
||||
fsm.states[path.join(".")] = state;
|
||||
|
||||
|
@ -112,6 +112,7 @@ function LoadPlayerSettings(settings)
|
||||
player.SetName(pDefs.Name);
|
||||
player.SetCiv(pDefs.Civ);
|
||||
player.SetColour(pDefs.Colour.r, pDefs.Colour.g, pDefs.Colour.b);
|
||||
player.SetDiplomacy(diplomacy);
|
||||
}
|
||||
|
||||
// Add player to player manager
|
||||
|
@ -22,14 +22,14 @@
|
||||
<Circle radius="0.75"/>
|
||||
<Height>1.5</Height>
|
||||
</Footprint>
|
||||
<AnimalAI>
|
||||
<UnitAI>
|
||||
<RoamDistance>4.0</RoamDistance>
|
||||
<FleeDistance>12.0</FleeDistance>
|
||||
<RoamTimeMin>2000</RoamTimeMin>
|
||||
<RoamTimeMax>8000</RoamTimeMax>
|
||||
<FeedTimeMin>10000</FeedTimeMin>
|
||||
<FeedTimeMax>40000</FeedTimeMax>
|
||||
</AnimalAI>
|
||||
</UnitAI>
|
||||
<UnitMotion>
|
||||
<WalkSpeed>1.0</WalkSpeed>
|
||||
<Run>
|
||||
|
@ -35,7 +35,7 @@
|
||||
<Altitude>-4.0</Altitude>
|
||||
<Floating>true</Floating>
|
||||
</Position>
|
||||
<AnimalAI>
|
||||
<UnitAI>
|
||||
<NaturalBehaviour>skittish</NaturalBehaviour>
|
||||
<RoamDistance>20.0</RoamDistance>
|
||||
<FleeDistance>40.0</FleeDistance>
|
||||
@ -43,7 +43,7 @@
|
||||
<RoamTimeMax>30000</RoamTimeMax>
|
||||
<FeedTimeMin>1000</FeedTimeMin>
|
||||
<FeedTimeMax>2000</FeedTimeMax>
|
||||
</AnimalAI>
|
||||
</UnitAI>
|
||||
<ResourceSupply>
|
||||
<KillBeforeGather>true</KillBeforeGather>
|
||||
<Amount>2000</Amount>
|
||||
|
@ -25,13 +25,12 @@
|
||||
<Speed>6.0</Speed>
|
||||
</Run>
|
||||
</UnitMotion>
|
||||
<UnitAI disable=""/>
|
||||
<AnimalAI>
|
||||
<UnitAI>
|
||||
<RoamDistance>8.0</RoamDistance>
|
||||
<FleeDistance>32.0</FleeDistance>
|
||||
<RoamTimeMin>2000</RoamTimeMin>
|
||||
<RoamTimeMax>8000</RoamTimeMax>
|
||||
<FeedTimeMin>15000</FeedTimeMin>
|
||||
<FeedTimeMax>60000</FeedTimeMax>
|
||||
</AnimalAI>
|
||||
</UnitAI>
|
||||
</Entity>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<Entity parent="template_unit_fauna_breed">
|
||||
<Identity>
|
||||
</Identity>
|
||||
<AnimalAI>
|
||||
<UnitAI>
|
||||
<NaturalBehaviour>passive</NaturalBehaviour>
|
||||
</AnimalAI>
|
||||
</UnitAI>
|
||||
</Entity>
|
||||
|
@ -18,7 +18,7 @@
|
||||
<Floating>true</Floating>
|
||||
</Position>
|
||||
<UnitMotion disable=""/>
|
||||
<AnimalAI disable=""/>
|
||||
<UnitAI disable=""/>
|
||||
<ResourceSupply>
|
||||
<KillBeforeGather>false</KillBeforeGather>
|
||||
<Amount>1000</Amount>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<Entity parent="template_unit_fauna_herd">
|
||||
<Identity>
|
||||
</Identity>
|
||||
<AnimalAI>
|
||||
<UnitAI>
|
||||
<NaturalBehaviour>passive</NaturalBehaviour>
|
||||
</AnimalAI>
|
||||
</UnitAI>
|
||||
</Entity>
|
||||
|
@ -2,7 +2,16 @@
|
||||
<Entity parent="template_unit_fauna_hunt">
|
||||
<Identity>
|
||||
</Identity>
|
||||
<AnimalAI>
|
||||
<UnitAI>
|
||||
<NaturalBehaviour>aggressive</NaturalBehaviour>
|
||||
</AnimalAI>
|
||||
</UnitAI>
|
||||
<Attack>
|
||||
<Melee>
|
||||
<Hack>1.0</Hack>
|
||||
<Pierce>1.0</Pierce>
|
||||
<Crush>0.0</Crush>
|
||||
<MaxRange>4.0</MaxRange>
|
||||
<RepeatTime>1000</RepeatTime>
|
||||
</Melee>
|
||||
</Attack>
|
||||
</Entity>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<Entity parent="template_unit_fauna_hunt">
|
||||
<Identity>
|
||||
</Identity>
|
||||
<AnimalAI>
|
||||
<UnitAI>
|
||||
<NaturalBehaviour>defensive</NaturalBehaviour>
|
||||
</AnimalAI>
|
||||
</UnitAI>
|
||||
</Entity>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<Entity parent="template_unit_fauna_hunt">
|
||||
<Identity>
|
||||
</Identity>
|
||||
<AnimalAI>
|
||||
<UnitAI>
|
||||
<NaturalBehaviour>passive</NaturalBehaviour>
|
||||
</AnimalAI>
|
||||
</UnitAI>
|
||||
</Entity>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<Entity parent="template_unit_fauna_hunt">
|
||||
<Identity>
|
||||
</Identity>
|
||||
<AnimalAI>
|
||||
<UnitAI>
|
||||
<NaturalBehaviour>skittish</NaturalBehaviour>
|
||||
</AnimalAI>
|
||||
</UnitAI>
|
||||
</Entity>
|
||||
|
@ -2,7 +2,16 @@
|
||||
<Entity parent="template_unit_fauna_hunt">
|
||||
<Identity>
|
||||
</Identity>
|
||||
<AnimalAI>
|
||||
<UnitAI>
|
||||
<NaturalBehaviour>violent</NaturalBehaviour>
|
||||
</AnimalAI>
|
||||
</UnitAI>
|
||||
<Attack>
|
||||
<Melee>
|
||||
<Hack>1.0</Hack>
|
||||
<Pierce>1.0</Pierce>
|
||||
<Crush>0.0</Crush>
|
||||
<MaxRange>4.0</MaxRange>
|
||||
<RepeatTime>1000</RepeatTime>
|
||||
</Melee>
|
||||
</Attack>
|
||||
</Entity>
|
||||
|
@ -2,7 +2,16 @@
|
||||
<Entity parent="template_unit_fauna_wild">
|
||||
<Identity>
|
||||
</Identity>
|
||||
<AnimalAI>
|
||||
<NaturalBehaviour>violent</NaturalBehaviour>
|
||||
</AnimalAI>
|
||||
<UnitAI>
|
||||
<NaturalBehaviour>aggressive</NaturalBehaviour>
|
||||
</UnitAI>
|
||||
<Attack>
|
||||
<Melee>
|
||||
<Hack>1.0</Hack>
|
||||
<Pierce>1.0</Pierce>
|
||||
<Crush>0.0</Crush>
|
||||
<MaxRange>4.0</MaxRange>
|
||||
<RepeatTime>1000</RepeatTime>
|
||||
</Melee>
|
||||
</Attack>
|
||||
</Entity>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<Entity parent="template_unit_fauna_wild">
|
||||
<Identity>
|
||||
</Identity>
|
||||
<AnimalAI>
|
||||
<UnitAI>
|
||||
<NaturalBehaviour>defensive</NaturalBehaviour>
|
||||
</AnimalAI>
|
||||
</UnitAI>
|
||||
</Entity>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<Entity parent="template_unit_fauna_wild">
|
||||
<Identity>
|
||||
</Identity>
|
||||
<AnimalAI>
|
||||
<UnitAI>
|
||||
<NaturalBehaviour>passive</NaturalBehaviour>
|
||||
</AnimalAI>
|
||||
</UnitAI>
|
||||
</Entity>
|
||||
|
@ -2,7 +2,16 @@
|
||||
<Entity parent="template_unit_fauna_wild">
|
||||
<Identity>
|
||||
</Identity>
|
||||
<AnimalAI>
|
||||
<UnitAI>
|
||||
<NaturalBehaviour>violent</NaturalBehaviour>
|
||||
</AnimalAI>
|
||||
</UnitAI>
|
||||
<Attack>
|
||||
<Melee>
|
||||
<Hack>1.0</Hack>
|
||||
<Pierce>1.0</Pierce>
|
||||
<Crush>0.0</Crush>
|
||||
<MaxRange>4.0</MaxRange>
|
||||
<RepeatTime>1000</RepeatTime>
|
||||
</Melee>
|
||||
</Attack>
|
||||
</Entity>
|
||||
|
Loading…
Reference in New Issue
Block a user