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 + + + + + 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 + + + + + 0 + 1.4 + -2.5 + + + units/britons/hero_chariot_javelinist_boudicca_m.xml