-
Posts
21 -
Joined
-
Last visited
mahdi's Achievements
Discens (2/14)
2
Reputation
-
Portable 0.A.D with C#, .NET, Windows 8 Modern / RT
mahdi replied to mahdi's topic in Applications and Contributions
Maybe you are taking it wrong ? Or maybe i just understood the legal stuff wrong. My port will be open source when it finished. I am not going to do illegal. I wont sell it if that's not ETHICALLY true. its just that. Look: It is a hobbyist port. And has potentials. -
Portable 0.A.D with C#, .NET, Windows 8 Modern / RT
mahdi replied to mahdi's topic in Applications and Contributions
Already asked -
Portable 0.A.D with C#, .NET, Windows 8 Modern / RT
mahdi replied to mahdi's topic in Applications and Contributions
To support the comment, i'd like to mention i have developed for C/C++, Java, JavaScript and C# for 10 years. Among these languages (and also including languages i didn't name them) i believe C# is easier to learn, construct a stable code-base/framework and to debug. However, C# is too much Microsoft-dependent and solutions with Mono/Xamarin are not mature enough to consider non-Microsoft platforms with C#. But with C#, at least we will have Windows Phone, Xbox, Windows Desktop, Windows Modern and WinRT versions of the game even without recompilation effort. It is too much i believe. One more point is that, this port is not going to replace JavaScript or C++. It is just a hobbyist port of the game with potential to come commercially in Microsoft Store. -
Portable 0.A.D with C#, .NET, Windows 8 Modern / RT
mahdi replied to mahdi's topic in Applications and Contributions
We can go beyond this and say this fork supports Windows 8, Windows Phone and Xbox. -
Portable 0.A.D with C#, .NET, Windows 8 Modern / RT
mahdi replied to mahdi's topic in Applications and Contributions
There are two separate platforms in Windows 8: - Desktop, which is as same as Windows 7, XP and ... with improvements and yet backward compatible. Yes supports OpenGL, but you cannot put a Desktop app in Store. - WinRT, which is a new platform introduced in Windows 8, does not support OpenGL, apps can be sold via Store. Tablets with Windows 8 only support WinRT. There is no Desktop. -
Portable 0.A.D with C#, .NET, Windows 8 Modern / RT
mahdi replied to mahdi's topic in Applications and Contributions
Sideloading the apps to windows 8 is possible for enterprise only. Not all members have enterprise license and using sideloading hack tools should be illegal. However there is no hack for ARM-based Windows RT as far as i know. -
Portable 0.A.D with C#, .NET, Windows 8 Modern / RT
mahdi replied to mahdi's topic in Applications and Contributions
WinRT is a completely new platform. There is no 'backward' to be compatible. Also, there is no direct download. all the apps must be downloaded/purchased from Microsoft Store (and/or their partners). -
Portable 0.A.D with C#, .NET, Windows 8 Modern / RT
mahdi replied to mahdi's topic in Applications and Contributions
The store requires a completely different coding style, mainly it does not support OpenGL (we have DirectX). Also coding in a C++ for WinRT is completely different from coding in old-fashion Desktop applications. Absolutely the answer is Yes, we only have to develop a new user experience as Xbox does not have a mouse/touch device. The player should be able to control his nation with a GamePad. Hopefully the are some RTS titles available for Xbox and we can inspires ideas for this issue. I think, after the project is separated/branched from the mother project, it could be a totally new and of course independent project. Indeed, They still can exchange ideas/improvement. However, even keeping the changes back and forth is so easy thanks to SVN, as i am currently applying any change from original source to the ported one. Thanks friends. -
Portable 0.A.D with C#, .NET, Windows 8 Modern / RT
mahdi replied to mahdi's topic in Applications and Contributions
Spending nearly 1 year to port the game from C++, JS and OpenGL to a Windows-base platform C# and DirectX, it seems i cannot continue to finish the rest of job without getting support from a sponsor. The latest status is this: - Graphics almost done, except shadows and water effects. Terrain rendering works fine (the same as original CPP version) but it could be optimized to some extend. - Artificial Intelligence needs tons of work. Specially on porting Bot scripts from JS. - User Interface has minimum progress. - Needs a new user-experience to let the player play the game without needing a mouse device. Only touch would be enough for Modern (Metro) UI and Windows Phone. - The game could be commercial, worth 6-8 US dollar (has to check license and copyright stuff). - The game is currently in playable states, if you do not care about enemy AI. If there are any candidate sponsors for the project, i would share the source code to them for their insurance (this is not a fake project). The main reason to port the game to C# and DirectX is to hit Microsoft Stores (Windows 8 and Windows Phone). I wish to get comments from you readers, even if you cannot sponsor. -
Portable 0.A.D with C#, .NET, Windows 8 Modern / RT
mahdi replied to mahdi's topic in Applications and Contributions
Here's C# code for UnitAI component, Originally created in Javascript. 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<string, UnitStance> g_Stances = new SortedDictionary<string, UnitStance> { { "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<OrderItem> orderQueue = new List<OrderItem>(); 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<dynamic>((msg) => { // ignore spurious movement messages // (these can happen when stopping moving at the same time // as switching states) }); stateROOT.Handlers["MoveStarted"] = new Action<dynamic>((msg) => { // ignore spurious movement messages }); stateROOT.Handlers["ConstructionFinished"] = new Action<dynamic>((msg) => { // ignore uninteresting construction messages }); stateROOT.Handlers["LosRangeUpdate"] = new Action<dynamic>((msg) => { // ignore newly-seen units by default }); stateROOT.Handlers["LosGaiaRangeUpdate"] = new Action<dynamic>((msg) => { // ignore newly-seen Gaia units by default }); stateROOT.Handlers["LosHealRangeUpdate"] = new Action<dynamic>((msg) => { // ignore newly-seen injured units by default }); stateROOT.Handlers["Attacked"] = new Action<dynamic>((msg) => { // ignore attacker }); stateROOT.Handlers["HealthChanged"] = new Action<dynamic>((msg) => { // ignore }); stateROOT.Handlers["EntityRenamed"] = new Action<dynamic>((msg) => { // ignore }); // Formation handlers: stateROOT.Handlers["FormationLeave"] = new Action<dynamic>((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<dynamic>((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<dynamic>((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<dynamic>((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<dynamic>((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<dynamic>((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<dynamic>((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<dynamic>((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<dynamic>((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<dynamic>((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<dynamic>((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<dynamic>((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<dynamic>((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<dynamic>((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<dynamic>((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<dynamic>((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<dynamic>((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<dynamic>((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<dynamic>((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<dynamic>((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<dynamic>((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<dynamic>((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<dynamic>((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<dynamic>((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<dynamic>((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<dynamic>((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<dynamic>((msg) => { var cmpFormation = Engine.QueryInterface(this.entity, InterfaceId.Formation) as ICmpFormation; cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true); }); stateFORMATIONCONTROLLER_WALKING.Handlers["MoveCompleted"] = new Action<dynamic>((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<dynamic>((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<dynamic>((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<dynamic>((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<dynamic>((msg) => this.SelectAnimation("idle")); } { var stateFORMATIONMEMBER_WALKING = stateFORMATIONMEMBER.SubStates["WALKING"] = new StateNode(); stateFORMATIONMEMBER_WALKING.Handlers["enter"] = new Action<dynamic>((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<dynamic>((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<dynamic>((msg) => { var cmpFormation = Engine.QueryInterface(this.formationController, InterfaceId.Formation) as ICmpFormation; cmpFormation.UnsetInPosition(this.entity); this.SelectAnimation("move"); }); stateFORMATIONMEMBER_WALKINGTOPOINT.Handlers["MoveCompleted"] = new Action<dynamic>((msg) => { this.FinishOrder(); }); } } { var stateINDIVIDUAL = stateROOT.SubStates["INDIVIDUAL"] = new StateNode(); stateINDIVIDUAL.Handlers["enter"] = new Action<dynamic>((msg) => { // Sanity-checking if (this.IsAnimal()) { //error("Animal got moved into INDIVIDUAL.* state"); } }); stateINDIVIDUAL.Handlers["Attacked"] = new Action<dynamic>((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<dynamic, bool>((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<dynamic>((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<dynamic>((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<dynamic>((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<dynamic>((msg) => { this.RespondToHealableEntities(msg.data.added); }); stateINDIVIDUAL_IDLE.Handlers["Timer"] = new Action<dynamic>((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<dynamic>((msg) => { this.SelectAnimation("move"); }); stateINDIVIDUAL_WALKING.Handlers["MoveCompleted"] = new Action<dynamic>((msg) => { this.FinishOrder(); }); } { var stateINDIVIDUAL_FLEEING = stateINDIVIDUAL.SubStates["FLEEING"] = new StateNode(); stateINDIVIDUAL_FLEEING.Handlers["enter"] = new Action<dynamic>((msg) => { this.PlaySound("panic"); // Run quickly var speed = this.GetRunSpeed(); this.SelectAnimation("move"); this.SetMoveSpeed(speed); }); stateINDIVIDUAL_FLEEING.Handlers["leave"] = new Action<dynamic>((msg) => { // Reset normal speed this.SetMoveSpeed(this.GetWalkSpeed()); }); stateINDIVIDUAL_FLEEING.Handlers["MoveCompleted"] = new Action<dynamic>((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<dynamic, bool>((msg) => { // Ignore the order as we're busy. //return new { discardOrder = true }; return true; }); stateINDIVIDUAL_COMBAT.Handlers["EntityRenamed"] = new Action<dynamic>((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<dynamic>((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<dynamic>((msg) => { // Show weapons rather than carried resources. this.SetGathererAnimationOverride(true); this.SelectAnimation("move"); this.StartTimer(1000, 1000); }); stateINDIVIDUAL_COMBAT_APPROACHING.Handlers["leave"] = new Action<dynamic>((msg) => { // Show carried resources when walking. this.SetGathererAnimationOverride(false); this.StopTimer(); }); stateINDIVIDUAL_COMBAT_APPROACHING.Handlers["Timer"] = new Action<dynamic>((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<dynamic>((msg) => { this.SetNextState("ATTACKING"); }); stateINDIVIDUAL_COMBAT_APPROACHING.Handlers["Attacked"] = new Action<dynamic>((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<dynamic>((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<dynamic>((msg) => { this.StopTimer(); }); stateINDIVIDUAL_COMBAT_ATTACKING.Handlers["Timer"] = new Action<dynamic>((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<dynamic>((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> { (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<dynamic>((msg) => { // Show weapons rather than carried resources. this.SetGathererAnimationOverride(true); this.SelectAnimation("move"); this.StartTimer(1000, 1000); }); stateINDIVIDUAL_COMBAT_CHASING.Handlers["leave"] = new Action<dynamic>((msg) => { // Show carried resources when walking. this.SetGathererAnimationOverride(false); this.StopTimer(); }); stateINDIVIDUAL_COMBAT_CHASING.Handlers["Timer"] = new Action<dynamic>((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<dynamic>((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<dynamic>((msg) => { this.SelectAnimation("move"); }); stateINDIVIDUAL_GATHER_APPROACHING.Handlers["MoveCompleted"] = new Action<dynamic>((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<dynamic>((msg) => { this.SelectAnimation("move"); }); stateINDIVIDUAL_GATHER_WALKING.Handlers["MoveCompleted"] = new Action<dynamic>((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<dynamic, bool>((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<dynamic>((msg) => { this.StopTimer(); // Show the carried resource, if we've gathered anything. this.SetGathererAnimationOverride(false); }); stateINDIVIDUAL_GATHER_GATHERING.Handlers["Timer"] = new Action<dynamic>((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<dynamic>((msg) => { this.SelectAnimation("move"); this.StartTimer(1000, 1000); }); stateINDIVIDUAL_HEAL_APPROACHING.Handlers["leave"] = new Action<dynamic>((msg) => { this.StopTimer(); }); stateINDIVIDUAL_HEAL_APPROACHING.Handlers["Timer"] = new Action<dynamic>((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<dynamic>((msg) => { this.SetNextState("HEALING"); }); } { var stateINDIVIDUAL_HEAL_HEALING = stateINDIVIDUAL_HEAL.SubStates["HEALING"] = new StateNode(); stateINDIVIDUAL_HEAL_HEALING.Handlers["enter"] = new Action<dynamic>((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<dynamic>((msg) => { this.StopTimer(); }); stateINDIVIDUAL_HEAL_HEALING.Handlers["Timer"] = new Action<dynamic>((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<dynamic>((msg) => { this.SelectAnimation("move"); this.StartTimer(1000, 1000); }); stateINDIVIDUAL_HEAL_CHASING.Handlers["leave"] = new Action<dynamic>((msg) => { this.StopTimer(); }); stateINDIVIDUAL_HEAL_CHASING.Handlers["Timer"] = new Action<dynamic>((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<dynamic>((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<dynamic>((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<dynamic>((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<dynamic>((msg) => { // Ignore attack // TODO: Inform player }); { var stateINDIVIDUAL_TRADE_APPROACHINGFIRSTMARKET = stateINDIVIDUAL_TRADE.SubStates["APPROACHINGFIRSTMARKET"] = new StateNode(); stateINDIVIDUAL_TRADE_APPROACHINGFIRSTMARKET.Handlers["enter"] = new Action<dynamic>((msg) => { this.SelectAnimation("move"); }); stateINDIVIDUAL_TRADE_APPROACHINGFIRSTMARKET.Handlers["MoveCompleted"] = new Action<dynamic>((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<dynamic>((msg) => { this.SelectAnimation("move"); }); stateINDIVIDUAL_TRADE_APPROACHINGSECONDMARKET.Handlers["MoveCompleted"] = new Action<dynamic>((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<dynamic>((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<dynamic>((msg) => { this.SelectAnimation("move"); }); stateINDIVIDUAL_REPAIR_APPROACHING.Handlers["MoveCompleted"] = new Action<dynamic>((msg) => { this.SetNextState("REPAIRING"); }); } { var stateINDIVIDUAL_REPAIR_REPAIRING = stateINDIVIDUAL_REPAIR.SubStates["REPAIRING"] = new StateNode(); stateINDIVIDUAL_REPAIR_REPAIRING.Handlers["enter"] = new Func<dynamic, bool>((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<dynamic>((msg) => { this.StopTimer(); }); stateINDIVIDUAL_REPAIR_REPAIRING.Handlers["Timer"] = new Action<dynamic>((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<dynamic>((msg) => { this.SelectAnimation("walk", false, this.GetWalkSpeed()); }); stateINDIVIDUAL_GARRISON_APPROACHING.Handlers["MoveCompleted"] = new Action<dynamic>((msg) => { this.SetNextState("GARRISONED"); }); stateINDIVIDUAL_GARRISON_APPROACHING.Handlers["leave"] = new Action<dynamic>((msg) => { this.StopTimer(); }); } { var stateINDIVIDUAL_GARRISON_GARRISONED = stateINDIVIDUAL_GARRISON.SubStates["GARRISONED"] = new StateNode(); stateINDIVIDUAL_GARRISON_GARRISONED.Handlers["enter"] = new Func<dynamic, bool>((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<dynamic>((msg) => { if (this.FinishOrder()) { return; } }); stateINDIVIDUAL_GARRISON_GARRISONED.Handlers["leave"] = new Action<dynamic>((msg) => { this.isGarrisoned = false; }); } } { var stateINDIVIDUAL_CHEERING = stateINDIVIDUAL.SubStates["CHEERING"] = new StateNode(); stateINDIVIDUAL_CHEERING.Handlers["enter"] = new Func<dynamic, bool>((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<dynamic>((msg) => { this.StopTimer(); var cmpDamageReceiver = Engine.QueryInterface(this.entity, InterfaceId.Armour) as ICmpArmour; cmpDamageReceiver.SetInvulnerability(false); }); stateINDIVIDUAL_CHEERING.Handlers["Timer"] = new Action<dynamic>((msg) => { this.FinishOrder(); }); } } { var stateANIMAL = stateROOT.SubStates["ANIMAL"] = new StateNode(); stateANIMAL.Handlers["Attacked"] = new Action<dynamic>((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<dynamic>((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<OrderItem>(); // 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<int>(); 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<int>(); 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<OrderItem>(new[] { cheeringOrder, order }); } else { this.orderQueue = new List<OrderItem>(); this.PushOrder(type, data); } } public override OrderItem[] GetOrders() { return orderQueue.ToArray(); } public override void AddOrders(IEnumerable<OrderItem> orders) { foreach (var order in orders) { this.PushOrder(order.type, order.data); } } public override List<dynamic> GetOrderData() { var orders = new List<dynamic>(); 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<uint, Name, string, bool> 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<int>(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<int>(); 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<int>(); 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<uint> 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<uint> 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); } /// <summary> /// Adds stop order to queue, forced by the player. /// </summary> 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); } } /// <summary> /// Adds gather order to the queue, forced by the player until the target is reached . /// </summary> 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<uint> 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, => cmpAttack.CompareEntitiesByPreference(a, ); return this.RespondToTargetedEntities(xx.ToArray()); } public override bool AttackGaiaEntitiesByPreference(IEnumerable<uint> ents) { var cmpAttack = Engine.QueryInterface(this.entity, InterfaceId.Attack) as ICmpAttack; if (cmpAttack == null) { return false; } Func<uint, bool> 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, => cmpAttack.CompareEntitiesByPreference(a, ); return this.RespondToTargetedEntities(xx.ToArray()); } } } CCmpUnitAI.cs.txt -
Portable 0.A.D with C#, .NET, Windows 8 Modern / RT
mahdi replied to mahdi's topic in Applications and Contributions
Here is another screenshot from Arcadia II scenario. http://www.hilva.com/0ad/ArcadiaII.png -
Portable 0.A.D with C#, .NET, Windows 8 Modern / RT
mahdi replied to mahdi's topic in Applications and Contributions
Thanks, - No i have changed model rendering system to enable GPU animations and GPU instancing, though the public interface of the code is as same as 0 A.D. - I have not experienced an RTS on Tablet yet, but i'll have to struggle about it when i buy a Windows 8 RT tablet. Yet, tablets will have USB port (iPad dont have) so we can connect a mouse. However right clicking in a tablet would be holding your finger 2 more seconds, or tap a finger and swiftly tap another finger. I have played an RTS title on Xbox with GamePad. It was a great XP and maybe a source of inspiration. -
Portable 0.A.D with C#, .NET, Windows 8 Modern / RT
mahdi replied to mahdi's topic in Applications and Contributions
- Having SVN, it is not a very hard job to keep the source code in sync. Because the SVN shows you the exact file names changed in a particular revision and which lines of code and how it changed. Thanks to Diff. This can be done with every stable version or per revision. - Yes, possible. many games do this for years. - I don't know any better cross platform javascript interpreter for C++. But on Windows and C++.NET, you can still run C# as both compiled code and/or runtime interpreted code (Thanks to Microsoft's Roslyn). I had used Roslyn in several cases (for other projects, not 0.A.D. port), thou it runs as a script, it's still much faster than compiled C# code. Weird. Roslyn is still a CTP. I hope these answers help. -
Portable 0.A.D with C#, .NET, Windows 8 Modern / RT
mahdi replied to mahdi's topic in Applications and Contributions
Thou C# is slower than C++ (i say C# is 95% of C++), the main reason for getting this gain would be: I used DirectX 11 Feature Level, so i did GPU-Instancing for static models/particles and GPU-Skinning for animated ones; Which saves millions of matrix multiplications done by CPU per frame. The OpenGL code is eating CPU which makes the game compatible with any computer, But please consider that most of users have graphics cards not older than 4 years, so they support modern GPU features. I translated Javascript to C#, so they are running code closer to machine language. I bet Microsoft's C# compiler beats Mozilla's Javascript interpreter, called SpiderMonkey (see file mozjs185-ps-release-1.0.dll in binaries folder). At the moment, i am planning to debug, maintain and contribute to 0.A.D. Also i have to check legal stuff before distributing the code. So there would be no plan on moving to OpenGL/Mono; the game is already OpenGL and runs on Linux/Android. EDIT: DirectX is not faster than OpenGL, it completely depends on the feature level you ask the device. 0.A.D's opengl level is equivalent to DirectX 9.0, but my port uses DirectX 11. Migrating current opengl level to a higher one results in the same performance improvement, but less compatibility with devices. I think Android and iOS do not support advanced opengl codes. -
Portable 0.A.D with C#, .NET, Windows 8 Modern / RT
mahdi replied to mahdi's topic in Applications and Contributions
OK i compared the frame rates of the two versions the with shadows and GUI enabled. Both on 1920x1080 pixels screen size. My C# port runs 167 fps. Original C++ version runs [41-54] fps. Same scenario, same camera view.