2010-02-05 23:00:39 +01:00
|
|
|
function UnitAI() {}
|
|
|
|
|
2010-04-23 18:09:03 +02:00
|
|
|
UnitAI.prototype.Schema =
|
|
|
|
"<a:help>Controls the unit's movement, attacks, etc, in response to commands from the player.</a:help>" +
|
|
|
|
"<a:example/>" +
|
2010-09-03 11:55:14 +02:00
|
|
|
"<element name='FormationController'>" +
|
|
|
|
"<data type='boolean'/>" +
|
|
|
|
"</element>";
|
2010-04-23 18:09:03 +02:00
|
|
|
|
2010-08-04 23:15:41 +02:00
|
|
|
// Very basic stance support (currently just for test maps where we don't want
|
|
|
|
// everyone killing each other immediately after loading)
|
|
|
|
var g_Stances = {
|
|
|
|
"aggressive": {
|
|
|
|
attackOnSight: true,
|
|
|
|
},
|
|
|
|
"holdfire": {
|
|
|
|
attackOnSight: false,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
var UnitFsmSpec = {
|
|
|
|
|
2010-09-03 11:55:14 +02:00
|
|
|
// Default event handlers:
|
|
|
|
|
|
|
|
"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
|
|
|
|
},
|
|
|
|
|
|
|
|
"ConstructionFinished": function(msg) {
|
|
|
|
// ignore uninteresting construction messages
|
|
|
|
},
|
|
|
|
|
|
|
|
"LosRangeUpdate": function(msg) {
|
|
|
|
// ignore newly-seen units by default
|
|
|
|
},
|
|
|
|
|
|
|
|
"Attacked": function(msg) {
|
|
|
|
// ignore attacker
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// Formation handlers:
|
|
|
|
|
|
|
|
"FormationLeave": function(msg) {
|
|
|
|
// ignore when we're not in FORMATIONMEMBER
|
|
|
|
},
|
|
|
|
|
2010-10-03 19:58:49 +02:00
|
|
|
// Called when being told to walk as part of a formation
|
2010-09-03 11:55:14 +02:00
|
|
|
"Order.FormationWalk": function(msg) {
|
|
|
|
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
|
|
|
|
cmpUnitMotion.MoveToFormationOffset(msg.data.target, msg.data.x, msg.data.z);
|
|
|
|
|
|
|
|
this.SetNextState("FORMATIONMEMBER.WALKING");
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// Individual orders:
|
|
|
|
// (these will switch the unit out of formation mode)
|
|
|
|
|
|
|
|
"Order.Walk": function(msg) {
|
|
|
|
this.MoveToPoint(this.order.data.x, this.order.data.z);
|
|
|
|
this.SetNextState("INDIVIDUAL.WALKING");
|
|
|
|
},
|
|
|
|
|
|
|
|
"Order.WalkToTarget": function(msg) {
|
|
|
|
var ok = this.MoveToTarget(this.order.data.target);
|
|
|
|
if (ok)
|
|
|
|
{
|
|
|
|
// We've started walking to the given point
|
|
|
|
this.SetNextState("INDIVIDUAL.WALKING");
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// We are already at the target, or can't move at all
|
|
|
|
this.FinishOrder();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
"Order.Attack": function(msg) {
|
2010-10-06 23:37:55 +02:00
|
|
|
// Check the target is alive
|
|
|
|
if (!this.TargetIsAlive(this.order.data.target))
|
|
|
|
{
|
|
|
|
this.FinishOrder();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2010-09-03 11:55:14 +02:00
|
|
|
// Work out how to attack the given target
|
|
|
|
var type = this.GetBestAttack();
|
|
|
|
if (!type)
|
|
|
|
{
|
|
|
|
// Oops, we can't attack at all
|
|
|
|
this.FinishOrder();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.attackType = type;
|
|
|
|
|
|
|
|
// Try to move within attack range
|
|
|
|
if (this.MoveToTargetRange(this.order.data.target, IID_Attack, this.attackType))
|
|
|
|
{
|
|
|
|
// We've started walking to the given point
|
|
|
|
this.SetNextState("INDIVIDUAL.COMBAT.APPROACHING");
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// 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
|
|
|
|
this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING");
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
"Order.Gather": function(msg) {
|
2010-10-06 23:37:55 +02:00
|
|
|
// If the target is still alive, we need to kill it first
|
2010-10-13 17:14:15 +02:00
|
|
|
if (this.MustKillGatherTarget(this.order.data.target))
|
2010-10-06 23:37:55 +02:00
|
|
|
{
|
|
|
|
// Make sure we can attack the target, else we'll get very stuck
|
|
|
|
if (!this.GetBestAttack())
|
|
|
|
{
|
|
|
|
// Oops, we can't attack at all - give up
|
|
|
|
// TODO: should do something so the player knows why this failed
|
|
|
|
this.FinishOrder();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.PushOrderFront("Attack", { "target": this.order.data.target });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2010-09-03 11:55:14 +02:00
|
|
|
// Try to move within range
|
|
|
|
if (this.MoveToTargetRange(this.order.data.target, IID_ResourceGatherer))
|
|
|
|
{
|
|
|
|
// We've started walking to the given point
|
|
|
|
this.SetNextState("INDIVIDUAL.GATHER.APPROACHING");
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// We are already at the target, or can't move at all,
|
|
|
|
// so try gathering it from here.
|
|
|
|
// TODO: need better handling of the can't-reach-target case
|
|
|
|
this.SetNextState("INDIVIDUAL.GATHER.GATHERING");
|
|
|
|
}
|
|
|
|
},
|
2010-11-13 20:15:29 +01:00
|
|
|
|
|
|
|
"Order.ReturnResource": function(msg) {
|
|
|
|
// Try to move to the dropsite
|
|
|
|
if (this.MoveToTarget(this.order.data.target))
|
|
|
|
{
|
|
|
|
// We've started walking to the target
|
|
|
|
this.SetNextState("INDIVIDUAL.RETURNRESOURCE.APPROACHING");
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// Oops, we can't reach the dropsite.
|
|
|
|
// Maybe we should try to pick another dropsite, to find an
|
|
|
|
// accessible one?
|
|
|
|
// For now, just give up.
|
|
|
|
this.FinishOrder();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
},
|
2010-09-03 11:55:14 +02:00
|
|
|
|
|
|
|
"Order.Repair": function(msg) {
|
|
|
|
// Try to move within range
|
|
|
|
if (this.MoveToTargetRange(this.order.data.target, IID_Builder))
|
|
|
|
{
|
|
|
|
// We've started walking to the given point
|
|
|
|
this.SetNextState("INDIVIDUAL.REPAIR.APPROACHING");
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// We are already at the target, or can't move at all,
|
|
|
|
// so try repairing it from here.
|
|
|
|
// TODO: need better handling of the can't-reach-target case
|
|
|
|
this.SetNextState("INDIVIDUAL.REPAIR.REPAIRING");
|
|
|
|
}
|
|
|
|
},
|
2010-10-24 00:43:15 +02:00
|
|
|
|
|
|
|
"Order.Garrison": function(msg) {
|
|
|
|
if (this.MoveToTarget(this.order.data.target))
|
|
|
|
{
|
|
|
|
this.SetNextState("INDIVIDUAL.GARRISON.APPROACHING");
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
this.SetNextState("INDIVIDUAL.GARRISON.GARRISONED");
|
|
|
|
}
|
|
|
|
},
|
2010-09-03 11:55:14 +02:00
|
|
|
|
|
|
|
// States for the special entity representing a group of units moving in formation:
|
|
|
|
"FORMATIONCONTROLLER": {
|
|
|
|
|
|
|
|
"Order.Walk": function(msg) {
|
|
|
|
this.MoveToPoint(this.order.data.x, this.order.data.z);
|
|
|
|
this.SetNextState("WALKING");
|
|
|
|
},
|
|
|
|
|
|
|
|
"Order.Attack": function(msg) {
|
|
|
|
// TODO: we should move in formation towards the target,
|
|
|
|
// then break up into individuals when close enough to it
|
|
|
|
|
|
|
|
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
|
2010-09-03 22:04:11 +02:00
|
|
|
cmpFormation.CallMemberFunction("Attack", [msg.data.target, false]);
|
2010-09-03 11:55:14 +02:00
|
|
|
|
|
|
|
// TODO: we should wait until the target is killed, then
|
|
|
|
// move on to the next queued order.
|
|
|
|
// Don't bother now, just disband the formation immediately.
|
|
|
|
cmpFormation.Disband();
|
|
|
|
},
|
|
|
|
|
|
|
|
"Order.Repair": function(msg) {
|
|
|
|
// TODO: see notes in Order.Attack
|
|
|
|
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
|
2010-09-03 22:04:11 +02:00
|
|
|
cmpFormation.CallMemberFunction("Repair", [msg.data.target, false]);
|
2010-09-03 11:55:14 +02:00
|
|
|
cmpFormation.Disband();
|
|
|
|
},
|
|
|
|
|
|
|
|
"Order.Gather": function(msg) {
|
|
|
|
// TODO: see notes in Order.Attack
|
|
|
|
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
|
2010-09-03 22:04:11 +02:00
|
|
|
cmpFormation.CallMemberFunction("Gather", [msg.data.target, false]);
|
2010-09-03 11:55:14 +02:00
|
|
|
cmpFormation.Disband();
|
|
|
|
},
|
|
|
|
|
2010-11-13 20:15:29 +01:00
|
|
|
"Order.ReturnResource": function(msg) {
|
|
|
|
// TODO: see notes in Order.Attack
|
|
|
|
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
|
|
|
|
cmpFormation.CallMemberFunction("ReturnResource", [msg.data.target, false]);
|
|
|
|
cmpFormation.Disband();
|
|
|
|
},
|
|
|
|
|
2010-09-03 11:55:14 +02:00
|
|
|
"IDLE": {
|
|
|
|
"enter": function() {
|
|
|
|
this.SelectAnimation("idle");
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
"WALKING": {
|
|
|
|
"MoveStarted": function(msg) {
|
|
|
|
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
|
2010-10-15 21:25:17 +02:00
|
|
|
cmpFormation.MoveMembersIntoFormation(true);
|
2010-09-03 11:55:14 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
"MoveCompleted": function(msg) {
|
|
|
|
if (this.FinishOrder())
|
|
|
|
return;
|
2010-07-21 18:09:58 +02:00
|
|
|
|
2010-09-03 11:55:14 +02:00
|
|
|
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
|
|
|
|
cmpFormation.Disband();
|
|
|
|
},
|
2010-07-21 18:09:58 +02:00
|
|
|
},
|
2010-09-03 11:55:14 +02:00
|
|
|
},
|
2010-07-21 18:09:58 +02:00
|
|
|
|
2010-09-03 11:55:14 +02:00
|
|
|
|
|
|
|
// States for entities moving as part of a formation:
|
|
|
|
"FORMATIONMEMBER": {
|
|
|
|
|
|
|
|
"FormationLeave": function(msg) {
|
|
|
|
this.SetNextState("INDIVIDUAL.IDLE");
|
2010-07-21 18:09:58 +02:00
|
|
|
},
|
|
|
|
|
2010-09-03 11:55:14 +02:00
|
|
|
"IDLE": {
|
|
|
|
"enter": function() {
|
|
|
|
this.SelectAnimation("idle");
|
|
|
|
},
|
2010-07-29 22:39:23 +02:00
|
|
|
},
|
|
|
|
|
2010-09-03 11:55:14 +02:00
|
|
|
"WALKING": {
|
|
|
|
"enter": function () {
|
|
|
|
this.SelectAnimation("move");
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// States for entities not part of a formation:
|
|
|
|
"INDIVIDUAL": {
|
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
"Attacked": function(msg) {
|
|
|
|
// Default behaviour: attack back at our attacker
|
|
|
|
if (this.CanAttack(msg.data.attacker))
|
|
|
|
{
|
|
|
|
this.PushOrderFront("Attack", { "target": msg.data.attacker });
|
|
|
|
}
|
2010-11-13 03:36:53 +01:00
|
|
|
else
|
|
|
|
{ // TODO: If unit can't attack, run away
|
|
|
|
}
|
2010-07-21 18:09:58 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
"IDLE": {
|
|
|
|
"enter": function() {
|
2010-07-29 22:39:23 +02:00
|
|
|
// If we entered the idle state we must have nothing better to do,
|
|
|
|
// so immediately check whether there's anybody nearby to attack.
|
|
|
|
// (If anyone approaches later, it'll be handled via LosRangeUpdate.)
|
|
|
|
if (this.losRangeQuery)
|
|
|
|
{
|
|
|
|
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
|
|
|
var ents = rangeMan.ResetActiveQuery(this.losRangeQuery);
|
2010-08-04 23:15:41 +02:00
|
|
|
if (this.GetStance().attackOnSight && this.AttackVisibleEntity(ents))
|
2010-07-29 22:39:23 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Nobody to attack - switch to idle
|
2010-07-21 18:09:58 +02:00
|
|
|
this.SelectAnimation("idle");
|
2010-07-29 22:39:23 +02:00
|
|
|
return false;
|
2010-07-21 18:09:58 +02:00
|
|
|
},
|
|
|
|
|
2010-07-29 22:39:23 +02:00
|
|
|
"leave": function() {
|
|
|
|
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
|
|
|
rangeMan.DisableActiveQuery(this.losRangeQuery);
|
|
|
|
},
|
|
|
|
|
|
|
|
"LosRangeUpdate": function(msg) {
|
2010-08-04 23:15:41 +02:00
|
|
|
if (this.GetStance().attackOnSight)
|
|
|
|
{
|
|
|
|
// Start attacking one of the newly-seen enemy (if any)
|
|
|
|
this.AttackVisibleEntity(msg.data.added);
|
|
|
|
}
|
2010-07-29 22:39:23 +02:00
|
|
|
},
|
|
|
|
},
|
2010-07-21 18:09:58 +02:00
|
|
|
|
|
|
|
"WALKING": {
|
2010-09-03 11:55:14 +02:00
|
|
|
"enter": function () {
|
|
|
|
this.SelectAnimation("move");
|
2010-07-21 18:09:58 +02:00
|
|
|
},
|
|
|
|
|
2010-09-03 11:55:14 +02:00
|
|
|
"MoveCompleted": function() {
|
2010-07-21 18:09:58 +02:00
|
|
|
this.FinishOrder();
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
"COMBAT": {
|
|
|
|
"Attacked": function(msg) {
|
|
|
|
// If we're already in combat mode, ignore anyone else
|
|
|
|
// who's attacking us
|
|
|
|
},
|
|
|
|
|
|
|
|
"APPROACHING": {
|
2010-09-03 11:55:14 +02:00
|
|
|
"enter": function () {
|
|
|
|
this.SelectAnimation("move");
|
2010-07-21 18:09:58 +02:00
|
|
|
},
|
|
|
|
|
2010-09-03 11:55:14 +02:00
|
|
|
"MoveCompleted": function() {
|
2010-07-21 18:09:58 +02:00
|
|
|
this.SetNextState("ATTACKING");
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
"ATTACKING": {
|
|
|
|
"enter": function() {
|
|
|
|
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
|
|
|
|
this.attackTimers = cmpAttack.GetTimers(this.attackType);
|
|
|
|
|
|
|
|
this.SelectAnimation("melee", false, 1.0, "attack");
|
|
|
|
this.SetAnimationSync(this.attackTimers.prepare, this.attackTimers.repeat);
|
|
|
|
this.StartTimer(this.attackTimers.prepare, this.attackTimers.repeat);
|
|
|
|
// TODO: we should probably only bother syncing projectile attacks, not melee
|
|
|
|
|
|
|
|
// TODO: if .prepare is short, players can cheat by cycling attack/stop/attack
|
|
|
|
// to beat the .repeat time; should enforce a minimum time
|
|
|
|
},
|
|
|
|
|
|
|
|
"leave": function() {
|
|
|
|
this.StopTimer();
|
|
|
|
},
|
|
|
|
|
|
|
|
"Timer": function(msg) {
|
2010-10-06 23:37:55 +02:00
|
|
|
// Check the target is still alive
|
|
|
|
if (this.TargetIsAlive(this.order.data.target))
|
2010-07-21 18:09:58 +02:00
|
|
|
{
|
2010-10-06 23:37:55 +02:00
|
|
|
// Check we can still reach the target
|
|
|
|
if (this.CheckTargetRange(this.order.data.target, IID_Attack, this.attackType))
|
2010-07-21 18:09:58 +02:00
|
|
|
{
|
2010-10-06 23:37:55 +02:00
|
|
|
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
|
|
|
|
cmpAttack.PerformAttack(this.attackType, this.order.data.target);
|
|
|
|
return;
|
2010-07-21 18:09:58 +02:00
|
|
|
}
|
2010-10-06 23:37:55 +02:00
|
|
|
|
|
|
|
// Can't reach it - try to chase after it
|
|
|
|
if (this.MoveToTargetRange(this.order.data.target, IID_Attack, this.attackType))
|
2010-07-21 18:09:58 +02:00
|
|
|
{
|
2010-10-06 23:37:55 +02:00
|
|
|
this.SetNextState("COMBAT.CHASING");
|
|
|
|
return;
|
2010-07-21 18:09:58 +02:00
|
|
|
}
|
|
|
|
}
|
2010-10-06 23:37:55 +02:00
|
|
|
|
|
|
|
// Can't reach it, or it doesn't exist any more - give up
|
|
|
|
this.FinishOrder();
|
|
|
|
|
|
|
|
// TODO: see if we can switch to a new nearby enemy
|
2010-07-21 18:09:58 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
// TODO: respond to target deaths immediately, rather than waiting
|
|
|
|
// until the next Timer event
|
|
|
|
},
|
|
|
|
|
|
|
|
"CHASING": {
|
2010-09-03 11:55:14 +02:00
|
|
|
"enter": function () {
|
|
|
|
this.SelectAnimation("move");
|
2010-07-21 18:09:58 +02:00
|
|
|
},
|
|
|
|
|
2010-09-03 11:55:14 +02:00
|
|
|
"MoveCompleted": function() {
|
2010-07-21 18:09:58 +02:00
|
|
|
this.SetNextState("ATTACKING");
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
"GATHER": {
|
|
|
|
"APPROACHING": {
|
2010-09-03 11:55:14 +02:00
|
|
|
"enter": function () {
|
|
|
|
this.SelectAnimation("move");
|
2010-07-21 18:09:58 +02:00
|
|
|
},
|
2010-09-03 11:55:14 +02:00
|
|
|
|
|
|
|
"MoveCompleted": function() {
|
2010-07-21 18:09:58 +02:00
|
|
|
this.SetNextState("GATHERING");
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
"GATHERING": {
|
|
|
|
"enter": function() {
|
2010-07-31 23:21:42 +02:00
|
|
|
var typename = "gather_" + this.order.data.type.specific;
|
2010-07-21 18:09:58 +02:00
|
|
|
this.SelectAnimation(typename, false, 1.0, typename);
|
|
|
|
this.StartTimer(1000, 1000);
|
|
|
|
},
|
|
|
|
|
|
|
|
"leave": function() {
|
|
|
|
this.StopTimer();
|
|
|
|
},
|
|
|
|
|
|
|
|
"Timer": function(msg) {
|
|
|
|
// Check we can still reach the target
|
|
|
|
if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer))
|
|
|
|
{
|
2010-11-13 20:15:29 +01:00
|
|
|
// Gather the resources:
|
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
|
2010-11-13 20:15:29 +01:00
|
|
|
|
2010-11-20 22:37:31 +01:00
|
|
|
// Try to gather treasure
|
|
|
|
if (cmpResourceGatherer.TryInstantGather(this.order.data.target))
|
|
|
|
return;
|
|
|
|
|
2010-11-13 20:15:29 +01:00
|
|
|
// If we've already got some resources but they're the wrong type,
|
|
|
|
// drop them first to ensure we're only ever carrying one type
|
|
|
|
if (cmpResourceGatherer.IsCarryingAnythingExcept(this.order.data.type.generic))
|
|
|
|
cmpResourceGatherer.DropResources();
|
|
|
|
|
|
|
|
// Collect from the target
|
2010-07-21 18:09:58 +02:00
|
|
|
var status = cmpResourceGatherer.PerformGather(this.order.data.target);
|
2010-11-13 20:15:29 +01:00
|
|
|
|
|
|
|
// TODO: if exhausted, we should probably stop immediately
|
|
|
|
// and choose a new target
|
|
|
|
|
|
|
|
// If we've collected as many resources as possible,
|
|
|
|
// return to the nearest dropsite
|
|
|
|
if (status.filled)
|
|
|
|
{
|
|
|
|
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
|
|
|
|
|
|
|
// Find dropsites owned by this unit's player
|
|
|
|
var players = [];
|
|
|
|
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
|
|
|
|
if (cmpOwnership)
|
|
|
|
players.push(cmpOwnership.GetOwner());
|
|
|
|
|
2010-11-16 21:43:15 +01:00
|
|
|
var dropsites = cmpRangeManager.ExecuteQuery(this.entity, 0, -1, players, IID_ResourceDropsite);
|
2010-11-13 20:15:29 +01:00
|
|
|
|
|
|
|
// Try to find the first (nearest) dropsite which supports this resource type
|
|
|
|
for each (var dropsite in dropsites)
|
|
|
|
{
|
|
|
|
var cmpDropsite = Engine.QueryInterface(dropsite, IID_ResourceDropsite);
|
|
|
|
if (!cmpDropsite.AcceptsType(this.order.data.type.generic))
|
|
|
|
continue;
|
|
|
|
|
|
|
|
// This dropsite is okay - return our resources to it
|
|
|
|
this.PushOrderFront("ReturnResource", { "target": dropsite });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Oh no, couldn't find any drop sites.
|
|
|
|
// Give up and stand here like a lemon.
|
|
|
|
}
|
2010-07-21 18:09:58 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// Try to follow it
|
|
|
|
if (this.MoveToTargetRange(this.order.data.target, IID_ResourceGatherer))
|
|
|
|
{
|
|
|
|
this.SetNextState("APPROACHING");
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2010-07-31 23:21:42 +02:00
|
|
|
// Save the current order's type in case we need it later
|
|
|
|
var oldType = this.order.data.type;
|
|
|
|
|
|
|
|
// Can't reach it, or it doesn't exist any more - give up on this order
|
|
|
|
if (this.FinishOrder())
|
|
|
|
return;
|
|
|
|
|
|
|
|
// No remaining orders - pick a useful default behaviour
|
|
|
|
|
|
|
|
// Try to find a nearby target of the same type
|
|
|
|
|
2010-08-11 12:53:53 +02:00
|
|
|
var range = 64; // TODO: what's a sensible number?
|
|
|
|
|
|
|
|
// Accept any resources owned by Gaia
|
|
|
|
var players = [0];
|
|
|
|
// Also accept resources owned by this unit's player:
|
|
|
|
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
|
|
|
|
if (cmpOwnership)
|
|
|
|
players.push(cmpOwnership.GetOwner());
|
|
|
|
|
2010-07-31 23:21:42 +02:00
|
|
|
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
2010-11-16 21:43:15 +01:00
|
|
|
var nearby = rangeMan.ExecuteQuery(this.entity, 0, range, players, IID_ResourceSupply);
|
2010-07-31 23:21:42 +02:00
|
|
|
for each (var ent in nearby)
|
|
|
|
{
|
|
|
|
var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
|
|
|
|
var type = cmpResourceSupply.GetType();
|
|
|
|
if (type.specific == oldType.specific)
|
|
|
|
{
|
|
|
|
this.Gather(ent, true);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Nothing else to gather - just give up
|
2010-07-21 18:09:58 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
2010-11-13 20:15:29 +01:00
|
|
|
// Returning to dropsite
|
|
|
|
"RETURNRESOURCE": {
|
|
|
|
"APPROACHING": {
|
|
|
|
"enter": function () {
|
|
|
|
// Work out what we're carrying, in order to select an appropriate animation
|
|
|
|
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
|
2010-11-13 22:39:39 +01:00
|
|
|
var type = cmpResourceGatherer.GetLastCarriedType();
|
|
|
|
var typename = "carry_" + type.generic;
|
|
|
|
|
|
|
|
// Special case for meat
|
|
|
|
if (type.specific == "meat")
|
|
|
|
typename = "carry_" + type.specific;
|
|
|
|
|
2010-11-13 20:15:29 +01:00
|
|
|
this.SelectAnimation(typename, false, this.GetWalkSpeed());
|
|
|
|
},
|
|
|
|
|
|
|
|
"MoveCompleted": function() {
|
|
|
|
var cmpResourceDropsite = Engine.QueryInterface(this.order.data.target, IID_ResourceDropsite);
|
|
|
|
if (cmpResourceDropsite)
|
|
|
|
{
|
|
|
|
// TODO: check the dropsite really is in range
|
|
|
|
// (we didn't get stopped before reaching it)
|
|
|
|
|
|
|
|
var dropsiteTypes = cmpResourceDropsite.GetTypes();
|
|
|
|
|
|
|
|
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
|
|
|
|
cmpResourceGatherer.CommitResources(dropsiteTypes);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// The dropsite was destroyed or something.
|
|
|
|
// TODO: We ought to look for a new one, probably.
|
|
|
|
}
|
|
|
|
|
|
|
|
this.FinishOrder();
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
"REPAIR": {
|
|
|
|
"APPROACHING": {
|
2010-09-03 11:55:14 +02:00
|
|
|
"enter": function () {
|
|
|
|
this.SelectAnimation("move");
|
2010-07-21 18:09:58 +02:00
|
|
|
},
|
|
|
|
|
2010-09-03 11:55:14 +02:00
|
|
|
"MoveCompleted": function() {
|
2010-07-21 18:09:58 +02:00
|
|
|
this.SetNextState("REPAIRING");
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
"REPAIRING": {
|
|
|
|
"enter": function() {
|
|
|
|
this.SelectAnimation("build", false, 1.0, "build");
|
|
|
|
this.StartTimer(1000, 1000);
|
|
|
|
},
|
|
|
|
|
|
|
|
"leave": function() {
|
|
|
|
this.StopTimer();
|
|
|
|
},
|
|
|
|
|
|
|
|
"Timer": function(msg) {
|
|
|
|
var target = this.order.data.target;
|
|
|
|
// Check we can still reach the target
|
|
|
|
if (!this.CheckTargetRange(target, IID_Builder))
|
|
|
|
{
|
|
|
|
// Can't reach it, or it doesn't exist any more
|
|
|
|
this.FinishOrder();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
|
2010-08-21 23:53:51 +02:00
|
|
|
cmpBuilder.PerformBuilding(target);
|
2010-07-21 18:09:58 +02:00
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
"ConstructionFinished": function(msg) {
|
|
|
|
if (msg.data.entity != this.order.data.target)
|
|
|
|
return; // ignore other buildings
|
|
|
|
|
|
|
|
// We finished building it.
|
|
|
|
// Switch to the next order (if any)
|
|
|
|
if (this.FinishOrder())
|
|
|
|
return;
|
|
|
|
|
|
|
|
// No remaining orders - pick a useful default behaviour
|
|
|
|
|
|
|
|
// If this building was e.g. a farm, we should start gathering from it
|
|
|
|
// if we are capable of doing so
|
|
|
|
if (this.CanGather(msg.data.newentity))
|
|
|
|
{
|
2010-07-31 23:21:42 +02:00
|
|
|
this.Gather(msg.data.newentity, true);
|
2010-07-21 18:09:58 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// TODO: look for a nearby foundation to help with
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
2010-10-24 00:43:15 +02:00
|
|
|
|
|
|
|
"GARRISON": {
|
|
|
|
"APPROACHING": {
|
|
|
|
"enter": function() {
|
|
|
|
this.SelectAnimation("walk", false, this.GetWalkSpeed());
|
|
|
|
this.PlaySound("walk");
|
|
|
|
},
|
|
|
|
|
|
|
|
"MoveCompleted": function() {
|
|
|
|
this.SetNextState("GARRISONED");
|
|
|
|
},
|
|
|
|
|
|
|
|
"leave": function() {
|
|
|
|
this.StopTimer();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
"GARRISONED": {
|
|
|
|
"enter": function() {
|
|
|
|
var cmpGarrisonHolder = Engine.QueryInterface(this.order.data.target, IID_GarrisonHolder);
|
|
|
|
if (cmpGarrisonHolder)
|
|
|
|
{
|
|
|
|
cmpGarrisonHolder.Garrison(this.entity);
|
|
|
|
|
|
|
|
}
|
|
|
|
if (this.FinishOrder())
|
|
|
|
return;
|
|
|
|
},
|
|
|
|
|
|
|
|
"leave": function() {
|
|
|
|
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
var UnitFsm = new FSM(UnitFsmSpec);
|
|
|
|
|
2010-02-05 23:00:39 +01:00
|
|
|
UnitAI.prototype.Init = function()
|
|
|
|
{
|
2010-07-21 18:09:58 +02:00
|
|
|
this.orderQueue = []; // current order is at the front of the list
|
|
|
|
this.order = undefined; // always == this.orderQueue[0]
|
2010-09-03 11:55:14 +02:00
|
|
|
this.formationController = INVALID_ENTITY; // entity with IID_Formation that we belong to
|
2010-08-04 23:15:41 +02:00
|
|
|
|
|
|
|
this.SetStance("aggressive");
|
2010-07-21 18:09:58 +02:00
|
|
|
};
|
2010-02-05 23:00:39 +01:00
|
|
|
|
2010-09-03 11:55:14 +02:00
|
|
|
UnitAI.prototype.IsFormationController = function()
|
|
|
|
{
|
|
|
|
return (this.template.FormationController == "true");
|
|
|
|
};
|
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitAI.prototype.OnCreate = function()
|
|
|
|
{
|
2010-09-03 11:55:14 +02:00
|
|
|
if (this.IsFormationController())
|
|
|
|
UnitFsm.Init(this, "FORMATIONCONTROLLER.IDLE");
|
|
|
|
else
|
|
|
|
UnitFsm.Init(this, "INDIVIDUAL.IDLE");
|
2010-02-07 21:06:16 +01:00
|
|
|
};
|
|
|
|
|
2010-07-29 22:39:23 +02:00
|
|
|
UnitAI.prototype.OnOwnershipChanged = function(msg)
|
|
|
|
{
|
|
|
|
this.SetupRangeQuery(msg.to);
|
|
|
|
};
|
|
|
|
|
|
|
|
UnitAI.prototype.OnDestroy = function()
|
|
|
|
{
|
|
|
|
// Clean up any timers that are now obsolete
|
|
|
|
this.StopTimer();
|
|
|
|
|
|
|
|
// Clean up range queries
|
|
|
|
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
|
|
|
if (this.losRangeQuery)
|
|
|
|
rangeMan.DestroyActiveQuery(this.losRangeQuery);
|
|
|
|
};
|
|
|
|
|
|
|
|
// Set up a range query for all enemy units within LOS range
|
|
|
|
// which can be attacked.
|
|
|
|
// This should be called whenever our ownership changes.
|
|
|
|
UnitAI.prototype.SetupRangeQuery = function(owner)
|
|
|
|
{
|
|
|
|
var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
|
|
|
|
if (!cmpVision)
|
|
|
|
return;
|
2010-11-13 03:36:53 +01:00
|
|
|
|
2010-07-29 22:39:23 +02:00
|
|
|
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
|
|
|
var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
|
2010-11-13 03:36:53 +01:00
|
|
|
|
2010-07-29 22:39:23 +02:00
|
|
|
if (this.losRangeQuery)
|
|
|
|
rangeMan.DestroyActiveQuery(this.losRangeQuery);
|
|
|
|
|
|
|
|
var range = cmpVision.GetRange();
|
2010-11-13 03:36:53 +01:00
|
|
|
|
2010-07-29 22:39:23 +02:00
|
|
|
var players = [];
|
2010-11-13 03:36:53 +01:00
|
|
|
|
|
|
|
if(owner != -1)
|
|
|
|
{ // If unit not just killed, get enemy players via diplomacy
|
|
|
|
var player = Engine.QueryInterface(playerMan.GetPlayerByID(owner), IID_Player);
|
|
|
|
|
|
|
|
// Get our diplomacy array
|
|
|
|
var diplomacy = player.GetDiplomacy();
|
|
|
|
var numPlayers = playerMan.GetNumPlayers();
|
|
|
|
|
|
|
|
for (var i = 1; i < numPlayers; ++i)
|
|
|
|
{ // Exclude gaia, allies, and self
|
|
|
|
// TODO: How to handle neutral players - Special query to attack military only?
|
|
|
|
if (i != owner && diplomacy[i - 1] < 0)
|
|
|
|
players.push(i);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-11-16 21:43:15 +01:00
|
|
|
this.losRangeQuery = rangeMan.CreateActiveQuery(this.entity, 0, range, players, IID_DamageReceiver);
|
2010-07-29 22:39:23 +02:00
|
|
|
rangeMan.EnableActiveQuery(this.losRangeQuery);
|
|
|
|
};
|
|
|
|
|
|
|
|
//// FSM linkage functions ////
|
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitAI.prototype.SetNextState = function(state)
|
2010-02-05 23:00:39 +01:00
|
|
|
{
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitFsm.SetNextState(this, state);
|
2010-02-05 23:00:39 +01:00
|
|
|
};
|
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitAI.prototype.DeferMessage = function(msg)
|
2010-07-08 22:08:08 +02:00
|
|
|
{
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitFsm.DeferMessage(this, msg);
|
|
|
|
};
|
2010-07-08 22:08:08 +02:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
/**
|
|
|
|
* Call when the current order has been completed (or failed).
|
|
|
|
* Removes the current order from the queue, and processes the
|
|
|
|
* next one (if any). Returns false and defaults to IDLE
|
|
|
|
* if there are no remaining orders.
|
|
|
|
*/
|
|
|
|
UnitAI.prototype.FinishOrder = function()
|
2010-02-05 23:00:39 +01:00
|
|
|
{
|
2010-07-21 18:09:58 +02:00
|
|
|
if (!this.orderQueue.length)
|
|
|
|
error("FinishOrder called when order queue is empty");
|
2010-02-05 23:00:39 +01:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
this.orderQueue.shift();
|
|
|
|
this.order = this.orderQueue[0];
|
2010-02-12 23:46:53 +01:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
if (this.orderQueue.length)
|
2010-07-08 22:08:08 +02:00
|
|
|
{
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitFsm.ProcessMessage(this, {"type": "Order."+this.order.type, "data": this.order.data});
|
|
|
|
return true;
|
2010-07-08 22:08:08 +02:00
|
|
|
}
|
2010-07-21 18:09:58 +02:00
|
|
|
else
|
2010-04-30 01:36:05 +02:00
|
|
|
{
|
2010-07-21 18:09:58 +02:00
|
|
|
this.SetNextState("IDLE");
|
|
|
|
return false;
|
2010-04-30 01:36:05 +02:00
|
|
|
}
|
2010-02-12 23:46:53 +01:00
|
|
|
};
|
2010-02-05 23:00:39 +01:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
/**
|
|
|
|
* Add an order onto the back of the queue,
|
|
|
|
* and execute it if we didn't already have an order.
|
|
|
|
*/
|
|
|
|
UnitAI.prototype.PushOrder = function(type, data)
|
2010-03-12 22:41:40 +01:00
|
|
|
{
|
2010-07-21 18:09:58 +02:00
|
|
|
var order = { "type": type, "data": data };
|
|
|
|
this.orderQueue.push(order);
|
2010-03-12 22:41:40 +01:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
// If we didn't already have an order, then process this new one
|
|
|
|
if (this.orderQueue.length == 1)
|
2010-04-30 01:36:05 +02:00
|
|
|
{
|
2010-07-21 18:09:58 +02:00
|
|
|
this.order = order;
|
|
|
|
UnitFsm.ProcessMessage(this, {"type": "Order."+this.order.type, "data": this.order.data});
|
2010-04-30 01:36:05 +02:00
|
|
|
}
|
2010-03-12 22:41:40 +01:00
|
|
|
};
|
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
/**
|
|
|
|
* Add an order onto the front of the queue,
|
|
|
|
* and execute it immediately.
|
|
|
|
*/
|
|
|
|
UnitAI.prototype.PushOrderFront = function(type, data)
|
2010-02-12 23:46:53 +01:00
|
|
|
{
|
2010-07-21 18:09:58 +02:00
|
|
|
var order = { "type": type, "data": data };
|
|
|
|
this.orderQueue.unshift(order);
|
2010-02-12 23:46:53 +01:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
this.order = order;
|
|
|
|
UnitFsm.ProcessMessage(this, {"type": "Order."+this.order.type, "data": this.order.data});
|
2010-02-10 20:28:46 +01:00
|
|
|
};
|
2010-02-05 23:00:39 +01:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitAI.prototype.ReplaceOrder = function(type, data)
|
2010-02-10 20:28:46 +01:00
|
|
|
{
|
2010-07-21 18:09:58 +02:00
|
|
|
this.orderQueue = [];
|
|
|
|
this.PushOrder(type, data);
|
2010-02-10 20:28:46 +01:00
|
|
|
};
|
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitAI.prototype.TimerHandler = function(data, lateness)
|
2010-02-10 20:28:46 +01:00
|
|
|
{
|
2010-07-21 18:09:58 +02:00
|
|
|
// Reset the timer
|
|
|
|
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
|
|
|
|
this.timer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "TimerHandler", data.timerRepeat - lateness, data);
|
2010-03-12 22:41:40 +01:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitFsm.ProcessMessage(this, {"type": "Timer", "data": data, "lateness": lateness});
|
|
|
|
};
|
2010-02-12 23:46:53 +01:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitAI.prototype.StartTimer = function(offset, repeat)
|
|
|
|
{
|
|
|
|
if (this.timer)
|
|
|
|
error("Called StartTimer when there's already an active timer");
|
2010-02-12 23:46:53 +01:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
|
|
|
|
this.timer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "TimerHandler", offset, { "timerRepeat": repeat });
|
2010-02-07 21:06:16 +01:00
|
|
|
};
|
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitAI.prototype.StopTimer = function()
|
2010-04-30 01:36:05 +02:00
|
|
|
{
|
2010-07-21 18:09:58 +02:00
|
|
|
if (!this.timer)
|
|
|
|
return;
|
|
|
|
|
2010-04-30 01:36:05 +02:00
|
|
|
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
|
2010-07-21 18:09:58 +02:00
|
|
|
cmpTimer.CancelTimer(this.timer);
|
|
|
|
this.timer = undefined;
|
|
|
|
};
|
2010-04-30 01:36:05 +02:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
//// Message handlers /////
|
2010-04-30 01:36:05 +02:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitAI.prototype.OnMotionChanged = function(msg)
|
2010-02-12 23:46:53 +01:00
|
|
|
{
|
2010-09-03 11:55:14 +02:00
|
|
|
if (msg.starting && !msg.error)
|
|
|
|
{
|
|
|
|
UnitFsm.ProcessMessage(this, {"type": "MoveStarted", "data": msg});
|
|
|
|
}
|
|
|
|
else if (!msg.starting || msg.error)
|
|
|
|
{
|
|
|
|
UnitFsm.ProcessMessage(this, {"type": "MoveCompleted", "data": msg});
|
|
|
|
}
|
2010-04-30 01:36:05 +02:00
|
|
|
};
|
2010-02-12 23:46:53 +01:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitAI.prototype.OnGlobalConstructionFinished = function(msg)
|
2010-02-12 23:46:53 +01:00
|
|
|
{
|
2010-07-21 18:09:58 +02:00
|
|
|
// TODO: This is a bit inefficient since every unit listens to every
|
|
|
|
// construction message - ideally we could scope it to only the one we're building
|
2010-07-18 17:19:49 +02:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitFsm.ProcessMessage(this, {"type": "ConstructionFinished", "data": msg});
|
2010-04-30 01:36:05 +02:00
|
|
|
};
|
2010-02-12 23:46:53 +01:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitAI.prototype.OnAttacked = function(msg)
|
2010-02-12 23:46:53 +01:00
|
|
|
{
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitFsm.ProcessMessage(this, {"type": "Attacked", "data": msg});
|
|
|
|
};
|
2010-02-12 23:46:53 +01:00
|
|
|
|
2010-07-29 22:39:23 +02:00
|
|
|
UnitAI.prototype.OnRangeUpdate = function(msg)
|
|
|
|
{
|
|
|
|
if (msg.tag == this.losRangeQuery)
|
|
|
|
UnitFsm.ProcessMessage(this, {"type": "LosRangeUpdate", "data": msg});
|
|
|
|
};
|
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
//// Helper functions to be called by the FSM ////
|
2010-03-12 22:41:40 +01:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitAI.prototype.GetWalkSpeed = function()
|
|
|
|
{
|
|
|
|
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
|
|
|
|
return cmpMotion.GetWalkSpeed();
|
2010-02-12 23:46:53 +01:00
|
|
|
};
|
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitAI.prototype.GetRunSpeed = function()
|
2010-03-12 22:41:40 +01:00
|
|
|
{
|
2010-04-30 01:36:05 +02:00
|
|
|
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
|
2010-07-21 18:09:58 +02:00
|
|
|
return cmpMotion.GetRunSpeed();
|
|
|
|
};
|
2010-02-10 20:28:46 +01:00
|
|
|
|
2010-10-13 17:14:15 +02:00
|
|
|
/**
|
|
|
|
* Returns true if the target exists and has non-zero hitpoints.
|
|
|
|
*/
|
2010-10-06 23:37:55 +02:00
|
|
|
UnitAI.prototype.TargetIsAlive = function(ent)
|
|
|
|
{
|
|
|
|
var cmpHealth = Engine.QueryInterface(ent, IID_Health);
|
|
|
|
if (!cmpHealth)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
return (cmpHealth.GetHitpoints() != 0);
|
|
|
|
};
|
|
|
|
|
2010-10-13 17:14:15 +02:00
|
|
|
/**
|
|
|
|
* Returns true if the target exists and needs to be killed before
|
|
|
|
* beginning to gather resources from it.
|
|
|
|
*/
|
|
|
|
UnitAI.prototype.MustKillGatherTarget = function(ent)
|
|
|
|
{
|
|
|
|
var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
|
|
|
|
if (!cmpResourceSupply)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if (!cmpResourceSupply.GetKillBeforeGather())
|
|
|
|
return false;
|
|
|
|
|
|
|
|
return this.TargetIsAlive(ent);
|
|
|
|
};
|
|
|
|
|
2010-10-03 19:58:49 +02:00
|
|
|
/**
|
|
|
|
* Play a sound appropriate to the current entity.
|
|
|
|
*/
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitAI.prototype.PlaySound = function(name)
|
|
|
|
{
|
2010-10-03 19:58:49 +02:00
|
|
|
// If we're a formation controller, use the sounds from our first member
|
|
|
|
if (this.IsFormationController())
|
|
|
|
{
|
|
|
|
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
|
|
|
|
var member = cmpFormation.GetPrimaryMember();
|
|
|
|
if (member)
|
|
|
|
PlaySound(name, member);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// Otherwise use our own sounds
|
|
|
|
PlaySound(name, this.entity);
|
|
|
|
}
|
2010-02-10 20:28:46 +01:00
|
|
|
};
|
|
|
|
|
2010-04-06 01:09:34 +02:00
|
|
|
UnitAI.prototype.SelectAnimation = function(name, once, speed, sound)
|
|
|
|
{
|
|
|
|
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
|
|
|
|
if (!cmpVisual)
|
|
|
|
return;
|
|
|
|
|
2010-09-03 11:55:14 +02:00
|
|
|
// Special case: the "move" animation gets turned into a special
|
|
|
|
// movement mode that deals with speeds and walk/run automatically
|
|
|
|
if (name == "move")
|
|
|
|
{
|
|
|
|
// Speed to switch from walking to running animations
|
|
|
|
var runThreshold = (this.GetWalkSpeed() + this.GetRunSpeed()) / 2;
|
|
|
|
|
|
|
|
cmpVisual.SelectMovementAnimation(runThreshold);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2010-05-09 15:56:06 +02:00
|
|
|
var soundgroup;
|
2010-04-06 01:09:34 +02:00
|
|
|
if (sound)
|
|
|
|
{
|
|
|
|
var cmpSound = Engine.QueryInterface(this.entity, IID_Sound);
|
|
|
|
if (cmpSound)
|
|
|
|
soundgroup = cmpSound.GetSoundGroup(sound);
|
|
|
|
}
|
|
|
|
|
2010-05-09 15:56:06 +02:00
|
|
|
// Set default values if unspecified
|
|
|
|
if (typeof once == "undefined")
|
|
|
|
once = false;
|
|
|
|
if (typeof speed == "undefined")
|
|
|
|
speed = 1.0;
|
|
|
|
if (typeof soundgroup == "undefined")
|
|
|
|
soundgroup = "";
|
|
|
|
|
2010-04-06 01:09:34 +02:00
|
|
|
cmpVisual.SelectAnimation(name, once, speed, soundgroup);
|
|
|
|
};
|
|
|
|
|
|
|
|
UnitAI.prototype.SetAnimationSync = function(actiontime, repeattime)
|
2010-02-07 21:06:16 +01:00
|
|
|
{
|
|
|
|
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
|
|
|
|
if (!cmpVisual)
|
|
|
|
return;
|
|
|
|
|
2010-06-06 00:23:28 +02:00
|
|
|
cmpVisual.SetAnimationSyncRepeat(repeattime);
|
|
|
|
cmpVisual.SetAnimationSyncOffset(actiontime);
|
2010-02-07 21:06:16 +01:00
|
|
|
};
|
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitAI.prototype.MoveToPoint = function(x, z)
|
2010-02-05 23:00:39 +01:00
|
|
|
{
|
2010-07-21 18:09:58 +02:00
|
|
|
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
|
|
|
|
return cmpMotion.MoveToPoint(x, z);
|
|
|
|
};
|
|
|
|
|
|
|
|
UnitAI.prototype.MoveToTarget = function(target)
|
|
|
|
{
|
|
|
|
var cmpPosition = Engine.QueryInterface(target, IID_Position);
|
|
|
|
if (!cmpPosition)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if (!cmpPosition.IsInWorld())
|
|
|
|
return false;
|
|
|
|
|
|
|
|
var pos = cmpPosition.GetPosition();
|
|
|
|
return this.MoveToPoint(pos.x, pos.z);
|
|
|
|
};
|
|
|
|
|
|
|
|
UnitAI.prototype.MoveToTargetRange = function(target, iid, type)
|
|
|
|
{
|
|
|
|
var cmpRanged = Engine.QueryInterface(this.entity, iid);
|
|
|
|
var range = cmpRanged.GetRange(type);
|
|
|
|
|
2010-02-05 23:00:39 +01:00
|
|
|
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
|
2010-04-30 01:36:05 +02:00
|
|
|
return cmpMotion.MoveToAttackRange(target, range.min, range.max);
|
2010-02-05 23:00:39 +01:00
|
|
|
};
|
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitAI.prototype.CheckTargetRange = function(target, iid, type)
|
2010-02-05 23:00:39 +01:00
|
|
|
{
|
2010-07-21 18:09:58 +02:00
|
|
|
var cmpRanged = Engine.QueryInterface(this.entity, iid);
|
|
|
|
var range = cmpRanged.GetRange(type);
|
2010-02-05 23:00:39 +01:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
|
|
|
|
return cmpMotion.IsInAttackRange(target, range.min, range.max);
|
|
|
|
};
|
2010-02-05 23:00:39 +01:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitAI.prototype.GetBestAttack = function()
|
|
|
|
{
|
2010-03-12 22:41:40 +01:00
|
|
|
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
|
2010-07-21 18:09:58 +02:00
|
|
|
if (!cmpAttack)
|
|
|
|
return undefined;
|
|
|
|
return cmpAttack.GetBestAttack();
|
|
|
|
};
|
2010-03-12 22:41:40 +01:00
|
|
|
|
2010-07-29 22:39:23 +02:00
|
|
|
/**
|
|
|
|
* Try to find one of the given entities which can be attacked,
|
|
|
|
* and start attacking it.
|
|
|
|
* Returns true if it found something to attack.
|
|
|
|
*/
|
|
|
|
UnitAI.prototype.AttackVisibleEntity = function(ents)
|
|
|
|
{
|
|
|
|
for each (var target in ents)
|
|
|
|
{
|
|
|
|
if (this.CanAttack(target))
|
|
|
|
{
|
|
|
|
this.PushOrderFront("Attack", { "target": target });
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
};
|
2010-02-05 23:00:39 +01:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
//// External interface functions ////
|
2010-02-10 20:28:46 +01:00
|
|
|
|
2010-09-03 11:55:14 +02:00
|
|
|
UnitAI.prototype.SetFormationController = function(ent)
|
|
|
|
{
|
|
|
|
this.formationController = ent;
|
|
|
|
|
|
|
|
// Set obstruction group, so we can walk through members
|
|
|
|
// of our own formation (or ourself if not in formation)
|
|
|
|
var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
|
|
|
|
if (cmpObstruction)
|
|
|
|
{
|
|
|
|
if (ent == INVALID_ENTITY)
|
|
|
|
cmpObstruction.SetControlGroup(this.entity);
|
|
|
|
else
|
|
|
|
cmpObstruction.SetControlGroup(ent);
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we were removed from a formation, let the FSM switch back to INDIVIDUAL
|
|
|
|
if (ent == INVALID_ENTITY)
|
|
|
|
UnitFsm.ProcessMessage(this, { "type": "FormationLeave" });
|
|
|
|
};
|
|
|
|
|
|
|
|
UnitAI.prototype.GetFormationController = function()
|
|
|
|
{
|
|
|
|
return this.formationController;
|
|
|
|
};
|
|
|
|
|
2010-10-02 21:40:30 +02:00
|
|
|
/**
|
|
|
|
* Returns the estimated distance that this unit will travel before either
|
|
|
|
* finishing all of its orders, or reaching a non-walk target (attack, gather, etc).
|
|
|
|
* Intended for Formation to switch to column layout on long walks.
|
|
|
|
*/
|
|
|
|
UnitAI.prototype.ComputeWalkingDistance = function()
|
|
|
|
{
|
|
|
|
var distance = 0;
|
|
|
|
|
|
|
|
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
|
|
|
|
if (!cmpPosition || !cmpPosition.IsInWorld())
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
// Keep track of the position at the start of each order
|
|
|
|
var pos = cmpPosition.GetPosition();
|
|
|
|
|
|
|
|
for (var i = 0; i < this.orderQueue.length; ++i)
|
|
|
|
{
|
|
|
|
var order = this.orderQueue[i];
|
|
|
|
switch (order.type)
|
|
|
|
{
|
|
|
|
case "Walk":
|
|
|
|
// Add the distance to the target point
|
|
|
|
var dx = order.data.x - pos.x;
|
|
|
|
var dz = order.data.z - pos.z;
|
|
|
|
var d = Math.sqrt(dx*dx + dz*dz);
|
|
|
|
distance += d;
|
|
|
|
|
|
|
|
// Remember this as the start position for the next order
|
|
|
|
pos = order.data;
|
|
|
|
|
|
|
|
break; // and continue the loop
|
|
|
|
|
|
|
|
case "WalkToTarget":
|
|
|
|
case "Attack":
|
|
|
|
case "Gather":
|
2010-11-13 20:15:29 +01:00
|
|
|
case "ReturnResource":
|
2010-10-02 21:40:30 +02:00
|
|
|
case "Repair":
|
|
|
|
// Find the target unit's position
|
|
|
|
var cmpTargetPosition = Engine.QueryInterface(order.data.target, IID_Position);
|
|
|
|
if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
|
|
|
|
return distance;
|
|
|
|
var targetPos = cmpTargetPosition.GetPosition();
|
|
|
|
|
|
|
|
// Add the distance to the target unit
|
|
|
|
var dx = targetPos.x - pos.x;
|
|
|
|
var dz = targetPos.z - pos.z;
|
|
|
|
var d = Math.sqrt(dx*dx + dz*dz);
|
|
|
|
distance += d;
|
|
|
|
|
|
|
|
// Return the total distance to the target
|
|
|
|
return distance;
|
|
|
|
|
|
|
|
default:
|
2010-11-13 20:15:29 +01:00
|
|
|
error("ComputeWalkingDistance: Unrecognised order type '"+order.type+"'");
|
2010-10-02 21:40:30 +02:00
|
|
|
return distance;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return the total distance to the end of the order queue
|
|
|
|
return distance;
|
|
|
|
};
|
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitAI.prototype.AddOrder = function(type, data, queued)
|
|
|
|
{
|
|
|
|
if (queued)
|
|
|
|
this.PushOrder(type, data);
|
|
|
|
else
|
|
|
|
this.ReplaceOrder(type, data);
|
|
|
|
};
|
2010-02-10 20:28:46 +01:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitAI.prototype.Walk = function(x, z, queued)
|
|
|
|
{
|
|
|
|
this.AddOrder("Walk", { "x": x, "z": z }, queued);
|
2010-02-05 23:00:39 +01:00
|
|
|
};
|
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitAI.prototype.WalkToTarget = function(target, queued)
|
2010-03-12 22:41:40 +01:00
|
|
|
{
|
2010-09-03 11:55:14 +02:00
|
|
|
this.AddOrder("WalkToTarget", { "target": target }, queued);
|
2010-07-21 18:09:58 +02:00
|
|
|
};
|
2010-03-12 22:41:40 +01:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitAI.prototype.Attack = function(target, queued)
|
|
|
|
{
|
|
|
|
if (!this.CanAttack(target))
|
|
|
|
{
|
|
|
|
this.WalkToTarget(target, queued);
|
2010-03-12 22:41:40 +01:00
|
|
|
return;
|
2010-07-21 18:09:58 +02:00
|
|
|
}
|
2010-03-12 22:41:40 +01:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
this.AddOrder("Attack", { "target": target }, queued);
|
|
|
|
};
|
2010-03-12 22:41:40 +01:00
|
|
|
|
2010-10-24 00:43:15 +02:00
|
|
|
UnitAI.prototype.Garrison = function(target, queued)
|
|
|
|
{
|
|
|
|
if (!this.CanGarrison(target))
|
|
|
|
{
|
|
|
|
this.WalkToTarget(target, queued);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.AddOrder("Garrison", { "target": target }, queued);
|
|
|
|
};
|
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitAI.prototype.Gather = function(target, queued)
|
|
|
|
{
|
|
|
|
if (!this.CanGather(target))
|
|
|
|
{
|
|
|
|
this.WalkToTarget(target, queued);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2010-07-31 23:21:42 +02:00
|
|
|
// Save the resource type now, so if the resource gets destroyed
|
|
|
|
// before we process the order then we still know what resource
|
|
|
|
// type to look for more of
|
|
|
|
var cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply);
|
|
|
|
var type = cmpResourceSupply.GetType();
|
|
|
|
|
|
|
|
this.AddOrder("Gather", { "target": target, "type": type }, queued);
|
2010-07-21 18:09:58 +02:00
|
|
|
};
|
2010-03-12 22:41:40 +01:00
|
|
|
|
2010-11-13 20:15:29 +01:00
|
|
|
UnitAI.prototype.ReturnResource = function(target, queued)
|
|
|
|
{
|
|
|
|
if (!this.CanReturnResource(target))
|
|
|
|
{
|
|
|
|
this.WalkToTarget(target, queued);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.AddOrder("ReturnResource", { "target": target }, queued);
|
|
|
|
};
|
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitAI.prototype.Repair = function(target, queued)
|
|
|
|
{
|
2010-09-03 11:55:14 +02:00
|
|
|
if (!this.CanRepair(target))
|
2010-03-12 22:41:40 +01:00
|
|
|
{
|
2010-07-21 18:09:58 +02:00
|
|
|
this.WalkToTarget(target, queued);
|
2010-03-12 22:41:40 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
this.AddOrder("Repair", { "target": target }, queued);
|
2010-03-12 22:41:40 +01:00
|
|
|
};
|
|
|
|
|
2010-08-04 23:15:41 +02:00
|
|
|
UnitAI.prototype.SetStance = function(stance)
|
|
|
|
{
|
|
|
|
if (g_Stances[stance])
|
|
|
|
this.stance = stance;
|
|
|
|
else
|
|
|
|
error("UnitAI: Setting to invalid stance '"+stance+"'");
|
|
|
|
};
|
|
|
|
|
|
|
|
UnitAI.prototype.GetStance = function()
|
|
|
|
{
|
|
|
|
return g_Stances[this.stance];
|
|
|
|
};
|
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
//// Helper functions ////
|
|
|
|
|
|
|
|
UnitAI.prototype.CanAttack = function(target)
|
2010-02-12 23:46:53 +01:00
|
|
|
{
|
2010-09-03 11:55:14 +02:00
|
|
|
// Formation controllers should always respond to commands
|
|
|
|
// (then the individual units can make up their own minds)
|
|
|
|
if (this.IsFormationController())
|
|
|
|
return true;
|
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
// Verify that we're able to respond to Attack commands
|
|
|
|
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
|
|
|
|
if (!cmpAttack)
|
|
|
|
return false;
|
2010-02-12 23:46:53 +01:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
// TODO: verify that this is a valid target
|
2010-02-12 23:46:53 +01:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
return true;
|
|
|
|
};
|
2010-03-12 22:41:40 +01:00
|
|
|
|
2010-10-24 00:43:15 +02:00
|
|
|
UnitAI.prototype.CanGarrison = function(target)
|
|
|
|
{
|
|
|
|
var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
|
|
|
|
if (!cmpGarrisonHolder)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
return true;
|
|
|
|
};
|
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitAI.prototype.CanGather = function(target)
|
|
|
|
{
|
2010-09-03 11:55:14 +02:00
|
|
|
// Formation controllers should always respond to commands
|
|
|
|
// (then the individual units can make up their own minds)
|
|
|
|
if (this.IsFormationController())
|
|
|
|
return true;
|
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
// Verify that we're able to respond to Gather commands
|
|
|
|
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
|
|
|
|
if (!cmpResourceGatherer)
|
|
|
|
return false;
|
2010-02-12 23:46:53 +01:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
// Verify that we can gather from this target
|
|
|
|
if (!cmpResourceGatherer.GetTargetGatherRate(target))
|
|
|
|
return false;
|
2010-02-12 23:46:53 +01:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
// TODO: should verify it's owned by the correct player, etc
|
2010-02-12 23:46:53 +01:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
return true;
|
2010-02-12 23:46:53 +01:00
|
|
|
};
|
|
|
|
|
2010-11-13 20:15:29 +01:00
|
|
|
UnitAI.prototype.CanReturnResource = function(target)
|
|
|
|
{
|
|
|
|
// Formation controllers should always respond to commands
|
|
|
|
// (then the individual units can make up their own minds)
|
|
|
|
if (this.IsFormationController())
|
|
|
|
return true;
|
|
|
|
|
|
|
|
// Verify that we're able to respond to ReturnResource commands
|
|
|
|
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
|
|
|
|
if (!cmpResourceGatherer)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// Verify that the target is a dropsite
|
|
|
|
var cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite);
|
|
|
|
if (!cmpResourceDropsite)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// Verify that we are carrying some resources,
|
|
|
|
// and can return our current resource to this target
|
|
|
|
var type = cmpResourceGatherer.GetMainCarryingType();
|
|
|
|
if (!type || !cmpResourceDropsite.AcceptsType(type))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// TODO: should verify it's owned by the correct player, etc
|
|
|
|
|
|
|
|
return true;
|
|
|
|
};
|
|
|
|
|
2010-09-03 11:55:14 +02:00
|
|
|
UnitAI.prototype.CanRepair = function(target)
|
|
|
|
{
|
|
|
|
// Formation controllers should always respond to commands
|
|
|
|
// (then the individual units can make up their own minds)
|
|
|
|
if (this.IsFormationController())
|
|
|
|
return true;
|
|
|
|
|
2010-09-03 22:18:54 +02:00
|
|
|
// Verify that we're able to respond to Repair (Builder) commands
|
2010-09-03 11:55:14 +02:00
|
|
|
var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
|
2010-09-03 22:04:11 +02:00
|
|
|
if (!cmpBuilder)
|
2010-09-03 11:55:14 +02:00
|
|
|
return false;
|
|
|
|
|
|
|
|
// TODO: verify that this is a valid target
|
|
|
|
|
|
|
|
return true;
|
|
|
|
};
|
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
|
2010-02-05 23:00:39 +01:00
|
|
|
Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI);
|