Index: binaries/data/mods/public/art/actors/units/britons/chariot_javelinist_c_m.xml
===================================================================
--- binaries/data/mods/public/art/actors/units/britons/chariot_javelinist_c_m.xml
+++ binaries/data/mods/public/art/actors/units/britons/chariot_javelinist_c_m.xml
@@ -18,7 +18,6 @@
-
Index: binaries/data/mods/public/art/actors/units/britons/hero_chariot_javelinist_boudicca_m.xml
===================================================================
--- binaries/data/mods/public/art/actors/units/britons/hero_chariot_javelinist_boudicca_m.xml
+++ binaries/data/mods/public/art/actors/units/britons/hero_chariot_javelinist_boudicca_m.xml
@@ -17,7 +17,6 @@
-
Index: binaries/data/mods/public/simulation/components/Attack.js
===================================================================
--- binaries/data/mods/public/simulation/components/Attack.js
+++ binaries/data/mods/public/simulation/components/Attack.js
@@ -207,6 +207,9 @@
Attack.prototype.bonusesSchema +
Attack.prototype.preferredClassesSchema +
Attack.prototype.restrictedClassesSchema +
+ ""+
+ "" +
+ "" +
"" +
"" +
"" +
@@ -499,8 +502,11 @@
* and should only be called after GetTimers().repeat msec has passed since the last
* call to PerformAttack.
*/
-Attack.prototype.PerformAttack = function(type, target)
+Attack.prototype.PerformAttack = function(type, target, turretId)
{
+ if (this.template[type].TurretsOnly && !turretId && this.template[type].TurretsOnly == "true")
+ return;
+
let attackerOwner = Engine.QueryInterface(this.entity, IID_Ownership).GetOwner();
let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage);
@@ -562,8 +568,8 @@
// TODO: Use unit rotation to implement x/z offsets.
let deltaLaunchPoint = new Vector3D(0, this.template[type].Projectile.LaunchPoint["@y"], 0.0);
let launchPoint = Vector3D.add(selfPosition, deltaLaunchPoint);
-
- let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
+
+ let cmpVisual = Engine.QueryInterface(turretId ||this.entity, IID_Visual);
if (cmpVisual)
{
// if the projectile definition is missing from the template
Index: binaries/data/mods/public/simulation/components/TurretAI.js
===================================================================
--- binaries/data/mods/public/simulation/components/TurretAI.js
+++ binaries/data/mods/public/simulation/components/TurretAI.js
@@ -0,0 +1,382 @@
+function TurretAI() {}
+TurretAI.prototype.Schema =
+ "";
+
+TurretAI.prototype.g_MaxPreferenceBonus = 2;
+TurretAI.prototype.g_LatestTarget = INVALID_ENTITY ;
+
+/**
+ * Initialize TurretAI Component.
+ */
+TurretAI.prototype.Init = function()
+{
+
+};
+
+/**
+ * Get the turrent parent.
+ */
+TurretAI.prototype.GetTurretParent = function()
+{
+ var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
+ if (!cmpPosition || !cmpPosition.IsInWorld())
+ return INVALID_ENTITY;
+
+ return cmpPosition.GetTurretParent();
+};
+
+/**
+ * Get the attack component
+ */
+TurretAI.prototype.GetCmpAttack = function()
+{
+ // use own attack, or turretHolder's attack
+ var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
+ if (cmpAttack)
+ return cmpAttack;
+
+ return Engine.QueryInterface(this.GetTurretParent(), IID_Attack);
+};
+
+/**
+ * Sends the message to change ownership
+ */
+TurretAI.prototype.OnOwnershipChanged = function(msg)
+{
+ if (msg.to == INVALID_PLAYER)
+ return;
+
+ var cmpAttack = this.GetCmpAttack();
+ if (!cmpAttack)
+ return;
+
+ // Remove current targets, to prevent them from being added twice
+ this.targetUnits = [];
+ this.bestAttack = cmpAttack.GetAttackTypes()[0];
+
+ this.SetupRangeQuery(msg.to);
+
+ // Non-Gaia buildings should attack certain Gaia units.
+ if (msg.to != 0 || this.gaiaUnitsQuery)
+ this.SetupGaiaRangeQuery(msg.to);
+};
+
+/**
+ * Change Diplomacy State
+ */
+TurretAI.prototype.OnDiplomacyChanged = function(msg)
+{
+ if (!IsOwnedByPlayer(msg.player, this.entity))
+ return;
+
+ // Remove maybe now allied/neutral units
+ this.targetUnits = [];
+ this.SetupRangeQuery(msg.player);
+ this.SetupGaiaRangeQuery(msg.player);
+};
+
+/**
+ * Cleanup on destroy
+ */
+TurretAI.prototype.OnDestroy = function()
+{
+ if (this.timer)
+ {
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ cmpTimer.CancelTimer(this.timer);
+ this.timer = undefined;
+ }
+
+ // Clean up range queries
+ let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+ if (this.enemyUnitsQuery)
+ cmpRangeManager.DestroyActiveQuery(this.enemyUnitsQuery);
+ if (this.gaiaUnitsQuery)
+ cmpRangeManager.DestroyActiveQuery(this.gaiaUnitsQuery);
+};
+
+/**
+ * Setup the Range Query to detect units coming in & out of range
+ */
+TurretAI.prototype.SetupRangeQuery = function(owner)
+{
+ let cmpAttack = this.GetCmpAttack();
+ if (!cmpAttack)
+ return;
+
+ let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+ if (this.enemyUnitsQuery)
+ {
+ cmpRangeManager.DestroyActiveQuery(this.enemyUnitsQuery);
+ this.enemyUnitsQuery = undefined;
+ }
+
+ let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
+ let cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(owner), IID_Player);
+ if (!cmpPlayer)
+ return;
+
+ let enemies = cmpPlayer.GetEnemies();
+ if (enemies.length && enemies[0] == 0)
+ enemies.shift(); // remove gaia
+
+ if (!enemies.length)
+ return;
+
+ let range = cmpAttack.GetRange(this.GetBestAttack());
+ this.enemyUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery(
+ this.entity, range.min, range.max, range.elevationBonus,
+ enemies, IID_DamageReceiver, cmpRangeManager.GetEntityFlagMask("normal"));
+
+ cmpRangeManager.EnableActiveQuery(this.enemyUnitsQuery);
+};
+
+/**
+ *Set up a range query for Gaia units within LOS range which can be attacked.
+ * This should be called whenever our ownership changes.
+ */
+TurretAI.prototype.SetupGaiaRangeQuery = function()
+{
+ var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
+ var owner = cmpOwnership.GetOwner();
+
+ var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+ var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
+ var cmpAttack = this.GetCmpAttack();
+ if (!cmpAttack)
+ return;
+
+ if (this.gaiaUnitsQuery)
+ {
+ cmpRangeManager.DestroyActiveQuery(this.gaiaUnitsQuery);
+ this.gaiaUnitsQuery = undefined;
+ }
+
+ if (owner == -1)
+ return;
+
+ var cmpPlayer = Engine.QueryInterface(playerMan.GetPlayerByID(owner), IID_Player);
+ if (!cmpPlayer.IsEnemy(0))
+ return;
+
+ var range = cmpAttack.GetRange(this.GetBestAttack());
+
+ // This query is only interested in Gaia entities that can attack.
+ this.gaiaUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery(this.entity, range.min, range.max, range.elevationBonus, [0], IID_Attack, cmpRangeManager.GetEntityFlagMask("normal"));
+ cmpRangeManager.EnableActiveQuery(this.gaiaUnitsQuery);
+};
+
+/**
+ * Called when units enter or leave range
+ */
+TurretAI.prototype.OnRangeUpdate = function(msg)
+{
+ var cmpAttack = this.GetCmpAttack();
+ if (!cmpAttack)
+ return;
+
+ // Target enemy units except non-dangerous animals
+ if (msg.tag == this.gaiaUnitsQuery)
+ {
+ msg.added = msg.added.filter(e => {
+ let cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI);
+ return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal());
+ });
+ }
+ else if (msg.tag != this.enemyUnitsQuery)
+ return;
+
+ // Add new targets
+ for (let entity of msg.added)
+ if (cmpAttack.CanAttack(entity))
+ this.targetUnits.push(entity);
+
+ // Remove targets outside of vision-range
+ for (let entity of msg.removed)
+ {
+ let index = this.targetUnits.indexOf(entity);
+ if (index > -1)
+ this.targetUnits.splice(index, 1);
+ }
+
+ if (this.targetUnits.length)
+ this.StartTimer();
+};
+
+TurretAI.prototype.StartTimer = function()
+{
+ if (this.timer)
+ return;
+
+ var cmpAttack = this.GetCmpAttack();
+ if (!cmpAttack)
+ return;
+
+ var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ var attackTimers = cmpAttack.GetTimers(this.GetBestAttack());
+
+ this.timer = cmpTimer.SetInterval(this.entity, IID_TurretAI, "Attack",
+ attackTimers.prepare, attackTimers.repeat, null);
+};
+
+TurretAI.prototype.GetBestAttack = function()
+{
+ let cmpUnitAI = Engine.QueryInterface(this.GetTurretParent(), IID_UnitAI);
+ if (cmpUnitAI && cmpUnitAI.order && cmpUnitAI.order.data.force && cmpUnitAI.order.data.target && cmpUnitAI.order.data.target != INVALID_ENTITY)
+ return cmpUnitAI.order.data.attackType
+
+ return this.bestAttack;
+};
+
+/**
+ * Get Previous Target
+ */
+TurretAI.prototype.GetPreviousTarget = function()
+{
+ var cmpAttack = this.GetCmpAttack();
+ if (!cmpAttack)
+ return INVALID_ENTITY;
+ var previousTarget = this.g_LatestTarget;
+ var cmpUnitAI = Engine.QueryInterface(this.GetTurretParent(), IID_UnitAI);
+ // if a unit ai, use that to overcome the difference between rangeManager range and unitMotion range
+ if (cmpUnitAI)
+ {
+ if (cmpUnitAI.CheckTargetAttackRange(previousTarget, this.GetBestAttack()))
+ return previousTarget;
+ return INVALID_ENTITY;
+ }
+ // else just use the range manager (which gives us the list of possible targets);
+ if (this.targetUnits.indexOf(previousTarget) != -1)
+ return previousTarget;
+ return INVALID_ENTITY;
+};
+
+/**
+ * Attack other entities
+ */
+TurretAI.prototype.Attack = function()
+{
+ if (!this.targetUnits.length)
+ {
+ if (this.timer)
+ {
+ // stop the timer
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ cmpTimer.CancelTimer(this.timer);
+ this.timer = undefined;
+
+ // TODO Reset Rotation
+ }
+ this.SelectAnimation("idle");
+ return;
+ }
+
+ var cmpAttack = this.GetCmpAttack();
+ if (!cmpAttack)
+ return;
+
+ var target = this.GetPreviousTarget();
+ // if no target, select a random one
+ if (target == INVALID_ENTITY)
+ {
+ var selectedIndex = -1;
+ var targets = new WeightedList();
+ for (let i = 0; i < this.targetUnits.length; i++)
+ {
+ let target = this.targetUnits[i];
+ let preference = cmpAttack.GetPreference(target);
+ let weight = 1;
+ if (preference !== null && preference !== undefined)
+ {
+ // Lower preference scores indicate a higher preference so they should result in a higher weight.
+ weight = 1 + this.g_MaxPreferenceBonus / (1 + preference);
+ }
+ targets.push(target, weight);
+ }
+ selectedIndex = targets.randomIndex()
+ target = targets.itemAt(selectedIndex);
+ }
+
+ let cmpUnitAI = Engine.QueryInterface(this.GetTurretParent(), IID_UnitAI);
+ warn(uneval(cmpUnitAI.order))
+
+ let parentTarget = cmpUnitAI.order.data.target;
+ if (cmpUnitAI.order && cmpUnitAI.order.type == "Attack" && cmpUnitAI.order.data.force && parentTarget && parentTarget != INVALID_ENTITY)
+ {
+ this.AttackTarget(parentTarget, cmpUnitAI.order.data.attackType)
+ return;
+ }
+ warn("Attack");
+
+ // Now we hope there's a target
+ if (!target || target == INVALID_ENTITY)
+ return;
+ warn(this.bestAttack);
+
+ this.AttackTarget(target, this.bestAttack)
+};
+
+TurretAI.prototype.AttackTarget = function(target, attackType)
+{
+ var cmpAttack = this.GetCmpAttack();
+ if (!cmpAttack)
+ return;
+
+ this.TurnTowardsTarget(target);
+ let attackTimers = cmpAttack.GetTimers(attackType);
+ this.SetAnimationSync(attackTimers.prepare, attackTimers.repeat);
+ this.SelectAnimation("attack_" + attackType.toLowerCase());
+ cmpAttack.PerformAttack(attackType, target, this.entity);
+ this.g_LatestTarget = target;
+}
+
+/**
+ * Selection the animations for the turret
+ */
+TurretAI.prototype.SelectAnimation = function(name, once = false, speed = 1.0)
+{
+ var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
+ if (!cmpVisual)
+ 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
+ cmpVisual.SelectMovementAnimation(this.GetWalkSpeed());
+ return;
+ }
+
+ cmpVisual.SelectAnimation(name, once, speed);
+};
+
+/**
+ * Synchronise Animations
+ */
+TurretAI.prototype.SetAnimationSync = function(actiontime, repeattime)
+{
+ let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
+ if (!cmpVisual)
+ return;
+
+ cmpVisual.SetAnimationSyncRepeat(repeattime);
+ cmpVisual.SetAnimationSyncOffset(actiontime);
+};
+
+/**
+ * Orient the turret toward his foe.
+ */
+TurretAI.prototype.TurnTowardsTarget = function(target)
+{
+ var cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position);
+ var cmpThisPositionParent = Engine.QueryInterface(this.GetTurretParent(), IID_Position);
+ var cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
+ if (!cmpThisPositionParent || !cmpThisPosition || !cmpTargetPosition || !cmpThisPositionParent.IsInWorld() || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld())
+ return;
+
+ var pos = cmpTargetPosition.GetPosition2D().sub(cmpThisPosition.GetPosition2D());
+ cmpThisPosition.TurnTo(Math.atan2(pos.x, pos.y));
+};
+
+Engine.RegisterComponentType(IID_TurretAI, "TurretAI", TurretAI);
\ No newline at end of file
Index: binaries/data/mods/public/simulation/components/TurretHolder.js
===================================================================
--- binaries/data/mods/public/simulation/components/TurretHolder.js
+++ binaries/data/mods/public/simulation/components/TurretHolder.js
@@ -0,0 +1,98 @@
+function TurretHolder() {}
+
+TurretHolder.prototype.Schema =
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "";
+
+/**
+ * Initialize TurretHolder Component.
+ */
+TurretHolder.prototype.Init = function()
+{
+ this.turrets = [];
+ // hack for atlas, don't create the turrets in Atlas, as the references get lost
+ // TODO implement some sort of tag for the turrets so they never get saved by Atlas
+/* if (cmpTimer.GetTime() == 0)
+ cmpTimer.SetTimeout(this.entity, IID_TurretHolder, "CreateTurrets", 100, null);
+ else*/
+ this.CreateTurrets();
+};
+
+/**
+ * Create the Turrets.
+ */
+TurretHolder.prototype.CreateTurrets = function()
+{
+ for (let key in this.template.TurretPoints)
+ {
+ let turretPoint = this.template.TurretPoints[key];
+ let ent = Engine.AddEntity(turretPoint.Template);
+ let cmpPosition = Engine.QueryInterface(ent, IID_Position);
+ if (cmpPosition)
+ cmpPosition.SetTurretParent(this.entity, new Vector3D(+turretPoint.X, +turretPoint.Y, +turretPoint.Z));
+ this.turrets.push(ent);
+ }
+
+ let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
+ if (cmpOwnership && cmpOwnership.GetOwner() != -1)
+ this.ChangeTurretOwnership(cmpOwnership.GetOwner());
+};
+
+/**
+ * Return the list of entities garrisoned inside.
+ */
+TurretHolder.prototype.GetTurrets = function()
+{
+ return this.turrets;
+};
+
+/**
+ * Destroys the entities.
+ */
+TurretHolder.prototype.OnDestroy = function()
+{
+ for (let ent of this.turrets)
+ Engine.DestroyEntity(ent);
+};
+
+/**
+ * Change the ownership of the turret.
+ */
+TurretHolder.prototype.OnOwnershipChanged = function(msg)
+{
+ this.ChangeTurretOwnership(msg.to);
+};
+
+/**
+ * Set the ownership of all present turrets to the same owner.
+ */
+TurretHolder.prototype.ChangeTurretOwnership = function(owner)
+{
+ for (let ent of this.turrets)
+ {
+ let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
+ if (cmpOwnership)
+ cmpOwnership.SetOwner(owner);
+ }
+};
+
+Engine.RegisterComponentType(IID_TurretHolder, "TurretHolder", TurretHolder);
\ No newline at end of file
Index: binaries/data/mods/public/simulation/components/interfaces/TurretAI.js
===================================================================
--- binaries/data/mods/public/simulation/components/interfaces/TurretAI.js
+++ binaries/data/mods/public/simulation/components/interfaces/TurretAI.js
@@ -0,0 +1 @@
+Engine.RegisterInterface("TurretAI");
Index: binaries/data/mods/public/simulation/components/interfaces/TurretHolder.js
===================================================================
--- binaries/data/mods/public/simulation/components/interfaces/TurretHolder.js
+++ binaries/data/mods/public/simulation/components/interfaces/TurretHolder.js
@@ -0,0 +1 @@
+Engine.RegisterInterface("TurretHolder");
Index: binaries/data/mods/public/simulation/templates/template_turret.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_turret.xml
+++ binaries/data/mods/public/simulation/templates/template_turret.xml
@@ -0,0 +1,45 @@
+
+
+
+
+ false
+ false
+ 80.0
+ 0.01
+ 0.0
+
+
+ unit
+
+
+
+
+
+ 0
+ upright
+ false
+ 0.0
+ 6.0
+
+
+
+
+ interface/alarm/alarm_attackplayer.xml
+
+
+
+
+ false
+ false
+ true
+ true
+
+
+ 100
+
+
+ true
+ false
+ false
+
+
\ No newline at end of file
Index: binaries/data/mods/public/simulation/templates/units/brit_champion_cavalry.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/units/brit_champion_cavalry.xml
+++ binaries/data/mods/public/simulation/templates/units/brit_champion_cavalry.xml
@@ -12,6 +12,16 @@
units/brit_champion_chariot
units/brit_champion_chariot.png
+
+
+
+ units/brit_champion_cavalry_r
+ 0
+ 1.4
+ -2.5
+
+
+
units/britons/chariot_javelinist_c_m.xml
Index: binaries/data/mods/public/simulation/templates/units/brit_champion_cavalry_r.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/units/brit_champion_cavalry_r.xml
+++ binaries/data/mods/public/simulation/templates/units/brit_champion_cavalry_r.xml
@@ -0,0 +1,6 @@
+
+
+
+ units/britons/chariot_javelinist_c_r.xml
+
+
\ No newline at end of file
Index: binaries/data/mods/public/simulation/templates/units/brit_hero_bouddica_r.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/units/brit_hero_bouddica_r.xml
+++ binaries/data/mods/public/simulation/templates/units/brit_hero_bouddica_r.xml
@@ -0,0 +1,6 @@
+
+
+
+ units/britons/hero_chariot_javelinist_boudicca_r.xml
+
+
\ No newline at end of file
Index: binaries/data/mods/public/simulation/templates/units/brit_hero_boudicca.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/units/brit_hero_boudicca.xml
+++ binaries/data/mods/public/simulation/templates/units/brit_hero_boudicca.xml
@@ -3,6 +3,11 @@
units/heroes/brit_hero_boudicca
+
+
+ true
+
+
5.0
@@ -15,6 +20,16 @@
female
units/brit_hero_boudicca.png
+
+
+
+ units/brit_hero_bouddica_r
+ 0
+ 1.4
+ -2.5
+
+
+
units/britons/hero_chariot_javelinist_boudicca_m.xml