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-07-20 10:45:09 +02:00
|
|
|
"<empty/>";
|
2010-04-23 18:09:03 +02:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
var UnitFsmSpec = {
|
|
|
|
|
|
|
|
"INDIVIDUAL": {
|
|
|
|
|
|
|
|
"MoveStopped": function() {
|
|
|
|
// ignore spurious movement messages
|
|
|
|
// (these can happen when stopping moving at the same time
|
|
|
|
// as switching states)
|
|
|
|
},
|
|
|
|
|
|
|
|
"ConstructionFinished": function(msg) {
|
|
|
|
// ignore uninteresting construction messages
|
|
|
|
},
|
|
|
|
|
2010-07-29 22:39:23 +02:00
|
|
|
"LosRangeUpdate": function(msg) {
|
|
|
|
// ignore newly-seen units by default
|
|
|
|
},
|
|
|
|
|
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 });
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
"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);
|
|
|
|
if (this.AttackVisibleEntity(ents))
|
|
|
|
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) {
|
|
|
|
// TODO: implement stances (ignore this message if hold-fire stance)
|
|
|
|
|
|
|
|
// Start attacking one of the newly-seen enemy (if any)
|
|
|
|
this.AttackVisibleEntity(msg.data.added);
|
|
|
|
},
|
|
|
|
},
|
2010-07-21 18:09:58 +02:00
|
|
|
|
|
|
|
"Order.Walk": function(msg) {
|
|
|
|
var ok;
|
|
|
|
if (this.order.data.target)
|
|
|
|
ok = this.MoveToTarget(this.order.data.target);
|
|
|
|
else
|
|
|
|
ok = this.MoveToPoint(this.order.data.x, this.order.data.z);
|
|
|
|
|
|
|
|
if (ok)
|
|
|
|
{
|
|
|
|
// We've started walking to the given point
|
|
|
|
this.SetNextState("WALKING");
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// We are already at the target, or can't move at all
|
|
|
|
this.FinishOrder();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
"WALKING": {
|
|
|
|
"enter": function() {
|
|
|
|
this.SelectAnimation("walk", false, this.GetWalkSpeed());
|
|
|
|
this.PlaySound("walk");
|
|
|
|
},
|
|
|
|
|
|
|
|
"MoveStopped": function() {
|
|
|
|
this.FinishOrder();
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
"Order.Attack": function(msg) {
|
|
|
|
// 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("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("COMBAT.ATTACKING");
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
"COMBAT": {
|
|
|
|
|
|
|
|
"Attacked": function(msg) {
|
|
|
|
// If we're already in combat mode, ignore anyone else
|
|
|
|
// who's attacking us
|
|
|
|
},
|
|
|
|
|
|
|
|
"APPROACHING": {
|
|
|
|
"enter": function() {
|
|
|
|
this.SelectAnimation("walk", false, this.GetWalkSpeed());
|
|
|
|
this.PlaySound("walk");
|
|
|
|
},
|
|
|
|
|
|
|
|
"MoveStopped": function() {
|
|
|
|
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) {
|
|
|
|
// Check we can still reach the target
|
|
|
|
if (this.CheckTargetRange(this.order.data.target, IID_Attack, this.attackType))
|
|
|
|
{
|
|
|
|
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
|
|
|
|
cmpAttack.PerformAttack(this.attackType, this.order.data.target);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// Try to chase after it
|
|
|
|
if (this.MoveToTargetRange(this.order.data.target, IID_Attack, this.attackType))
|
|
|
|
{
|
|
|
|
this.SetNextState("COMBAT.CHASING");
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
// TODO: respond to target deaths immediately, rather than waiting
|
|
|
|
// until the next Timer event
|
|
|
|
},
|
|
|
|
|
|
|
|
"CHASING": {
|
|
|
|
"enter": function() {
|
|
|
|
this.SelectAnimation("walk", false, this.GetWalkSpeed());
|
|
|
|
this.PlaySound("walk");
|
|
|
|
},
|
|
|
|
|
|
|
|
"MoveStopped": function() {
|
|
|
|
this.SetNextState("ATTACKING");
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
"Order.Gather": function(msg) {
|
|
|
|
var cmpResourceSupply = Engine.QueryInterface(this.order.data.target, IID_ResourceSupply);
|
|
|
|
var type = cmpResourceSupply.GetType();
|
|
|
|
this.gatherType = type;
|
|
|
|
|
|
|
|
// Try to move within range
|
|
|
|
if (this.MoveToTargetRange(this.order.data.target, IID_ResourceGatherer))
|
|
|
|
{
|
|
|
|
// We've started walking to the given point
|
|
|
|
this.SetNextState("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("GATHER.GATHERING");
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
"GATHER": {
|
|
|
|
"APPROACHING": {
|
|
|
|
"enter": function() {
|
|
|
|
this.SelectAnimation("walk", false, this.GetWalkSpeed());
|
|
|
|
this.PlaySound("walk");
|
|
|
|
},
|
|
|
|
|
|
|
|
"MoveStopped": function() {
|
|
|
|
this.SetNextState("GATHERING");
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
"GATHERING": {
|
|
|
|
"enter": function() {
|
|
|
|
var typename = "gather_" + (this.gatherType.specific || this.gatherType.generic);
|
|
|
|
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))
|
|
|
|
{
|
|
|
|
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
|
|
|
|
var status = cmpResourceGatherer.PerformGather(this.order.data.target);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// Try to follow it
|
|
|
|
if (this.MoveToTargetRange(this.order.data.target, IID_ResourceGatherer))
|
|
|
|
{
|
|
|
|
this.SetNextState("APPROACHING");
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// 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 target of the same type
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
"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("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("REPAIR.REPAIRING");
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
"REPAIR": {
|
|
|
|
"APPROACHING": {
|
|
|
|
"enter": function() {
|
|
|
|
this.SelectAnimation("walk", false, this.GetWalkSpeed());
|
|
|
|
this.PlaySound("walk");
|
|
|
|
},
|
|
|
|
|
|
|
|
"MoveStopped": function() {
|
|
|
|
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);
|
|
|
|
var status = cmpBuilder.PerformBuilding(target);
|
|
|
|
if (!status.finished)
|
|
|
|
return; // continue repairing it
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
"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))
|
|
|
|
{
|
|
|
|
this.PushOrder("Gather", { "target": msg.data.newentity });
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// TODO: look for a nearby foundation to help with
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
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-02-05 23:00:39 +01:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitAI.prototype.OnCreate = function()
|
|
|
|
{
|
|
|
|
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;
|
|
|
|
|
|
|
|
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
|
|
|
var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
|
|
|
|
|
|
|
|
if (this.losRangeQuery)
|
|
|
|
rangeMan.DestroyActiveQuery(this.losRangeQuery);
|
|
|
|
|
|
|
|
var range = cmpVision.GetRange();
|
|
|
|
|
|
|
|
// Find all enemy players (i.e. exclude Gaia and ourselves)
|
|
|
|
var players = [];
|
|
|
|
for (var i = 1; i < playerMan.GetNumPlayers(); ++i)
|
|
|
|
if (i != owner)
|
|
|
|
players.push(i);
|
|
|
|
|
|
|
|
this.losRangeQuery = rangeMan.CreateActiveQuery(this.entity, range, players, IID_DamageReceiver);
|
|
|
|
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-07-21 18:09:58 +02:00
|
|
|
if (!msg.speed)
|
|
|
|
UnitFsm.ProcessMessage(this, {"type": "MoveStopped"});
|
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-07-21 18:09:58 +02:00
|
|
|
UnitAI.prototype.PlaySound = function(name)
|
|
|
|
{
|
|
|
|
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-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-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-07-21 18:09:58 +02:00
|
|
|
this.AddOrder("Walk", { "target": target }, queued);
|
|
|
|
};
|
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-07-21 18:09:58 +02:00
|
|
|
UnitAI.prototype.Gather = function(target, queued)
|
|
|
|
{
|
|
|
|
if (!this.CanGather(target))
|
|
|
|
{
|
|
|
|
this.WalkToTarget(target, queued);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.AddOrder("Gather", { "target": target }, queued);
|
|
|
|
};
|
2010-03-12 22:41:40 +01:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
UnitAI.prototype.Repair = function(target, queued)
|
|
|
|
{
|
|
|
|
// Verify that we're able to respond to Repair commands
|
|
|
|
var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
|
|
|
|
if (!cmpBuilder)
|
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
|
|
|
// TODO: verify that this is a valid target
|
2010-03-12 22:41:40 +01:00
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
this.AddOrder("Repair", { "target": target }, queued);
|
2010-03-12 22:41:40 +01:00
|
|
|
};
|
|
|
|
|
2010-07-21 18:09:58 +02:00
|
|
|
//// Helper functions ////
|
|
|
|
|
|
|
|
UnitAI.prototype.CanAttack = function(target)
|
2010-02-12 23:46:53 +01:00
|
|
|
{
|
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-07-21 18:09:58 +02:00
|
|
|
UnitAI.prototype.CanGather = function(target)
|
|
|
|
{
|
|
|
|
// 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-07-21 18:09:58 +02:00
|
|
|
|
2010-02-05 23:00:39 +01:00
|
|
|
Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI);
|