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