diff --git a/binaries/data/mods/public/simulation/components/Attack.js b/binaries/data/mods/public/simulation/components/Attack.js index a86d531d4e..1685af7a91 100644 --- a/binaries/data/mods/public/simulation/components/Attack.js +++ b/binaries/data/mods/public/simulation/components/Attack.js @@ -213,6 +213,9 @@ Attack.prototype.CauseDamage = function(data) if (!cmpDamageReceiver) return; cmpDamageReceiver.TakeDamage(strengths.hack, strengths.pierce, strengths.crush); + + Engine.PostMessage(data.target, MT_Attacked, + { "attacker": this.entity, "target": data.target }); }; Engine.RegisterComponentType(IID_Attack, "Attack", Attack); diff --git a/binaries/data/mods/public/simulation/components/Builder.js b/binaries/data/mods/public/simulation/components/Builder.js index d940d92154..d93c0b987f 100644 --- a/binaries/data/mods/public/simulation/components/Builder.js +++ b/binaries/data/mods/public/simulation/components/Builder.js @@ -43,6 +43,7 @@ Builder.prototype.GetRange = function() /** * Build/repair the target entity. This should only be called after a successful range check. * It should be called at a rate of once per second. + * Returns obj with obj.finished==true if this is a repair and it's fully repaired. */ Builder.prototype.PerformBuilding = function(target) { @@ -52,8 +53,8 @@ Builder.prototype.PerformBuilding = function(target) var cmpFoundation = Engine.QueryInterface(target, IID_Foundation); if (cmpFoundation) { - var finished = cmpFoundation.Build(this.entity, rate); - return { "finished": finished }; + cmpFoundation.Build(this.entity, rate); + return { "finished": false }; } else { diff --git a/binaries/data/mods/public/simulation/components/Foundation.js b/binaries/data/mods/public/simulation/components/Foundation.js index c0a66d1f04..93ba606fbf 100644 --- a/binaries/data/mods/public/simulation/components/Foundation.js +++ b/binaries/data/mods/public/simulation/components/Foundation.js @@ -58,8 +58,10 @@ Foundation.prototype.OnDestroy = function() Foundation.prototype.Build = function(builderEnt, work) { // Do nothing if we've already finished building + // (The entity will be destroyed soon after completion so + // this won't happen much) if (this.buildProgress == 1.0) - return true; + return; // Calculate the amount of progress that will be added (where 1.0 = completion) var cmpCost = Engine.QueryInterface(this.entity, IID_Cost); @@ -110,13 +112,10 @@ Foundation.prototype.Build = function(builderEnt, work) var cmpBuildingHealth = Engine.QueryInterface(building, IID_Health); cmpBuildingHealth.SetHitpoints(cmpHealth.GetHitpoints()); - Engine.DestroyEntity(this.entity); + Engine.PostMessage(this.entity, MT_ConstructionFinished, + { "entity": this.entity, "newentity": building }); - return true; - } - else - { - return false; + Engine.DestroyEntity(this.entity); } }; diff --git a/binaries/data/mods/public/simulation/components/ResourceGatherer.js b/binaries/data/mods/public/simulation/components/ResourceGatherer.js index 84a4df93c5..0420313bb2 100644 --- a/binaries/data/mods/public/simulation/components/ResourceGatherer.js +++ b/binaries/data/mods/public/simulation/components/ResourceGatherer.js @@ -42,7 +42,7 @@ ResourceGatherer.prototype.GetGatherRates = function() ResourceGatherer.prototype.GetRange = function() { - return { "max": 4, "min": 0 }; + return { "max": 2, "min": 0 }; // maybe this should depend on the unit or target or something? } @@ -67,7 +67,11 @@ ResourceGatherer.prototype.PerformGather = function(target) var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(cmpOwnership.GetOwner()), IID_Player); cmpPlayer.AddResource(type.generic, status.amount); - + + // Tell the target we're gathering from it + Engine.PostMessage(target, MT_ResourceGather, + { "entity": target, "gatherer": this.entity }); + return status; }; diff --git a/binaries/data/mods/public/simulation/components/Timer.js b/binaries/data/mods/public/simulation/components/Timer.js index f721472e9f..eac2f154ea 100644 --- a/binaries/data/mods/public/simulation/components/Timer.js +++ b/binaries/data/mods/public/simulation/components/Timer.js @@ -32,14 +32,19 @@ Timer.prototype.OnUpdate = function(msg) for each (var id in run) { var t = this.timers[id]; + if (!t) + continue; // an earlier timer might have cancelled this one, so skip it + var cmp = Engine.QueryInterface(t[0], t[1]); + if (!cmp) + continue; // the entity was probably destroyed + try { var lateness = this.time - t[3]; cmp[t[2]](t[4], lateness); } catch (e) { var stack = e.stack.trimRight().replace(/^/mg, ' '); // indent the stack trace - print("Error in timer on entity "+t[0]+", IID "+t[1]+", function "+t[2]+": "+e+"\n"+stack+"\n"); - // TODO: should report in an error log + error("Error in timer on entity "+t[0]+", IID "+t[1]+", function "+t[2]+": "+e+"\n"+stack+"\n"); } delete this.timers[id]; } diff --git a/binaries/data/mods/public/simulation/components/UnitAI.js b/binaries/data/mods/public/simulation/components/UnitAI.js index d65b246f19..a1d928d277 100644 --- a/binaries/data/mods/public/simulation/components/UnitAI.js +++ b/binaries/data/mods/public/simulation/components/UnitAI.js @@ -1,37 +1,3 @@ -/* - -This is currently just a very simplistic state machine that lets units be commanded around -and then autonomously carry out the orders. It might need to be entirely redesigned. - -*/ - -const STATE_IDLE = 0; -const STATE_WALKING = 1; -const STATE_ATTACKING = 2; -const STATE_REPAIRING = 3; -const STATE_GATHERING = 4; - -/* Attack process: - * When starting attack: - * Activate attack animation (with appropriate repeat speed and offset) - * Set this.attackTimer to run at maximum of: - * GetTimers().prepare msec from now - * this.attackRechargeTime - * Loop: - * Wait for the timer - * Perform the attack - * Set this.attackRechargeTime to now plus GetTimers().recharge - * Set this.attackTimer to run after GetTimers().repeat - * At any point it's safe to cancel the attack and switch to a different action - * (The rechargeTime is to prevent people spamming the attack command and getting - * faster-than-normal attacks) - */ - -/* Repeat/Gather process is about the same, except with less synchronisation - the action - * is just performed 1sec after initiated, and then repeated every 1sec. - * (TODO: it'd be nice to avoid most of the duplication between Attack and Repeat and Gather code) - */ - function UnitAI() {} UnitAI.prototype.Schema = @@ -39,312 +5,481 @@ UnitAI.prototype.Schema = "" + ""; +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.state = STATE_IDLE; - - // The earliest time at which we'll have 'recovered' from the previous attack, and - // can start preparing a new attack - this.attackRechargeTime = 0; - // Timer for AttackTimeout - this.attackTimer = undefined; - // Current attack type - this.attackType = undefined; - // Current target entity ID - this.attackTarget = undefined; - - // Timer for RepairTimeout - this.repairTimer = undefined; - // Current target entity ID - this.repairTarget = undefined; - - // Timer for GatherTimeout - this.gatherTimer = undefined; - // Current target entity ID - this.gatherTarget = undefined; + this.orderQueue = []; // current order is at the front of the list + this.order = undefined; // always == this.orderQueue[0] }; -//// Interface functions //// -UnitAI.prototype.Walk = function(x, z) +//// FSM linkage functions //// + +UnitAI.prototype.OnCreate = function() { - var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - if (!cmpMotion) - return; + UnitFsm.Init(this, "INDIVIDUAL.IDLE"); +}; - if (cmpMotion.MoveToPoint(x, z)) +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) { - this.state = STATE_WALKING; - PlaySound("walk", this.entity); + UnitFsm.ProcessMessage(this, {"type": "Order."+this.order.type, "data": this.order.data}); + return true; } else { - this.state = STATE_IDLE; - } -}; - -UnitAI.prototype.WalkToTarget = function(target) -{ - var cmpPosition = Engine.QueryInterface(target, IID_Position); - if (!cmpPosition) - return; - - if (!cmpPosition.IsInWorld()) - return; - - var pos = cmpPosition.GetPosition(); - this.Walk(pos.x, pos.z); -} - -UnitAI.prototype.Attack = function(target) -{ - // Verify that we're able to respond to Attack commands - var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); - if (!cmpAttack) - { - this.WalkToTarget(target); - return; - } - - // TODO: verify that this is a valid target - - var type = cmpAttack.GetBestAttack(); - if (!type) - { - this.WalkToTarget(target); - return; - } - - // Stop any previous action timers - this.CancelTimers(); - - // Remember the target, and start moving towards it - this.attackType = type; - this.attackTarget = target; - this.state = STATE_ATTACKING; - if (!this.MoveToTarget(target, cmpAttack.GetRange(type))) - { - // We're in range already, do the attack - // (TODO: this could also happen if we couldn't move anywhere) - this.StartAttack(); - } - // else we've started moving and the attack will start in OnMotionChanged -}; - -UnitAI.prototype.Repair = function(target) -{ - // Verify that we're able to respond to Repair commands - var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder); - if (!cmpBuilder) - { - this.WalkToTarget(target); - return; - } - - // TODO: verify that this is a valid target - - // Stop any previous action timers - this.CancelTimers(); - - // Remember the target, and start moving towards it - this.repairTarget = target; - this.state = STATE_REPAIRING; - if (!this.MoveToTarget(target, cmpBuilder.GetRange())) - { - // We're in range already, do the repairing - // (TODO: this could also happen if we couldn't move anywhere) - this.StartRepair(); - } - // else we've started moving and the repair will start in OnMotionChanged -}; - -UnitAI.prototype.Gather = function(target) -{ - // Verify that we're able to respond to Gather commands - var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); - if (!cmpResourceGatherer) - { - this.WalkToTarget(target); - return; - } - - // Verify that we can gather from this target - if (!cmpResourceGatherer.GetTargetGatherRate(target)) - { - this.WalkToTarget(target); - return; - } - - // TODO: verify that this is a valid target - - // Stop any previous action timers - this.CancelTimers(); - - // Remember the target, and start moving towards it - this.gatherTarget = target; - this.state = STATE_GATHERING; - if (!this.MoveToTarget(target, cmpResourceGatherer.GetRange())) - { - // We're in range already, do the gathering - // (TODO: this could also happen if we couldn't move anywhere) - this.StartGather(); - } - // else we've started moving and the gather will start in OnMotionChanged -}; - -//// Message handlers //// - -UnitAI.prototype.OnDestroy = function() -{ - // Clean up any timers that are now obsolete - this.CancelTimers(); -}; - -UnitAI.prototype.OnMotionChanged = function(msg) -{ - if (msg.speed) - { - // Started moving - // => play the appropriate animation - this.SelectAnimation("walk", false, msg.speed); - } - else - { - if (this.state == STATE_WALKING) - { - // Stopped walking - this.state = STATE_IDLE; - this.SelectAnimation("idle"); - } - else if (this.state == STATE_ATTACKING) - { - // We were attacking, and have stopped moving - // => check if we can still reach the target now - - if (this.MoveIntoRange(IID_Attack, this.attackTarget, this.attackType)) - return; - - // In range, so perform the attack - this.StartAttack(); - } - else if (this.state == STATE_REPAIRING) - { - // We were repairing, and have stopped moving - // => check if we can still reach the target now - - if (this.MoveIntoRange(IID_Builder, this.repairTarget)) - return; - - // In range, so perform the repairing - this.StartRepair(); - } - else if (this.state == STATE_GATHERING) - { - // We were gathering, and have stopped moving - // => check if we can still reach the target now - - if (this.MoveIntoRange(IID_ResourceGatherer, this.gatherTarget)) - return; - - // In range, so perform the gathering - this.StartGather(); - } - } -}; - -//// Private functions //// - -UnitAI.prototype.StartAttack = function() -{ - // Perform the attack after the prepare time but not before the previous attack's recharge - var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); - var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - - var timers = cmpAttack.GetTimers(this.attackType); - var time = Math.max(timers.prepare, this.attackRechargeTime - cmpTimer.GetTime()); - this.attackTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "AttackTimeout", time, {}); - - // Start the attack animation and sound, but synced to the timers - this.SelectAnimation("melee", false, 1.0, "attack"); - this.SetAnimationSync(time, timers.repeat); - // TODO: this drifts since the sim is quantised to sim turns and these timers aren't - // TODO: we should probably only bother syncing projectile attacks, not melee -}; - -UnitAI.prototype.StartRepair = function() -{ - var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - this.repairTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "RepairTimeout", 1000, {}); - - // Start the repair/build animation and sound - this.SelectAnimation("build", false, 1.0, "build"); -}; - -UnitAI.prototype.StartGather = function() -{ - var cmpResourceSupply = Engine.QueryInterface(this.gatherTarget, IID_ResourceSupply); - var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - - // Get the animation/sound type name - var type = cmpResourceSupply.GetType(); - var typename = "gather_" + (type.specific || type.generic); - - this.gatherTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "GatherTimeout", 1000, {"typename": typename}); - - // Start the gather animation and sound - this.SelectAnimation(typename, false, 1.0, typename); - - // Tell the target we're gathering from it - Engine.PostMessage(this.gatherTarget, MT_ResourceGather, { "entity": this.gatherTarget, "gatherer": this.entity }); -}; - -UnitAI.prototype.CancelTimers = function() -{ - if (this.attackTimer) - { - var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - cmpTimer.CancelTimer(this.attackTimer); - this.attackTimer = undefined; - } - - if (this.repairTimer) - { - var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - cmpTimer.CancelTimer(this.repairTimer); - this.repairTimer = undefined; - } - - if (this.gatherTimer) - { - var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - cmpTimer.CancelTimer(this.gatherTimer); - this.gatherTimer = undefined; + this.SetNextState("IDLE"); + return false; } }; /** - * Tries to move into range of the target. - * Returns true if the unit has started walking or on pathing failure, false if already in range. + * Add an order onto the back of the queue, + * and execute it if we didn't already have an order. */ -UnitAI.prototype.MoveIntoRange = function(iid, target, type) +UnitAI.prototype.PushOrder = function(type, data) { - var cmpRanged = Engine.QueryInterface(this.entity, iid); - var range = cmpRanged.GetRange(type); + 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); - if (cmpMotion.IsInAttackRange(target, range.min, range.max)) - return false; + return cmpMotion.GetWalkSpeed(); +}; - // Out of range => need to move closer - // (The target has probably moved while we were chasing it) - if (this.MoveToTarget(target, range)) - return true; +UnitAI.prototype.GetRunSpeed = function() +{ + var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + return cmpMotion.GetRunSpeed(); +}; - // If it's impossible to reach the target, give up - // and switch back to idle - this.state = STATE_IDLE; - this.SelectAnimation("idle"); - return true; +UnitAI.prototype.PlaySound = function(name) +{ + PlaySound(name, this.entity); }; UnitAI.prototype.SelectAnimation = function(name, once, speed, sound) @@ -382,97 +517,138 @@ UnitAI.prototype.SetAnimationSync = function(actiontime, repeattime) cmpVisual.SetAnimationSyncOffset(actiontime); }; -/** - * Tries to move to the specified range of the target. - * This might synchronously trigger a MotionChanged message. - * Returns true if the unit has started walking, false on error or if already in range. - */ -UnitAI.prototype.MoveToTarget = function(target, range) +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.AttackTimeout = function(data, lateness) +UnitAI.prototype.CheckTargetRange = function(target, iid, type) { - // If we stopped attacking before this timeout, then don't do any processing here - if (this.state != STATE_ATTACKING) - return; + var cmpRanged = Engine.QueryInterface(this.entity, iid); + var range = cmpRanged.GetRange(type); - // Check if we can still reach the target - if (this.MoveIntoRange(IID_Attack, this.attackTarget, this.attackType)) - return; + 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); - - // Hit the target - cmpAttack.PerformAttack(this.attackType, this.attackTarget); - - // Set a timer to hit the target again - - var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - - var timers = cmpAttack.GetTimers(this.attackType); - this.attackRechargeTime = cmpTimer.GetTime() + timers.recharge; - this.attackTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "AttackTimeout", timers.repeat - lateness, data); + if (!cmpAttack) + return undefined; + return cmpAttack.GetBestAttack(); }; -UnitAI.prototype.RepairTimeout = function(data, lateness) + +//// External interface functions //// + +UnitAI.prototype.AddOrder = function(type, data, queued) { - // If we stopped repairing before this timeout, then don't do any processing here - if (this.state != STATE_REPAIRING) - return; + if (queued) + this.PushOrder(type, data); + else + this.ReplaceOrder(type, data); +}; - // Check if we can still reach the target - if (this.MoveIntoRange(IID_Builder, this.repairTarget)) - return; +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); - - // Repair/build the target - var status = cmpBuilder.PerformBuilding(this.repairTarget); - - // If the target is fully built and repaired, then stop and go back to idle - if (status.finished) + if (!cmpBuilder) { - this.state = STATE_IDLE; - this.SelectAnimation("idle"); + this.WalkToTarget(target, queued); return; } - // Set a timer to gather again + // TODO: verify that this is a valid target - var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - this.repairTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "RepairTimeout", 1000 - lateness, data); + this.AddOrder("Repair", { "target": target }, queued); }; -UnitAI.prototype.GatherTimeout = function(data, lateness) +//// Helper functions //// + +UnitAI.prototype.CanAttack = function(target) { - // If we stopped gathering before this timeout, then don't do any processing here - if (this.state != STATE_GATHERING) - return; + // Verify that we're able to respond to Attack commands + var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); + if (!cmpAttack) + return false; - // Check if we can still reach the target - if (this.MoveIntoRange(IID_ResourceGatherer, this.gatherTarget)) - return; + // TODO: verify that this is a valid target - var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); - - // Gather from the target - var status = cmpResourceGatherer.PerformGather(this.gatherTarget); - - // If the resource is exhausted, then stop and go back to idle - if (status.exhausted) - { - this.state = STATE_IDLE; - this.SelectAnimation("idle"); - return; - } - - // Set a timer to gather again - - var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - this.gatherTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "GatherTimeout", 1000 - lateness, data); + 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); diff --git a/binaries/data/mods/public/simulation/components/interfaces/Attack.js b/binaries/data/mods/public/simulation/components/interfaces/Attack.js index 2e93256889..48aaf484e2 100644 --- a/binaries/data/mods/public/simulation/components/interfaces/Attack.js +++ b/binaries/data/mods/public/simulation/components/interfaces/Attack.js @@ -1 +1,6 @@ Engine.RegisterInterface("Attack"); + +// Message sent from Attack to the target entity, each +// time the target is damaged. +// Data: { attacker: 123, target: 234 } +Engine.RegisterMessageType("Attacked"); diff --git a/binaries/data/mods/public/simulation/components/interfaces/Foundation.js b/binaries/data/mods/public/simulation/components/interfaces/Foundation.js index 2b8d5e8038..e81584dd5e 100644 --- a/binaries/data/mods/public/simulation/components/interfaces/Foundation.js +++ b/binaries/data/mods/public/simulation/components/interfaces/Foundation.js @@ -1 +1,7 @@ Engine.RegisterInterface("Foundation"); + +// Message sent from Foundation to its own entity when construction +// has been completed. +// Units can watch for this and change task once it's complete. +// Data: { entity: 123, newentity: 234 } +Engine.RegisterMessageType("ConstructionFinished"); diff --git a/binaries/data/mods/public/simulation/components/interfaces/ResourceGatherer.js b/binaries/data/mods/public/simulation/components/interfaces/ResourceGatherer.js index 3c5be75d57..220fde4844 100644 --- a/binaries/data/mods/public/simulation/components/interfaces/ResourceGatherer.js +++ b/binaries/data/mods/public/simulation/components/interfaces/ResourceGatherer.js @@ -1 +1,5 @@ Engine.RegisterInterface("ResourceGatherer"); + +// Message sent from ResourceGatherers to a ResourceSupply entity +// each time they gather resources from it +Engine.RegisterMessageType("ResourceGather"); diff --git a/binaries/data/mods/public/simulation/components/interfaces/ResourceSupply.js b/binaries/data/mods/public/simulation/components/interfaces/ResourceSupply.js index 2b3ebb26cc..5b63e5ea10 100644 --- a/binaries/data/mods/public/simulation/components/interfaces/ResourceSupply.js +++ b/binaries/data/mods/public/simulation/components/interfaces/ResourceSupply.js @@ -1,5 +1 @@ Engine.RegisterInterface("ResourceSupply"); - -// Message sent from gatherers to ResourceSupply entities -// when beginning to gather -Engine.RegisterMessageType("ResourceGather"); diff --git a/binaries/data/mods/public/simulation/helpers/Commands.js b/binaries/data/mods/public/simulation/helpers/Commands.js index 0b20930b6c..ae8008bbbb 100644 --- a/binaries/data/mods/public/simulation/helpers/Commands.js +++ b/binaries/data/mods/public/simulation/helpers/Commands.js @@ -16,7 +16,7 @@ function ProcessCommand(player, cmd) { var ai = Engine.QueryInterface(ent, IID_UnitAI); if (ai) - ai.Walk(cmd.x, cmd.z); + ai.Walk(cmd.x, cmd.z, cmd.queued); } break; @@ -25,7 +25,7 @@ function ProcessCommand(player, cmd) { var ai = Engine.QueryInterface(ent, IID_UnitAI); if (ai) - ai.Attack(cmd.target); + ai.Attack(cmd.target, cmd.queued); } break; @@ -35,7 +35,7 @@ function ProcessCommand(player, cmd) { var ai = Engine.QueryInterface(ent, IID_UnitAI); if (ai) - ai.Repair(cmd.target); + ai.Repair(cmd.target, cmd.queued); } break; @@ -44,7 +44,7 @@ function ProcessCommand(player, cmd) { var ai = Engine.QueryInterface(ent, IID_UnitAI); if (ai) - ai.Gather(cmd.target); + ai.Gather(cmd.target, cmd.queued); } break; @@ -118,7 +118,8 @@ function ProcessCommand(player, cmd) ProcessCommand(player, { "type": "repair", "entities": cmd.entities, - "target": ent + "target": ent, + "queued": cmd.queued }); break; diff --git a/binaries/data/mods/public/simulation/helpers/FSM.js b/binaries/data/mods/public/simulation/helpers/FSM.js index ab821abe21..64115238cd 100644 --- a/binaries/data/mods/public/simulation/helpers/FSM.js +++ b/binaries/data/mods/public/simulation/helpers/FSM.js @@ -152,7 +152,7 @@ FSM.prototype.SetNextState = function(obj, state) FSM.prototype.ProcessMessage = function(obj, msg) { -// print("ProcessMessage(obj, "+uneval(msg)+")\n"); +// warn("ProcessMessage(obj, "+uneval(msg)+")"); var func = this.states[obj.fsmStateName][msg.type]; if (!func)