function Attack() {} var bonusesSchema = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; Attack.prototype.Schema = "Controls the attack abilities and strengths of the unit." + "" + "" + "10.0" + "0.0" + "5.0" + "4.0" + "1000" + "" + "" + "pers" + "Infantry" + "1.5" + "" + "" + "Cavalry Melee" + "1.5" + "" + "" + "" + "" + "0.0" + "10.0" + "0.0" + "44.0" + "20.0" + "800" + "1600" + "50.0" + "" + "" + "Cavalry" + "2" + "" + "" + "" + "" + "10.0" + "0.0" + "50.0" + "24.0" + "20.0" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + // TODO: it shouldn't be stretched "" + "" + bonusesSchema + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + bonusesSchema + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + // TODO: how do these work? "" + bonusesSchema + "" + "" + ""; Attack.prototype.Init = function() { }; Attack.prototype.Serialize = null; // we have no dynamic state to save /** * Return the type of the best attack. * TODO: this should probably depend on range, target, etc, * so we can automatically switch between ranged and melee */ Attack.prototype.GetBestAttack = function() { if (this.template.Ranged) return "Ranged"; else if (this.template.Melee) return "Melee"; else if (this.template.Charge) return "Charge"; else return undefined; }; Attack.prototype.GetTimers = function(type) { var cmpTechMan = QueryOwnerInterface(this.entity, IID_TechnologyManager); var prepare = cmpTechMan.ApplyModifications("Attack/" + type + "/PrepareTime", +(this.template[type].PrepareTime||0), this.entity); var repeat = cmpTechMan.ApplyModifications("Attack/" + type + "/RepeatTime", +(this.template[type].RepeatTime||1000), this.entity); return { "prepare": prepare, "repeat": repeat, "recharge": repeat - prepare }; }; Attack.prototype.GetAttackStrengths = function(type) { // Work out the attack values with technology effects var self = this; var cmpTechMan = QueryOwnerInterface(this.entity, IID_TechnologyManager); var applyTechs = function(damageType) { var allComponent = cmpTechMan.ApplyModifications("Attack/" + type + "/All", (+self.template[type][damageType] || 0), self.entity) - self.template[type][damageType]; return allComponent + cmpTechMan.ApplyModifications("Attack/" + type + "/" + damageType, (+self.template[type][damageType] || 0), self.entity); }; return { hack: applyTechs("Hack"), pierce: applyTechs("Pierce"), crush: applyTechs("Crush") }; }; Attack.prototype.GetRange = function(type) { var cmpTechMan = QueryOwnerInterface(this.entity, IID_TechnologyManager); var max = cmpTechMan.ApplyModifications("Attack/" + type + "/MaxRange", +this.template[type].MaxRange, this.entity); var min = cmpTechMan.ApplyModifications("Attack/" + type + "/MinRange", +(this.template[type].MinRange || 0), this.entity); return { "max": max, "min": min }; }; // Calculate the attack damage multiplier against a target Attack.prototype.GetAttackBonus = function(type, target) { var attackBonus = 1; if (this.template[type].Bonuses) { var cmpIdentity = Engine.QueryInterface(target, IID_Identity); if (!cmpIdentity) return 1; // Multiply the bonuses for all matching classes for (var key in this.template[type].Bonuses) { var bonus = this.template[type].Bonuses[key]; var hasClasses = true; if (bonus.Classes){ var classes = bonus.Classes.split(/\s+/); for (var key in classes) hasClasses = hasClasses && cmpIdentity.HasClass(classes[key]); } if (hasClasses && (!bonus.Civ || bonus.Civ === cmpIdentity.GetCiv())) attackBonus *= bonus.Multiplier; } } return attackBonus; }; /** * Attack the target entity. This should only be called after a successful range check, * and should only be called after GetTimers().repeat msec has passed since the last * call to PerformAttack. */ Attack.prototype.PerformAttack = function(type, target) { // If this is a ranged attack, then launch a projectile if (type == "Ranged") { // To implement (in)accuracy, for arrows and javelins, we want to do the following: // * Compute an accuracy rating, based on the entity's characteristics and the distance to the target // * Pick a random point 'close' to the target (based on the accuracy) which is the real target point // * Pick a real target unit, based on their footprint's proximity to the real target point // * If there is none, then harmlessly shoot to the real target point instead // * If the real target unit moves after being targeted, the projectile will follow it and hit it anyway // // In the future this should be extended: // * If the target unit moves too far, the projectile should 'detach' and not hit it, so that // players can dodge projectiles. (Or it should pick a new target after detaching, so it can still // hit somebody.) // * Obstacles like trees could reduce the probability of the target being hit // * Obstacles like walls should block projectiles entirely // * There should be more control over the probabilities of hitting enemy units vs friendly units vs missing, // for gameplay balance tweaks // * Larger, slower projectiles (catapults etc) shouldn't pick targets first, they should just // hurt anybody near their landing point // Get some data about the entity var horizSpeed = +this.template[type].ProjectileSpeed; var gravity = 9.81; // this affects the shape of the curve; assume it's constant for now var accuracy = 6; // TODO: get from entity template //horizSpeed /= 8; gravity /= 8; // slow it down for testing // Find the distance to the target var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return; var selfPosition = cmpPosition.GetPosition(); var cmpTargetPosition = Engine.QueryInterface(target, IID_Position); if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) return; var targetPosition = cmpTargetPosition.GetPosition(); var horizDistance = Math.sqrt(Math.pow(targetPosition.x - selfPosition.x, 2) + Math.pow(targetPosition.z - selfPosition.z, 2)); // Compute the real target point (based on accuracy) var angle = Math.random() * 2*Math.PI; var r = 1 - Math.sqrt(Math.random()); // triangular distribution [0,1] (cluster around the center) var offset = r * accuracy; // TODO: should be affected by range var offsetX = offset * Math.sin(angle); var offsetZ = offset * Math.cos(angle); var realTargetPosition = { "x": targetPosition.x + offsetX, "y": targetPosition.y, "z": targetPosition.z + offsetZ }; // TODO: what we should really do here is select the unit whose footprint is closest to the realTargetPosition // (and harmlessly hit the ground if there's none), but as a simplification let's just randomly decide whether to // hit the original target or not. var realTargetUnit = undefined; if (Math.random() < 0.5) // TODO: this is yucky and hardcoded { // Hit the original target realTargetUnit = target; realTargetPosition = targetPosition; } else { // Hit the ground // TODO: ought to make sure Y is on the ground } // Hurt the target after the appropriate time if (realTargetUnit) { var realHorizDistance = Math.sqrt(Math.pow(realTargetPosition.x - selfPosition.x, 2) + Math.pow(realTargetPosition.z - selfPosition.z, 2)); var timeToTarget = realHorizDistance / horizSpeed; var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.SetTimeout(this.entity, IID_Attack, "CauseDamage", timeToTarget*1000, {"type": type, "target": target}); } // Launch the graphical projectile var cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); if (realTargetUnit) cmpProjectileManager.LaunchProjectileAtEntity(this.entity, realTargetUnit, horizSpeed, gravity); else cmpProjectileManager.LaunchProjectileAtPoint(this.entity, realTargetPosition, horizSpeed, gravity); } else { // Melee attack - hurt the target immediately this.CauseDamage({"type": type, "target": target}); } // TODO: charge attacks (need to design how they work) }; /** * Called when some units kills something (another unit, building, animal etc) */ Attack.prototype.TargetKilled = function(killerEntity, targetEntity) { var cmpKillerPlayerStatisticsTracker = QueryOwnerInterface(killerEntity, IID_StatisticsTracker); if (cmpKillerPlayerStatisticsTracker) cmpKillerPlayerStatisticsTracker.KilledEntity(targetEntity); var cmpTargetPlayerStatisticsTracker = QueryOwnerInterface(targetEntity, IID_StatisticsTracker); if (cmpTargetPlayerStatisticsTracker) cmpTargetPlayerStatisticsTracker.LostEntity(targetEntity); // if unit can collect loot, lets try to collect it var cmpLooter = Engine.QueryInterface(killerEntity, IID_Looter); if (cmpLooter) { cmpLooter.Collect(targetEntity); } }; /** * Inflict damage on the target */ Attack.prototype.CauseDamage = function(data) { var strengths = this.GetAttackStrengths(data.type); var attackBonus = this.GetAttackBonus(data.type, data.target); var cmpDamageReceiver = Engine.QueryInterface(data.target, IID_DamageReceiver); if (!cmpDamageReceiver) return; var targetState = cmpDamageReceiver.TakeDamage(strengths.hack * attackBonus, strengths.pierce * attackBonus, strengths.crush * attackBonus); // if target killed pick up loot and credit experience if (targetState.killed == true) { this.TargetKilled(this.entity, data.target); } Engine.PostMessage(data.target, MT_Attacked, { "attacker": this.entity, "target": data.target, "type": data.type }); PlaySound("attack_impact", this.entity); }; Engine.RegisterComponentType(IID_Attack, "Attack", Attack);