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 && this.template[type].TurretsOnly == "true" && !turretId)
+ 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,397 @@
+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()
+{
+ let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
+ return !cmpPosition || !cmpPosition.IsInWorld() ? INVALID_ENTITY : cmpPosition.GetTurretParent();
+};
+
+/**
+ * Get the attack component
+ */
+TurretAI.prototype.GetCmpAttack = function()
+{
+ // use own attack, or turretHolder's attack
+ let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
+ return cmpAttack ? cmpAttack : Engine.QueryInterface(this.GetTurretParent(), IID_Attack);
+};
+
+/**
+ * Sends the message to change ownership
+ */
+TurretAI.prototype.OnOwnershipChanged = function(msg)
+{
+ if (msg.to == INVALID_PLAYER)
+ return;
+
+ let cmpAttack = this.GetCmpAttack();
+ if (!cmpAttack)
+ return;
+
+ this.targetUnits = [];
+ this.SetupRangeQuery();
+};
+
+/**
+ * Change Diplomacy State
+ */
+TurretAI.prototype.OnDiplomacyChanged = function(msg)
+{
+ if (!IsOwnedByPlayer(msg.player, this.entity))
+ return;
+
+ this.targetUnits = [];
+ this.SetupRangeQuery();
+};
+
+/**
+ * Cleanup on destroy
+ */
+TurretAI.prototype.OnDestroy = function()
+{
+ if (this.timer)
+ {
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ cmpTimer.CancelTimer(this.timer);
+ delete this.timer;
+ }
+
+ // Clean up range queries
+ let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+ if (this.enemyUnitsQuery)
+ cmpRangeManager.DestroyActiveQuery(this.enemyUnitsQuery);
+};
+
+/**
+ * Setup the Range Query to detect units coming in & out of range
+ */
+TurretAI.prototype.SetupRangeQuery = function()
+{
+ let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+
+ if (this.losRangeQuery)
+ {
+ cmpRangeManager.DestroyActiveQuery(this.losRangeQuery);
+ delete this.losRangeQuery;
+ }
+
+ let cmpPlayer = QueryOwnerInterface(this.entity);
+ // If we are being destructed (owner -1), creating a range query is pointless
+ if (!cmpPlayer)
+ return;
+
+ // Exclude allies, and self
+ // TODO: How to handle neutral players - Special query to attack military only?
+ let players = cmpPlayer.GetEnemies();
+ let range = this.GetQueryRange(IID_Attack);
+
+ this.losRangeQuery = cmpRangeManager.CreateActiveQuery(
+ this.entity,
+ range.min,
+ range.max,
+ players,
+ IID_DamageReceiver,
+ cmpRangeManager.GetEntityFlagMask("normal")
+ );
+
+ cmpRangeManager.EnableActiveQuery(this.losRangeQuery);
+};
+
+TurretAI.prototype.GetStance = function()
+{
+ let cmpUnitAI = Engine.QueryInterface(this.GetTurretParent(), IID_UnitAI);
+ return cmpUnitAI.GetStance();
+};
+
+TurretAI.prototype.GetQueryRange = function(iid)
+{
+ let ret = { "min": 0, "max": 0 };
+ if (this.GetStance().respondStandGround)
+ {
+ let cmpRanged = Engine.QueryInterface(this.entity, iid);
+ if (!cmpRanged)
+ return ret;
+ let range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetFullAttackRange();
+ let cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
+ if (!cmpVision)
+ return ret;
+ ret.min = range.min;
+ ret.max = Math.min(range.max, cmpVision.GetRange());
+ }
+ else if (this.GetStance().respondChase)
+ {
+ let cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
+ if (!cmpVision)
+ return ret;
+ let range = cmpVision.GetRange();
+ ret.max = range;
+ }
+ else if (this.GetStance().respondHoldGround)
+ {
+ let cmpRanged = Engine.QueryInterface(this.entity, iid);
+ if (!cmpRanged)
+ return ret;
+ let range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetFullAttackRange();
+ let cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
+ if (!cmpVision)
+ return ret;
+ let vision = cmpVision.GetRange();
+ ret.max = Math.min(range.max + vision / 2, vision);
+ }
+ // 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 === IID_Heal)
+ {
+ let cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
+ if (!cmpVision)
+ return ret;
+ let range = cmpVision.GetRange();
+ ret.max = range;
+ }
+ return ret;
+};
+
+/**
+ * Called when units enter or leave range
+ */
+TurretAI.prototype.OnRangeUpdate = function(msg)
+{
+ this.targetUnits = [];
+ let cmpAttack = this.GetCmpAttack();
+ if (!cmpAttack)
+ return;
+
+ // Target enemy units except non-dangerous animals
+ if (msg.tag == this.enemyUnitsQuery)
+ {
+ msg.added = msg.added.filter(e => {
+ let cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI);
+ return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal());
+ });
+ }
+
+ // 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();
+ return;
+ }
+
+ let cmpUnitAI = Engine.QueryInterface(this.GetTurretParent(), IID_UnitAI);
+ if (!this.targetUnits.length
+ && cmpUnitAI.order
+ && cmpUnitAI.order.type == "Attack"
+ && cmpUnitAI.order.data
+ && cmpUnitAI.order.data.force
+ && cmpUnitAI.order.data.target
+ && cmpUnitAI.order.data.target != INVALID_ENTITY)
+ {
+ this.targetUnits.push(cmpUnitAI.order.data.target)
+ this.StartTimer();
+ }
+};
+
+TurretAI.prototype.StartTimer = function()
+{
+ if (this.timer)
+ return;
+
+ let cmpAttack = this.GetCmpAttack();
+ if (!cmpAttack)
+ return;
+
+ let attackTimers = cmpAttack.GetTimers(this.GetBestAttack());
+
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ this.timer = cmpTimer.SetInterval(
+ this.entity,
+ IID_TurretAI,
+ "Attack",
+ attackTimers.prepare,
+ attackTimers.repeat,
+ undefined
+ );
+};
+
+TurretAI.prototype.GetBestAttack = function()
+{
+ let cmpUnitAI = Engine.QueryInterface(this.GetTurretParent(), IID_UnitAI);
+ if (cmpUnitAI
+ && cmpUnitAI.order
+ && cmpUnitAI.order.data
+ && cmpUnitAI.order.data.force
+ && cmpUnitAI.order.data.target
+ && cmpUnitAI.order.data.target != INVALID_ENTITY)
+ return cmpUnitAI.order.data.attackType
+
+ let cmpAttack = this.GetCmpAttack();
+ if (!cmpAttack)
+ return;
+
+ return cmpAttack.GetAttackTypes()[0];
+};
+
+/**
+ * Attack other entities
+ */
+TurretAI.prototype.Attack = function(data, lateness)
+{
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ if (!this.targetUnits.length)
+ {
+ if (this.timer)
+ {
+ cmpTimer.CancelTimer(this.timer);
+ delete this.timer;
+ }
+
+ let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
+ if (cmpPosition && cmpPosition.IsInWorld())
+ cmpPosition.SetYRotation(cmpPosition.GetRotation().y);
+
+ this.SelectAnimation("idle");
+ this.SetAnimationVariant("");
+ return;
+ }
+
+ let cmpAttack = this.GetCmpAttack();
+ if (!cmpAttack)
+ return;
+
+ let target = this.targetUnits.indexOf(this.g_LatestTarget) != -1 ? this.g_LatestTarget : INVALID_ENTITY
+ // if no target, select a random one
+ if (target == INVALID_ENTITY)
+ {
+ let targets = new WeightedList();
+ for (let weightedTarget of this.targetUnits)
+ {
+ let preference = cmpAttack.GetPreference(weightedTarget);
+ // Lower preference scores indicate a higher preference so they should result in a higher weight.
+ let weight = !preference ? 1 + this.g_MaxPreferenceBonus / (1 + preference) : 1;
+ targets.push(weightedTarget, weight);
+ }
+
+ let selectedIndex = targets.randomIndex();
+ target = targets.itemAt(selectedIndex);
+ }
+
+ if (target == INVALID_ENTITY)
+ return;
+
+ let attackType = this.GetBestAttack();
+ this.lastAttacked = cmpTimer.GetTime() - lateness;
+ let attackTimers = cmpAttack.GetTimers(this.GetBestAttack());
+ let prepare = attackTimers.prepare;
+ if (this.lastAttacked)
+ {
+ let repeatLeft = this.lastAttacked + attackTimers.repeat - cmpTimer.GetTime();
+ prepare = Math.max(prepare, repeatLeft);
+ }
+
+ this.SelectAnimation("attack_" + attackType.toLowerCase());
+ this.SetAnimationVariant("combat");
+ this.SetAnimationSync(prepare, attackTimers.repeat);
+ this.previousAttack = "attack_" + attackType.toLowerCase();
+ this.FaceTowardsTarget(target);
+ 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)
+{
+ let 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(offset, repeattime)
+{
+ let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
+ if (!cmpVisual)
+ return;
+
+ cmpVisual.SetAnimationSyncRepeat(repeattime);
+ cmpVisual.SetAnimationSyncOffset(offset);
+};
+
+/**
+ * Orient the turret toward his foe.
+ */
+TurretAI.prototype.FaceTowardsTarget = function(target)
+{
+ if (!target)
+ return;
+
+ let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
+ if (!cmpPosition || !cmpPosition.IsInWorld())
+ return;
+
+ let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
+ if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
+ return;
+
+ let pos = cmpTargetPosition.GetPosition2D().sub(cmpPosition.GetPosition2D());
+ cmpPosition.TurnTo(Math.atan2(pos.x, pos.y));
+};
+
+/*
+ * Set a visualActor animation variant.
+ * By changing the animation variant, you can change animations based on unitAI state.
+ * If there are no specific variants or the variant doesn't exist in the actor,
+ * the actor fallbacks to any existing animation.
+ * @param type if present, switch to a specific animation variant.
+ */
+TurretAI.prototype.SetAnimationVariant = function(type)
+{
+ let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
+ if (!cmpVisual)
+ return;
+
+ cmpVisual.SetVariant("animationVariant", type);
+ return;
+};
+
+Engine.RegisterComponentType(IID_TurretAI, "TurretAI", TurretAI);
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,102 @@
+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
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ 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() != INVALID_PLAYER)
+ 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)
+{
+ if (msg.to == INVALID_PLAYER)
+ return;
+
+ 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);
Index: binaries/data/mods/public/simulation/components/UnitAI.js
===================================================================
--- binaries/data/mods/public/simulation/components/UnitAI.js
+++ binaries/data/mods/public/simulation/components/UnitAI.js
@@ -5080,7 +5080,7 @@
this.workOrders.length && this.workOrders[0].type == "Trade")
{
let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
- if (cmpTrader.HasBothMarkets() &&
+ if (cmpTrader.HasBothMarkets() &&
(cmpTrader.GetFirstMarket() == target && cmpTrader.GetSecondMarket() == source ||
cmpTrader.GetFirstMarket() == source && cmpTrader.GetSecondMarket() == target))
{
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,52 @@
+
+
+
+
+ false
+ false
+ 80.0
+ 0.01
+ 0.0
+
+
+ unit
+
+
+
+
+
+
+
+
+
+ 0
+ upright
+ false
+ 0.0
+ 6.0
+
+
+
+
+ interface/alarm/alarm_attackplayer.xml
+
+
+
+
+ false
+ false
+ false
+ false
+
+
+ 100
+
+
+ false
+
+
+ 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
+
+
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
+
+
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