function UnitAI() {} UnitAI.prototype.Schema = "Controls the unit's movement, attacks, etc, in response to commands from the player." + "" + ""; 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 }, "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() { this.SelectAnimation("idle"); }, }, "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); UnitAI.prototype.Init = function() { this.orderQueue = []; // current order is at the front of the list this.order = undefined; // always == this.orderQueue[0] }; //// FSM linkage functions //// UnitAI.prototype.OnCreate = function() { UnitFsm.Init(this, "INDIVIDUAL.IDLE"); }; UnitAI.prototype.SetNextState = function(state) { UnitFsm.SetNextState(this, state); }; UnitAI.prototype.DeferMessage = function(msg) { UnitFsm.DeferMessage(this, msg); }; /** * 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() { if (!this.orderQueue.length) error("FinishOrder called when order queue is empty"); this.orderQueue.shift(); this.order = this.orderQueue[0]; if (this.orderQueue.length) { UnitFsm.ProcessMessage(this, {"type": "Order."+this.order.type, "data": this.order.data}); return true; } else { this.SetNextState("IDLE"); return false; } }; /** * 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) { var order = { "type": type, "data": data }; this.orderQueue.push(order); // If we didn't already have an order, then process this new one if (this.orderQueue.length == 1) { this.order = order; UnitFsm.ProcessMessage(this, {"type": "Order."+this.order.type, "data": this.order.data}); } }; /** * Add an order onto the front of the queue, * and execute it immediately. */ UnitAI.prototype.PushOrderFront = function(type, data) { var order = { "type": type, "data": data }; this.orderQueue.unshift(order); this.order = order; UnitFsm.ProcessMessage(this, {"type": "Order."+this.order.type, "data": this.order.data}); }; UnitAI.prototype.ReplaceOrder = function(type, data) { this.orderQueue = []; this.PushOrder(type, data); }; UnitAI.prototype.TimerHandler = function(data, lateness) { // Reset the timer var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "TimerHandler", data.timerRepeat - lateness, data); UnitFsm.ProcessMessage(this, {"type": "Timer", "data": data, "lateness": lateness}); }; UnitAI.prototype.StartTimer = function(offset, repeat) { 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_UnitAI, "TimerHandler", offset, { "timerRepeat": repeat }); }; UnitAI.prototype.StopTimer = function() { if (!this.timer) return; var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); this.timer = undefined; }; //// Message handlers ///// UnitAI.prototype.OnDestroy = function() { // Clean up any timers that are now obsolete this.StopTimer(); }; UnitAI.prototype.OnMotionChanged = function(msg) { if (!msg.speed) UnitFsm.ProcessMessage(this, {"type": "MoveStopped"}); }; UnitAI.prototype.OnGlobalConstructionFinished = function(msg) { // 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 UnitFsm.ProcessMessage(this, {"type": "ConstructionFinished", "data": msg}); }; UnitAI.prototype.OnAttacked = function(msg) { UnitFsm.ProcessMessage(this, {"type": "Attacked", "data": msg}); }; //// Helper functions to be called by the FSM //// UnitAI.prototype.GetWalkSpeed = function() { var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpMotion.GetWalkSpeed(); }; UnitAI.prototype.GetRunSpeed = function() { var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpMotion.GetRunSpeed(); }; UnitAI.prototype.PlaySound = function(name) { PlaySound(name, this.entity); }; UnitAI.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); }; UnitAI.prototype.SetAnimationSync = function(actiontime, repeattime) { var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; cmpVisual.SetAnimationSyncRepeat(repeattime); cmpVisual.SetAnimationSyncOffset(actiontime); }; UnitAI.prototype.MoveToPoint = function(x, z) { 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); var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpMotion.MoveToAttackRange(target, range.min, range.max); }; UnitAI.prototype.CheckTargetRange = function(target, iid, type) { var cmpRanged = Engine.QueryInterface(this.entity, iid); var range = cmpRanged.GetRange(type); var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpMotion.IsInAttackRange(target, range.min, range.max); }; UnitAI.prototype.GetBestAttack = function() { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return undefined; return cmpAttack.GetBestAttack(); }; //// External interface functions //// UnitAI.prototype.AddOrder = function(type, data, queued) { if (queued) this.PushOrder(type, data); else this.ReplaceOrder(type, data); }; UnitAI.prototype.Walk = function(x, z, queued) { this.AddOrder("Walk", { "x": x, "z": z }, queued); }; UnitAI.prototype.WalkToTarget = function(target, queued) { this.AddOrder("Walk", { "target": target }, queued); }; UnitAI.prototype.Attack = function(target, queued) { if (!this.CanAttack(target)) { this.WalkToTarget(target, queued); return; } this.AddOrder("Attack", { "target": target }, queued); }; UnitAI.prototype.Gather = function(target, queued) { if (!this.CanGather(target)) { this.WalkToTarget(target, queued); return; } this.AddOrder("Gather", { "target": target }, queued); }; 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) { this.WalkToTarget(target, queued); return; } // TODO: verify that this is a valid target this.AddOrder("Repair", { "target": target }, queued); }; //// Helper functions //// UnitAI.prototype.CanAttack = function(target) { // Verify that we're able to respond to Attack commands var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return false; // TODO: verify that this is a valid target return true; }; 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; // Verify that we can gather from this target if (!cmpResourceGatherer.GetTargetGatherRate(target)) return false; // TODO: should verify it's owned by the correct player, etc return true; }; Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI);