Index: ps/trunk/binaries/data/mods/public/simulation/components/Attack.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Attack.js (revision 14230) +++ ps/trunk/binaries/data/mods/public/simulation/components/Attack.js (revision 14231) @@ -1,719 +1,620 @@ function Attack() {} var bonusesSchema = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; var preferredClassesSchema = "" + "" + "" + "tokens" + "" + "" + "" + ""; var restrictedClassesSchema = "" + "" + "" + "tokens" + "" + "" + "" + ""; 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" + "" + "" + "Champion" + "Cavalry Infantry" + "" + "" + "0.0" + "10.0" + "0.0" + "44.0" + "20.0" + "15.0" + "800" + "1600" + "50.0" + "2.5" + "" + "" + "Cavalry" + "2" + "" + "" + "Champion" + "" + "Circular" + "20" + "false" + "0.0" + "10.0" + "0.0" + "" + "" + "" + "10.0" + "0.0" + "50.0" + "24.0" + "20.0" + "" + "" + "1000.0" + "0.0" + "0.0" + "4.0" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + // TODO: it shouldn't be stretched "" + "" + bonusesSchema + preferredClassesSchema + restrictedClassesSchema + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""+ "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + bonusesSchema + preferredClassesSchema + restrictedClassesSchema + "" + "" + "" + "" + "" + "" + "" + "" + "" + bonusesSchema + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + // TODO: how do these work? "" + bonusesSchema + preferredClassesSchema + restrictedClassesSchema + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + // TODO: how do these work? bonusesSchema + preferredClassesSchema + restrictedClassesSchema + "" + "" + ""; Attack.prototype.Init = function() { }; Attack.prototype.Serialize = null; // we have no dynamic state to save Attack.prototype.GetAttackTypes = function() { var ret = []; if (this.template.Charge) ret.push("Charge"); if (this.template.Melee) ret.push("Melee"); if (this.template.Ranged) ret.push("Ranged"); return ret; }; Attack.prototype.GetPreferredClasses = function(type) { if (this.template[type] && this.template[type].PreferredClasses && this.template[type].PreferredClasses._string) { return this.template[type].PreferredClasses._string.split(/\s+/); } return []; }; Attack.prototype.GetRestrictedClasses = function(type) { if (this.template[type] && this.template[type].RestrictedClasses && this.template[type].RestrictedClasses._string) { return this.template[type].RestrictedClasses._string.split(/\s+/); } return []; }; Attack.prototype.CanAttack = function(target) { const cmpIdentity = Engine.QueryInterface(target, IID_Identity); if (!cmpIdentity) return undefined; const targetClasses = cmpIdentity.GetClassesList(); for each (var type in this.GetAttackTypes()) { var canAttack = true; var restrictedClasses = this.GetRestrictedClasses(type); for each (var targetClass in targetClasses) { if (restrictedClasses.indexOf(targetClass) != -1) { canAttack = false; break; } } if (canAttack) { return true; } } return false; }; /** * Returns null if we have no preference or the lowest index of a preferred class. */ Attack.prototype.GetPreference = function(target) { const cmpIdentity = Engine.QueryInterface(target, IID_Identity); if (!cmpIdentity) return undefined; const targetClasses = cmpIdentity.GetClassesList(); - + var minPref = null; for each (var type in this.GetAttackTypes()) { for each (var targetClass in targetClasses) { var pref = this.GetPreferredClasses(type).indexOf(targetClass); if (pref != -1 && (minPref === null || minPref > pref)) { minPref = pref; } } } return minPref; }; /** * 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() { return this.GetAttackTypes().pop(); }; Attack.prototype.GetBestAttackAgainst = function(target) { const cmpIdentity = Engine.QueryInterface(target, IID_Identity); if (!cmpIdentity) return undefined; const targetClasses = cmpIdentity.GetClassesList(); const isTargetClass = function (value, i, a) { return targetClasses.indexOf(value) != -1; }; const types = this.GetAttackTypes(); const attack = this; const isAllowed = function (value, i, a) { return !attack.GetRestrictedClasses(value).some(isTargetClass); } const isPreferred = function (value, i, a) { return attack.GetPreferredClasses(value).some(isTargetClass); } const byPreference = function (a, b) { return (types.indexOf(a) + (isPreferred(a) ? types.length : 0) ) - (types.indexOf(b) + (isPreferred(b) ? types.length : 0) ); } - + // Always slaughter domestic animals instead of using a normal attack if (isTargetClass("Domestic") && this.template.Slaughter) return "Slaughter"; return types.filter(isAllowed).sort(byPreference).pop(); }; Attack.prototype.CompareEntitiesByPreference = function(a, b) { var aPreference = this.GetPreference(a); var bPreference = this.GetPreference(b); if (aPreference === null && bPreference === null) return 0; if (aPreference === null) return 1; if (bPreference === null) return -1; return aPreference - bPreference; }; Attack.prototype.GetTimers = function(type) { var prepare = +(this.template[type].PrepareTime || 0); prepare = ApplyValueModificationsToEntity("Attack/" + type + "/PrepareTime", prepare, this.entity); var repeat = +(this.template[type].RepeatTime || 1000); repeat = ApplyValueModificationsToEntity("Attack/" + type + "/RepeatTime", repeat, 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 template = this.template[type]; var splash = ""; if (!template) { template = this.template[type.split(".")[0]].Splash; splash = "/Splash"; } var applyMods = function(damageType) { return ApplyValueModificationsToEntity("Attack/" + type + splash + "/" + damageType, +(template[damageType] || 0), self.entity); }; return { hack: applyMods("Hack"), pierce: applyMods("Pierce"), crush: applyMods("Crush") }; }; Attack.prototype.GetRange = function(type) { var max = +this.template[type].MaxRange; max = ApplyValueModificationsToEntity("Attack/" + type + "/MaxRange", max, this.entity); var min = +(this.template[type].MinRange || 0); min = ApplyValueModificationsToEntity("Attack/" + type + "/MinRange", min, this.entity); var elevationBonus = +(this.template[type].ElevationBonus || 0); elevationBonus = ApplyValueModificationsToEntity("Attack/" + type + "/ElevationBonus", elevationBonus, this.entity); return { "max": max, "min": min, "elevationBonus": elevationBonus}; }; // Calculate the attack damage multiplier against a target Attack.prototype.GetAttackBonus = function(type, target) { var attackBonus = 1; var template = this.template[type]; if (!template) template = this.template[type.split(".")[0]].Splash; - + if (template.Bonuses) { var cmpIdentity = Engine.QueryInterface(target, IID_Identity); if (!cmpIdentity) return 1; - + // Multiply the bonuses for all matching classes for (var key in template.Bonuses) { var bonus = template.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; }; // Returns a 2d random distribution scaled for a spread of scale 1. // The current implementation is a 2d gaussian with sigma = 1 Attack.prototype.GetNormalDistribution = function(){ - + // Use the Box-Muller transform to get a gaussian distribution var a = Math.random(); var b = Math.random(); - + var c = Math.sqrt(-2*Math.log(a)) * Math.cos(2*Math.PI*b); var d = Math.sqrt(-2*Math.log(a)) * Math.sin(2*Math.PI*b); - + return [c, d]; }; /** * 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") { // In the future this could be extended: // * Obstacles like trees could reduce the probability of the target being hit // * Obstacles like walls should block projectiles entirely // 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 spread = +this.template.Ranged.Spread; spread = ApplyValueModificationsToEntity("Attack/Ranged/Spread", spread, this.entity); - + //horizSpeed /= 2; gravity /= 2; // slow it down for testing - + 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 relativePosition = {"x": targetPosition.x - selfPosition.x, "z": targetPosition.z - selfPosition.z} var previousTargetPosition = Engine.QueryInterface(target, IID_Position).GetPreviousPosition(); - + var targetVelocity = {"x": (targetPosition.x - previousTargetPosition.x) / this.turnLength, "z": (targetPosition.z - previousTargetPosition.z) / this.turnLength} // the component of the targets velocity radially away from the archer var radialSpeed = this.VectorDot(relativePosition, targetVelocity) / this.VectorLength(relativePosition); - + var horizDistance = this.VectorDistance(targetPosition, selfPosition); - + // This is an approximation of the time ot the target, it assumes that the target has a constant radial // velocity, but since units move in straight lines this is not true. The exact value would be more // difficult to calculate and I think this is sufficiently accurate. (I tested and for cavalry it was // about 5% of the units radius out in the worst case) var timeToTarget = horizDistance / (horizSpeed - radialSpeed); - + // Predict where the unit is when the missile lands. var predictedPosition = {"x": targetPosition.x + targetVelocity.x * timeToTarget, "z": targetPosition.z + targetVelocity.z * timeToTarget}; - + // Compute the real target point (based on spread and target speed) var randNorm = this.GetNormalDistribution(); var offsetX = randNorm[0] * spread * (1 + this.VectorLength(targetVelocity) / 20); var offsetZ = randNorm[1] * spread * (1 + this.VectorLength(targetVelocity) / 20); var realTargetPosition = { "x": predictedPosition.x + offsetX, "y": targetPosition.y, "z": predictedPosition.z + offsetZ }; - + // Calculate when the missile will hit the target position var realHorizDistance = this.VectorDistance(realTargetPosition, selfPosition); var timeToTarget = realHorizDistance / horizSpeed; - + var missileDirection = {"x": (realTargetPosition.x - selfPosition.x) / realHorizDistance, "z": (realTargetPosition.z - selfPosition.z) / realHorizDistance}; - + // Make the arrow appear to land slightly behind the target so that arrows landing next to a guys foot don't count but arrows that go through the torso do var graphicalPosition = {"x": realTargetPosition.x + 2*missileDirection.x, "y": realTargetPosition.y + 2*missileDirection.y}; // Launch the graphical projectile var cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); var id = cmpProjectileManager.LaunchProjectileAtPoint(this.entity, realTargetPosition, horizSpeed, gravity); - + + var playerId = Engine.QueryInterface(this.entity, IID_Ownership).GetOwner() var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - cmpTimer.SetTimeout(this.entity, IID_Attack, "MissileHit", timeToTarget*1000, {"type": type, "target": target, "position": realTargetPosition, "direction": missileDirection, "projectileId": id}); + cmpTimer.SetTimeout(this.entity, IID_Attack, "MissileHit", timeToTarget*1000, {"type": type, "target": target, "position": realTargetPosition, "direction": missileDirection, "projectileId": id, "playerId":playerId}); } else { // Melee attack - hurt the target immediately - this.CauseDamage({"type": type, "target": target}); + Damage.CauseDamage({"strengths":this.GetAttackStrengths(type), "target":target, "attacker":this.entity, "multiplier":this.GetAttackBonus(type, target), "type":type}); } // 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); - } -}; - Attack.prototype.InterpolatedLocation = function(ent, lateness) { var cmpTargetPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) // TODO: handle dead target properly return undefined; var curPos = cmpTargetPosition.GetPosition(); var prevPos = cmpTargetPosition.GetPreviousPosition(); lateness /= 1000; return {"x": (curPos.x * (this.turnLength - lateness) + prevPos.x * lateness) / this.turnLength, "z": (curPos.z * (this.turnLength - lateness) + prevPos.z * lateness) / this.turnLength}; }; Attack.prototype.VectorDistance = function(p1, p2) { return Math.sqrt((p1.x - p2.x)*(p1.x - p2.x) + (p1.z - p2.z)*(p1.z - p2.z)); }; Attack.prototype.VectorDot = function(p1, p2) { return (p1.x * p2.x + p1.z * p2.z); }; -Attack.prototype.VectorCross = function(p1, p2) -{ - return (p1.x * p2.z - p1.z * p2.x); -}; - Attack.prototype.VectorLength = function(p) { return Math.sqrt(p.x*p.x + p.z*p.z); }; // Tests whether it point is inside of ent's footprint Attack.prototype.testCollision = function(ent, point, lateness) { var targetPosition = this.InterpolatedLocation(ent, lateness); if (!targetPosition) return false; var cmpFootprint = Engine.QueryInterface(ent, IID_Footprint); if (!cmpFootprint) return false; var targetShape = cmpFootprint.GetShape(); - + if (!targetShape || !targetPosition) return false; - + if (targetShape.type === 'circle') { return (this.VectorDistance(point, targetPosition) < targetShape.radius); } else { var targetRotation = Engine.QueryInterface(ent, IID_Position).GetRotation().y; - + var dx = point.x - targetPosition.x; var dz = point.z - targetPosition.z; - + var dxr = Math.cos(targetRotation) * dx - Math.sin(targetRotation) * dz; var dzr = Math.sin(targetRotation) * dx + Math.cos(targetRotation) * dz; - + return (-targetShape.width/2 <= dxr && dxr < targetShape.width/2 && -targetShape.depth/2 <= dzr && dzr < targetShape.depth/2); } }; Attack.prototype.MissileHit = function(data, lateness) { var targetPosition = this.InterpolatedLocation(data.target, lateness); if (!targetPosition) return; if (this.template.Ranged.Splash) // splash damage, do this first in case the direct hit kills the target { var friendlyFire = this.template.Ranged.Splash.FriendlyFire; var splashRadius = this.template.Ranged.Splash.Range; var splashShape = this.template.Ranged.Splash.Shape; - - var ents = this.GetNearbyEntities(data.target, this.VectorDistance(data.position, targetPosition) * 2 + splashRadius, friendlyFire); - ents.push(data.target); // Add the original unit to the list of splash damage targets - - for (var i = 0; i < ents.length; i++) + var playersToDamage; + // If friendlyFire isn't enabled, get all player enemies to pass to "Damage.CauseSplashDamage". + if (friendlyFire == false) { - var entityPosition = this.InterpolatedLocation(ents[i], lateness); - var radius = this.VectorDistance(data.position, entityPosition); - - if (radius < splashRadius) - { - var multiplier = 1; - if (splashShape == "Circular") // quadratic falloff - { - multiplier *= 1 - ((radius * radius) / (splashRadius * splashRadius)); - } - else if (splashShape == "Linear") - { - // position of entity relative to where the missile hit - var relPos = {"x": entityPosition.x - data.position.x, "z": entityPosition.z - data.position.z}; - - var splashWidth = splashRadius / 5; - var parallelDist = this.VectorDot(relPos, data.direction); - var perpDist = Math.abs(this.VectorCross(relPos, data.direction)); - - // Check that the unit is within the distance splashWidth of the line starting at the missile's - // landing point which extends in the direction of the missile for length splashRadius. - if (parallelDist > -splashWidth && perpDist < splashWidth) - { - // Use a quadratic falloff in both directions - multiplier = (splashRadius*splashRadius - parallelDist*parallelDist) / (splashRadius*splashRadius) - * (splashWidth*splashWidth - perpDist*perpDist) / (splashWidth*splashWidth); - } - else - { - multiplier = 0; - } - } - var newData = {"type": data.type + ".Splash", "target": ents[i], "damageMultiplier": multiplier}; - this.CauseDamage(newData); - } + var cmpPlayer = Engine.QueryInterface(Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetPlayerByID(data.playerId), IID_Player) + playersToDamage = cmpPlayer.GetEnemies(); } + // Damage the units. + Damage.CauseSplashDamage({"attacker":this.entity, "origin":data.position, "radius":splashRadius, "shape":splashShape, "strengths":this.GetAttackStrengths(data.type), "direction":data.direction, "playersToDamage":playersToDamage, "type":data.type}); } - + if (this.testCollision(data.target, data.position, lateness)) { + data.attacker = this.entity + data.multiplier = this.GetAttackBonus(data.type, data.target) + data.strengths = this.GetAttackStrengths(data.type) // Hit the primary target - this.CauseDamage(data); - + Damage.CauseDamage(data); + // Remove the projectile var cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); cmpProjectileManager.RemoveProjectile(data.projectileId); } else { // If we didn't hit the main target look for nearby units - var ents = this.GetNearbyEntities(data.target, this.VectorDistance(data.position, targetPosition) * 2); - + var cmpPlayer = Engine.QueryInterface(Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetPlayerByID(data.playerId), IID_Player) + var ents = Damage.EntitiesNearPoint(data.position, this.VectorDistance(data.position, targetPosition) * 2, cmpPlayer.GetEnemies()); + for (var i = 0; i < ents.length; i++) { if (this.testCollision(ents[i], data.position, lateness)) { - var newData = {"type": data.type, "target": ents[i]}; - this.CauseDamage(newData); - + var newData = {"strengths":this.GetAttackStrengths(data.type), "target":ents[i], "attacker":this.entity, "multiplier":this.GetAttackBonus(data.type, ents[i]), "type":data.type}; + Damage.CauseDamage(newData); // Remove the projectile var cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); cmpProjectileManager.RemoveProjectile(data.projectileId); } } } }; -Attack.prototype.GetNearbyEntities = function(startEnt, range, friendlyFire) -{ - var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); - var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); - var owner = cmpOwnership.GetOwner(); - var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(owner), IID_Player); - var numPlayers = cmpPlayerManager.GetNumPlayers(); - var players = []; - - for (var i = 1; i < numPlayers; ++i) - { - // Only target enemies unless friendly fire is on - if (cmpPlayer.IsEnemy(i) || friendlyFire) - players.push(i); - } - - var rangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); - return rangeManager.ExecuteQuery(startEnt, 0, range, players, IID_DamageReceiver); -} - -/** - * Inflict damage on the target - */ -Attack.prototype.CauseDamage = function(data) -{ - var strengths = this.GetAttackStrengths(data.type); - - var damageMultiplier = this.GetAttackBonus(data.type, data.target); - if (data.damageMultiplier !== undefined) - damageMultiplier *= data.damageMultiplier; - - var cmpDamageReceiver = Engine.QueryInterface(data.target, IID_DamageReceiver); - if (!cmpDamageReceiver) - return; - var targetState = cmpDamageReceiver.TakeDamage(strengths.hack * damageMultiplier, strengths.pierce * damageMultiplier, strengths.crush * damageMultiplier, this.entity); - // 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, "damage": -targetState.change }); - - PlaySound("attack_impact", this.entity); -}; - Attack.prototype.OnUpdate = function(msg) { this.turnLength = msg.turnLength; } Engine.RegisterComponentType(IID_Attack, "Attack", Attack); Index: ps/trunk/binaries/data/mods/public/simulation/components/Player.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Player.js (revision 14230) +++ ps/trunk/binaries/data/mods/public/simulation/components/Player.js (revision 14231) @@ -1,614 +1,626 @@ function Player() {} Player.prototype.Schema = ""; Player.prototype.Init = function() { this.playerID = undefined; this.name = undefined; // define defaults elsewhere (supporting other languages) this.civ = undefined; this.colour = { "r": 0.0, "g": 0.0, "b": 0.0, "a": 1.0 }; this.popUsed = 0; // population of units owned or trained by this player this.popBonuses = 0; // sum of population bonuses of player's entities this.maxPop = 300; // maximum population this.trainingBlocked = false; // indicates whether any training queue is currently blocked this.resourceCount = { "food": 300, "wood": 300, "metal": 300, "stone": 300 }; this.team = -1; // team number of the player, players on the same team will always have ally diplomatic status - also this is useful for team emblems, scoring, etc. this.teamsLocked = false; this.state = "active"; // game state - one of "active", "defeated", "won" this.diplomacy = []; // array of diplomatic stances for this player with respect to other players (including gaia and self) this.conquestCriticalEntitiesCount = 0; // number of owned units with ConquestCritical class this.phase = "village"; this.formations = []; this.startCam = undefined; this.controlAllUnits = false; this.isAI = false; this.gatherRateMultiplier = 1; this.cheatsEnabled = false; this.cheatTimeMultiplier = 1; this.heroes = []; Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager).CheckPlayers(); }; Player.prototype.SetPlayerID = function(id) { this.playerID = id; }; Player.prototype.GetPlayerID = function() { return this.playerID; }; Player.prototype.SetName = function(name) { this.name = name; }; Player.prototype.GetName = function() { return this.name; }; Player.prototype.SetCiv = function(civcode) { this.civ = civcode; }; Player.prototype.GetCiv = function() { return this.civ; }; Player.prototype.SetColour = function(r, g, b) { this.colour = { "r": r/255.0, "g": g/255.0, "b": b/255.0, "a": 1.0 }; }; Player.prototype.GetColour = function() { return this.colour; }; // Try reserving num population slots. Returns 0 on success or number of missing slots otherwise. Player.prototype.TryReservePopulationSlots = function(num) { if (num != 0 && num > (this.GetPopulationLimit() - this.GetPopulationCount())) return num - (this.GetPopulationLimit() - this.GetPopulationCount()); this.popUsed += num; return 0; }; Player.prototype.UnReservePopulationSlots = function(num) { this.popUsed -= num; }; Player.prototype.GetPopulationCount = function() { return this.popUsed; }; Player.prototype.SetPopulationBonuses = function(num) { this.popBonuses = num; }; Player.prototype.GetPopulationLimit = function() { return Math.min(this.GetMaxPopulation(), this.popBonuses); }; Player.prototype.SetMaxPopulation = function(max) { this.maxPop = max; }; Player.prototype.GetMaxPopulation = function() { return Math.round(ApplyValueModificationsToPlayer("Player/MaxPopulation", this.maxPop, this.entity)); }; Player.prototype.SetGatherRateMultiplier = function(value) { this.gatherRateMultiplier = value; }; Player.prototype.GetGatherRateMultiplier = function() { return this.gatherRateMultiplier; }; Player.prototype.GetHeroes = function() { return this.heroes; }; Player.prototype.IsTrainingBlocked = function() { return this.trainingBlocked; }; Player.prototype.BlockTraining = function() { this.trainingBlocked = true; }; Player.prototype.UnBlockTraining = function() { this.trainingBlocked = false; }; Player.prototype.SetResourceCounts = function(resources) { if (resources.food !== undefined) this.resourceCount.food = resources.food; if (resources.wood !== undefined) this.resourceCount.wood = resources.wood; if (resources.stone !== undefined) this.resourceCount.stone = resources.stone; if (resources.metal !== undefined) this.resourceCount.metal = resources.metal; }; Player.prototype.GetResourceCounts = function() { return this.resourceCount; }; /** * Add resource of specified type to player * @param type Generic type of resource (string) * @param amount Amount of resource, which should be added (integer) */ Player.prototype.AddResource = function(type, amount) { this.resourceCount[type] += (+amount); }; /** * Add resources to player */ Player.prototype.AddResources = function(amounts) { for (var type in amounts) { this.resourceCount[type] += (+amounts[type]); } }; Player.prototype.GetNeededResources = function(amounts) { // Check if we can afford it all var amountsNeeded = {}; for (var type in amounts) if (this.resourceCount[type] != undefined && amounts[type] > this.resourceCount[type]) amountsNeeded[type] = amounts[type] - this.resourceCount[type]; if (Object.keys(amountsNeeded).length == 0) return undefined; return amountsNeeded; }; Player.prototype.SubtractResourcesOrNotify = function(amounts) { var amountsNeeded = this.GetNeededResources(amounts); // If we don't have enough resources, send a notification to the player if (amountsNeeded) { var formatted = []; for (var type in amountsNeeded) formatted.push(amountsNeeded[type] + " " + type[0].toUpperCase() + type.substr(1) ); var notification = {"player": this.playerID, "message": "Insufficient resources - " + formatted.join(", ")}; var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification(notification); return false; } // Subtract the resources for (var type in amounts) this.resourceCount[type] -= amounts[type]; return true; }; Player.prototype.TrySubtractResources = function(amounts) { if (!this.SubtractResourcesOrNotify(amounts)) return false; var cmpStatisticsTracker = QueryPlayerIDInterface(this.playerID, IID_StatisticsTracker); if (cmpStatisticsTracker) for (var type in amounts) cmpStatisticsTracker.IncreaseResourceUsedCounter(type, amounts[type]); return true; }; Player.prototype.GetState = function() { return this.state; }; Player.prototype.SetState = function(newState) { this.state = newState; }; Player.prototype.GetConquestCriticalEntitiesCount = function() { return this.conquestCriticalEntitiesCount; }; Player.prototype.GetTeam = function() { return this.team; }; Player.prototype.SetTeam = function(team) { if (!this.teamsLocked) { this.team = team; var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (cmpPlayerManager && this.team != -1) { // Set all team members as allies for (var i = 0; i < cmpPlayerManager.GetNumPlayers(); ++i) { var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(i), IID_Player); if (this.team == cmpPlayer.GetTeam()) { this.SetAlly(i); cmpPlayer.SetAlly(this.playerID); } } } Engine.BroadcastMessage(MT_DiplomacyChanged, {"player": this.playerID}); } }; Player.prototype.SetLockTeams = function(value) { this.teamsLocked = value; }; Player.prototype.GetLockTeams = function() { return this.teamsLocked; }; Player.prototype.GetDiplomacy = function() { return this.diplomacy; }; Player.prototype.SetDiplomacy = function(dipl) { // Should we check for teamsLocked here? this.diplomacy = dipl; Engine.BroadcastMessage(MT_DiplomacyChanged, {"player": this.playerID}); }; Player.prototype.SetDiplomacyIndex = function(idx, value) { var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (!cmpPlayerManager) return; var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(idx), IID_Player); if (!cmpPlayer) return; if (this.state != "active" || cmpPlayer.state != "active") return; // You can have alliances with other players, if (this.teamsLocked) { // but can't stab your team members in the back if (this.team == -1 || cmpPlayer && this.team != cmpPlayer.GetTeam()) { // Break alliance or declare war if (Math.min(this.diplomacy[idx],cmpPlayer.diplomacy[this.playerID]) > value) { this.diplomacy[idx] = value; cmpPlayer.SetDiplomacyIndex(this.playerID, value); } else { this.diplomacy[idx] = value; } Engine.BroadcastMessage(MT_DiplomacyChanged, {"player": this.playerID}); } } else { // Break alliance or declare war (worsening of relations is mutual) if (Math.min(this.diplomacy[idx],cmpPlayer.diplomacy[this.playerID]) > value) { // This is duplicated because otherwise we get too much recursion this.diplomacy[idx] = value; cmpPlayer.SetDiplomacyIndex(this.playerID, value); } else { this.diplomacy[idx] = value; } Engine.BroadcastMessage(MT_DiplomacyChanged, {"player": this.playerID}); } }; Player.prototype.UpdateSharedLos = function() { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (!cmpRangeManager) return; var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (!cmpPlayerManager) return; var sharedLos = []; for (var i = 0; i < cmpPlayerManager.GetNumPlayers(); ++i) if (this.IsMutualAlly(i)) sharedLos.push(i); cmpRangeManager.SetSharedLos(this.playerID, sharedLos); }; Player.prototype.GetFormations = function() { return this.formations; }; Player.prototype.SetFormations = function(formations) { this.formations = formations; }; Player.prototype.GetStartingCameraPos = function() { return this.startCam.position; }; Player.prototype.GetStartingCameraRot = function() { return this.startCam.rotation; }; Player.prototype.SetStartingCamera = function(pos, rot) { this.startCam = {"position": pos, "rotation": rot}; }; Player.prototype.HasStartingCamera = function() { return (this.startCam !== undefined); }; Player.prototype.SetControlAllUnits = function(c) { this.controlAllUnits = c; }; Player.prototype.CanControlAllUnits = function() { return this.controlAllUnits; }; Player.prototype.SetAI = function(flag) { this.isAI = flag; }; Player.prototype.IsAI = function() { return this.isAI; }; Player.prototype.SetAlly = function(id) { this.SetDiplomacyIndex(id, 1); }; /** * Check if given player is our ally */ Player.prototype.IsAlly = function(id) { return this.diplomacy[id] > 0; }; /** * Check if given player is our ally, and we are its ally */ Player.prototype.IsMutualAlly = function(id) { var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (!cmpPlayerManager) return false; var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(id), IID_Player); return this.IsAlly(id) && cmpPlayer && cmpPlayer.IsAlly(this.playerID); }; Player.prototype.SetEnemy = function(id) { this.SetDiplomacyIndex(id, -1); }; /** + * Get all enemies of a given player. + */ +Player.prototype.GetEnemies = function() +{ + var enemies = []; + for (var i = 0; i < this.diplomacy.length; i++) + if (this.diplomacy[i] < 0) + enemies.push(i); + return enemies; +}; + +/** * Check if given player is our enemy */ Player.prototype.IsEnemy = function(id) { return this.diplomacy[id] < 0; }; Player.prototype.SetNeutral = function(id) { this.SetDiplomacyIndex(id, 0); }; /** * Check if given player is neutral */ Player.prototype.IsNeutral = function(id) { return this.diplomacy[id] == 0; }; /** * Keep track of population effects of all entities that * become owned or unowned by this player */ Player.prototype.OnGlobalOwnershipChanged = function(msg) { if (msg.from != this.playerID && msg.to != this.playerID) return; var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity); var cmpCost = Engine.QueryInterface(msg.entity, IID_Cost); if (msg.from == this.playerID) { if (cmpIdentity && cmpIdentity.HasClass("ConquestCritical")) this.conquestCriticalEntitiesCount--; if (this.conquestCriticalEntitiesCount == 0) // end game when needed Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager).CheckPlayers(); if (cmpCost) { this.popUsed -= cmpCost.GetPopCost(); this.popBonuses -= cmpCost.GetPopBonus(); } if (cmpIdentity && cmpIdentity.HasClass("Hero")) { //Remove from Heroes list var index = this.heroes.indexOf(msg.entity); if (index >= 0) this.heroes.splice(index, 1); } } if (msg.to == this.playerID) { if (cmpIdentity && cmpIdentity.HasClass("ConquestCritical")) this.conquestCriticalEntitiesCount++; if (cmpCost) { this.popUsed += cmpCost.GetPopCost(); this.popBonuses += cmpCost.GetPopBonus(); } if (cmpIdentity && cmpIdentity.HasClass("Hero")) this.heroes.push(msg.entity); } }; Player.prototype.OnPlayerDefeated = function(msg) { this.state = "defeated"; // TODO: Tribute all resources to this player's active allies (if any) // Reassign all player's entities to Gaia var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var entities = cmpRangeManager.GetEntitiesByPlayer(this.playerID); // The ownership change is done in two steps so that entities don't hit idle // (and thus possibly look for "enemies" to attack) before nearby allies get // converted to Gaia as well. for each (var entity in entities) { var cmpOwnership = Engine.QueryInterface(entity, IID_Ownership); cmpOwnership.SetOwnerQuiet(0); } // With the real ownership change complete, send OwnershipChanged messages. for each (var entity in entities) Engine.PostMessage(entity, MT_OwnershipChanged, { "entity": entity, "from": this.playerID, "to": 0 }); // Reveal the map for this player. cmpRangeManager.SetLosRevealAll(this.playerID, true); // Send a chat message notifying of the player's defeat. var notification = {"type": "defeat", "player": this.playerID}; var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification(notification); }; Player.prototype.OnDiplomacyChanged = function() { this.UpdateSharedLos(); }; Player.prototype.SetCheatsEnabled = function(flag) { this.cheatsEnabled = flag; }; Player.prototype.GetCheatsEnabled = function() { return this.cheatsEnabled; }; Player.prototype.SetCheatTimeMultiplier = function(time) { this.cheatTimeMultiplier = time; }; Player.prototype.GetCheatTimeMultiplier = function() { return this.cheatTimeMultiplier; }; Player.prototype.TributeResource = function(player, amounts) { var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (!cmpPlayerManager) return; var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(player), IID_Player); if (!cmpPlayer) return; if (this.state != "active" || cmpPlayer.state != "active") return; if (!this.SubtractResourcesOrNotify(amounts)) return; cmpPlayer.AddResources(amounts); var total = Object.keys(amounts).reduce(function (sum, type){ return sum + amounts[type]; }, 0); var cmpOurStatisticsTracker = QueryPlayerIDInterface(this.playerID, IID_StatisticsTracker); if (cmpOurStatisticsTracker) cmpOurStatisticsTracker.IncreaseTributesSentCounter(total); var cmpTheirStatisticsTracker = QueryPlayerIDInterface(player, IID_StatisticsTracker); if (cmpTheirStatisticsTracker) cmpTheirStatisticsTracker.IncreaseTributesReceivedCounter(total); var notification = {"type": "tribute", "player": player, "player1": this.playerID, "amounts": amounts}; var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); if (cmpGUIInterface) cmpGUIInterface.PushNotification(notification); }; Engine.RegisterComponentType(IID_Player, "Player", Player); Index: ps/trunk/binaries/data/mods/public/simulation/components/PlayerManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/PlayerManager.js (revision 14230) +++ ps/trunk/binaries/data/mods/public/simulation/components/PlayerManager.js (revision 14231) @@ -1,49 +1,53 @@ function PlayerManager() {} PlayerManager.prototype.Schema = ""; PlayerManager.prototype.Init = function() { this.playerEntities = []; // list of player entity IDs }; PlayerManager.prototype.AddPlayer = function(ent) { var id = this.playerEntities.length; Engine.QueryInterface(ent, IID_Player).SetPlayerID(id); this.playerEntities.push(ent); return id; }; /** * Returns the player entity ID for the given player ID. * The player ID must be valid (else there will be an error message). */ PlayerManager.prototype.GetPlayerByID = function(id) { if (id in this.playerEntities) return this.playerEntities[id]; var stack = new Error().stack.trimRight().replace(/^/mg, ' '); // indent each line warn("GetPlayerByID: no player defined for id '"+id+"'\n"+stack); return INVALID_ENTITY; }; PlayerManager.prototype.GetNumPlayers = function() { return this.playerEntities.length; }; PlayerManager.prototype.RemoveAllPlayers = function() { // Destroy existing player entities for each (var id in this.playerEntities) { Engine.DestroyEntity(id); } this.playerEntities = []; }; +PlayerManager.prototype.GetAllPlayerEntities = function() +{ + return this.playerEntities; +}; Engine.RegisterComponentType(IID_PlayerManager, "PlayerManager", PlayerManager); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Damage.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Damage.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Damage.js (revision 14231) @@ -0,0 +1,161 @@ +// Create global Damage object. +var Damage = {}; + +/** + * Damages units around a given origin. + * data.attacker = + * data.origin = {'x':, 'z':} + * data.radius = + * data.shape = + * data.strengths = {'hack':, 'pierce':, 'crush':} + * data.type = + * ***Optional Variables*** + * data.direction = + * data.playersToDamage = + */ +Damage.CauseSplashDamage = function(data) +{ + // Get nearby entities and define variables + var nearEnts = Damage.EntitiesNearPoint(data.origin, data.radius, data.playersToDamage); + var damageMultiplier = 1; + // Cycle through all the nearby entities and damage it appropriately based on its distance from the origin. + for each (var entity in nearEnts) + { + var entityPosition = Engine.QueryInterface(entity, IID_Position).GetPosition(); + if(data.shape == 'Circular') // circular effect with quadratic falloff in every direction + { + var squaredDistanceFromOrigin = Damage.VectorDistanceSquared(data.origin, entityPosition); + damageMultiplier == 1 - squaredDistanceFromOrigin / (data.radius * data.radius); + } + else if(data.shape == 'Linear') // linear effect with quadratic falloff in two directions (only used for certain missiles) + { + // Get position of entity relative to splash origin. + var relativePos = {"x":entityPosition.x - data.origin.x, "z":entityPosition.z - data.origin.z}; + + // The width of linear splash is one fifth of the normal splash radius. + var width = data.radius/5; + + // Effectivly rotate the axis to align with the missile direction. + var parallelDist = Damage.VectorDot(relativePos, data.direction); // z axis + var perpDist = Math.abs(Damage.VectorCross(relativePos, data.direction)); // y axis + + // Check that the unit is within the distance at which it will get damaged. + if (parallelDist > -width && perpDist < width) // If in radius, quadratic falloff in both directions + damageMultiplier = (data.radius * data.radius - parallelDist * parallelDist) / (data.radius * data.radius) + * (width * width - perpDist * perpDist) / (width * width); + else + damageMultiplier = 0; + } + else // In case someone calls this function with an invalid shape. + { + warn("The " + data.shape + " splash damage shape is not implemented!"); + } + // Call CauseDamage which reduces the hitpoints, posts network command, plays sounds.... + Damage.CauseDamage({"strengths":data.strengths, "target":entity, "attacker":data.attacker, "multiplier":damageMultiplier, "type":data.type + ".Splash"}) + } +}; + +/** + * Causes damage on a given unit + * data.strengths = {'hack':, 'pierce':, 'crush':} + * data.target = + * data.attacker = + * data.multiplier = + * data.type = + */ +Damage.CauseDamage = function(data) +{ + // Check the target can be damaged otherwise don't do anything. + var cmpDamageReceiver = Engine.QueryInterface(data.target, IID_DamageReceiver); + if (!cmpDamageReceiver) + return; + + // Damage the target + var targetState = cmpDamageReceiver.TakeDamage(data.strengths.hack * data.multiplier, data.strengths.pierce * data.multiplier, data.strengths.crush * data.multiplier, data.attacker); + + // If the target was killed run some cleanup + if (targetState.killed) + Damage.TargetKilled(data.attacker, data.target); + + // Post the network command (make it work in multiplayer) + Engine.PostMessage(data.target, MT_Attacked, {"attacker":data.attacker, "target":data.target, "type":data.type, "damage":-targetState.change}); + + // Play attacking sounds + PlaySound("attack_impact", data.attacker); +}; + +/** + * Gets entities near a give point for given players. + * origin = {'x':, 'z':} + * radius = + * players = + * If players is not included, entities from all players are used. + */ +Damage.EntitiesNearPoint = function(origin, radius, players) +{ + // If there is insufficient data return an empty array. + if (!origin || !radius) + return []; + // Create the dummy entity used for range calculations if it doesn't exist. + if (!Damage.dummyTargetEntity) + Damage.dummyTargetEntity = Engine.AddEntity('special/dummy'); + // Move the dummy entity to the origin of the query. + var cmpDummyPosition = Engine.QueryInterface(Damage.dummyTargetEntity, IID_Position); + if (!cmpDummyPosition) + return []; + cmpDummyPosition.JumpTo(origin.x, origin.z); + + // If the players parameter is not specified use all players. + if (!players) + { + var playerEntities = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayerEntities(); + players = []; + for (var entity in playerEntities) + players.append(Engine.QueryInterface(entity, IID_Player).GetPlayerID()); + } + + // Call RangeManager with dummy entity and return the result. + var rangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + var rangeQuery = rangeManager.ExecuteQuery(Damage.dummyTargetEntity, 0, radius, players, IID_DamageReceiver); + return rangeQuery; +}; + +/** + * Called when some units kills something (another unit, building, animal etc) + * killerEntity = + * targetEntity = + */ +Damage.TargetKilled = function(killerEntity, targetEntity) +{ + // Add to killer statistics. + var cmpKillerPlayerStatisticsTracker = QueryOwnerInterface(killerEntity, IID_StatisticsTracker); + if (cmpKillerPlayerStatisticsTracker) + cmpKillerPlayerStatisticsTracker.KilledEntity(targetEntity); + // Add to loser statistics. + var cmpTargetPlayerStatisticsTracker = QueryOwnerInterface(targetEntity, IID_StatisticsTracker); + if (cmpTargetPlayerStatisticsTracker) + cmpTargetPlayerStatisticsTracker.LostEntity(targetEntity); + + // If killer can collect loot, let's try to collect it. + var cmpLooter = Engine.QueryInterface(killerEntity, IID_Looter); + if (cmpLooter) + cmpLooter.Collect(targetEntity); +}; + +// Gets the straight line distance between p1 and p2 +Damage.VectorDistanceSquared = function(p1, p2) +{ + return (p1.x - p2.x) * (p1.x - p2.x) + (p1.z - p2.z) * (p1.z - p2.z); +}; + +// Gets the dot product of two vectors. +Damage.VectorDot = function(p1, p2) +{ + return p1.x * p2.x + p1.z * p2.z; +}; + +// Gets the 2D interpreted version of the cross product of two vectors. +Damage.VectorCross = function(p1, p2) +{ + return p1.x * p2.z - p1.z * p2.x; +}; Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/dummy.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/dummy.xml (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/dummy.xml (revision 14231) @@ -0,0 +1,8 @@ + + + 0 + upright + false + 6.0 + +