using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using ZeroAD.Helpers; namespace ZeroAD.Components { public class CCmpUnitAI : ICmpUnitAI { public static void RegisterMessageTypes(CComponentManager mgr) { mgr.SubscribeToMessageType(ComponentTypeId.CID_UnitAI, MessageTypeId.Create); mgr.SubscribeToMessageType(ComponentTypeId.CID_UnitAI, MessageTypeId.Destroy); mgr.SubscribeToMessageType(ComponentTypeId.CID_UnitAI, MessageTypeId.OwnershipChanged); mgr.SubscribeToMessageType(ComponentTypeId.CID_UnitAI, MessageTypeId.DiplomacyChanged); mgr.SubscribeToMessageType(ComponentTypeId.CID_UnitAI, MessageTypeId.MotionChanged); mgr.SubscribeGloballyToMessageType(ComponentTypeId.CID_UnitAI, MessageTypeId.ConstructionFinished); mgr.SubscribeGloballyToMessageType(ComponentTypeId.CID_UnitAI, MessageTypeId.EntityRenamed); mgr.SubscribeToMessageType(ComponentTypeId.CID_UnitAI, MessageTypeId.Attacked); mgr.SubscribeToMessageType(ComponentTypeId.CID_UnitAI, MessageTypeId.HealthChanged); mgr.SubscribeToMessageType(ComponentTypeId.CID_UnitAI, MessageTypeId.RangeUpdate); } private static readonly SortedDictionary g_Stances = new SortedDictionary { { "violent", new UnitStance { targetVisibleEnemies= true, targetAttackersAlways= true, targetAttackersPassive= true, respondFlee= false, respondChase= true, respondChaseBeyondVision= true, respondStandGround= false, respondHoldGround= false, } }, { "aggressive", new UnitStance { targetVisibleEnemies= true, targetAttackersAlways= false, targetAttackersPassive= true, respondFlee= false, respondChase= true, respondChaseBeyondVision= false, respondStandGround= false, respondHoldGround= false, } }, { "defensive", new UnitStance { targetVisibleEnemies= true, targetAttackersAlways= false, targetAttackersPassive= true, respondFlee= false, respondChase= false, respondChaseBeyondVision= false, respondStandGround= false, respondHoldGround= true, } }, { "passive", new UnitStance { targetVisibleEnemies= false, targetAttackersAlways= false, targetAttackersPassive= true, respondFlee= true, respondChase= false, respondChaseBeyondVision= false, respondStandGround= false, respondHoldGround= false, } }, { "standground", new UnitStance { targetVisibleEnemies= true, targetAttackersAlways= false, targetAttackersPassive= true, respondFlee= false, respondChase= false, respondChaseBeyondVision= false, respondStandGround= true, respondHoldGround= false, } }, }; ZeroAD.Helpers.FSM UnitFsm; private bool isIdle; private bool isGarrisoned; private List orderQueue = new List(); OrderItem order; private string attackType; private uint formationController; private uint? losRangeQuery; private uint? losHealRangeQuery; private uint? losGaiaRangeQuery; private bool resyncAnimation; private int? lastAttacked; private int? lastHealed; private ICmpAttack.AttackTimer attackTimers; private string stance; public string fsmNextState; public string fsmStateName; public bool fsmReenter; private int? timer; CVector3D? heldPosition; private string lastFormationName; private ICmpHeal.HealTimer healTimers; public CCmpUnitAI() { StateNode stateROOT = new StateNode(); // Default event handlers: stateROOT.Handlers["MoveCompleted"] = new Action((msg) => { // ignore spurious movement messages // (these can happen when stopping moving at the same time // as switching states) }); stateROOT.Handlers["MoveStarted"] = new Action((msg) => { // ignore spurious movement messages }); stateROOT.Handlers["ConstructionFinished"] = new Action((msg) => { // ignore uninteresting construction messages }); stateROOT.Handlers["LosRangeUpdate"] = new Action((msg) => { // ignore newly-seen units by default }); stateROOT.Handlers["LosGaiaRangeUpdate"] = new Action((msg) => { // ignore newly-seen Gaia units by default }); stateROOT.Handlers["LosHealRangeUpdate"] = new Action((msg) => { // ignore newly-seen injured units by default }); stateROOT.Handlers["Attacked"] = new Action((msg) => { // ignore attacker }); stateROOT.Handlers["HealthChanged"] = new Action((msg) => { // ignore }); stateROOT.Handlers["EntityRenamed"] = new Action((msg) => { // ignore }); // Formation handlers: stateROOT.Handlers["FormationLeave"] = new Action((msg) => { // ignore when we're not in FORMATIONMEMBER }); // Called when being told to walk as part of a formation stateROOT.Handlers["Order.FormationWalk"] = new Action((msg) => { // Let players move captured domestic animals around if (this.IsAnimal() && !this.IsDomestic()) { this.FinishOrder(); return; } var cmpUnitMotion = Engine.QueryInterface(this.entity, InterfaceId.UnitMotion) as ICmpUnitMotion; cmpUnitMotion.MoveToFormationOffset((uint)msg.data.target, (float)msg.data.x, (float)msg.data.z); this.SetNextState("FORMATIONMEMBER.WALKING"); }); // Special orders: // (these will be overridden by various states) stateROOT.Handlers["Order.LeaveFoundation"] = new Action((msg) => { if (!PlayerHelper.IsOwnedByAllyOfEntity(Engine, this.entity, (uint)msg.data.target)) { this.FinishOrder(); return; } // Move a tile outside the building var range = 4; var ok = this.MoveToTargetRangeExplicit((uint)msg.data.target, range, range); 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(); } }); stateROOT.Handlers["Order.Stop"] = new Action((msg) => { // We have no control over non-domestic animals. if (this.IsAnimal() && !this.IsDomestic()) { this.FinishOrder(); return; } // Stop moving immediately. this.StopMoving(); this.FinishOrder(); // No orders left, we're an individual now if (this.IsAnimal()) { this.SetNextState("ANIMAL.IDLE"); } else { this.SetNextState("INDIVIDUAL.IDLE"); } }); stateROOT.Handlers["Order.Walk"] = new Action((msg) => { // Let players move captured domestic animals around if (this.IsAnimal() && !this.IsDomestic()) { this.FinishOrder(); return; } this.SetHeldPosition((float)this.order.data.x, (float)this.order.data.z); this.MoveToPoint((float)this.order.data.x, (float)this.order.data.z); if (this.IsAnimal()) { this.SetNextState("ANIMAL.WALKING"); } else { this.SetNextState("INDIVIDUAL.WALKING"); } }); stateROOT.Handlers["Order.WalkToTarget"] = new Action((msg) => { // Let players move captured domestic animals around if (this.IsAnimal() && !this.IsDomestic()) { this.FinishOrder(); return; } var ok = this.MoveToTarget((uint)this.order.data.target); if (ok) { // We've started walking to the given point if (this.IsAnimal()) { this.SetNextState("ANIMAL.WALKING"); } else { this.SetNextState("INDIVIDUAL.WALKING"); } } else { // We are already at the target, or can't move at all this.StopMoving(); this.FinishOrder(); } }); stateROOT.Handlers["Order.Flee"] = new Action((msg) => { // TODO: if we were attacked by a ranged unit, we need to flee much further away var ok = this.MoveToTargetRangeExplicit((uint)this.order.data.target, this.template["FleeDistance"].ToFloat(), -1); if (ok) { // We've started fleeing from the given target if (this.IsAnimal()) { this.SetNextState("ANIMAL.FLEEING"); } else { this.SetNextState("INDIVIDUAL.FLEEING"); } } else { // We are already at the target, or can't move at all this.StopMoving(); this.FinishOrder(); } }); stateROOT.Handlers["Order.Attack"] = new Action((msg) => { // Check the target is alive if (!this.TargetIsAlive(this.order.data.target)) { this.FinishOrder(); return; } // Work out how to attack the given target var type = this.GetBestAttackAgainst(this.order.data.target); if (type == null) { // Oops, we can't attack at all this.FinishOrder(); return; } this.attackType = type; // If we are already at the target, try attacking it from here if (this.CheckTargetRange(this.order.data.target, InterfaceId.Attack, this.attackType)) { this.StopMoving(); if (this.IsAnimal()) { this.SetNextState("ANIMAL.COMBAT.ATTACKING"); } else { this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING"); } return; } // If we can't reach the target, but are standing ground, then abandon this attack order. // Unless we're hunting, that's a special case where we should continue attacking our target. if (this.GetStance().respondStandGround && !this.order.data.force && !this.order.data.hunting) { this.FinishOrder(); return; } // Try to move within attack range if (this.MoveToTargetRange(this.order.data.target, InterfaceId.Attack, this.attackType)) { // We've started walking to the given point if (this.IsAnimal()) { this.SetNextState("ANIMAL.COMBAT.APPROACHING"); } else { this.SetNextState("INDIVIDUAL.COMBAT.APPROACHING"); } return; } // We can't reach the target, and can't move towards it, // so abandon this attack order this.FinishOrder(); }); stateROOT.Handlers["Order.Heal"] = new Action((msg) => { // Check the target is alive if (!this.TargetIsAlive(this.order.data.target)) { this.FinishOrder(); return; } // Healers can't heal themselves. if (this.order.data.target == this.entity) { this.FinishOrder(); return; } // Check if the target is in range if (this.CheckTargetRange(this.order.data.target, InterfaceId.Heal)) { this.StopMoving(); this.SetNextState("INDIVIDUAL.HEAL.HEALING"); return; } // If we can't reach the target, but are standing ground, // then abandon this heal order if (this.GetStance().respondStandGround && !this.order.data.force) { this.FinishOrder(); return; } // Try to move within heal range if (this.MoveToTargetRange(this.order.data.target, InterfaceId.Heal)) { // We've started walking to the given point this.SetNextState("INDIVIDUAL.HEAL.APPROACHING"); return; } // We can't reach the target, and can't move towards it, // so abandon this heal order this.FinishOrder(); }); stateROOT.Handlers["Order.Gather"] = new Action((msg) => { // If the target is still alive, we need to kill it first if (this.MustKillGatherTarget((uint)this.order.data.target) && this.CheckTargetVisible((uint)this.order.data.target)) { // Make sure we can attack the target, else we'll get very stuck if (this.GetBestAttackAgainst((uint)this.order.data.target) == null) { // 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", new AllInOneData { target = this.order.data.target, force = false, hunting = true }); return; } // Try to move within range if (this.MoveToTargetRange(this.order.data.target, InterfaceId.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.StopMoving(); this.SetNextStateAlwaysEntering("INDIVIDUAL.GATHER.GATHERING"); } }); stateROOT.Handlers["Order.GatherNearPosition"] = new Action((msg) => { // Move the unit to the position to gather from. this.MoveToPoint(this.order.data.x, this.order.data.z); this.SetNextState("INDIVIDUAL.GATHER.WALKING"); }); stateROOT.Handlers["Order.ReturnResource"] = new Action((msg) => { // Try to move to the dropsite if (this.MoveToTarget((uint)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.StopMoving(); this.FinishOrder(); return; } }); stateROOT.Handlers["Order.Trade"] = new Action((msg) => { if (this.MoveToMarket(this.order.data.firstMarket)) { // We've started walking to the first market this.SetNextState("INDIVIDUAL.TRADE.APPROACHINGFIRSTMARKET"); } }); stateROOT.Handlers["Order.Repair"] = new Action((msg) => { // Try to move within range if (this.MoveToTargetRange(this.order.data.target, InterfaceId.Builder, null)) { // 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.StopMoving(); this.SetNextState("INDIVIDUAL.REPAIR.REPAIRING"); } }); stateROOT.Handlers["Order.Garrison"] = new Action((msg) => { if (this.MoveToTarget(this.order.data.target)) { this.SetNextState("INDIVIDUAL.GARRISON.APPROACHING"); } else { // We do a range check before actually garrisoning this.StopMoving(); this.SetNextState("INDIVIDUAL.GARRISON.GARRISONED"); } }); stateROOT.Handlers["Order.Cheering"] = new Action((msg) => { this.SetNextState("INDIVIDUAL.CHEERING"); }); { // States for the special entity representing a group of units moving in formation: var stateFORMATIONCONTROLLER = stateROOT.SubStates["FORMATIONCONTROLLER"] = new StateNode(); stateFORMATIONCONTROLLER.Handlers["Order.Walk"] = new Action((msg) => { var cmpFormation = Engine.QueryInterface(this.entity, InterfaceId.Formation) as ICmpFormation; cmpFormation.CallMemberFunction(ai => ai.SetHeldPosition((float)msg.data.x, (float)msg.data.z)); this.MoveToPoint(this.order.data.x, this.order.data.z); this.SetNextState("WALKING"); }); // Only used by other orders to walk there in formation stateFORMATIONCONTROLLER.Handlers["Order.WalkToTargetRange"] = new Action((msg) => { if (this.MoveToTargetRangeExplicit(this.order.data.target, this.order.data.min, this.order.data.max)) { this.SetNextState("WALKING"); } else { this.FinishOrder(); } }); stateFORMATIONCONTROLLER.Handlers["Order.Stop"] = new Action((msg) => { var cmpFormation = Engine.QueryInterface(this.entity, InterfaceId.Formation) as ICmpFormation; cmpFormation.CallMemberFunction(ai => ai.Stop(false)); cmpFormation.Disband(); }); stateFORMATIONCONTROLLER.Handlers["Order.Attack"] = new Action((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, InterfaceId.Formation) as ICmpFormation; cmpFormation.CallMemberFunction(ai => ai.Attack((uint)msg.data.target, false)); // 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(); }); stateFORMATIONCONTROLLER.Handlers["Order.Heal"] = new Action((msg) => { // TODO: see notes in Order.Attack var cmpFormation = Engine.QueryInterface(this.entity, InterfaceId.Formation) as ICmpFormation; cmpFormation.CallMemberFunction(ai => ai.Heal((uint)msg.data.target, false)); cmpFormation.Disband(); }); stateFORMATIONCONTROLLER.Handlers["Order.Repair"] = new Action((msg) => { // TODO on what should we base this range? // Check if we are already in range, otherwise walk there if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) { if (!this.TargetIsAlive(msg.data.target)) // The building was finished or destroyed this.FinishOrder(); else // Out of range move there in formation this.PushOrderFront("WalkToTargetRange", new AllInOneData { target = msg.data.target, min = 0, max = 10 }); return; } var cmpFormation = Engine.QueryInterface(this.entity, InterfaceId.Formation) as ICmpFormation; // We don't want to rearrange the formation if the individual units are carrying // out a task and one of the members dies/leaves the formation. cmpFormation.SetRearrange(false); cmpFormation.CallMemberFunction(ai => ai.Repair((uint)msg.data.target, (bool)msg.data.autocontinue, false)); this.SetNextState("REPAIR"); }); stateFORMATIONCONTROLLER.Handlers["Order.Gather"] = new Action((msg) => { // TODO: see notes in Order.Attack // If the resource no longer exists, send a GatherNearPosition order var cmpFormation = Engine.QueryInterface(this.entity, InterfaceId.Formation) as ICmpFormation; if (this.CanGather(msg.data.target)) { cmpFormation.CallMemberFunction(ai => ai.Gather((uint)msg.data.target, false)); } else { cmpFormation.CallMemberFunction(ai => ai.GatherNearPosition(msg.data.lastPos.X, msg.data.lastPos.Z, msg.data.type, msg.data.template, false)); } cmpFormation.Disband(); }); stateFORMATIONCONTROLLER.Handlers["Order.GatherNearPosition"] = new Action((msg) => { // TODO: see notes in Order.Attack var cmpFormation = Engine.QueryInterface(this.entity, InterfaceId.Formation) as ICmpFormation; cmpFormation.CallMemberFunction(ai => ai.GatherNearPosition(msg.data.x, msg.data.z, msg.data.type, msg.data.template, false)); cmpFormation.Disband(); }); stateFORMATIONCONTROLLER.Handlers["Order.ReturnResource"] = new Action((msg) => { // TODO: see notes in Order.Attack var cmpFormation = Engine.QueryInterface(this.entity, InterfaceId.Formation) as ICmpFormation; cmpFormation.CallMemberFunction(ai => ai.ReturnResource((uint)msg.data.target, false)); cmpFormation.Disband(); }); stateFORMATIONCONTROLLER.Handlers["Order.Garrison"] = new Action((msg) => { // TODO: see notes in Order.Attack var cmpFormation = Engine.QueryInterface(this.entity, InterfaceId.Formation) as ICmpFormation; cmpFormation.CallMemberFunction(ai => ai.Garrison((uint)msg.data.target, false)); cmpFormation.Disband(); }); stateFORMATIONCONTROLLER.SubStates["IDLE"] = new StateNode(); { var stateFORMATIONCONTROLLER_WALKING = stateFORMATIONCONTROLLER.SubStates["WALKING"] = new StateNode(); stateFORMATIONCONTROLLER_WALKING.Handlers["MoveStarted"] = new Action((msg) => { var cmpFormation = Engine.QueryInterface(this.entity, InterfaceId.Formation) as ICmpFormation; cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true); }); stateFORMATIONCONTROLLER_WALKING.Handlers["MoveCompleted"] = new Action((msg) => { if (this.FinishOrder()) return; // If this was the last order, attempt to disband the formation. var cmpFormation = Engine.QueryInterface(this.entity, InterfaceId.Formation) as ICmpFormation; cmpFormation.FindInPosition(); }); } { var stateFORMATIONCONTROLLER_REPAIR = stateFORMATIONCONTROLLER.SubStates["REPAIR"] = new StateNode(); stateFORMATIONCONTROLLER_REPAIR.Handlers["ConstructionFinished"] = new Action((msg) => { if (msg.data.entity != this.order.data.target) { return; } if (this.FinishOrder()) { return; } var cmpFormation = Engine.QueryInterface(this.entity, InterfaceId.Formation) as ICmpFormation; cmpFormation.Disband(); }); } } { // States for entities moving as part of a formation: var stateFORMATIONMEMBER = stateROOT.SubStates["FORMATIONMEMBER"] = new StateNode(); stateFORMATIONMEMBER.Handlers["FormationLeave"] = new Action((msg) => { // Stop moving as soon as the formation disbands this.StopMoving(); // If the controller handled an order but some members rejected it, // they will have no orders and be in the FORMATIONMEMBER.IDLE state. if (this.orderQueue.Count != 0) { // We're leaving the formation, so stop our FormationWalk order if (this.FinishOrder()) { return; } } // No orders left, we're an individual now if (this.IsAnimal()) { this.SetNextState("ANIMAL.IDLE"); } else { this.SetNextState("INDIVIDUAL.IDLE"); } }); // Override the LeaveFoundation order since we're not doing // anything more important (and we might be stuck in the WALKING // state forever and need to get out of foundations in that case) stateFORMATIONMEMBER.Handlers["Order.LeaveFoundation"] = new Action((msg) => { if (!PlayerHelper.IsOwnedByAllyOfEntity(Engine, this.entity, msg.data.target)) { this.FinishOrder(); return; } // Move a tile outside the building var range = 4; var ok = this.MoveToTargetRangeExplicit(msg.data.target, range, range); if (ok) { // We've started walking to the given point this.SetNextState("WALKINGTOPOINT"); } else { // We are already at the target, or can't move at all this.FinishOrder(); } }); { var stateFORMATIONMEMBER_IDLE = stateFORMATIONMEMBER.SubStates["IDLE"] = new StateNode(); stateFORMATIONMEMBER_IDLE.Handlers["enter"] = new Action((msg) => this.SelectAnimation("idle")); } { var stateFORMATIONMEMBER_WALKING = stateFORMATIONMEMBER.SubStates["WALKING"] = new StateNode(); stateFORMATIONMEMBER_WALKING.Handlers["enter"] = new Action((msg) => this.SelectAnimation("move")); // Occurs when the unit has reached its destination and the controller // is done moving. The controller is notified, and will disband the // formation if all units are in formation and no orders remain. stateFORMATIONMEMBER_WALKING.Handlers["MoveCompleted"] = new Action((msg) => { var cmpFormation = Engine.QueryInterface(this.formationController, InterfaceId.Formation) as ICmpFormation; cmpFormation.SetInPosition(this.entity); }); } { var stateFORMATIONMEMBER_WALKINGTOPOINT = stateFORMATIONMEMBER.SubStates["WALKINGTOPOINT"] = new StateNode(); stateFORMATIONMEMBER_WALKINGTOPOINT.Handlers["enter"] = new Action((msg) => { var cmpFormation = Engine.QueryInterface(this.formationController, InterfaceId.Formation) as ICmpFormation; cmpFormation.UnsetInPosition(this.entity); this.SelectAnimation("move"); }); stateFORMATIONMEMBER_WALKINGTOPOINT.Handlers["MoveCompleted"] = new Action((msg) => { this.FinishOrder(); }); } } { var stateINDIVIDUAL = stateROOT.SubStates["INDIVIDUAL"] = new StateNode(); stateINDIVIDUAL.Handlers["enter"] = new Action((msg) => { // Sanity-checking if (this.IsAnimal()) { //error("Animal got moved into INDIVIDUAL.* state"); } }); stateINDIVIDUAL.Handlers["Attacked"] = new Action((msg) => { // Respond to attack if we always target attackers, or if we target attackers // during passive orders (e.g. gathering/repairing are never forced) if (this.GetStance().targetAttackersAlways || (this.GetStance().targetAttackersPassive && (this.order == null || this.order.data == null || !this.order.data.force))) { this.RespondToTargetedEntities(new[] { (uint)msg.data.attacker }); } }); { var stateINDIVIDUAL_IDLE = stateINDIVIDUAL.SubStates["IDLE"] = new StateNode(); stateINDIVIDUAL_IDLE.Handlers["enter"] = new Func((msg) => { // Switch back to idle animation to guarantee we won't // get stuck with an incorrect animation this.SelectAnimation("idle"); // The GUI and AI want to know when a unit is idle, but we don't // want to send frequent spurious messages if the unit's only // idle for an instant and will quickly go off and do something else. // So we'll set a timer here and only report the idle event if we // remain idle this.StartTimer(1000, 0); // If a unit can heal and attack we first want to heal wounded units, // so check if we are a healer and find whether there's anybody nearby to heal. // (If anyone approaches later it'll be handled via LosHealRangeUpdate.) // If anyone in sight gets hurt that will be handled via LosHealRangeUpdate. if (this.IsHealer() && this.FindNewHealTargets()) { return true; // (abort the FSM transition since we may have already switched state) } // 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.FindNewTargets()) { return true; // (abort the FSM transition since we may have already switched state) } // Nobody to attack - stay in idle return false; }); stateINDIVIDUAL_IDLE.Handlers["leave"] = new Action((msg) => { var rangeMan = (ICmpRangeManager)Engine.QueryInterface(InterfaceId.RangeManager); rangeMan.DisableActiveQuery(this.losRangeQuery.Value); if (this.losHealRangeQuery != null) { rangeMan.DisableActiveQuery(this.losHealRangeQuery.Value); } this.StopTimer(); if (this.isIdle) { this.isIdle = false; Engine.PostMessage(this.entity, new CMessageUnitIdleChanged { idle = this.isIdle }); } }); stateINDIVIDUAL_IDLE.Handlers["LosRangeUpdate"] = new Action((msg) => { if (this.GetStance().targetVisibleEnemies) { // Start attacking one of the newly-seen enemy (if any) this.AttackEntitiesByPreference(msg.data.added); } }); stateINDIVIDUAL_IDLE.Handlers["LosGaiaRangeUpdate"] = new Action((msg) => { if (this.GetStance().targetVisibleEnemies) { // Start attacking one of the newly-seen enemy (if any) this.AttackGaiaEntitiesByPreference(msg.data.added); } }); stateINDIVIDUAL_IDLE.Handlers["LosHealRangeUpdate"] = new Action((msg) => { this.RespondToHealableEntities(msg.data.added); }); stateINDIVIDUAL_IDLE.Handlers["Timer"] = new Action((msg) => { if (!this.isIdle) { this.isIdle = true; Engine.PostMessage(this.entity, new CMessageUnitIdleChanged { idle = this.isIdle }); } }); } { var stateINDIVIDUAL_WALKING = stateINDIVIDUAL.SubStates["WALKING"] = new StateNode(); stateINDIVIDUAL_WALKING.Handlers["enter"] = new Action((msg) => { this.SelectAnimation("move"); }); stateINDIVIDUAL_WALKING.Handlers["MoveCompleted"] = new Action((msg) => { this.FinishOrder(); }); } { var stateINDIVIDUAL_FLEEING = stateINDIVIDUAL.SubStates["FLEEING"] = new StateNode(); stateINDIVIDUAL_FLEEING.Handlers["enter"] = new Action((msg) => { this.PlaySound("panic"); // Run quickly var speed = this.GetRunSpeed(); this.SelectAnimation("move"); this.SetMoveSpeed(speed); }); stateINDIVIDUAL_FLEEING.Handlers["leave"] = new Action((msg) => { // Reset normal speed this.SetMoveSpeed(this.GetWalkSpeed()); }); stateINDIVIDUAL_FLEEING.Handlers["MoveCompleted"] = new Action((msg) => { // When we've run far enough, stop fleeing this.FinishOrder(); }); // TODO: what if we run into more enemies while fleeing? } { var stateINDIVIDUAL_COMBAT = stateINDIVIDUAL.SubStates["COMBAT"] = new StateNode(); stateINDIVIDUAL_COMBAT.Handlers["Order.LeaveFoundation"] = new Func((msg) => { // Ignore the order as we're busy. //return new { discardOrder = true }; return true; }); stateINDIVIDUAL_COMBAT.Handlers["EntityRenamed"] = new Action((msg) => { if (this.order.data.target == msg.entity) { this.order.data.target = msg.newentity; // If we're hunting, that means we have a queued gather // order whose target also needs to be updated. if (this.order.data.hunting && this.orderQueue[1] != null && this.orderQueue[1].type == "Gather") { this.orderQueue[1].data.target = msg.newentity; } } }); stateINDIVIDUAL_COMBAT.Handlers["Attacked"] = new Action((msg) => { // If we're already in combat mode, ignore anyone else // who's attacking us }); { var stateINDIVIDUAL_COMBAT_APPROACHING = stateINDIVIDUAL_COMBAT.SubStates["APPROACHING"] = new StateNode(); stateINDIVIDUAL_COMBAT_APPROACHING.Handlers["enter"] = new Action((msg) => { // Show weapons rather than carried resources. this.SetGathererAnimationOverride(true); this.SelectAnimation("move"); this.StartTimer(1000, 1000); }); stateINDIVIDUAL_COMBAT_APPROACHING.Handlers["leave"] = new Action((msg) => { // Show carried resources when walking. this.SetGathererAnimationOverride(false); this.StopTimer(); }); stateINDIVIDUAL_COMBAT_APPROACHING.Handlers["Timer"] = new Action((msg) => { if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, InterfaceId.Attack)) { this.StopMoving(); this.FinishOrder(); // Return to our original position if (this.GetStance().respondHoldGround) { this.WalkToHeldPosition(); } } }); stateINDIVIDUAL_COMBAT_APPROACHING.Handlers["MoveCompleted"] = new Action((msg) => { this.SetNextState("ATTACKING"); }); stateINDIVIDUAL_COMBAT_APPROACHING.Handlers["Attacked"] = new Action((msg) => { // If we're attacked by a close enemy, we should try to defend ourself // but only if we're not forced to target something else if ((string)msg.data.type == "Melee" && (this.GetStance().targetAttackersAlways || (this.GetStance().targetAttackersPassive && !(bool)this.order.data.force))) { this.RespondToTargetedEntities(new[] { (uint)msg.data.attacker }); } }); } { var stateINDIVIDUAL_COMBAT_ATTACKING = stateINDIVIDUAL_COMBAT.SubStates["ATTACKING"] = new StateNode(); stateINDIVIDUAL_COMBAT_ATTACKING.Handlers["enter"] = new Action((msg) => { var cmpAttack = Engine.QueryInterface(this.entity, InterfaceId.Attack) as ICmpAttack; this.attackTimers = cmpAttack.GetTimers(this.attackType); // If the repeat time since the last attack hasn't elapsed, // delay this attack to avoid attacking too fast. var prepare = this.attackTimers.prepare; if (this.lastAttacked != null) { var cmpTimer = Engine.QueryInterface(InterfaceId.Timer) as ICmpTimer; var repeatLeft = this.lastAttacked.GetValueOrDefault() + this.attackTimers.repeat - cmpTimer.GetTime(); prepare = Math.Max(prepare, repeatLeft); } this.SelectAnimation("melee", false, 1.0f, "attack"); this.SetAnimationSync(prepare, this.attackTimers.repeat); this.StartTimer(prepare, this.attackTimers.repeat); // TODO: we should probably only bother syncing projectile attacks, not melee // If using a non-default prepare time, re-sync the animation when the timer runs. this.resyncAnimation = (prepare != this.attackTimers.prepare) ? true : false; this.FaceTowardsTarget((uint)this.order.data.target); }); stateINDIVIDUAL_COMBAT_ATTACKING.Handlers["leave"] = new Action((msg) => { this.StopTimer(); }); stateINDIVIDUAL_COMBAT_ATTACKING.Handlers["Timer"] = new Action((msg) => { var target = (uint)this.order.data.target; // Check the target is still alive and attackable if (this.TargetIsAlive(target) && this.CanAttack(target, (bool)this.order.data.forceResponse)) { // Check we can still reach the target if (this.CheckTargetRange(target, InterfaceId.Attack, this.attackType)) { var cmpTimer = Engine.QueryInterface(InterfaceId.Timer) as ICmpTimer; this.lastAttacked = cmpTimer.GetTime() - (int)msg.lateness; this.FaceTowardsTarget(target); var cmpAttack = Engine.QueryInterface(this.entity, InterfaceId.Attack) as ICmpAttack; cmpAttack.PerformAttack(this.attackType, target); if (this.resyncAnimation) { this.SetAnimationSync(this.attackTimers.repeat, this.attackTimers.repeat); this.resyncAnimation = false; } return; } // Can't reach it - try to chase after it if (this.ShouldChaseTargetedEntity(target, this.order.data.force)) { if (this.MoveToTargetRange(target, InterfaceId.Attack, this.attackType)) { this.SetNextState("COMBAT.CHASING"); return; } } } // Can't reach it, no longer owned by enemy, or it doesn't exist any more - give up if (this.FinishOrder()) { return; } // See if we can switch to a new nearby enemy if (this.FindNewTargets()) { // Attempt to immediately re-enter the timer function, to avoid wasting the attack. this.TimerHandler(msg.data, msg.lateness); { return; } } // Return to our original position if (this.GetStance().respondHoldGround) { this.WalkToHeldPosition(); } }); // TODO: respond to target deaths immediately, rather than waiting // until the next Timer event stateINDIVIDUAL_COMBAT_ATTACKING.Handlers["Attacked"] = new Action((msg) => { if (this.order.data.target != msg.data.attacker) { // If we're attacked by a close enemy, stronger than our current target, // we choose to attack it, but only if we're not forced to target something else if (msg.data.type == "Melee" && (this.GetStance().targetAttackersAlways || (this.GetStance().targetAttackersPassive && !this.order.data.force))) { var ents = new List { (uint)this.order.data.target, (uint)msg.data.attacker }; EntityHelper.SortEntitiesByPriority(Engine, ents); if (ents[0] != this.order.data.target) { this.RespondToTargetedEntities(ents.ToArray()); } } } }); } { var stateINDIVIDUAL_COMBAT_CHASING = stateINDIVIDUAL_COMBAT.SubStates["CHASING"] = new StateNode(); stateINDIVIDUAL_COMBAT_CHASING.Handlers["enter"] = new Action((msg) => { // Show weapons rather than carried resources. this.SetGathererAnimationOverride(true); this.SelectAnimation("move"); this.StartTimer(1000, 1000); }); stateINDIVIDUAL_COMBAT_CHASING.Handlers["leave"] = new Action((msg) => { // Show carried resources when walking. this.SetGathererAnimationOverride(false); this.StopTimer(); }); stateINDIVIDUAL_COMBAT_CHASING.Handlers["Timer"] = new Action((msg) => { if (this.ShouldAbandonChase((uint)this.order.data.target, (bool)this.order.data.force, InterfaceId.Attack)) { this.StopMoving(); this.FinishOrder(); // Return to our original position if (this.GetStance().respondHoldGround) { this.WalkToHeldPosition(); } } }); stateINDIVIDUAL_COMBAT_CHASING.Handlers["MoveCompleted"] = new Action((msg) => { this.SetNextState("ATTACKING"); }); } } { var stateINDIVIDUAL_GATHER = stateINDIVIDUAL.SubStates["GATHER"] = new StateNode(); { var stateINDIVIDUAL_GATHER_APPROACHING = stateINDIVIDUAL_GATHER.SubStates["APPROACHING"] = new StateNode(); stateINDIVIDUAL_GATHER_APPROACHING.Handlers["enter"] = new Action((msg) => { this.SelectAnimation("move"); }); stateINDIVIDUAL_GATHER_APPROACHING.Handlers["MoveCompleted"] = new Action((msg) => { if ((bool)msg.data.error) { // We failed to reach the target // Save the current order's data in case we need it later var oldType = this.order.data.type; var oldTarget = (uint)this.order.data.target; var oldTemplate = (string)this.order.data.template; // Try the next queued order if there is any if (this.FinishOrder()) { return; } // Try to find another nearby target of the same specific type // Also don't switch to a different type of huntable animal var nearby = this.FindNearbyResource((ent, type, template) => { return ( ent != oldTarget && ((type.generic == "treasure" && oldType.generic == "treasure") || (type.specific == oldType.specific && (type.specific != "meat" || oldTemplate == template))) ); }); if (nearby != null) { this.PerformGather(nearby.Value, false, false); return; } // Couldn't find anything else. Just try this one again, // maybe we'll succeed next time this.PerformGather(oldTarget, false, false); return; } // We reached the target - start gathering from it now this.SetNextState("GATHERING"); }); } { var stateINDIVIDUAL_GATHER_WALKING = stateINDIVIDUAL_GATHER.SubStates["WALKING"] = new StateNode(); stateINDIVIDUAL_GATHER_WALKING.Handlers["enter"] = new Action((msg) => { this.SelectAnimation("move"); }); stateINDIVIDUAL_GATHER_WALKING.Handlers["MoveCompleted"] = new Action((msg) => { var resourceType = this.order.data.type; var resourceTemplate = this.order.data.template; // Try to find another nearby target of the same specific type // Also don't switch to a different type of huntable animal var nearby = this.FindNearbyResource((ent, type, template) => { return ( (type.generic == "treasure" && resourceType.generic == "treasure") || (type.specific == resourceType.specific && (type.specific != "meat" || resourceTemplate == template)) ); }); // If there is a nearby resource start gathering if (nearby != null) { this.PerformGather(nearby.Value, false, false); return; } // Couldn't find nearby resources, so give up this.FinishOrder(); }); } { var stateINDIVIDUAL_GATHER_GATHERING = stateINDIVIDUAL_GATHER.SubStates["GATHERING"] = new StateNode(); stateINDIVIDUAL_GATHER_GATHERING.Handlers["enter"] = new Func((msg) => { var target = (uint)this.order.data.target; // If this order was forced, the player probably gave it, but now we've reached the target // switch to an unforced order (can be interrupted by attacks) this.order.data.force = false; this.order.data.autoharvest = true; // Calculate timing based on gather rates // This allows the gather rate to control how often we gather, instead of how much. var cmpResourceGatherer = Engine.QueryInterface(this.entity, InterfaceId.ResourceGatherer) as ICmpResourceGatherer; var rate = cmpResourceGatherer.GetTargetGatherRate(target); if (rate == 0) { // Try to find another target if the current one stopped existing if (Engine.QueryInterface(target, InterfaceId.Identity) == null) { // Let the Timer logic handle this this.StartTimer(0, null); return false; } // No rate, give up on gathering this.FinishOrder(); return true; } // Scale timing interval based on rate, and start timer // The offset should be at least as long as the repeat time so we use the same value for both. var offset = 1000 / rate; var repeat = offset; this.StartTimer(offset, repeat); // We want to start the gather animation as soon as possible, // but only if we're actually at the target and it's still alive // (else it'll look like we're chopping empty air). // (If it's not alive, the Timer handler will deal with sending us // off to a different target.) if (this.CheckTargetRange(target, InterfaceId.ResourceGatherer)) { var typename = "gather_" + this.order.data.type.specific; this.SelectAnimation(typename, false, 1.0f, typename); } return false; }); stateINDIVIDUAL_GATHER_GATHERING.Handlers["leave"] = new Action((msg) => { this.StopTimer(); // Show the carried resource, if we've gathered anything. this.SetGathererAnimationOverride(false); }); stateINDIVIDUAL_GATHER_GATHERING.Handlers["Timer"] = new Action((msg) => { var target = this.order.data.target; var resourceTemplate = this.order.data.template; var resourceType = this.order.data.type as Name; // Check we can still reach and gather from the target if (this.CheckTargetRange(target, InterfaceId.ResourceGatherer) && this.CanGather(target)) { // Gather the resources: var cmpResourceGatherer = Engine.QueryInterface(this.entity, InterfaceId.ResourceGatherer) as ICmpResourceGatherer; // Try to gather treasure if (cmpResourceGatherer.TryInstantGather(target)) { return; } // 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(resourceType.generic)) { cmpResourceGatherer.DropResources(); } // Collect from the target var status = cmpResourceGatherer.PerformGather(target); // If we've collected as many resources as possible, // return to the nearest dropsite if (status.filled) { var nearby = this.FindNearestDropsite(resourceType.generic); if (nearby != null) { // (Keep this Gather order on the stack so we'll // continue gathering after returning) this.PushOrderFront("ReturnResource", new AllInOneData { target = nearby.Value, force = false }); return; } // Oh no, couldn't find any drop sites. Give up on gathering. this.FinishOrder(); return; } // We can gather more from this target, do so in the next timer if (!status.exhausted) { return; } } else { // Try to follow the target if (this.MoveToTargetRange(target, InterfaceId.ResourceGatherer)) { this.SetNextState("APPROACHING"); return; } // Can't reach the target, or it doesn't exist any more // We want to carry on gathering resources in the same area as // the old one. So try to get close to the old resource's // last known position var maxRange = 8; // get close but not too close if (this.order.data.lastPos != null && this.MoveToPointRange(this.order.data.lastPos.Value.X, this.order.data.lastPos.Value.Z, 0, maxRange)) { this.SetNextState("APPROACHING"); return; } } // We're already in range, can't get anywhere near it or the target is exhausted. // Give up on this order and try our next queued order if (this.FinishOrder()) { return; } // No remaining orders - pick a useful default behaviour // Try to find a new resource of the same specific type near our current position: // Also don't switch to a different type of huntable animal var nearby0 = this.FindNearbyResource((ent, type, template) => { return ( (type.generic == "treasure" && resourceType.generic == "treasure") || (type.specific == resourceType.specific && (type.specific != "meat" || resourceTemplate == template)) ); }); if (nearby0 != null) { this.PerformGather(nearby0.Value, false, false); return; } // Nothing else to gather - if we're carrying anything then we should // drop it off, and if not then we might as well head to the dropsite // anyway because that's a nice enough place to congregate and idle nearby0 = this.FindNearestDropsite(resourceType.generic); if (nearby0 != null) { this.PushOrderFront("ReturnResource", new AllInOneData { target = nearby0.Value, force = false }); return; } // No dropsites - just give up }); } } { var stateINDIVIDUAL_HEAL = stateINDIVIDUAL.SubStates["HEAL"] = new StateNode(); { var stateINDIVIDUAL_HEAL_APPROACHING = stateINDIVIDUAL_HEAL.SubStates["APPROACHING"] = new StateNode(); stateINDIVIDUAL_HEAL_APPROACHING.Handlers["enter"] = new Action((msg) => { this.SelectAnimation("move"); this.StartTimer(1000, 1000); }); stateINDIVIDUAL_HEAL_APPROACHING.Handlers["leave"] = new Action((msg) => { this.StopTimer(); }); stateINDIVIDUAL_HEAL_APPROACHING.Handlers["Timer"] = new Action((msg) => { if (this.ShouldAbandonChase((uint)this.order.data.target, (bool)this.order.data.force, InterfaceId.Heal)) { this.StopMoving(); this.FinishOrder(); // Return to our original position if (this.GetStance().respondHoldGround) { this.WalkToHeldPosition(); } } }); stateINDIVIDUAL_HEAL_APPROACHING.Handlers["MoveCompleted"] = new Action((msg) => { this.SetNextState("HEALING"); }); } { var stateINDIVIDUAL_HEAL_HEALING = stateINDIVIDUAL_HEAL.SubStates["HEALING"] = new StateNode(); stateINDIVIDUAL_HEAL_HEALING.Handlers["enter"] = new Action((msg) => { var cmpHeal = Engine.QueryInterface(this.entity, InterfaceId.Heal) as ICmpHeal; this.healTimers = cmpHeal.GetTimers(); // If the repeat time since the last heal hasn't elapsed, // delay the action to avoid healing too fast. var prepare = this.healTimers.prepare; if (this.lastHealed != null) { var cmpTimer = Engine.QueryInterface(InterfaceId.Timer) as ICmpTimer; var repeatLeft = this.lastHealed + this.healTimers.repeat - cmpTimer.GetTime(); prepare = Math.Max(prepare, repeatLeft.Value); } this.SelectAnimation("heal", false, 1.0f, "heal"); this.SetAnimationSync(prepare, this.healTimers.repeat); this.StartTimer(prepare, this.healTimers.repeat); // If using a non-default prepare time, re-sync the animation when the timer runs. this.resyncAnimation = (prepare != this.healTimers.prepare) ? true : false; this.FaceTowardsTarget(this.order.data.target); }); stateINDIVIDUAL_HEAL_HEALING.Handlers["leave"] = new Action((msg) => { this.StopTimer(); }); stateINDIVIDUAL_HEAL_HEALING.Handlers["Timer"] = new Action((msg) => { var target = (uint)this.order.data.target; // Check the target is still alive and healable if (this.TargetIsAlive(target) && this.CanHeal(target)) { // Check if we can still reach the target if (this.CheckTargetRange(target, InterfaceId.Heal)) { var cmpTimer = Engine.QueryInterface(InterfaceId.Timer) as ICmpTimer; this.lastHealed = cmpTimer.GetTime() - msg.lateness; this.FaceTowardsTarget(target); var cmpHeal = Engine.QueryInterface(this.entity, InterfaceId.Heal) as ICmpHeal; cmpHeal.PerformHeal(target); if (this.resyncAnimation) { this.SetAnimationSync(this.healTimers.repeat, this.healTimers.repeat); this.resyncAnimation = false; } return; } // Can't reach it - try to chase after it if (this.ShouldChaseTargetedEntity(target, this.order.data.force)) { if (this.MoveToTargetRange(target, InterfaceId.Heal)) { this.SetNextState("HEAL.CHASING"); return; } } } // Can't reach it, healed to max hp or doesn't exist any more - give up if (this.FinishOrder()) { return; } // Heal another one if (this.FindNewHealTargets()) { return; } // Return to our original position if (this.GetStance().respondHoldGround) { this.WalkToHeldPosition(); } }); } { var stateINDIVIDUAL_HEAL_CHASING = stateINDIVIDUAL_HEAL.SubStates["CHASING"] = new StateNode(); stateINDIVIDUAL_HEAL_CHASING.Handlers["enter"] = new Action((msg) => { this.SelectAnimation("move"); this.StartTimer(1000, 1000); }); stateINDIVIDUAL_HEAL_CHASING.Handlers["leave"] = new Action((msg) => { this.StopTimer(); }); stateINDIVIDUAL_HEAL_CHASING.Handlers["Timer"] = new Action((msg) => { if (this.ShouldAbandonChase((uint)this.order.data.target, (bool)this.order.data.force, InterfaceId.Heal)) { this.StopMoving(); this.FinishOrder(); // Return to our original position if (this.GetStance().respondHoldGround) { this.WalkToHeldPosition(); } } }); stateINDIVIDUAL_HEAL_CHASING.Handlers["MoveCompleted"] = new Action((msg) => { this.SetNextState("HEALING"); }); } } { var stateINDIVIDUAL_RETURNRESOURCE = stateINDIVIDUAL.SubStates["RETURNRESOURCE"] = new StateNode(); { var stateINDIVIDUAL_RETURNRESOURCE_APPROACHING = stateINDIVIDUAL_RETURNRESOURCE.SubStates["APPROACHING"] = new StateNode(); stateINDIVIDUAL_RETURNRESOURCE_APPROACHING.Handlers["enter"] = new Action((msg) => { // Work out what we're carrying, in order to select an appropriate animation var cmpResourceGatherer = Engine.QueryInterface(this.entity, InterfaceId.ResourceGatherer) as ICmpResourceGatherer; var type = cmpResourceGatherer.GetLastCarriedType(); if (type != null) { var typename = "carry_" + type.generic; // Special case for meat if (type.specific == "meat") { typename = "carry_" + type.specific; } this.SelectAnimation(typename, false, this.GetWalkSpeed()); } else { // We're returning empty-handed this.SelectAnimation("move"); } }); stateINDIVIDUAL_RETURNRESOURCE_APPROACHING.Handlers["MoveCompleted"] = new Action((msg) => { // Switch back to idle animation to guarantee we won't // get stuck with the carry animation after stopping moving this.SelectAnimation("idle"); // Check the dropsite is in range and we can return our resource there // (we didn't get stopped before reaching it) if (this.CheckTargetRange((uint)this.order.data.target, InterfaceId.ResourceGatherer) && this.CanReturnResource((uint)this.order.data.target, true)) { var cmpResourceDropsite = Engine.QueryInterface((uint)this.order.data.target, InterfaceId.ResourceDropsite) as ICmpResourceDropsite; if (cmpResourceDropsite != null) { // Dump any resources we can var dropsiteTypes = cmpResourceDropsite.GetTypes(); var cmpResourceGatherer = Engine.QueryInterface(this.entity, InterfaceId.ResourceGatherer) as ICmpResourceGatherer; cmpResourceGatherer.CommitResources(dropsiteTypes); // Our next order should always be a Gather, // so just switch back to that order this.FinishOrder(); return; } } // The dropsite was destroyed, or we couldn't reach it, or ownership changed // Look for a new one. var cmpResourceGatherer0 = Engine.QueryInterface(this.entity, InterfaceId.ResourceGatherer) as ICmpResourceGatherer; var genericType = cmpResourceGatherer0.GetMainCarryingType(); var nearby = this.FindNearestDropsite(genericType); if (nearby != null) { this.FinishOrder(); this.PushOrderFront("ReturnResource", new AllInOneData { target = nearby.Value, force = false }); return; } // Oh no, couldn't find any drop sites. Give up on returning. this.FinishOrder(); }); } } { var stateINDIVIDUAL_TRADE = stateINDIVIDUAL.SubStates["TRADE"] = new StateNode(); stateINDIVIDUAL_TRADE.Handlers["Attacked"] = new Action((msg) => { // Ignore attack // TODO: Inform player }); { var stateINDIVIDUAL_TRADE_APPROACHINGFIRSTMARKET = stateINDIVIDUAL_TRADE.SubStates["APPROACHINGFIRSTMARKET"] = new StateNode(); stateINDIVIDUAL_TRADE_APPROACHINGFIRSTMARKET.Handlers["enter"] = new Action((msg) => { this.SelectAnimation("move"); }); stateINDIVIDUAL_TRADE_APPROACHINGFIRSTMARKET.Handlers["MoveCompleted"] = new Action((msg) => { this.PerformTradeAndMoveToNextMarket(this.order.data.firstMarket, this.order.data.secondMarket, "INDIVIDUAL.TRADE.APPROACHINGSECONDMARKET"); }); } { var stateINDIVIDUAL_TRADE_APPROACHINGSECONDMARKET = stateINDIVIDUAL_TRADE.SubStates["APPROACHINGSECONDMARKET"] = new StateNode(); stateINDIVIDUAL_TRADE_APPROACHINGSECONDMARKET.Handlers["enter"] = new Action((msg) => { this.SelectAnimation("move"); }); stateINDIVIDUAL_TRADE_APPROACHINGSECONDMARKET.Handlers["MoveCompleted"] = new Action((msg) => { this.order.data.firstPass = false; this.PerformTradeAndMoveToNextMarket(this.order.data.secondMarket, this.order.data.firstMarket, "INDIVIDUAL.TRADE.APPROACHINGFIRSTMARKET"); }); } } { var stateINDIVIDUAL_REPAIR = stateINDIVIDUAL.SubStates["REPAIR"] = new StateNode(); stateINDIVIDUAL_REPAIR.Handlers["ConstructionFinished"] = new Action((msg) => { if (msg.data.entity != this.order.data.target) return; // ignore other buildings // Save the current order's data in case we need it later var oldData = this.order.data; // Save the current state so we can continue walking if necessary // FinishOrder() below will switch to IDLE if there's no order, which sets the idle animation. // Idle animation while moving towards finished construction looks weird (ghosty). var oldState = this.GetCurrentState(); // We finished building it. // Switch to the next order (if any) if (this.FinishOrder()) return; // No remaining orders - pick a useful default behaviour // If autocontinue explicitly disabled (e.g. by AI) then // do nothing automatically if (!oldData.autocontinue) return; // If this building was e.g. a farm of ours, the entities that recieved // the build command should start gathering from it if ((oldData.force || oldData.autoharvest) && this.CanGather(msg.data.newentity)) { this.PerformGather(msg.data.newentity, true, false); return; } // If this building was e.g. a farmstead of ours, entities that received // the build command should look for nearby resources to gather if ((oldData.force || oldData.autoharvest) && this.CanReturnResource(msg.data.newentity, false)) { var cmpResourceDropsite = Engine.QueryInterface(msg.data.newentity, InterfaceId.ResourceDropsite) as ICmpResourceDropsite; var types = cmpResourceDropsite.GetTypes(); // TODO: Slightly undefined behavior here, we don't know what type of resource will be collected, // may cause problems for AIs (especially hunting fast animals), but avoid ugly hacks to fix that! var nearby = this.FindNearbyResource((ent, type, template) => { return (types.indexOf(type.generic) != -1); }); if (nearby != null) { this.PerformGather(nearby.Value, true, false); return; } } // Look for a nearby foundation to help with var nearbyFoundation = this.FindNearbyFoundation(); if (nearbyFoundation != null) { this.AddOrder("Repair", new AllInOneData { target = nearbyFoundation.Value, autocontinue = (bool)oldData.autocontinue, force = false }, true); return; } // Unit was approaching and there's nothing to do now, so switch to walking if (oldState == "INDIVIDUAL.REPAIR.APPROACHING") { // We're already walking to the given point, so add this as a order. this.WalkToTarget(msg.data.newentity, true); } }); { var stateINDIVIDUAL_REPAIR_APPROACHING = stateINDIVIDUAL_REPAIR.SubStates["APPROACHING"] = new StateNode(); stateINDIVIDUAL_REPAIR_APPROACHING.Handlers["enter"] = new Action((msg) => { this.SelectAnimation("move"); }); stateINDIVIDUAL_REPAIR_APPROACHING.Handlers["MoveCompleted"] = new Action((msg) => { this.SetNextState("REPAIRING"); }); } { var stateINDIVIDUAL_REPAIR_REPAIRING = stateINDIVIDUAL_REPAIR.SubStates["REPAIRING"] = new StateNode(); stateINDIVIDUAL_REPAIR_REPAIRING.Handlers["enter"] = new Func((msg) => { // If this order was forced, the player probably gave it, but now we've reached the target // switch to an unforced order (can be interrupted by attacks) if ((bool)this.order.data.force) { this.order.data.autoharvest = true; } this.order.data.force = false; var target = this.order.data.target; // Check we can still reach and repair the target if (!this.CheckTargetRange(target, InterfaceId.Builder) || !this.CanRepair(target)) { // Can't reach it, no longer owned by ally, or it doesn't exist any more this.FinishOrder(); return true; } else { var cmpFoundation = Engine.QueryInterface(target, InterfaceId.Foundation) as ICmpFoundation; if (cmpFoundation != null) { cmpFoundation.AddBuilder(this.entity); } } this.SelectAnimation("build", false, 1.0f, "build"); this.StartTimer(1000, 1000); return false; }); stateINDIVIDUAL_REPAIR_REPAIRING.Handlers["leave"] = new Action((msg) => { this.StopTimer(); }); stateINDIVIDUAL_REPAIR_REPAIRING.Handlers["Timer"] = new Action((msg) => { var target = (uint)this.order.data.target; // Check we can still reach and repair the target if (!this.CheckTargetRange(target, InterfaceId.Builder) || !this.CanRepair(target)) { // Can't reach it, no longer owned by ally, or it doesn't exist any more this.FinishOrder(); return; } var cmpBuilder = Engine.QueryInterface(this.entity, InterfaceId.Builder) as ICmpBuilder; cmpBuilder.PerformBuilding(target); }); } } { var stateINDIVIDUAL_GARRISON = stateINDIVIDUAL.SubStates["GARRISON"] = new StateNode(); { var stateINDIVIDUAL_GARRISON_APPROACHING = stateINDIVIDUAL_GARRISON.SubStates["APPROACHING"] = new StateNode(); stateINDIVIDUAL_GARRISON_APPROACHING.Handlers["enter"] = new Action((msg) => { this.SelectAnimation("walk", false, this.GetWalkSpeed()); }); stateINDIVIDUAL_GARRISON_APPROACHING.Handlers["MoveCompleted"] = new Action((msg) => { this.SetNextState("GARRISONED"); }); stateINDIVIDUAL_GARRISON_APPROACHING.Handlers["leave"] = new Action((msg) => { this.StopTimer(); }); } { var stateINDIVIDUAL_GARRISON_GARRISONED = stateINDIVIDUAL_GARRISON.SubStates["GARRISONED"] = new StateNode(); stateINDIVIDUAL_GARRISON_GARRISONED.Handlers["enter"] = new Func((msg) => { var target = this.order.data.target; var cmpGarrisonHolder = Engine.QueryInterface(target, InterfaceId.GarrisonHolder) as ICmpGarrisonHolder; // Check that we can garrison here if (this.CanGarrison(target)) { // Check that we're in range of the garrison target if (this.CheckGarrisonRange(target)) { // Check that garrisoning succeeds if (cmpGarrisonHolder.Garrison(this.entity)) { this.isGarrisoned = true; // Check if we are garrisoned in a dropsite var cmpResourceDropsite = Engine.QueryInterface(target, InterfaceId.ResourceDropsite) as ICmpResourceDropsite; if (cmpResourceDropsite != null) { // Dump any resources we can var dropsiteTypes = cmpResourceDropsite.GetTypes(); var cmpResourceGatherer = Engine.QueryInterface(this.entity, InterfaceId.ResourceGatherer) as ICmpResourceGatherer; if (cmpResourceGatherer != null) { cmpResourceGatherer.CommitResources(dropsiteTypes); } } return false; } } else { // Unable to reach the target, try again // (or follow if it's a moving target) if (this.MoveToTarget(target)) { this.SetNextState("APPROACHING"); return false; } } } // Garrisoning failed for some reason, so finish the order this.FinishOrder(); return true; }); stateINDIVIDUAL_GARRISON_GARRISONED.Handlers["Order.Ungarrison"] = new Action((msg) => { if (this.FinishOrder()) { return; } }); stateINDIVIDUAL_GARRISON_GARRISONED.Handlers["leave"] = new Action((msg) => { this.isGarrisoned = false; }); } } { var stateINDIVIDUAL_CHEERING = stateINDIVIDUAL.SubStates["CHEERING"] = new StateNode(); stateINDIVIDUAL_CHEERING.Handlers["enter"] = new Func((msg) => { // Unit is invulnerable while cheering var cmpDamageReceiver = Engine.QueryInterface(this.entity, InterfaceId.Armour) as ICmpArmour; cmpDamageReceiver.SetInvulnerability(true); this.SelectAnimation("promotion"); this.StartTimer(4000, 4000); return false; }); stateINDIVIDUAL_CHEERING.Handlers["leave"] = new Action((msg) => { this.StopTimer(); var cmpDamageReceiver = Engine.QueryInterface(this.entity, InterfaceId.Armour) as ICmpArmour; cmpDamageReceiver.SetInvulnerability(false); }); stateINDIVIDUAL_CHEERING.Handlers["Timer"] = new Action((msg) => { this.FinishOrder(); }); } } { var stateANIMAL = stateROOT.SubStates["ANIMAL"] = new StateNode(); stateANIMAL.Handlers["Attacked"] = new Action((msg) => { if (this.template["NaturalBehaviour"].ToString() == "skittish" || this.template["NaturalBehaviour"].ToString() == "passive") { this.Flee((uint)msg.data.attacker, false); } else if (this.IsDangerousAnimal() || this.template["NaturalBehaviour"].ToString() == "defensive") { if (this.CanAttack((uint)msg.data.attacker, false)) { this.Attack((uint)msg.data.attacker, false); } } else if (this.template["NaturalBehaviour"].ToString() == "domestic") { // Never flee, stop what we were doing this.SetNextState("IDLE"); } }); stateANIMAL.Handlers["Order.LeaveFoundation"] = new Action((msg) => { // Run away from the foundation this.MoveToTargetRangeExplicit(msg.data.target, this.template["FleeDistance"].ToFloat(), this.template["FleeDistance"].ToFloat()); this.SetNextState("FLEEING"); }); { var stateANIMAL_IDLE = stateANIMAL.SubStates["IDLE"] = new StateNode(); } { var stateANIMAL_ROAMING = stateANIMAL.SubStates["ROAMING"] = new StateNode(); } { var stateANIMAL_FEEDING = stateANIMAL.SubStates["FEEDING"] = new StateNode(); } { var stateANIMAL_FLEEING = stateANIMAL.SubStates["FLEEING"] = new StateNode(); } { var stateANIMAL_COMBAT = stateANIMAL.SubStates["COMBAT"] = new StateNode(); } { var stateANIMAL_WALKING = stateANIMAL.SubStates["WALKING"] = new StateNode(); } } UnitFsm = new FSM(stateROOT); } public override void Init(CParamNode paramNode) { this.orderQueue = new List(); // current order is at the front of the list this.order = null; // always == this.orderQueue[0] this.formationController = CComponentManager.INVALID_ENTITY; // entity with IID_Formation that we belong to this.isGarrisoned = false; this.isIdle = false; this.lastFormationName = ""; // For preventing increased action rate due to Stop orders or target death. this.lastAttacked = null; this.lastHealed = null; this.SetStance(this.template["DefaultStance"].ToString()); } public override void HandleMessage(CMessage msg, bool global) { //OnVisionRangeChanged(CMessage msg); switch (msg.GetTypeID()) { case MessageTypeId.Create: OnCreate(); break; case MessageTypeId.Destroy: OnDestroy(); break; case MessageTypeId.OwnershipChanged: OnOwnershipChanged(msg as CMessageOwnershipChanged); break; case MessageTypeId.DiplomacyChanged: OnDiplomacyChanged(msg as CMessageDiplomacyChanged); break; case MessageTypeId.MotionChanged: OnMotionChanged(msg as CMessageMotionChanged); break; case MessageTypeId.ConstructionFinished: OnGlobalConstructionFinished(msg as CMessageConstructionFinished); break; case MessageTypeId.EntityRenamed: OnGlobalEntityRenamed(msg as CMessageEntityRenamed); break; case MessageTypeId.Attacked: OnAttacked(msg as CMessageAttacked); break; case MessageTypeId.HealthChanged: OnHealthChanged(msg as CMessageHealthChanged); break; case MessageTypeId.RangeUpdate: OnRangeUpdate(msg as CMessageRangeUpdate); break; } } public override bool IsFormationController() { return string.Equals(this.template["FormationController"], "true", StringComparison.OrdinalIgnoreCase); } public override bool IsAnimal() { return (this.template["NaturalBehaviour"].IsOk() ? true : false); } public override bool IsDangerousAnimal() { return (this.IsAnimal() && (this.template["NaturalBehaviour"].ToString().ToLower() == "violent" || this.template["NaturalBehaviour"].ToString().ToLower() == "aggressive")); } public override bool IsDomestic() { var cmpIdentity = Engine.QueryInterface(this.entity, InterfaceId.Identity) as ICmpIdentity; if (cmpIdentity == null) { return false; } return cmpIdentity.HasClass("Domestic"); } public override bool IsHealer() { return Engine.QueryInterface(this.entity, InterfaceId.Heal) != null; } public override bool IsIdle() { return this.isIdle; } public override bool IsGarrisoned() { return this.isGarrisoned; } public override bool IsWalking() { var state = this.GetCurrentState().Split(new[] { "." }, StringSplitOptions.RemoveEmptyEntries).LastOrDefault(); return (state == "WALKING"); } public override bool CanAttackGaia() { var cmpAttack = Engine.QueryInterface(this.entity, InterfaceId.Attack); if (cmpAttack == null) { return false; } // Rejects Gaia (0) and INVALID_PLAYER (-1) var cmpOwnership = Engine.QueryInterface(this.entity, InterfaceId.Ownership) as ICmpOwnership; if (cmpOwnership == null || cmpOwnership.GetOwner() <= 0) { return false; } return true; } void OnCreate() { if (this.IsAnimal()) { UnitFsm.Init(this, "ANIMAL.FEEDING"); } else if (this.IsFormationController()) { UnitFsm.Init(this, "FORMATIONCONTROLLER.IDLE"); } else { UnitFsm.Init(this, "INDIVIDUAL.IDLE"); } } void OnDiplomacyChanged(CMessageDiplomacyChanged msg) { var cmpOwnership = Engine.QueryInterface(this.entity, InterfaceId.Ownership) as ICmpOwnership; if (cmpOwnership != null && cmpOwnership.GetOwner() == msg.player) { this.SetupRangeQuery(); } } void OnOwnershipChanged(CMessageOwnershipChanged msg) { this.SetupRangeQueries(); // If the unit isn't being created or dying, clear orders and reset stance. if (msg.to != -1 && msg.from != -1) { this.SetStance(this.template["DefaultStance"].ToString()); this.Stop(false); } } void OnDestroy() { // Clean up any timers that are now obsolete this.StopTimer(); // Clean up range queries var rangeMan = Engine.QueryInterface(InterfaceId.RangeManager) as ICmpRangeManager; if (this.losRangeQuery != null) { rangeMan.DestroyActiveQuery(this.losRangeQuery.Value); } if (this.losHealRangeQuery != null) { rangeMan.DestroyActiveQuery(this.losHealRangeQuery.Value); } if (this.losGaiaRangeQuery != null) { rangeMan.DestroyActiveQuery(this.losGaiaRangeQuery.Value); } } void OnVisionRangeChanged(CMessage msg) { // Update range queries //if (this.entity == msg.entity) //{ // this.SetupRangeQueries(); //} } // Wrapper function that sets up the normal, healer, and Gaia range queries. public override void SetupRangeQueries() { this.SetupRangeQuery(); if (this.IsHealer()) this.SetupHealRangeQuery(); if (this.CanAttackGaia() || this.losGaiaRangeQuery != null) this.SetupGaiaRangeQuery(); } // Set up a range query for all enemy units within LOS range // which can be attacked. // This should be called whenever our ownership changes. public override void SetupRangeQuery() { var cmpOwnership = Engine.QueryInterface(this.entity, InterfaceId.Ownership) as ICmpOwnership; var owner = cmpOwnership.GetOwner(); var rangeMan = Engine.QueryInterface(InterfaceId.RangeManager) as ICmpRangeManager; var playerMan = Engine.QueryInterface(InterfaceId.PlayerManager) as ICmpPlayerManager; if (this.losRangeQuery != null) { rangeMan.DestroyActiveQuery(this.losRangeQuery.Value); this.losRangeQuery = null; } var players = new List(); if (owner != -1) { // If unit not just killed, get enemy players via diplomacy var cmpPlayer = Engine.QueryInterface(playerMan.GetPlayerByID(owner), InterfaceId.Player) as ICmpPlayer; 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 (cmpPlayer.IsEnemy(i)) players.Add(i); } } var range = this.GetQueryRange(InterfaceId.Attack); this.losRangeQuery = rangeMan.CreateActiveQuery(this.entity, range.Min, range.Max, players.ToArray(), InterfaceId.Armour, rangeMan.GetEntityFlagMask("normal")); rangeMan.EnableActiveQuery(this.losRangeQuery.Value); } // Set up a range query for all own or ally units within LOS range // which can be healed. // This should be called whenever our ownership changes. public override void SetupHealRangeQuery() { var cmpOwnership = Engine.QueryInterface(this.entity, InterfaceId.Ownership) as ICmpOwnership; var owner = cmpOwnership.GetOwner(); var rangeMan = Engine.QueryInterface(InterfaceId.RangeManager) as ICmpRangeManager; var playerMan = Engine.QueryInterface(InterfaceId.PlayerManager) as ICmpPlayerManager; if (this.losHealRangeQuery != null) { rangeMan.DestroyActiveQuery(this.losHealRangeQuery.Value); } var players = new List(); players.Add(owner); if (owner != -1) { // If unit not just killed, get ally players via diplomacy var cmpPlayer = Engine.QueryInterface(playerMan.GetPlayerByID(owner), InterfaceId.Player) as ICmpPlayer; var numPlayers = playerMan.GetNumPlayers(); for (var i = 1; i < numPlayers; ++i) { // Exclude gaia and enemies if (cmpPlayer.IsAlly(i)) { players.Add(i); } } } var range = this.GetQueryRange(InterfaceId.Heal); this.losHealRangeQuery = rangeMan.CreateActiveQuery(this.entity, range.Min, range.Max, players.ToArray(), InterfaceId.Health, rangeMan.GetEntityFlagMask("injured")); rangeMan.EnableActiveQuery(this.losHealRangeQuery.Value); } // Set up a range query for Gaia units within LOS range which can be attacked. // This should be called whenever our ownership changes. public override void SetupGaiaRangeQuery() { var cmpOwnership = Engine.QueryInterface(this.entity, InterfaceId.Ownership) as ICmpOwnership; var owner = cmpOwnership.GetOwner(); var rangeMan = Engine.QueryInterface(InterfaceId.RangeManager) as ICmpRangeManager; var playerMan = Engine.QueryInterface(InterfaceId.PlayerManager) as ICmpPlayerManager; if (this.losGaiaRangeQuery != null) { rangeMan.DestroyActiveQuery(this.losGaiaRangeQuery.Value); this.losGaiaRangeQuery = null; } // Only create the query if Gaia is our enemy and we can attack. if (this.CanAttackGaia()) { var range = this.GetQueryRange(InterfaceId.Attack); // This query is only interested in Gaia entities that can attack. this.losGaiaRangeQuery = rangeMan.CreateActiveQuery(this.entity, range.Min, range.Max, new[] { 0 }, InterfaceId.Attack, rangeMan.GetEntityFlagMask("normal")); rangeMan.EnableActiveQuery(this.losGaiaRangeQuery.Value); } } public override void SetNextState(string state) { UnitFsm.SetNextState(this, state); } public override void SetNextStateAlwaysEntering(string state) { UnitFsm.SetNextStateAlwaysEntering(this, state); } public override void DeferMessage(OrderItem msg) { UnitFsm.DeferMessage(this, msg); } public override string GetCurrentState() { return UnitFsm.GetCurrentState(this); } public override void FsmStateNameChanged(string state) { Engine.PostMessage(this.entity, new CMessageUnitAIStateChanged { to = state }); } public override bool FinishOrder() { if (this.orderQueue.Count == 0) { var cmpTemplateManager = Engine.QueryInterface(InterfaceId.TemplateManager) as ICmpTemplateManager; var template = cmpTemplateManager.GetCurrentTemplateName(this.entity); throw new Exception("FinishOrder called for entity " + this.entity + " (" + template + ") when order queue is empty"); } this.orderQueue.shift(); if (this.orderQueue.Count > 0) { this.order = this.orderQueue[0]; } else { this.order = null; } if (this.orderQueue.Count != 0) { var msg = new ZeroAD.Components.CCmpUnitAI.OrderItem { type = "Order." + this.order.type, data = this.order.data }; var ret = UnitFsm.ProcessMessage(this, msg); Engine.PostMessage(this.entity, new CMessageUnitAIOrderDataChanged { to = this.GetOrderData() }); // If the order was rejected then immediately take it off // and process the remaining queue if (ret != null && ret is bool && (bool)ret) { return this.FinishOrder(); } // Otherwise we've successfully processed a new order return true; } else { this.SetNextState("IDLE"); return false; } } public override void PushOrder(string type, AllInOneData data) { var order = new OrderItem { type = type, data = data }; this.orderQueue.Add(order); // If we didn't already have an order, then process this new one if (this.orderQueue.Count == 1) { this.order = order; var ret = UnitFsm.ProcessMessage(this, new OrderItem { type = "Order." + this.order.type, data = this.order.data }); Engine.PostMessage(this.entity, new CMessageUnitAIOrderDataChanged { to = this.GetOrderData() }); // If the order was rejected then immediately take it off // and process the remaining queue if (ret != null && ret is bool && (bool)ret) { this.FinishOrder(); } } } public override void PushOrderFront(string type, AllInOneData data) { var order = new OrderItem { type = type, data = data }; // If current order is cheering then add new order after it if (this.order != null && this.order.type == "Cheering") { var cheeringOrder = this.orderQueue.shift(); this.orderQueue.unshift(cheeringOrder, order); } else { this.orderQueue.unshift(order); this.order = order; var ret = UnitFsm.ProcessMessage(this, new OrderItem { type = "Order." + this.order.type, data = this.order.data }); Engine.PostMessage(this.entity, new CMessageUnitAIOrderDataChanged { to = this.GetOrderData() }); // If the order was rejected then immediately take it off again; // assume the previous active order is still valid (the short-lived // new order hasn't changed state or anything) so we can carry on // as if nothing had happened if (ret != null && ret is bool && (bool)ret) { this.orderQueue.shift(); this.order = this.orderQueue[0]; } } } public override void ReplaceOrder(string type, AllInOneData data) { // If current order is cheering then add new order after it if (this.order != null && this.order.type == "Cheering") { var order = new OrderItem { type = type, data = data }; var cheeringOrder = this.orderQueue.shift(); this.orderQueue = new List(new[] { cheeringOrder, order }); } else { this.orderQueue = new List(); this.PushOrder(type, data); } } public override OrderItem[] GetOrders() { return orderQueue.ToArray(); } public override void AddOrders(IEnumerable orders) { foreach (var order in orders) { this.PushOrder(order.type, order.data); } } public override List GetOrderData() { var orders = new List(); foreach (var i in this.orderQueue) { if (!object.ReferenceEquals(i.data, null)) { orders.Add(i.data); } } return orders; } public override void TimerHandler(dynamic data, int lateness) { // Reset the timer if (data.timerRepeat == null) { this.timer = null; } UnitFsm.ProcessMessage(this, new OrderItem { type = "Timer", data = data, lateness = lateness }); } public override void StartTimer(float offset, float? repeat) { if (this.timer.HasValue) { // error("Called StartTimer when there's already an active timer"); } var data = new { timerRepeat = repeat }; var cmpTimer = Engine.QueryInterface(InterfaceId.Timer) as ICmpTimer; if (repeat == null) { this.timer = cmpTimer.SetTimeout(this.entity, InterfaceId.UnitAI, () => TimerHandler(data, 0), offset); } else { this.timer = cmpTimer.SetInterval(this.entity, InterfaceId.UnitAI, () => TimerHandler(data, 0), offset, repeat.Value); } } public override void StopTimer() { if (this.timer == null) { return; } var cmpTimer = Engine.QueryInterface(InterfaceId.Timer) as ICmpTimer; cmpTimer.CancelTimer(this.timer.Value); this.timer = null; } void OnMotionChanged(CMessageMotionChanged msg) { if (msg.starting && !msg.error) { UnitFsm.ProcessMessage(this, new OrderItem { type = "MoveStarted", data = msg }); } else if (!msg.starting || msg.error) { UnitFsm.ProcessMessage(this, new OrderItem { type = "MoveCompleted", data = msg }); } } void OnGlobalConstructionFinished(CMessageConstructionFinished 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, new OrderItem { type = "ConstructionFinished", data = msg }); } void OnGlobalEntityRenamed(CMessageEntityRenamed msg) { UnitFsm.ProcessMessage(this, new OrderItem{ type = "EntityRenamed", entity = msg.entity, newentity = msg.newentity }); } void OnAttacked(CMessageAttacked msg) { UnitFsm.ProcessMessage(this, new OrderItem { type = "Attacked", data = msg }); } void OnHealthChanged(CMessageHealthChanged msg) { UnitFsm.ProcessMessage(this, new OrderItem { type = "HealthChanged", from = msg.from, to = msg.to }); } void OnRangeUpdate(CMessageRangeUpdate msg) { if (msg.tag == this.losRangeQuery) { UnitFsm.ProcessMessage(this, new OrderItem { type = "LosRangeUpdate", data = msg }); } else if (msg.tag == this.losGaiaRangeQuery) { UnitFsm.ProcessMessage(this, new OrderItem { type = "LosGaiaRangeUpdate", data = msg }); } else if (msg.tag == this.losHealRangeQuery) { UnitFsm.ProcessMessage(this, new OrderItem { type = "LosHealRangeUpdate", data = msg }); } } public override float GetWalkSpeed() { var cmpUnitMotion = Engine.QueryInterface(this.entity, InterfaceId.UnitMotion) as ICmpUnitMotion; return cmpUnitMotion.GetWalkSpeed(); } public override float GetRunSpeed() { var cmpUnitMotion = Engine.QueryInterface(this.entity, InterfaceId.UnitMotion) as ICmpUnitMotion; return cmpUnitMotion.GetRunSpeed(); } public override bool TargetIsAlive(uint ent) { var cmpHealth = Engine.QueryInterface(ent, InterfaceId.Health) as ICmpHealth; if (cmpHealth == null) { return false; } return (cmpHealth.GetHitpoints() != 0); } public override bool MustKillGatherTarget(uint ent) { var cmpResourceSupply = Engine.QueryInterface(ent, InterfaceId.ResourceSupply) as ICmpResourceSupply; if (cmpResourceSupply == null) { return false; } if (!cmpResourceSupply.GetKillBeforeGather()) { return false; } return this.TargetIsAlive(ent); } public override uint? FindNearbyResource(Func filter) { var range = 64; // TODO: what's a sensible number? var playerMan = Engine.QueryInterface(InterfaceId.PlayerManager) as ICmpPlayerManager; // We accept resources owned by Gaia or any player var players = new List(0); players.Add(0); for (var i = 1; i < playerMan.GetNumPlayers(); ++i) { players.Add(i); } var cmpTemplateManager = Engine.QueryInterface(InterfaceId.TemplateManager) as ICmpTemplateManager; var cmpRangeManager = Engine.QueryInterface(InterfaceId.RangeManager) as ICmpRangeManager; var nearby = cmpRangeManager.ExecuteQuery(this.entity, 0, range, players, InterfaceId.ResourceSupply); foreach (var ent in nearby) { var cmpResourceSupply = Engine.QueryInterface(ent, InterfaceId.ResourceSupply) as ICmpResourceSupply; var type = cmpResourceSupply.GetType(); var amount = cmpResourceSupply.GetCurrentAmount(); var template = cmpTemplateManager.GetCurrentTemplateName(ent); // Remove "resource|" prefix from template names, if present. if (template.IndexOf("resource|") != -1) { template = template.Substring(9); } if (amount > 0 && filter(ent, type, template)) { return ent; } } return null; } public override uint? FindNearestDropsite(string genericType) { // Find dropsites owned by this unit's player var players = new List(); var cmpOwnership = Engine.QueryInterface(this.entity, InterfaceId.Ownership) as ICmpOwnership; if (cmpOwnership != null) { players.Add(cmpOwnership.GetOwner()); } // Ships are unable to reach land dropsites and shouldn't attempt to do so. var excludeLand = (Engine.QueryInterface(this.entity, InterfaceId.Identity) as ICmpIdentity).HasClass("Ship"); var rangeMan = Engine.QueryInterface(InterfaceId.RangeManager) as ICmpRangeManager; var nearby = rangeMan.ExecuteQuery(this.entity, 0, -1, players, InterfaceId.ResourceDropsite); if (excludeLand) { nearby = nearby.Where((e) => { var iid = Engine.QueryInterface(e, InterfaceId.Identity) as ICmpIdentity; return iid != null && iid.HasClass("Naval"); }).ToList(); } foreach (var ent in nearby) { var cmpDropsite = Engine.QueryInterface(ent, InterfaceId.ResourceDropsite) as ICmpResourceDropsite; if (!cmpDropsite.AcceptsType(genericType)) continue; return ent; } return null; } public override uint? FindNearbyFoundation() { var range = 64; // TODO: what's a sensible number? // Find buildings owned by this unit's player var players = new List(); var cmpOwnership = Engine.QueryInterface(this.entity, InterfaceId.Ownership) as ICmpOwnership; if (cmpOwnership != null) { players.Add(cmpOwnership.GetOwner()); } var rangeMan = Engine.QueryInterface(InterfaceId.RangeManager) as ICmpRangeManager; var nearby = rangeMan.ExecuteQuery(this.entity, 0, range, players, InterfaceId.Foundation); foreach (var ent in nearby) { // Skip foundations that are already complete. (This matters since // we process the ConstructionFinished message before the foundation // we're working on has been deleted.) var cmpFoundation = Engine.QueryInterface(ent, InterfaceId.Foundation) as ICmpFoundation; if (cmpFoundation.IsFinished()) { continue; } return ent; } return null; } public override void PlaySound(string name) { // If we're a formation controller, use the sounds from our first member if (this.IsFormationController()) { var cmpFormation = Engine.QueryInterface(this.entity, InterfaceId.Formation) as ICmpFormation; var member = cmpFormation.GetPrimaryMember(); if (member != 0) { SoundHelper.PlaySound(Engine, name, member); } } else { // Otherwise use our own sounds SoundHelper.PlaySound(Engine, name, this.entity); } } public override void SetGathererAnimationOverride(bool disable) { var cmpResourceGatherer = Engine.QueryInterface(this.entity, InterfaceId.ResourceGatherer) as ICmpResourceGatherer; if (cmpResourceGatherer == null) return; var cmpVisual = Engine.QueryInterface(this.entity, InterfaceId.Visual) as ICmpVisual; if (cmpVisual == null) return; // Remove the animation override, so that weapons are shown again. if (disable) { cmpVisual.ResetMoveAnimation("walk"); return; } // Work out what we're carrying, in order to select an appropriate animation var type = cmpResourceGatherer.GetLastCarriedType(); if (type != null) { var typename = "carry_" + type.generic; // Special case for meat if (type.specific == "meat") { typename = "carry_" + type.specific; } cmpVisual.ReplaceMoveAnimation("walk", typename); } else { cmpVisual.ResetMoveAnimation("walk"); } } public override void SelectAnimation(string name, bool once = false, float speed = 1.0f, string sound = null) { var cmpVisual = Engine.QueryInterface(this.entity, InterfaceId.Visual) as ICmpVisual; if (cmpVisual == null) { return; } // 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; } string soundgroup = null; if (!string.IsNullOrEmpty(sound)) { var cmpSound = Engine.QueryInterface(this.entity, InterfaceId.Sound) as ICmpSound; if (cmpSound != null) { soundgroup = cmpSound.GetSoundGroup(sound); } } // Set default values if unspecified if (soundgroup == null) { soundgroup = ""; } cmpVisual.SelectAnimation(name, once, speed, soundgroup); } public override void SetAnimationSync(int actiontime, int repeattime) { var cmpVisual = Engine.QueryInterface(this.entity, InterfaceId.Visual) as ICmpVisual; if (cmpVisual == null) { return; } cmpVisual.SetAnimationSyncRepeat(repeattime); cmpVisual.SetAnimationSyncOffset(actiontime); } public override void StopMoving() { var cmpUnitMotion = Engine.QueryInterface(this.entity, InterfaceId.UnitMotion) as ICmpUnitMotion; cmpUnitMotion.StopMoving(); } public override bool MoveToPoint(float x, float z) { var cmpUnitMotion = Engine.QueryInterface(this.entity, InterfaceId.UnitMotion) as ICmpUnitMotion; return cmpUnitMotion.MoveToPointRange(x, z, 0, 0); } public override bool MoveToPointRange(float x, float z, float rangeMin, float rangeMax) { var cmpUnitMotion = Engine.QueryInterface(this.entity, InterfaceId.UnitMotion) as ICmpUnitMotion; return cmpUnitMotion.MoveToPointRange(x, z, rangeMin, rangeMax); } public override bool MoveToTarget(uint target) { if (!this.CheckTargetVisible(target)) { return false; } var cmpUnitMotion = Engine.QueryInterface(this.entity, InterfaceId.UnitMotion) as ICmpUnitMotion; return cmpUnitMotion.MoveToTargetRange(target, 0, 0); } public override bool MoveToTargetRange(uint target, InterfaceId iid, string type = null) { if (!this.CheckTargetVisible(target)) { return false; } var cmpRanged = Engine.QueryInterface(this.entity, iid) as IRangedComponent; var range = cmpRanged.GetRange(type); var cmpUnitMotion = Engine.QueryInterface(this.entity, InterfaceId.UnitMotion) as ICmpUnitMotion; return cmpUnitMotion.MoveToTargetRange(target, range.Min, range.Max); } public override bool MoveToTargetRangeExplicit(uint target, float min, float max) { if (!this.CheckTargetVisible(target)) { return false; } var cmpUnitMotion = Engine.QueryInterface(this.entity, InterfaceId.UnitMotion) as ICmpUnitMotion; return cmpUnitMotion.MoveToTargetRange(target, min, max); } public override bool CheckTargetRange(uint target, InterfaceId iid, string type = null) { var cmpRanged = Engine.QueryInterface(this.entity, iid) as IRangedComponent; var range = cmpRanged.GetRange(type); var cmpUnitMotion = Engine.QueryInterface(this.entity, InterfaceId.UnitMotion) as ICmpUnitMotion; return cmpUnitMotion.IsInTargetRange(target, range.Min, range.Max); } public override bool CheckTargetRangeExplicit(uint target, float min, float max) { var cmpUnitMotion = Engine.QueryInterface(this.entity, InterfaceId.UnitMotion) as ICmpUnitMotion; return cmpUnitMotion.IsInTargetRange(target, min, max); } public override bool CheckGarrisonRange(uint target) { var cmpGarrisonHolder = Engine.QueryInterface(target, InterfaceId.GarrisonHolder) as ICmpGarrisonHolder; var range = cmpGarrisonHolder.GetLoadingRange(); var cmpUnitMotion = Engine.QueryInterface(this.entity, InterfaceId.UnitMotion) as ICmpUnitMotion; return cmpUnitMotion.IsInTargetRange(target, range.Min, range.Max); } public override bool CheckTargetVisible(uint target) { var cmpOwnership = Engine.QueryInterface(this.entity, InterfaceId.Ownership) as ICmpOwnership; if (cmpOwnership == null) { return false; } var cmpRangeManager = Engine.QueryInterface(InterfaceId.RangeManager) as ICmpRangeManager; if (cmpRangeManager == null) { return false; } if (cmpRangeManager.GetLosVisibility(target, cmpOwnership.GetOwner(), false) == ICmpRangeManager.ELosVisibility.VIS_HIDDEN) { return false; } // Either visible directly, or visible in fog return true; } public override void FaceTowardsTarget(uint target) { var cmpPosition = Engine.QueryInterface(this.entity, InterfaceId.Position) as ICmpPosition; if (cmpPosition == null || !cmpPosition.IsInWorld()) { return; } var cmpTargetPosition = Engine.QueryInterface(target, InterfaceId.Position) as ICmpPosition; if (cmpTargetPosition == null || !cmpTargetPosition.IsInWorld()) { return; } var pos = cmpPosition.GetPosition(); var targetpos = cmpTargetPosition.GetPosition(); var angle = Math.Atan2(targetpos.X - pos.X, targetpos.Z - pos.Z); var rot = cmpPosition.GetRotation(); var delta = (rot.Y - angle + Math.PI) % (2 * Math.PI) - Math.PI; if (Math.Abs(delta) > 0.2) { var cmpUnitMotion = Engine.QueryInterface(this.entity, InterfaceId.UnitMotion) as ICmpUnitMotion; if (cmpUnitMotion != null) { cmpUnitMotion.FaceTowardsPoint(targetpos.X, targetpos.Z); } } } public override bool CheckTargetDistanceFromHeldPosition(uint target, InterfaceId iid, string type) { var cmpRanged = Engine.QueryInterface(this.entity, iid) as IRangedComponent; //var range = iid != InterfaceId.Attack ? cmpRanged.GetRange() : cmpRanged.GetRange(type); var range = cmpRanged.GetRange(type); var cmpPosition = Engine.QueryInterface(target, InterfaceId.Position) as ICmpPosition; if (cmpPosition == null || !cmpPosition.IsInWorld()) { return false; } var cmpVision = Engine.QueryInterface(this.entity, InterfaceId.Vision) as ICmpVision; if (cmpVision == null) { return false; } var halfvision = cmpVision.GetRange() / 2; var pos = cmpPosition.GetPosition(); var dx = this.heldPosition.Value.X - pos.X; var dz = this.heldPosition.Value.Z - pos.Z; var dist = Math.Sqrt(dx * dx + dz * dz); return dist < halfvision + range.Max; } public override bool CheckTargetIsInVisionRange(uint target) { var cmpVision = Engine.QueryInterface(this.entity, InterfaceId.Vision) as ICmpVision; if (cmpVision == null) { return false; } var range = cmpVision.GetRange(); var distance = EntityHelper.DistanceBetweenEntities(Engine, this.entity, target); return distance < range; } public override string GetBestAttack() { var cmpAttack = Engine.QueryInterface(this.entity, InterfaceId.Attack) as ICmpAttack; if (cmpAttack == null) { return null; } return cmpAttack.GetBestAttack(); } public override string GetBestAttackAgainst(uint target) { var cmpAttack = Engine.QueryInterface(this.entity, InterfaceId.Attack) as ICmpAttack; if (cmpAttack == null) { return null; } return cmpAttack.GetBestAttackAgainst(target); } public override bool AttackVisibleEntity(IEnumerable ents, bool forceResponse) { foreach (var target in ents) { if (this.CanAttack(target, forceResponse)) { this.PushOrderFront("Attack", new AllInOneData { target = target, force = false, forceResponse = forceResponse }); return true; } } return false; } public override bool AttackEntityInZone(IEnumerable ents, bool forceResponse) { foreach (var target in ents) { var type = this.GetBestAttackAgainst(target); if (this.CanAttack(target, forceResponse) && this.CheckTargetDistanceFromHeldPosition(target, InterfaceId.Attack, type)) { this.PushOrderFront("Attack", new AllInOneData { target = target, force = false, forceResponse = forceResponse }); return true; } } return false; } public override bool RespondToTargetedEntities(uint[] ents) { if (ents.Length == 0) { return false; } if (this.GetStance().respondChase) return this.AttackVisibleEntity(ents, true); if (this.GetStance().respondStandGround) return this.AttackVisibleEntity(ents, true); if (this.GetStance().respondHoldGround) return this.AttackEntityInZone(ents, true); if (this.GetStance().respondFlee) { this.PushOrderFront("Flee", new AllInOneData { target = ents[0], force = false }); return true; } return false; } public override bool RespondToHealableEntities(uint[] ents) { if (ents.Length == 0) { return false; } foreach (var ent in ents) { if (this.CanHeal(ent)) { this.PushOrderFront("Heal", new AllInOneData { target = ent, force = false }); return true; } } return false; } public override bool ShouldAbandonChase(uint target, bool force, InterfaceId iid) { // Forced orders shouldn't be interrupted. if (force) { return false; } // Stop if we're in hold-ground mode and it's too far from the holding point if (this.GetStance().respondHoldGround) { if (!this.CheckTargetDistanceFromHeldPosition(target, iid, this.attackType)) { return true; } } // Stop if it's left our vision range, unless we're especially persistent if (!this.GetStance().respondChaseBeyondVision) { if (!this.CheckTargetIsInVisionRange(target)) { return true; } } // (Note that CCmpUnitMotion will detect if the target is lost in FoW, // and will continue moving to its last seen position and then stop) return false; } public override bool ShouldChaseTargetedEntity(uint target, bool force) { if (this.GetStance().respondChase) { return true; } if (force) { return true; } return false; } public override void SetFormationController(uint 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, InterfaceId.Obstruction) as ICmpObstruction; if (cmpObstruction != null) { if (ent == CComponentManager.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 == CComponentManager.INVALID_ENTITY) { UnitFsm.ProcessMessage(this, new OrderItem { type = "FormationLeave" }); } } public override uint GetFormationController() { return this.formationController; } public override void SetLastFormationName(string name) { this.lastFormationName = name; } public override string GetLastFormationName() { return this.lastFormationName; } public override float ComputeWalkingDistance() { var distance = 0.0f; var cmpPosition = Engine.QueryInterface(this.entity, InterfaceId.Position) as ICmpPosition; if (cmpPosition == null || !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.Count; ++i) { var order = this.orderQueue[i]; switch (order.type) { case "Walk": case "GatherNearPosition": // Add the distance to the target point var dx0 = (float)order.data.x - pos.X; var dz0 = (float)order.data.z - pos.Z; var d0 = (float)Math.Sqrt(dx0 * dx0 + dz0 * dz0); distance += d0; // Remember this as the start position for the next order pos = new CVector3D(order.data.x, 0, order.data.z); break; // and continue the loop case "WalkToTarget": case "WalkToTargetRange": // This doesn't move to the target (just into range), but a later order will. case "Flee": case "LeaveFoundation": case "Attack": case "Heal": case "Gather": case "ReturnResource": case "Repair": case "Garrison": { // Find the target unit's position var cmpTargetPosition = Engine.QueryInterface(order.data.target, InterfaceId.Position) as ICmpPosition; if (cmpTargetPosition == null || !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 = (float)Math.Sqrt(dx * dx + dz * dz); distance += d; // Return the total distance to the target return distance; } case "Stop": { return 0; } default: { //error("ComputeWalkingDistance: Unrecognised order type '" + order.type + "'"); return distance; } } } // Return the total distance to the end of the order queue return distance; } public override void AddOrder(string type, dynamic data, bool queued) { if (queued) { this.PushOrder(type, data); } else { this.ReplaceOrder(type, data); } } public override void Walk(float x, float z, bool queued) { this.AddOrder("Walk", new AllInOneData { x = x, z = z, force = true }, queued); } /// /// Adds stop order to queue, forced by the player. /// public override void Stop(bool queued) { this.AddOrder("Stop", null, queued); } public override void WalkToTarget(uint target, bool queued) { this.AddOrder("WalkToTarget", new AllInOneData { target = target, force = true }, queued); } public override void LeaveFoundation(uint target) { // If we're already being told to leave a foundation, then // ignore this new request so we don't end up being too indecisive // to ever actually move anywhere if (this.order != null && this.order.type == "LeaveFoundation") { return; } this.PushOrderFront("LeaveFoundation", new AllInOneData { target = target, force = true }); } public override void Attack(uint target, bool queued) { if (!this.CanAttack(target, false)) { // We don't want to let healers walk to the target unit so they can be easily killed. // Instead we just let them get into healing range. if (this.IsHealer()) { this.MoveToTargetRange(target, InterfaceId.Heal); } else { this.WalkToTarget(target, queued); } return; } this.AddOrder("Attack", new AllInOneData { target = target, force = true }, queued); } public override void Garrison(uint target, bool queued) { if (!this.CanGarrison(target)) { this.WalkToTarget(target, queued); return; } this.AddOrder("Garrison", new AllInOneData { target = target, force = true }, queued); } public override void Ungarrison() { if (this.IsGarrisoned()) { this.AddOrder("Ungarrison", null, false); } } /// /// Adds gather order to the queue, forced by the player until the target is reached . /// public override void Gather(uint target, bool queued) { this.PerformGather(target, queued, true); } public override void PerformGather(uint target, bool queued, bool force) { if (!this.CanGather(target)) { this.WalkToTarget(target, queued); return; } // 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, InterfaceId.ResourceSupply) as ICmpResourceSupply; var type = cmpResourceSupply.GetType(); // Also save the target entity's template, so that if it's an animal, // we won't go from hunting slow safe animals to dangerous fast ones var cmpTemplateManager = Engine.QueryInterface(InterfaceId.TemplateManager) as ICmpTemplateManager; var template = cmpTemplateManager.GetCurrentTemplateName(target); // Remove "resource|" prefix from template name, if present. if (template.IndexOf("resource|") != -1) { template = template.Substring(9); } // Remember the position of our target, if any, in case it disappears // later and we want to head to its last known position // (TODO: if the target moves a lot (e.g. it's an animal), maybe we // need to update this lastPos regularly rather than just here?) CVector3D? lastPos = null; var cmpPosition = Engine.QueryInterface(target, InterfaceId.Position) as ICmpPosition; if (cmpPosition != null && cmpPosition.IsInWorld()) { lastPos = cmpPosition.GetPosition(); } this.AddOrder("Gather", new AllInOneData { target = target, type = type, template = template, lastPos = lastPos, force = force }, queued); } public override void GatherNearPosition(float x, float z, Name type, string template, bool queued) { // Remove "resource|" prefix from template name, if present. if (template.IndexOf("resource|") != -1) { template = template.Substring(9); } this.AddOrder("GatherNearPosition", new AllInOneData { type = type, template = template, x = x, z = z, force = false }, queued); } public override void Heal(uint target, bool queued) { if (!this.CanHeal(target)) { this.WalkToTarget(target, queued); return; } this.AddOrder("Heal", new AllInOneData { target = target, force = true }, queued); } public override void ReturnResource(uint target, bool queued) { if (!this.CanReturnResource(target, true)) { this.WalkToTarget(target, queued); return; } this.AddOrder("ReturnResource", new AllInOneData { target = target, force = true }, queued); } public override void SetupTradeRoute(uint target, uint source, bool queued) { if (!this.CanTrade(target)) { this.WalkToTarget(target, queued); return; } var cmpTrader = Engine.QueryInterface(this.entity, InterfaceId.Trader) as ICmpTrader; var marketsChanged = cmpTrader.SetTargetMarket(target, source); if (marketsChanged) { if (cmpTrader.HasBothMarkets()) { this.AddOrder("Trade", new AllInOneData { firstMarket = cmpTrader.GetFirstMarket(), secondMarket = cmpTrader.GetSecondMarket(), force = false }, queued); } else { this.WalkToTarget(cmpTrader.GetFirstMarket(), queued); } } } public override bool MoveToMarket(uint targetMarket) { if (this.MoveToTarget(targetMarket)) { // We've started walking to the market return true; } else { // We can't reach the market. // Give up. this.StopMoving(); this.StopTrading(); return false; } } public override void PerformTradeAndMoveToNextMarket(uint currentMarket, uint nextMarket, string nextFsmStateName) { if (!this.CanTrade(currentMarket)) { this.StopTrading(); return; } if (this.CheckTargetRange(currentMarket, InterfaceId.Trader)) { this.PerformTrade(); if (this.MoveToMarket(nextMarket)) { // We've started walking to the next market this.SetNextState(nextFsmStateName); } } else { // If the current market is not reached try again this.MoveToMarket(currentMarket); } } public override void PerformTrade() { var cmpTrader = Engine.QueryInterface(this.entity, InterfaceId.Trader) as ICmpTrader; cmpTrader.PerformTrade(); } public override void StopTrading() { this.FinishOrder(); var cmpTrader = Engine.QueryInterface(this.entity, InterfaceId.Trader) as ICmpTrader; cmpTrader.StopTrading(); } public override void Repair(uint target, bool autocontinue, bool queued) { if (!this.CanRepair(target)) { this.WalkToTarget(target, queued); return; } this.AddOrder("Repair", new AllInOneData { target = target, autocontinue = autocontinue, force = true }, queued); } public override void Flee(uint target, bool queued) { this.AddOrder("Flee", new AllInOneData { target = target, force = false }, queued); } public override void Cheer() { this.AddOrder("Cheering", new AllInOneData { force = true }, false); } public override void SetStance(string stance) { if (g_Stances.ContainsKey(stance)) { this.stance = stance; } else { //error("UnitAI: Setting to invalid stance '" + stance + "'"); } } public override void SwitchToStance(string stance) { var cmpPosition = Engine.QueryInterface(this.entity, InterfaceId.Position) as ICmpPosition; if (cmpPosition == null || !cmpPosition.IsInWorld()) { return; } var pos = cmpPosition.GetPosition(); this.SetHeldPosition(pos.X, pos.Z); this.SetStance(stance); // Stop moving if switching to stand ground // TODO: Also stop existing orders in a sensible way if (stance == "standground") { this.StopMoving(); } // Reset the range queries, since the range depends on stance. this.SetupRangeQueries(); } public override bool FindNewTargets() { if (this.losRangeQuery == null) { return false; } if (!this.GetStance().targetVisibleEnemies) { return false; } var rangeMan = Engine.QueryInterface(InterfaceId.RangeManager) as ICmpRangeManager; if (this.AttackEntitiesByPreference(rangeMan.ResetActiveQuery(this.losRangeQuery.Value))) { return true; } // If no regular enemies were found, attempt to attack a hostile Gaia entity. else if (this.losGaiaRangeQuery != null) { return this.AttackGaiaEntitiesByPreference(rangeMan.ResetActiveQuery(this.losGaiaRangeQuery.Value)); } return false; } public override bool FindNewHealTargets() { if (this.losHealRangeQuery == null) { return false; } var rangeMan = Engine.QueryInterface(InterfaceId.RangeManager) as ICmpRangeManager; var ents = rangeMan.ResetActiveQuery(this.losHealRangeQuery.Value); foreach (var ent in ents) { if (this.CanHeal(ent)) { this.PushOrderFront("Heal", new AllInOneData { target = ent, force = false }); return true; } } // We haven't found any target to heal return false; } public override Range GetQueryRange(InterfaceId iid) { var ret = new Range { Min = 0, Max = 0 }; if (this.GetStance().respondStandGround) { var cmpRanged = Engine.QueryInterface(this.entity, iid) as IRangedComponent; if (cmpRanged == null) { return ret; } //var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetRange(cmpRanged.GetBestAttack()); var range = cmpRanged.GetRange(cmpRanged.GetBestAttack()); ret.Min = range.Min; ret.Max = range.Max; } else if (this.GetStance().respondChase) { var cmpVision = Engine.QueryInterface(this.entity, InterfaceId.Vision) as ICmpVision; if (cmpVision == null) { return ret; } var range = cmpVision.GetRange(); ret.Max = range; } else if (this.GetStance().respondHoldGround) { var cmpRanged = Engine.QueryInterface(this.entity, iid) as IRangedComponent; if (cmpRanged == null) { return ret; } //var range = iid != IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetRange(cmpRanged.GetBestAttack()); var range = cmpRanged.GetRange(cmpRanged.GetBestAttack()); var cmpVision = Engine.QueryInterface(this.entity, InterfaceId.Vision) as ICmpVision; if (cmpVision == null) { return ret; } var halfvision = cmpVision.GetRange() / 2; ret.Max = range.Max + halfvision; } // We probably have stance 'passive' and we wouldn't have a range, // but as it is the default for healers we need to set it to something sane. else if (iid == InterfaceId.Heal) { var cmpVision = Engine.QueryInterface(this.entity, InterfaceId.Vision) as ICmpVision; if (cmpVision == null) { return ret; } var range = cmpVision.GetRange(); ret.Max = range; } return ret; } public override UnitStance GetStance() { return g_Stances[this.stance]; } public override string GetStanceName() { return this.stance; } public override void SetMoveSpeed(float speed) { var cmpMotion = Engine.QueryInterface(this.entity, InterfaceId.UnitMotion) as ICmpUnitMotion; cmpMotion.SetSpeed(speed); } public override void SetHeldPosition(float x, float z) { this.heldPosition = new CVector3D(x, 0, z); } public override CVector3D? GetHeldPosition(object pos) { return this.heldPosition; } public override bool WalkToHeldPosition() { if (this.heldPosition.HasValue) { this.AddOrder("Walk", new AllInOneData { x = this.heldPosition.Value.X, z = this.heldPosition.Value.Z, force = false }, false); return true; } return false; } public override bool CanAttack(uint target, bool forceResponse) { // 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 Attack commands var cmpAttack = Engine.QueryInterface(this.entity, InterfaceId.Attack) as ICmpAttack; if (cmpAttack == null) return false; if (!cmpAttack.CanAttack(target)) return false; // Verify that the target is alive if (!this.TargetIsAlive(target)) return false; var cmpOwnership = Engine.QueryInterface(this.entity, InterfaceId.Ownership) as ICmpOwnership; if (cmpOwnership == null) return false; // Verify that the target is an attackable resource supply like a domestic animal // or that it isn't owned by an ally of this entity's player or is responding to // an attack. var owner = cmpOwnership.GetOwner(); if (!this.MustKillGatherTarget(target) && !(PlayerHelper.IsOwnedByEnemyOfPlayer(Engine, owner, target) || PlayerHelper.IsOwnedByNeutralOfPlayer(Engine, owner, target) || (forceResponse && !PlayerHelper.IsOwnedByPlayer(Engine, owner, target)))) { return false; } return true; } public override bool CanGarrison(uint target) { // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) { return true; } var cmpGarrisonHolder = Engine.QueryInterface(target, InterfaceId.GarrisonHolder) as ICmpGarrisonHolder; if (cmpGarrisonHolder == null) { return false; } // Verify that the target is owned by this entity's player var cmpOwnership = Engine.QueryInterface(this.entity, InterfaceId.Ownership) as ICmpOwnership; if (cmpOwnership == null || !PlayerHelper.IsOwnedByPlayer(Engine, cmpOwnership.GetOwner(), target)) { return false; } // Don't let animals garrison for now // (If we want to support that, we'll need to change Order.Garrison so it // doesn't move the animal into an INVIDIDUAL.* state) if (this.IsAnimal()) { return false; } return true; } public override bool CanGather(uint target) { // The target must be a valid resource supply. var cmpResourceSupply = Engine.QueryInterface(target, InterfaceId.ResourceSupply) as ICmpResourceSupply; if (cmpResourceSupply == null) { return false; } // 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 Gather commands var cmpResourceGatherer = Engine.QueryInterface(this.entity, InterfaceId.ResourceGatherer) as ICmpResourceGatherer; if (cmpResourceGatherer == null) { return false; } // Verify that we can gather from this target if (cmpResourceGatherer.GetTargetGatherRate(target) <= 0) { return false; } // No need to verify ownership as we should be able to gather from // a target regardless of ownership. return true; } public override bool CanHeal(uint 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 Heal commands var cmpHeal = Engine.QueryInterface(this.entity, InterfaceId.Heal) as ICmpHeal; if (cmpHeal == null) { return false; } // Verify that the target is alive if (!this.TargetIsAlive(target)) { return false; } // Verify that the target is owned by the same player as the entity or of an ally var cmpOwnership = Engine.QueryInterface(this.entity, InterfaceId.Ownership) as ICmpOwnership; if (cmpOwnership == null || !(PlayerHelper.IsOwnedByPlayer(Engine, cmpOwnership.GetOwner(), target) || PlayerHelper.IsOwnedByAllyOfPlayer(Engine, cmpOwnership.GetOwner(), target))) { return false; } // Verify that the target is not unhealable (or at max health) var cmpHealth = Engine.QueryInterface(target, InterfaceId.Health) as ICmpHealth; if (cmpHealth == null || cmpHealth.IsUnhealable()) { return false; } // Verify that the target has no unhealable class var cmpIdentity = Engine.QueryInterface(target, InterfaceId.Identity) as ICmpIdentity; if (cmpIdentity == null) { return false; } foreach (var unhealableClass in cmpHeal.GetUnhealableClasses()) { if (cmpIdentity.HasClass(unhealableClass)) { return false; } } // Verify that the target is a healable class var healable = false; foreach (var healableClass in cmpHeal.GetHealableClasses()) { if (cmpIdentity.HasClass(healableClass)) { healable = true; } } if (!healable) { return false; } return true; } public override bool CanReturnResource(uint target, bool checkCarriedResource) { // 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, InterfaceId.ResourceGatherer) as ICmpResourceGatherer; if (cmpResourceGatherer == null) { return false; } // Verify that the target is a dropsite var cmpResourceDropsite = Engine.QueryInterface(target, InterfaceId.ResourceDropsite) as ICmpResourceDropsite; if (cmpResourceDropsite == null) { return false; } if (checkCarriedResource) { // Verify that we are carrying some resources, // and can return our current resource to this target var type = cmpResourceGatherer.GetMainCarryingType(); if (string.IsNullOrEmpty(type) || !cmpResourceDropsite.AcceptsType(type)) { return false; } } // Verify that the dropsite is owned by this entity's player var cmpOwnership = Engine.QueryInterface(this.entity, InterfaceId.Ownership) as ICmpOwnership; if (cmpOwnership == null || !PlayerHelper.IsOwnedByPlayer(Engine, cmpOwnership.GetOwner(), target)) { return false; } return true; } public override bool CanTrade(uint 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 Trade commands var cmpTrader = Engine.QueryInterface(this.entity, InterfaceId.Trader) as ICmpTrader; if (cmpTrader == null || !cmpTrader.CanTrade(target)) { return false; } return true; } public override bool CanRepair(uint 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 Repair (Builder) commands var cmpBuilder = Engine.QueryInterface(this.entity, InterfaceId.Builder) as ICmpBuilder; if (cmpBuilder == null) { return false; } // Verify that the target is owned by an ally of this entity's player var cmpOwnership = Engine.QueryInterface(this.entity, InterfaceId.Ownership) as ICmpOwnership; if (cmpOwnership == null || !PlayerHelper.IsOwnedByAllyOfPlayer(Engine, cmpOwnership.GetOwner(), target)) { return false; } return true; } public override void MoveRandomly(float distance) { // We want to walk in a random direction, but avoid getting stuck // in obstacles or narrow spaces. // So pick a circular range from approximately our current position, // and move outwards to the nearest point on that circle, which will // lead to us avoiding obstacles and moving towards free space. // TODO: we probably ought to have a 'home' point, and drift towards // that, so we don't spread out all across the whole map var cmpPosition = Engine.QueryInterface(this.entity, InterfaceId.Position) as ICmpPosition; if (cmpPosition == null) { return; } if (!cmpPosition.IsInWorld()) { return; } var pos = cmpPosition.GetPosition(); var jitter = 0.5f; Random rnd = new Random(); // Randomly adjust the range's center a bit, so we tend to prefer // moving in random directions (if there's nothing in the way) var tx = pos.X + (2 * (float)rnd.NextDouble() - 1) * jitter; var tz = pos.Z + (2 * (float)rnd.NextDouble() - 1) * jitter; var cmpMotion = Engine.QueryInterface(this.entity, InterfaceId.UnitMotion) as ICmpUnitMotion; cmpMotion.MoveToPointRange(tx, tz, distance, distance); } public override bool AttackEntitiesByPreference(IEnumerable ents) { var cmpAttack = Engine.QueryInterface(this.entity, InterfaceId.Attack) as ICmpAttack; if (cmpAttack == null) { return false; } var xx = ents.Where(v => cmpAttack.CanAttack(v)).ToList(); xx.Sort((a, b) => cmpAttack.CompareEntitiesByPreference(a, b)); return this.RespondToTargetedEntities(xx.ToArray()); } public override bool AttackGaiaEntitiesByPreference(IEnumerable ents) { var cmpAttack = Engine.QueryInterface(this.entity, InterfaceId.Attack) as ICmpAttack; if (cmpAttack == null) { return false; } Func filter = (e) => { var cmpUnitAI = Engine.QueryInterface(e, InterfaceId.UnitAI) as ICmpUnitAI; return (cmpUnitAI != null && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal())); }; var xx = ents.Where(v => cmpAttack.CanAttack(v) && filter(v)).ToList(); xx.Sort((a, b) => cmpAttack.CompareEntitiesByPreference(a, b)); return this.RespondToTargetedEntities(xx.ToArray()); } } }