Index: ps/trunk/binaries/data/mods/public/simulation/components/Attack.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Attack.js (revision 25012)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Attack.js (revision 25013)
@@ -1,668 +1,669 @@
function Attack() {}
var g_AttackTypes = ["Melee", "Ranged", "Capture"];
Attack.prototype.preferredClassesSchema =
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"";
Attack.prototype.restrictedClassesSchema =
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"";
Attack.prototype.Schema =
"Controls the attack abilities and strengths of the unit." +
"" +
"" +
"Spear" +
"" +
"10.0" +
"0.0" +
"5.0" +
"" +
"4.0" +
"1000" +
"" +
"" +
"pers" +
"Infantry" +
"1.5" +
"" +
"" +
"Cavalry Melee" +
"1.5" +
"" +
"" +
"Champion" +
"Cavalry Infantry" +
"" +
"" +
"Bow" +
"" +
"0.0" +
"10.0" +
"0.0" +
"" +
"44.0" +
"20.0" +
"15.0" +
"800" +
"1600" +
"1000" +
"" +
"" +
"Cavalry" +
"2" +
"" +
"" +
"" +
"50.0" +
"2.5" +
"props/units/weapons/rock_flaming.xml" +
"props/units/weapons/rock_explosion.xml" +
"0.1" +
"false" +
"" +
"Champion" +
"" +
"Circular" +
"20" +
"false" +
"" +
"0.0" +
"10.0" +
"0.0" +
"" +
"" +
"" +
"" +
"" +
"1000.0" +
"0.0" +
"0.0" +
"" +
"4.0" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
Attacking.BuildAttackEffectsSchema() +
"" +
"" +
"" +
"" +
"" + // TODO: it shouldn't be stretched
"" +
"" +
Attack.prototype.preferredClassesSchema +
Attack.prototype.restrictedClassesSchema +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
Attacking.BuildAttackEffectsSchema() +
"" +
"" +
""+
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
Attacking.BuildAttackEffectsSchema() +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
Attack.prototype.preferredClassesSchema +
Attack.prototype.restrictedClassesSchema +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
Attacking.BuildAttackEffectsSchema() +
"" +
"" + // TODO: it shouldn't be stretched
"" +
"" +
Attack.prototype.preferredClassesSchema +
Attack.prototype.restrictedClassesSchema +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
Attacking.BuildAttackEffectsSchema() +
"" + // TODO: how do these work?
Attack.prototype.preferredClassesSchema +
Attack.prototype.restrictedClassesSchema +
"" +
"" +
"";
Attack.prototype.Init = function()
{
};
Attack.prototype.Serialize = null; // we have no dynamic state to save
Attack.prototype.GetAttackTypes = function(wantedTypes)
{
let types = g_AttackTypes.filter(type => !!this.template[type]);
if (!wantedTypes)
return types;
let wantedTypesReal = wantedTypes.filter(wtype => wtype.indexOf("!") != 0);
return types.filter(type => wantedTypes.indexOf("!" + type) == -1 &&
(!wantedTypesReal || !wantedTypesReal.length || wantedTypesReal.indexOf(type) != -1));
};
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, wantedTypes)
{
let cmpFormation = Engine.QueryInterface(target, IID_Formation);
if (cmpFormation)
return true;
let cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position);
let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld())
return false;
let cmpIdentity = QueryMiragedInterface(target, IID_Identity);
if (!cmpIdentity)
return false;
let cmpHealth = QueryMiragedInterface(target, IID_Health);
let targetClasses = cmpIdentity.GetClassesList();
if (targetClasses.indexOf("Domestic") != -1 && this.template.Slaughter && cmpHealth && cmpHealth.GetHitpoints() &&
(!wantedTypes || !wantedTypes.filter(wType => wType.indexOf("!") != 0).length))
return true;
let cmpEntityPlayer = QueryOwnerInterface(this.entity);
let cmpTargetPlayer = QueryOwnerInterface(target);
if (!cmpTargetPlayer || !cmpEntityPlayer)
return false;
let types = this.GetAttackTypes(wantedTypes);
let entityOwner = cmpEntityPlayer.GetPlayerID();
let targetOwner = cmpTargetPlayer.GetPlayerID();
let cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
// Check if the relative height difference is larger than the attack range
// If the relative height is bigger, it means they will never be able to
// reach each other, no matter how close they come.
let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset());
for (let type of types)
{
if (type != "Capture" && (!cmpEntityPlayer.IsEnemy(targetOwner) || !cmpHealth || !cmpHealth.GetHitpoints()))
continue;
if (type == "Capture" && (!cmpCapturable || !cmpCapturable.CanCapture(entityOwner)))
continue;
if (heightDiff > this.GetRange(type).max)
continue;
let restrictedClasses = this.GetRestrictedClasses(type);
if (!restrictedClasses.length)
return true;
if (!MatchesClassList(targetClasses, restrictedClasses))
return true;
}
return false;
};
/**
* Returns null if we have no preference or the lowest index of a preferred class.
*/
Attack.prototype.GetPreference = function(target)
{
let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
if (!cmpIdentity)
return undefined;
let targetClasses = cmpIdentity.GetClassesList();
let minPref = null;
for (let type of this.GetAttackTypes())
{
let preferredClasses = this.GetPreferredClasses(type);
for (let pref = 0; pref < preferredClasses.length; ++pref)
{
if (MatchesClassList(targetClasses, preferredClasses[pref]))
{
if (pref === 0)
return pref;
if ((minPref === null || minPref > pref))
minPref = pref;
}
}
}
return minPref;
};
/**
* Get the full range of attack using all available attack types.
*/
Attack.prototype.GetFullAttackRange = function()
{
let ret = { "min": Infinity, "max": 0 };
for (let type of this.GetAttackTypes())
{
let range = this.GetRange(type);
ret.min = Math.min(ret.min, range.min);
ret.max = Math.max(ret.max, range.max);
}
return ret;
};
Attack.prototype.GetAttackEffectsData = function(type, splash)
{
let template = this.template[type];
if (splash)
template = template.Splash;
return Attacking.GetAttackEffectsData("Attack/" + type + (splash ? "/Splash" : ""), template, this.entity);
};
/**
* Find the best attack against a target.
* @param {number} target - The entity-ID of the target.
* @param {boolean} allowCapture - Whether capturing is allowed.
* @return {string} - The preferred attack type.
*/
Attack.prototype.GetBestAttackAgainst = function(target, allowCapture)
{
let cmpFormation = Engine.QueryInterface(target, IID_Formation);
if (cmpFormation)
{
// TODO: Formation against formation needs review
let types = this.GetAttackTypes();
return g_AttackTypes.find(attack => types.indexOf(attack) != -1);
}
let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
if (!cmpIdentity)
return undefined;
// Always slaughter domestic animals instead of using a normal attack
if (this.template.Slaughter && cmpIdentity.HasClass("Domestic"))
return "Slaughter";
let types = this.GetAttackTypes().filter(type => this.CanAttack(target, [type]));
// Check whether the target is capturable and prefer that when it is allowed.
let captureIndex = types.indexOf("Capture");
if (captureIndex != -1)
{
if (allowCapture)
return "Capture";
types.splice(captureIndex, 1);
}
let targetClasses = cmpIdentity.GetClassesList();
let isPreferred = attackType => MatchesClassList(targetClasses, this.GetPreferredClasses(attackType));
return types.sort((a, b) =>
(types.indexOf(a) + (isPreferred(a) ? types.length : 0)) -
(types.indexOf(b) + (isPreferred(b) ? types.length : 0))).pop();
};
Attack.prototype.CompareEntitiesByPreference = function(a, b)
{
let aPreference = this.GetPreference(a);
let 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.GetAttackName = function(type)
{
return {
"name": this.template[type].AttackName._string || this.template[type].AttackName,
"context": this.template[type].AttackName["@context"]
};
};
Attack.prototype.GetRepeatTime = function(type)
{
let repeatTime = 1000;
if (this.template[type] && this.template[type].RepeatTime)
repeatTime = +this.template[type].RepeatTime;
return ApplyValueModificationsToEntity("Attack/" + type + "/RepeatTime", repeatTime, this.entity);
};
Attack.prototype.GetTimers = function(type)
{
return {
"prepare": ApplyValueModificationsToEntity("Attack/" + type + "/PrepareTime", +(this.template[type].PrepareTime || 0), this.entity),
"repeat": this.GetRepeatTime(type)
};
};
Attack.prototype.GetSplashData = function(type)
{
if (!this.template[type].Splash)
return undefined;
return {
"attackData": this.GetAttackEffectsData(type, true),
"friendlyFire": this.template[type].Splash.FriendlyFire == "true",
"radius": ApplyValueModificationsToEntity("Attack/" + type + "/Splash/Range", +this.template[type].Splash.Range, this.entity),
"shape": this.template[type].Splash.Shape,
};
};
Attack.prototype.GetRange = function(type)
{
if (!type)
return this.GetFullAttackRange();
let max = +this.template[type].MaxRange;
max = ApplyValueModificationsToEntity("Attack/" + type + "/MaxRange", max, this.entity);
let min = +(this.template[type].MinRange || 0);
min = ApplyValueModificationsToEntity("Attack/" + type + "/MinRange", min, this.entity);
let elevationBonus = +(this.template[type].ElevationBonus || 0);
elevationBonus = ApplyValueModificationsToEntity("Attack/" + type + "/ElevationBonus", elevationBonus, this.entity);
return { "max": max, "min": min, "elevationBonus": elevationBonus };
};
/**
* 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)
{
let attackerOwner = Engine.QueryInterface(this.entity, IID_Ownership).GetOwner();
+ let data = {
+ "type": type,
+ "attackData": this.GetAttackEffectsData(type),
+ "target": target,
+ "attacker": this.entity,
+ "attackerOwner": attackerOwner,
+ };
+
// If this is a ranged attack, then launch a projectile
if (type == "Ranged")
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
let turnLength = cmpTimer.GetLatestTurnLength()/1000;
// 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
let horizSpeed = +this.template[type].Projectile.Speed;
let gravity = +this.template[type].Projectile.Gravity;
// horizSpeed /= 2; gravity /= 2; // slow it down for testing
// We will try to estimate the position of the target, where we can hit it.
// We first estimate the time-till-hit by extrapolating linearly the movement
// of the last turn. We compute the time till an arrow will intersect the target.
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
let selfPosition = cmpPosition.GetPosition();
let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
return;
let targetPosition = cmpTargetPosition.GetPosition();
let targetVelocity = Vector3D.sub(targetPosition, cmpTargetPosition.GetPreviousPosition()).div(turnLength);
let timeToTarget = PositionHelper.PredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity);
// 'Cheat' and use UnitMotion to predict the position in the near-future.
// This avoids 'dancing' issues with units zigzagging over very short distances.
// However, this could fail if the player gives several short move orders, so
// occasionally fall back to basic interpolation.
let predictedPosition = targetPosition;
if (timeToTarget !== false)
{
// Don't predict too far in the future, but avoid threshold effects.
// After 1 second, always use the 'dumb' interpolated past-motion prediction.
let useUnitMotion = randBool(Math.max(0, 0.75 - timeToTarget / 1.333));
if (useUnitMotion)
{
let cmpTargetUnitMotion = Engine.QueryInterface(target, IID_UnitMotion);
let cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI);
if (cmpTargetUnitMotion && (!cmpTargetUnitAI || !cmpTargetUnitAI.IsFormationMember()))
{
let pos2D = cmpTargetUnitMotion.EstimateFuturePosition(timeToTarget);
predictedPosition.x = pos2D.x;
predictedPosition.z = pos2D.y;
}
else
predictedPosition = Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition);
}
else
predictedPosition = Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition);
}
let predictedHeight = cmpTargetPosition.GetHeightAt(predictedPosition.x, predictedPosition.z);
// Add inaccuracy based on spread.
let distanceModifiedSpread = ApplyValueModificationsToEntity("Attack/Ranged/Spread", +this.template[type].Projectile.Spread, this.entity) *
predictedPosition.horizDistanceTo(selfPosition) / 100;
let randNorm = randomNormal2D();
let offsetX = randNorm[0] * distanceModifiedSpread;
let offsetZ = randNorm[1] * distanceModifiedSpread;
let realTargetPosition = new Vector3D(predictedPosition.x + offsetX, predictedHeight, predictedPosition.z + offsetZ);
// Recalculate when the missile will hit the target position.
let realHorizDistance = realTargetPosition.horizDistanceTo(selfPosition);
timeToTarget = realHorizDistance / horizSpeed;
let missileDirection = Vector3D.sub(realTargetPosition, selfPosition).div(realHorizDistance);
// Launch the graphical projectile.
let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager);
let actorName = "";
let impactActorName = "";
let impactAnimationLifetime = 0;
actorName = this.template[type].Projectile.ActorName || "";
impactActorName = this.template[type].Projectile.ImpactActorName || "";
impactAnimationLifetime = this.template[type].Projectile.ImpactAnimationLifetime || 0;
// TODO: Use unit rotation to implement x/z offsets.
let deltaLaunchPoint = new Vector3D(0, +this.template[type].Projectile.LaunchPoint["@y"], 0);
let launchPoint = Vector3D.add(selfPosition, deltaLaunchPoint);
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpVisual)
{
// if the projectile definition is missing from the template
// then fallback to the projectile name and launchpoint in the visual actor
if (!actorName)
actorName = cmpVisual.GetProjectileActor();
let visualActorLaunchPoint = cmpVisual.GetProjectileLaunchPoint();
if (visualActorLaunchPoint.length() > 0)
launchPoint = visualActorLaunchPoint;
}
let id = cmpProjectileManager.LaunchProjectileAtPoint(launchPoint, realTargetPosition, horizSpeed, gravity, actorName, impactActorName, impactAnimationLifetime);
let attackImpactSound = "";
let cmpSound = Engine.QueryInterface(this.entity, IID_Sound);
if (cmpSound)
attackImpactSound = cmpSound.GetSoundGroup("attack_impact_" + type.toLowerCase());
- let data = {
- "type": type,
- "attackData": this.GetAttackEffectsData(type),
- "target": target,
- "attacker": this.entity,
- "attackerOwner": attackerOwner,
- "position": realTargetPosition,
- "direction": missileDirection,
- "projectileId": id,
- "attackImpactSound": attackImpactSound,
- "splash": this.GetSplashData(type),
- "friendlyFire": this.template[type].Projectile.FriendlyFire == "true",
- };
+ data.position = realTargetPosition;
+ data.direction = missileDirection;
+ data.projectileId = id;
+ data.attackImpactSound = attackImpactSound;
+ data.splash = this.GetSplashData(type);
+ data.friendlyFire = this.template[type].Projectile.FriendlyFire == "true";
cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_DelayedDamage, "MissileHit", +this.template[type].Delay + timeToTarget * 1000, data);
}
else
- Attacking.HandleAttackEffects(target, type, this.GetAttackEffectsData(type), this.entity, attackerOwner);
+ Attacking.HandleAttackEffects(target, data);
};
Attack.prototype.OnValueModification = function(msg)
{
if (msg.component != "Attack")
return;
let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
if (!cmpUnitAI)
return;
if (this.GetAttackTypes().some(type =>
msg.valueNames.indexOf("Attack/" + type + "/MaxRange") != -1))
cmpUnitAI.UpdateRangeQueries();
};
Attack.prototype.GetRangeOverlays = function()
{
if (!this.template.Ranged || !this.template.Ranged.RangeOverlay)
return [];
let range = this.GetRange("Ranged");
let rangeOverlays = [];
for (let i in range)
if ((i == "min" || i == "max") && range[i])
rangeOverlays.push({
"radius": range[i],
"texture": this.template.Ranged.RangeOverlay.LineTexture,
"textureMask": this.template.Ranged.RangeOverlay.LineTextureMask,
"thickness": +this.template.Ranged.RangeOverlay.LineThickness,
});
return rangeOverlays;
};
Engine.RegisterComponentType(IID_Attack, "Attack", Attack);
Index: ps/trunk/binaries/data/mods/public/simulation/components/DelayedDamage.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/DelayedDamage.js (revision 25012)
+++ ps/trunk/binaries/data/mods/public/simulation/components/DelayedDamage.js (revision 25013)
@@ -1,92 +1,92 @@
function DelayedDamage() {}
DelayedDamage.prototype.Schema =
"";
DelayedDamage.prototype.Init = function()
{
};
/**
* When missiles miss their target, other units in MISSILE_HIT_RADIUS range are considered.
* Large missiles should probably implement splash damage anyways,
* so keep this value low for performance.
*/
DelayedDamage.prototype.MISSILE_HIT_RADIUS = 2;
/**
* Handles hit logic after the projectile travel time has passed.
* @param {Object} data - The data sent by the caller.
* @param {string} data.type - The type of damage.
* @param {Object} data.attackData - Data of the form { 'effectType': { ...opaque effect data... }, 'Bonuses': {...} }.
* @param {number} data.target - The entity id of the target.
* @param {number} data.attacker - The entity id of the attacker.
* @param {number} data.attackerOwner - The player id of the owner of the attacker.
* @param {Vector2D} data.origin - The origin of the projectile hit.
* @param {Vector3D} data.position - The expected position of the target.
* @param {number} data.projectileId - The id of the projectile.
* @param {Vector3D} data.direction - The unit vector defining the direction.
* @param {string} data.attackImpactSound - The name of the sound emited on impact.
* @param {boolean} data.friendlyFire - A flag indicating whether allied entities can also be damaged.
* ***When splash damage***
* @param {boolean} data.splash.friendlyFire - A flag indicating if allied entities are also damaged.
* @param {number} data.splash.radius - The radius of the splash damage.
* @param {string} data.splash.shape - The shape of the splash range.
* @param {Object} data.splash.attackData - same as attackData, for splash.
*/
DelayedDamage.prototype.MissileHit = function(data, lateness)
{
if (!data.position)
return;
let cmpSoundManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager);
if (cmpSoundManager && data.attackImpactSound)
cmpSoundManager.PlaySoundGroupAtPosition(data.attackImpactSound, data.position);
// Do this first in case the direct hit kills the target.
if (data.splash)
Attacking.CauseDamageOverArea({
"type": data.type,
"attackData": data.splash.attackData,
"attacker": data.attacker,
"attackerOwner": data.attackerOwner,
"origin": Vector2D.from3D(data.position),
"radius": data.splash.radius,
"shape": data.splash.shape,
"direction": data.direction,
"friendlyFire": data.splash.friendlyFire
});
let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager);
let target = data.target;
// Since we can't damage mirages, replace a miraged target by the real target.
let cmpMirage = Engine.QueryInterface(data.target, IID_Mirage);
if (cmpMirage)
target = cmpMirage.GetParent();
// Deal direct damage if we hit the main target
// and we could handle the attack.
if (PositionHelper.TestCollision(target, data.position, lateness) &&
- Attacking.HandleAttackEffects(target, data.type, data.attackData, data.attacker, data.attackerOwner))
+ Attacking.HandleAttackEffects(target, data))
{
cmpProjectileManager.RemoveProjectile(data.projectileId);
return;
}
// If we didn't hit the main target look for nearby units.
let ents = PositionHelper.EntitiesNearPoint(Vector2D.from3D(data.position), this.MISSILE_HIT_RADIUS,
Attacking.GetPlayersToDamage(data.attackerOwner, data.friendlyFire));
for (let ent of ents)
{
if (!PositionHelper.TestCollision(ent, data.position, lateness) ||
- !Attacking.HandleAttackEffects(ent, data.type, data.attackData, data.attacker, data.attackerOwner))
+ !Attacking.HandleAttackEffects(ent, data))
continue;
cmpProjectileManager.RemoveProjectile(data.projectileId);
break;
}
};
Engine.RegisterSystemComponentType(IID_DelayedDamage, "DelayedDamage", DelayedDamage);
Index: ps/trunk/binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js (revision 25012)
+++ ps/trunk/binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js (revision 25013)
@@ -1,165 +1,170 @@
function StatusEffectsReceiver() {}
StatusEffectsReceiver.prototype.DefaultInterval = 1000;
/**
* Initialises the status effects.
*/
StatusEffectsReceiver.prototype.Init = function()
{
this.activeStatusEffects = {};
};
/**
* Which status effects are active on this entity.
*
* @return {Object} - An object containing the status effects which currently affect the entity.
*/
StatusEffectsReceiver.prototype.GetActiveStatuses = function()
{
return this.activeStatusEffects;
};
/**
* Called by Attacking effects. Adds status effects for each entry in the effectData.
*
* @param {Object} effectData - An object containing the status effects to give to the entity.
* @param {number} attacker - The entity ID of the attacker.
* @param {number} attackerOwner - The player ID of the attacker.
* @param {number} bonusMultiplier - A value to multiply the damage with (not implemented yet for SE).
*
* @return {Object} - The codes of the status effects which were processed.
*/
StatusEffectsReceiver.prototype.ApplyStatus = function(effectData, attacker, attackerOwner)
{
let attackerData = { "entity": attacker, "owner": attackerOwner };
for (let effect in effectData)
this.AddStatus(effect, effectData[effect], attackerData);
// TODO: implement loot?
return { "inflictedStatuses": Object.keys(effectData) };
};
/**
* Adds a status effect to the entity.
*
* @param {string} statusCode - The code of the status effect.
* @param {Object} data - The various effects and timings.
* @param {Object} attackerData - The attacker and attackerOwner.
*/
StatusEffectsReceiver.prototype.AddStatus = function(baseCode, data, attackerData)
{
let statusCode = baseCode;
if (this.activeStatusEffects[statusCode])
{
if (data.Stackability == "Ignore")
return;
if (data.Stackability == "Extend")
{
this.activeStatusEffects[statusCode].Duration += data.Duration;
return;
}
if (data.Stackability == "Replace")
this.RemoveStatus(statusCode);
else if (data.Stackability == "Stack")
{
let i = 0;
let temp;
do
temp = statusCode + "_" + i++;
while (!!this.activeStatusEffects[temp]);
statusCode = temp;
}
}
this.activeStatusEffects[statusCode] = {
"baseCode": baseCode
};
let status = this.activeStatusEffects[statusCode];
Object.assign(status, data);
if (status.Modifiers)
{
let modifications = DeriveModificationsFromXMLTemplate(status.Modifiers);
let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager);
cmpModifiersManager.AddModifiers(statusCode, modifications, this.entity);
}
// With neither an interval nor a duration, there is no point in starting a timer.
if (!status.Duration && !status.Interval)
return;
// We need this to prevent Status Effects from giving XP
// to the entity that applied them.
status.StatusEffect = true;
// We want an interval to update the GUI to show how much time of the status effect
// is left even if the status effect itself has no interval.
if (!status.Interval)
status._interval = this.DefaultInterval;
status._timeElapsed = 0;
status._firstTime = true;
status.source = attackerData;
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
status._timer = cmpTimer.SetInterval(this.entity, IID_StatusEffectsReceiver, "ExecuteEffect", 0, +(status.Interval || status._interval), statusCode);
};
/**
* Removes a status effect from the entity.
*
* @param {string} statusCode - The status effect to be removed.
*/
StatusEffectsReceiver.prototype.RemoveStatus = function(statusCode)
{
let statusEffect = this.activeStatusEffects[statusCode];
if (!statusEffect)
return;
if (statusEffect.Modifiers)
{
let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager);
cmpModifiersManager.RemoveAllModifiers(statusCode, this.entity);
}
if (statusEffect._timer)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(statusEffect._timer);
}
delete this.activeStatusEffects[statusCode];
};
/**
* Called by the timers. Executes a status effect.
*
* @param {string} statusCode - The status effect to be executed.
* @param {number} lateness - The delay between the calling of the function and the actual execution (turn time?).
*/
StatusEffectsReceiver.prototype.ExecuteEffect = function(statusCode, lateness)
{
let status = this.activeStatusEffects[statusCode];
if (!status)
return;
if (status.Damage || status.Capture)
- Attacking.HandleAttackEffects(this.entity, statusCode, status, status.source.entity, status.source.owner);
+ Attacking.HandleAttackEffects(this.entity, {
+ "type": statusCode,
+ "attackData": status,
+ "attacker": status.source.entity,
+ "attackerOwner": status.source.owner
+ });
if (!status.Duration)
return;
if (status._firstTime)
{
status._firstTime = false;
status._timeElapsed += lateness;
}
else
status._timeElapsed += +(status.Interval || status._interval) + lateness;
if (status._timeElapsed >= +status.Duration)
this.RemoveStatus(statusCode);
};
Engine.RegisterComponentType(IID_StatusEffectsReceiver, "StatusEffectsReceiver", StatusEffectsReceiver);
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Damage.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Damage.js (revision 25012)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Damage.js (revision 25013)
@@ -1,707 +1,707 @@
AttackEffects = class AttackEffects
{
constructor() {}
Receivers()
{
return [{
"type": "Damage",
"IID": "IID_Health",
"method": "TakeDamage"
}];
}
};
Engine.LoadHelperScript("Attacking.js");
Engine.LoadHelperScript("Player.js");
Engine.LoadHelperScript("Position.js");
Engine.LoadHelperScript("ValueModification.js");
Engine.LoadComponentScript("interfaces/DelayedDamage.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/Loot.js");
Engine.LoadComponentScript("interfaces/Promotion.js");
Engine.LoadComponentScript("interfaces/ModifiersManager.js");
Engine.LoadComponentScript("interfaces/Resistance.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");
Engine.LoadComponentScript("Attack.js");
Engine.LoadComponentScript("DelayedDamage.js");
Engine.LoadComponentScript("Timer.js");
function Test_Generic()
{
ResetState();
let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer");
cmpTimer.OnUpdate({ "turnLength": 1 });
let attacker = 11;
let atkPlayerEntity = 1;
let attackerOwner = 6;
let cmpAttack = ConstructComponent(attacker, "Attack",
{
"Ranged": {
"Damage": {
"Crush": 5,
},
"MaxRange": 50,
"MinRange": 0,
"Delay": 0,
"Projectile": {
"Speed": 75.0,
"Spread": 0.5,
"Gravity": 9.81,
"FriendlyFire": "false",
"LaunchPoint": { "@y": 3 }
}
}
});
let damage = 5;
let target = 21;
let targetOwner = 7;
let targetPos = new Vector3D(3, 0, 3);
let type = "Melee";
let damageTaken = false;
cmpAttack.GetAttackStrengths = attackType => ({ "Hack": 0, "Pierce": 0, "Crush": damage });
let data = {
"type": "Melee",
"attackData": {
"Damage": { "Hack": 0, "Pierce": 0, "Crush": damage },
},
"target": target,
"attacker": attacker,
"attackerOwner": attackerOwner,
"position": targetPos,
"projectileId": 9,
"direction": new Vector3D(1, 0, 0)
};
AddMock(atkPlayerEntity, IID_Player, {
"GetEnemies": () => [targetOwner]
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": id => atkPlayerEntity,
"GetAllPlayers": () => [0, 1, 2, 3, 4]
});
AddMock(SYSTEM_ENTITY, IID_ProjectileManager, {
"RemoveProjectile": () => {},
"LaunchProjectileAtPoint": (ent, pos, speed, gravity) => {},
});
AddMock(target, IID_Position, {
"GetPosition": () => targetPos,
"GetPreviousPosition": () => targetPos,
"GetPosition2D": () => Vector2D.From(targetPos),
"GetHeightAt": () => 0,
"IsInWorld": () => true,
});
AddMock(target, IID_Health, {
"TakeDamage": (amount, __, ___) => {
damageTaken = true;
return { "healthChange": -amount };
},
});
AddMock(SYSTEM_ENTITY, IID_DelayedDamage, {
"MissileHit": () => {
damageTaken = true;
},
});
Engine.PostMessage = function(ent, iid, message)
{
TS_ASSERT_UNEVAL_EQUALS({
"type": type,
"target": target,
"attacker": attacker,
"attackerOwner": attackerOwner,
"damage": damage,
"capture": 0,
"statusEffects": [],
"fromStatusEffect": false
}, message);
};
AddMock(target, IID_Footprint, {
"GetShape": () => ({ "type": "circle", "radius": 20 }),
});
AddMock(attacker, IID_Ownership, {
"GetOwner": () => attackerOwner,
});
AddMock(attacker, IID_Position, {
"GetPosition": () => new Vector3D(2, 0, 3),
"GetRotation": () => new Vector3D(1, 2, 3),
"IsInWorld": () => true,
});
function TestDamage()
{
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT(damageTaken);
damageTaken = false;
}
- Attacking.HandleAttackEffects(target, data.type, data.attackData, data.attacker, data.attackerOwner);
+ Attacking.HandleAttackEffects(target, data);
TestDamage();
data.type = "Ranged";
type = data.type;
- Attacking.HandleAttackEffects(target, data.type, data.attackData, data.attacker, data.attackerOwner);
+ Attacking.HandleAttackEffects(target, data);
TestDamage();
// Check for damage still being dealt if the attacker dies
cmpAttack.PerformAttack("Ranged", target);
Engine.DestroyEntity(attacker);
TestDamage();
atkPlayerEntity = 1;
AddMock(atkPlayerEntity, IID_Player, {
"GetEnemies": () => [2, 3]
});
TS_ASSERT_UNEVAL_EQUALS(Attacking.GetPlayersToDamage(atkPlayerEntity, true), [0, 1, 2, 3, 4]);
TS_ASSERT_UNEVAL_EQUALS(Attacking.GetPlayersToDamage(atkPlayerEntity, false), [2, 3]);
}
Test_Generic();
function TestLinearSplashDamage()
{
ResetState();
Engine.PostMessage = (ent, iid, message) => {};
const attacker = 50;
const attackerOwner = 1;
const origin = new Vector2D(0, 0);
let data = {
"type": "Ranged",
"attackData": { "Damage": { "Hack": 100, "Pierce": 0, "Crush": 0 } },
"attacker": attacker,
"attackerOwner": attackerOwner,
"origin": origin,
"radius": 10,
"shape": "Linear",
"direction": new Vector3D(1, 747, 0),
"friendlyFire": false,
};
let fallOff = function(x, y)
{
return (1 - x * x / (data.radius * data.radius)) * (1 - 25 * y * y / (data.radius * data.radius));
};
let hitEnts = new Set();
AddMock(attackerOwner, IID_Player, {
"GetEnemies": () => [2]
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": id => attackerOwner,
"GetAllPlayers": () => [0, 1, 2]
});
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"ExecuteQueryAroundPos": () => [60, 61, 62],
});
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
"DistanceToPoint": (ent) => ({
"60": Math.sqrt(9.25),
"61": 0,
"62": Math.sqrt(29)
}[ent])
});
AddMock(60, IID_Position, {
"GetPosition2D": () => new Vector2D(3, -0.5),
});
AddMock(61, IID_Position, {
"GetPosition2D": () => new Vector2D(0, 0),
});
AddMock(62, IID_Position, {
"GetPosition2D": () => new Vector2D(5, 2),
});
AddMock(60, IID_Health, {
"TakeDamage": (amount, __, ___) => {
hitEnts.add(60);
TS_ASSERT_EQUALS(amount, 100 * fallOff(3, -0.5));
return { "healthChange": -amount };
}
});
AddMock(61, IID_Health, {
"TakeDamage": (amount, __, ___) => {
hitEnts.add(61);
TS_ASSERT_EQUALS(amount, 100 * fallOff(0, 0));
return { "healthChange": -amount };
}
});
AddMock(62, IID_Health, {
"TakeDamage": (amount, __, ___) => {
hitEnts.add(62);
// Minor numerical precision issues make this necessary
TS_ASSERT(amount < 0.00001);
return { "healthChange": -amount };
}
});
Attacking.CauseDamageOverArea(data);
TS_ASSERT(hitEnts.has(60));
TS_ASSERT(hitEnts.has(61));
TS_ASSERT(hitEnts.has(62));
hitEnts.clear();
data.direction = new Vector3D(0.6, 747, 0.8);
AddMock(60, IID_Health, {
"TakeDamage": (amount, __, ___) => {
hitEnts.add(60);
TS_ASSERT_EQUALS(amount, 100 * fallOff(1, 2));
return { "healthChange": -amount };
}
});
Attacking.CauseDamageOverArea(data);
TS_ASSERT(hitEnts.has(60));
TS_ASSERT(hitEnts.has(61));
TS_ASSERT(hitEnts.has(62));
hitEnts.clear();
}
TestLinearSplashDamage();
function TestCircularSplashDamage()
{
ResetState();
Engine.PostMessage = (ent, iid, message) => {};
const radius = 10;
let attackerOwner = 1;
let fallOff = function(r)
{
return 1 - r * r / (radius * radius);
};
AddMock(attackerOwner, IID_Player, {
"GetEnemies": () => [2]
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": id => attackerOwner,
"GetAllPlayers": () => [0, 1, 2]
});
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"ExecuteQueryAroundPos": () => [60, 61, 62, 64, 65],
});
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
"DistanceToPoint": (ent, x, z) => ({
"60": 0,
"61": 5,
"62": 1,
"63": Math.sqrt(85),
"64": 10,
"65": 2
}[ent])
});
AddMock(60, IID_Position, {
"GetPosition2D": () => new Vector2D(3, 4),
});
AddMock(61, IID_Position, {
"GetPosition2D": () => new Vector2D(0, 0),
});
AddMock(62, IID_Position, {
"GetPosition2D": () => new Vector2D(3.6, 3.2),
});
AddMock(63, IID_Position, {
"GetPosition2D": () => new Vector2D(10, -10),
});
// Target on the frontier of the shape (see distance above).
AddMock(64, IID_Position, {
"GetPosition2D": () => new Vector2D(9, -4),
});
// Big target far away (see distance above).
AddMock(65, IID_Position, {
"GetPosition2D": () => new Vector2D(23, 4),
});
AddMock(60, IID_Health, {
"TakeDamage": (amount, __, ___) => {
TS_ASSERT_EQUALS(amount, 100 * fallOff(0));
return { "healthChange": -amount };
}
});
AddMock(61, IID_Health, {
"TakeDamage": (amount, __, ___) => {
TS_ASSERT_EQUALS(amount, 100 * fallOff(5));
return { "healthChange": -amount };
}
});
AddMock(62, IID_Health, {
"TakeDamage": (amount, __, ___) => {
TS_ASSERT_EQUALS(amount, 100 * fallOff(1));
return { "healthChange": -amount };
}
});
AddMock(63, IID_Health, {
"TakeDamage": (amount, __, ___) => {
TS_ASSERT(false);
}
});
let cmphealth64 = AddMock(64, IID_Health, {
"TakeDamage": (amount, __, ___) => {
TS_ASSERT_EQUALS(amount, 0);
return { "healthChange": -amount };
}
});
let spy64 = new Spy(cmphealth64, "TakeDamage");
let cmpHealth65 = AddMock(65, IID_Health, {
"TakeDamage": (amount, __, ___) => {
TS_ASSERT_EQUALS(amount, 100 * fallOff(2));
return { "healthChange": -amount };
}
});
let spy65 = new Spy(cmpHealth65, "TakeDamage");
Attacking.CauseDamageOverArea({
"type": "Ranged",
"attackData": { "Damage": { "Hack": 100, "Pierce": 0, "Crush": 0 } },
"attacker": 50,
"attackerOwner": attackerOwner,
"origin": new Vector2D(3, 4),
"radius": radius,
"shape": "Circular",
"friendlyFire": false,
});
TS_ASSERT_EQUALS(spy64._called, 1);
TS_ASSERT_EQUALS(spy65._called, 1);
}
TestCircularSplashDamage();
function Test_MissileHit()
{
ResetState();
Engine.PostMessage = (ent, iid, message) => {};
let cmpDelayedDamage = ConstructComponent(SYSTEM_ENTITY, "DelayedDamage");
let target = 60;
let targetOwner = 1;
let targetPos = new Vector3D(3, 10, 0);
let hitEnts = new Set();
AddMock(SYSTEM_ENTITY, IID_Timer, {
"GetLatestTurnLength": () => 500
});
const radius = 10;
let data = {
"type": "Ranged",
"attackData": { "Damage": { "Hack": 0, "Pierce": 100, "Crush": 0 } },
"target": 60,
"attacker": 70,
"attackerOwner": 1,
"position": targetPos,
"direction": new Vector3D(1, 0, 0),
"projectileId": 9,
"friendlyFire": "false",
};
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": id => id == 1 ? 10 : 11,
"GetAllPlayers": () => [0, 1]
});
AddMock(SYSTEM_ENTITY, IID_ProjectileManager, {
"RemoveProjectile": () => {},
"LaunchProjectileAtPoint": (ent, pos, speed, gravity) => {},
});
AddMock(60, IID_Position, {
"GetPosition": () => targetPos,
"GetPreviousPosition": () => targetPos,
"GetPosition2D": () => Vector2D.From(targetPos),
"IsInWorld": () => true,
});
AddMock(60, IID_Health, {
"TakeDamage": (amount, __, ___) => {
hitEnts.add(60);
TS_ASSERT_EQUALS(amount, 100);
return { "healthChange": -amount };
}
});
AddMock(60, IID_Footprint, {
"GetShape": () => ({ "type": "circle", "radius": 20 }),
});
AddMock(70, IID_Ownership, {
"GetOwner": () => 1,
});
AddMock(70, IID_Position, {
"GetPosition": () => new Vector3D(0, 0, 0),
"GetRotation": () => new Vector3D(0, 0, 0),
"IsInWorld": () => true,
});
AddMock(10, IID_Player, {
"GetEnemies": () => [2]
});
cmpDelayedDamage.MissileHit(data, 0);
TS_ASSERT(hitEnts.has(60));
hitEnts.clear();
// Target is a mirage: hit the parent.
AddMock(60, IID_Mirage, {
"GetParent": () => 61
});
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
"DistanceToPoint": (ent) => 0
});
AddMock(61, IID_Position, {
"GetPosition": () => targetPos,
"GetPreviousPosition": () => targetPos,
"GetPosition2D": () => Vector2D.from3D(targetPos),
"IsInWorld": () => true
});
AddMock(61, IID_Health, {
"TakeDamage": (amount, __, ___) => {
hitEnts.add(61);
TS_ASSERT_EQUALS(amount, 100);
return { "healthChange": -amount };
}
});
AddMock(61, IID_Footprint, {
"GetShape": () => ({ "type": "circle", "radius": 20 })
});
cmpDelayedDamage.MissileHit(data, 0);
TS_ASSERT(hitEnts.has(61));
hitEnts.clear();
// Make sure we don't corrupt other tests.
DeleteMock(60, IID_Mirage);
cmpDelayedDamage.MissileHit(data, 0);
TS_ASSERT(hitEnts.has(60));
hitEnts.clear();
// The main target is not hit but another one is hit.
AddMock(60, IID_Position, {
"GetPosition": () => new Vector3D(900, 10, 0),
"GetPreviousPosition": () => new Vector3D(900, 10, 0),
"GetPosition2D": () => new Vector2D(900, 0),
"IsInWorld": () => true
});
AddMock(60, IID_Health, {
"TakeDamage": (amount, __, ___) => {
TS_ASSERT_EQUALS(false);
return { "healthChange": -amount };
}
});
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"ExecuteQueryAroundPos": () => [61]
});
cmpDelayedDamage.MissileHit(data, 0);
TS_ASSERT(hitEnts.has(61));
hitEnts.clear();
// Add a splash damage.
data.splash = {};
data.splash.friendlyFire = false;
data.splash.radius = 10;
data.splash.shape = "Circular";
data.splash.attackData = { "Damage": { "Hack": 0, "Pierce": 0, "Crush": 200 } };
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"ExecuteQueryAroundPos": () => [61, 62]
});
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
"DistanceToPoint": (ent) => ({
"61": 0,
"62": 5
}[ent])
});
let dealtDamage = 0;
AddMock(61, IID_Health, {
"TakeDamage": (amount, __, ___) => {
hitEnts.add(61);
dealtDamage += amount;
return { "healthChange": -amount };
}
});
AddMock(62, IID_Position, {
"GetPosition": () => new Vector3D(8, 10, 0),
"GetPreviousPosition": () => new Vector3D(8, 10, 0),
"GetPosition2D": () => new Vector2D(8, 0),
"IsInWorld": () => true,
});
AddMock(62, IID_Health, {
"TakeDamage": (amount, __, ___) => {
hitEnts.add(62);
TS_ASSERT_EQUALS(amount, 200 * 0.75);
return { "healthChange": -amount };
}
});
AddMock(62, IID_Footprint, {
"GetShape": () => ({ "type": "circle", "radius": 20 }),
});
cmpDelayedDamage.MissileHit(data, 0);
TS_ASSERT(hitEnts.has(61));
TS_ASSERT_EQUALS(dealtDamage, 100 + 200);
dealtDamage = 0;
TS_ASSERT(hitEnts.has(62));
hitEnts.clear();
// Add some hard counters bonus.
Engine.DestroyEntity(62);
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"ExecuteQueryAroundPos": () => [61]
});
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
"DistanceToPoint": (ent) => 0
});
- let bonus= { "BonusCav": { "Classes": "Cavalry", "Multiplier": 400 } };
+ let bonus = { "BonusCav": { "Classes": "Cavalry", "Multiplier": 400 } };
let splashBonus = { "BonusCav": { "Classes": "Cavalry", "Multiplier": 10000 } };
AddMock(61, IID_Identity, {
"GetClassesList": () => ["Cavalry"],
"GetCiv": () => "civ"
});
data.attackData.Bonuses = bonus;
cmpDelayedDamage.MissileHit(data, 0);
TS_ASSERT(hitEnts.has(61));
TS_ASSERT_EQUALS(dealtDamage, 400 * 100 + 200);
dealtDamage = 0;
hitEnts.clear();
data.splash.attackData.Bonuses = splashBonus;
cmpDelayedDamage.MissileHit(data, 0);
TS_ASSERT(hitEnts.has(61));
TS_ASSERT_EQUALS(dealtDamage, 400 * 100 + 10000 * 200);
dealtDamage = 0;
hitEnts.clear();
data.attackData.Bonuses = undefined;
cmpDelayedDamage.MissileHit(data, 0);
TS_ASSERT(hitEnts.has(61));
TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200);
dealtDamage = 0;
hitEnts.clear();
data.attackData.Bonuses = null;
cmpDelayedDamage.MissileHit(data, 0);
TS_ASSERT(hitEnts.has(61));
TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200);
dealtDamage = 0;
hitEnts.clear();
data.attackData.Bonuses = {};
cmpDelayedDamage.MissileHit(data, 0);
TS_ASSERT(hitEnts.has(61));
TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200);
dealtDamage = 0;
hitEnts.clear();
// Test splash damage with friendly fire.
data.splash = {};
data.splash.friendlyFire = true;
data.splash.radius = 10;
data.splash.shape = "Circular";
data.splash.attackData = { "Damage": { "Pierce": 0, "Crush": 200 } };
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"ExecuteQueryAroundPos": () => [61, 62]
});
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
"DistanceToPoint": (ent) => ({
"61": 0,
"62": 5
}[ent])
});
dealtDamage = 0;
AddMock(61, IID_Health, {
"TakeDamage": (amount, __, ___) => {
hitEnts.add(61);
dealtDamage += amount;
return { "healthChange": -amount };
}
});
AddMock(62, IID_Position, {
"GetPosition": () => new Vector3D(8, 10, 0),
"GetPreviousPosition": () => new Vector3D(8, 10, 0),
"GetPosition2D": () => new Vector2D(8, 0),
"IsInWorld": () => true,
});
AddMock(62, IID_Health, {
"TakeDamage": (amount, __, ___) => {
hitEnts.add(62);
TS_ASSERT_EQUALS(amount, 200 * 0.75);
return { "healtChange": -amount };
}
});
AddMock(62, IID_Footprint, {
"GetShape": () => ({ "type": "circle", "radius": 20 }),
});
cmpDelayedDamage.MissileHit(data, 0);
TS_ASSERT(hitEnts.has(61));
TS_ASSERT_EQUALS(dealtDamage, 100 + 200);
dealtDamage = 0;
TS_ASSERT(hitEnts.has(62));
hitEnts.clear();
}
Test_MissileHit();
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Resistance.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Resistance.js (revision 25012)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Resistance.js (revision 25013)
@@ -1,373 +1,419 @@
AttackEffects = class AttackEffects
{
constructor() {}
Receivers()
{
return [{
"type": "Damage",
"IID": "IID_Health",
"method": "TakeDamage"
},
{
"type": "Capture",
"IID": "IID_Capturable",
"method": "Capture"
},
{
"type": "ApplyStatus",
"IID": "IID_StatusEffectsReceiver",
"method": "ApplyStatus"
}];
}
};
Engine.LoadHelperScript("Attacking.js");
Engine.LoadHelperScript("Player.js");
Engine.LoadHelperScript("ValueModification.js");
Engine.LoadComponentScript("interfaces/Capturable.js");
Engine.LoadComponentScript("interfaces/Foundation.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/Looter.js");
Engine.LoadComponentScript("interfaces/ModifiersManager.js");
Engine.LoadComponentScript("interfaces/PlayerManager.js");
Engine.LoadComponentScript("interfaces/Promotion.js");
Engine.LoadComponentScript("interfaces/Resistance.js");
Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js");
Engine.LoadComponentScript("Resistance.js");
class testResistance
{
constructor()
{
this.cmpResistance = null;
this.PlayerID = 1;
this.EnemyID = 2;
this.EntityID = 3;
this.AttackerID = 4;
}
Reset(schema = {})
{
this.cmpResistance = ConstructComponent(this.EntityID, "Resistance", schema);
DeleteMock(this.EntityID, IID_Capturable);
DeleteMock(this.EntityID, IID_Health);
DeleteMock(this.EntityID, IID_Identity);
DeleteMock(this.EntityID, IID_StatusEffectsReceiver);
}
TestInvulnerability()
{
this.Reset();
let damage = 5;
let attackData = { "Damage": { "Name": damage } };
let attackType = "Test";
TS_ASSERT(!this.cmpResistance.IsInvulnerable());
let cmpHealth = AddMock(this.EntityID, IID_Health, {
"TakeDamage": (amount, __, ___) => {
TS_ASSERT_EQUALS(amount, damage);
return { "healthChange": -amount };
}
});
let spy = new Spy(cmpHealth, "TakeDamage");
+ let data = {
+ "type": attackType,
+ "attackData": attackData,
+ "attacker": this.AttackerID,
+ "attackerOwner": this.EnemyID
+ };
- Attacking.HandleAttackEffects(this.EntityID, attackType, attackData, this.AttackerID, this.EnemyID);
+ Attacking.HandleAttackEffects(this.EntityID, data);
TS_ASSERT_EQUALS(spy._called, 1);
this.cmpResistance.SetInvulnerability(true);
TS_ASSERT(this.cmpResistance.IsInvulnerable());
- Attacking.HandleAttackEffects(this.EntityID, attackType, attackData, this.AttackerID, this.EnemyID);
+ Attacking.HandleAttackEffects(this.EntityID, data);
TS_ASSERT_EQUALS(spy._called, 1);
}
TestBonus()
{
this.Reset();
let damage = 5;
let bonus = 2;
let classes = "Entity";
let attackData = {
"Damage": { "Name": damage },
"Bonuses": {
"bonus": {
"Classes": classes,
"Multiplier": bonus
}
}
};
AddMock(this.EntityID, IID_Identity, {
"GetClassesList": () => [classes],
"GetCiv": () => "civ"
});
let cmpHealth = AddMock(this.EntityID, IID_Health, {
"TakeDamage": (amount, __, ___) => {
TS_ASSERT_EQUALS(amount, damage * bonus);
return { "healthChange": -amount };
}
});
let spy = new Spy(cmpHealth, "TakeDamage");
- Attacking.HandleAttackEffects(this.EntityID, "Test", attackData, this.AttackerID, this.EnemyID);
+ Attacking.HandleAttackEffects(this.EntityID, {
+ "type": "Test",
+ "attackData": attackData,
+ "attacker": this.AttackerID,
+ "attackerOwner": this.EnemyID
+ });
TS_ASSERT_EQUALS(spy._called, 1);
}
TestDamageResistanceApplies()
{
let resistanceValue = 2;
let damageType = "Name";
this.Reset({
"Entity": {
"Damage": {
[damageType]: resistanceValue
}
}
});
let damage = 5;
let attackData = {
"Damage": { "Name": damage }
};
let cmpHealth = AddMock(this.EntityID, IID_Health, {
"TakeDamage": (amount, __, ___) => {
TS_ASSERT_EQUALS(amount, damage * Math.pow(0.9, resistanceValue));
return { "healthChange": -amount };
}
});
let spy = new Spy(cmpHealth, "TakeDamage");
- Attacking.HandleAttackEffects(this.EntityID, "Test", attackData, this.AttackerID, this.EnemyID);
+ Attacking.HandleAttackEffects(this.EntityID, {
+ "type": "Test",
+ "attackData": attackData,
+ "attacker": this.AttackerID,
+ "attackerOwner": this.EnemyID
+ });
TS_ASSERT_EQUALS(spy._called, 1);
}
TestCaptureResistanceApplies()
{
let resistanceValue = 2;
this.Reset({
"Entity": {
"Capture": resistanceValue
}
});
let damage = 5;
let attackData = {
"Capture": damage
};
let cmpCapturable = AddMock(this.EntityID, IID_Capturable, {
"Capture": (amount, __, ___) => {
TS_ASSERT_EQUALS(amount, damage * Math.pow(0.9, resistanceValue));
return { "captureChange": amount };
}
});
let spy = new Spy(cmpCapturable, "Capture");
- Attacking.HandleAttackEffects(this.EntityID, "Test", attackData, this.AttackerID, this.EnemyID);
+ Attacking.HandleAttackEffects(this.EntityID, {
+ "type": "Test",
+ "attackData": attackData,
+ "attacker": this.AttackerID,
+ "attackerOwner": this.EnemyID
+ });
TS_ASSERT_EQUALS(spy._called, 1);
}
TestStatusEffectsResistancesApplies()
{
// Test duration reduction.
let durationFactor = 0.5;
let statusName = "statusName";
this.Reset({
"Entity": {
"ApplyStatus": {
[statusName]: {
"Duration": durationFactor
}
}
}
});
let duration = 10;
let attackData = {
"ApplyStatus": {
[statusName]: {
"Duration": duration
}
}
};
let cmpStatusEffectsReceiver = AddMock(this.EntityID, IID_StatusEffectsReceiver, {
"ApplyStatus": (effectData, __, ___) => {
TS_ASSERT_EQUALS(effectData[statusName].Duration, duration * durationFactor);
return { "inflictedStatuses": Object.keys(effectData) };
}
});
let spy = new Spy(cmpStatusEffectsReceiver, "ApplyStatus");
- Attacking.HandleAttackEffects(this.EntityID, "Test", attackData, this.AttackerID, this.EnemyID);
+ Attacking.HandleAttackEffects(this.EntityID, {
+ "type": "Test",
+ "attackData": attackData,
+ "attacker": this.AttackerID,
+ "attackerOwner": this.EnemyID
+ });
TS_ASSERT_EQUALS(spy._called, 1);
// Test blocking.
this.Reset({
"Entity": {
"ApplyStatus": {
[statusName]: {
"BlockChance": "1"
}
}
}
});
cmpStatusEffectsReceiver = AddMock(this.EntityID, IID_StatusEffectsReceiver, {
"ApplyStatus": (effectData, __, ___) => {
TS_ASSERT_UNEVAL_EQUALS(effectData, {});
return { "inflictedStatuses": Object.keys(effectData) };
}
});
spy = new Spy(cmpStatusEffectsReceiver, "ApplyStatus");
- Attacking.HandleAttackEffects(this.EntityID, "Test", attackData, this.AttackerID, this.EnemyID);
+ Attacking.HandleAttackEffects(this.EntityID, {
+ "type": "Test",
+ "attackData": attackData,
+ "attacker": this.AttackerID,
+ "attackerOwner": this.EnemyID
+ });
TS_ASSERT_EQUALS(spy._called, 1);
// Test multiple resistances.
let reducedStatusName = "reducedStatus";
let blockedStatusName = "blockedStatus";
this.Reset({
"Entity": {
"ApplyStatus": {
[reducedStatusName]: {
"Duration": durationFactor
},
[blockedStatusName]: {
"BlockChance": "1"
}
}
}
});
attackData = {
"ApplyStatus": {
[reducedStatusName]: {
"Duration": duration
},
[blockedStatusName]: {
"Duration": duration
}
}
};
cmpStatusEffectsReceiver = AddMock(this.EntityID, IID_StatusEffectsReceiver, {
"ApplyStatus": (effectData, __, ___) => {
TS_ASSERT_EQUALS(effectData[reducedStatusName].Duration, duration * durationFactor);
TS_ASSERT_UNEVAL_EQUALS(Object.keys(effectData), [reducedStatusName]);
return { "inflictedStatuses": Object.keys(effectData) };
}
});
spy = new Spy(cmpStatusEffectsReceiver, "ApplyStatus");
- Attacking.HandleAttackEffects(this.EntityID, "Test", attackData, this.AttackerID, this.EnemyID);
+ Attacking.HandleAttackEffects(this.EntityID, {
+ "type": "Test",
+ "attackData": attackData,
+ "attacker": this.AttackerID,
+ "attackerOwner": this.EnemyID
+ });
TS_ASSERT_EQUALS(spy._called, 1);
}
TestResistanceAndBonus()
{
let resistanceValue = 2;
let damageType = "Name";
this.Reset({
"Entity": {
"Damage": {
[damageType]: resistanceValue
}
}
});
let damage = 5;
let bonus = 2;
let classes = "Entity";
let attackData = {
"Damage": { "Name": damage },
"Bonuses": {
"bonus": {
"Classes": classes,
"Multiplier": bonus
}
}
};
AddMock(this.EntityID, IID_Identity, {
"GetClassesList": () => [classes],
"GetCiv": () => "civ"
});
let cmpHealth = AddMock(this.EntityID, IID_Health, {
"TakeDamage": (amount, __, ___) => {
TS_ASSERT_EQUALS(amount, damage * bonus * Math.pow(0.9, resistanceValue));
return { "healthChange": -amount };
}
});
let spy = new Spy(cmpHealth, "TakeDamage");
- Attacking.HandleAttackEffects(this.EntityID, "Test", attackData, this.AttackerID, this.EnemyID);
+ Attacking.HandleAttackEffects(this.EntityID, {
+ "type": "Test",
+ "attackData": attackData,
+ "attacker": this.AttackerID,
+ "attackerOwner": this.EnemyID
+ });
TS_ASSERT_EQUALS(spy._called, 1);
}
TestMultipleEffects()
{
let captureResistanceValue = 2;
this.Reset({
"Entity": {
"Capture": captureResistanceValue
}
});
let damage = 5;
let bonus = 2;
let classes = "Entity";
let attackData = {
"Damage": { "Name": damage },
"Capture": damage,
"Bonuses": {
"bonus": {
"Classes": classes,
"Multiplier": bonus
}
}
};
AddMock(this.EntityID, IID_Identity, {
"GetClassesList": () => [classes],
"GetCiv": () => "civ"
});
let cmpCapturable = AddMock(this.EntityID, IID_Capturable, {
"Capture": (amount, __, ___) => {
TS_ASSERT_EQUALS(amount, damage * bonus * Math.pow(0.9, captureResistanceValue));
return { "captureChange": amount };
}
});
let cmpHealth = AddMock(this.EntityID, IID_Health, {
"TakeDamage": (amount, __, ___) => {
TS_ASSERT_EQUALS(amount, damage * bonus);
return { "healthChange": -amount };
},
"GetHitpoints": () => 1,
"GetMaxHitpoints": () => 1
});
let healthSpy = new Spy(cmpHealth, "TakeDamage");
let captureSpy = new Spy(cmpCapturable, "Capture");
- Attacking.HandleAttackEffects(this.EntityID, "Test", attackData, this.AttackerID, this.EnemyID);
+ Attacking.HandleAttackEffects(this.EntityID, {
+ "type": "Test",
+ "attackData": attackData,
+ "attacker": this.AttackerID,
+ "attackerOwner": this.EnemyID
+ });
TS_ASSERT_EQUALS(healthSpy._called, 1);
TS_ASSERT_EQUALS(captureSpy._called, 1);
}
}
let cmp = new testResistance();
cmp.TestInvulnerability();
cmp.TestBonus();
cmp.TestDamageResistanceApplies();
cmp.TestCaptureResistanceApplies();
cmp.TestStatusEffectsResistancesApplies();
cmp.TestResistanceAndBonus();
cmp.TestMultipleEffects();
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_StatusEffectsReceiver.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_StatusEffectsReceiver.js (revision 25012)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_StatusEffectsReceiver.js (revision 25013)
@@ -1,345 +1,345 @@
Engine.LoadHelperScript("MultiKeyMap.js");
Engine.LoadHelperScript("Player.js");
Engine.LoadHelperScript("ValueModification.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/ModifiersManager.js");
Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("Health.js");
Engine.LoadComponentScript("ModifiersManager.js");
Engine.LoadComponentScript("StatusEffectsReceiver.js");
Engine.LoadComponentScript("Timer.js");
let target = 42;
let cmpStatusReceiver = ConstructComponent(target, "StatusEffectsReceiver");
let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer");
let dealtDamage = 0;
let enemyEntity = 4;
let enemy = 2;
let statusName;
let Attacking = {
- "HandleAttackEffects": (_, __, attackData) => {
- for (let type in attackData.Damage)
- dealtDamage += attackData.Damage[type];
+ "HandleAttackEffects": (_, data) => {
+ for (let type in data.attackData.Damage)
+ dealtDamage += data.attackData.Damage[type];
}
};
Engine.RegisterGlobal("Attacking", Attacking);
function reset()
{
for (let status in cmpStatusReceiver.GetActiveStatuses())
cmpStatusReceiver.RemoveStatus(status);
dealtDamage = 0;
}
// Test adding a single effect.
statusName = "Burn";
// Damage scheduled: 0, 10, 20 seconds.
cmpStatusReceiver.AddStatus(statusName, {
"Duration": 20000,
"Interval": 10000,
"Damage": {
[statusName]: 1
}
},
{
"entity": enemyEntity,
"owner": enemy,
});
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 1); // 1 sec
cmpTimer.OnUpdate({ "turnLength": 8 });
TS_ASSERT_EQUALS(dealtDamage, 1); // 9 sec
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 2); // 10 sec
cmpTimer.OnUpdate({ "turnLength": 10 });
TS_ASSERT_EQUALS(dealtDamage, 3); // 20 sec
cmpTimer.OnUpdate({ "turnLength": 10 });
TS_ASSERT_EQUALS(dealtDamage, 3); // 30 sec
// Test adding multiple effects.
reset();
// Damage scheduled: 0, 1, 2, 10 seconds.
cmpStatusReceiver.ApplyStatus({
"Burn": {
"Duration": 20000,
"Interval": 10000,
"Damage": {
"Burn": 10
}
},
"Poison": {
"Duration": 3000,
"Interval": 1000,
"Damage": {
"Poison": 1
}
}
});
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 12); // 1 sec
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 13); // 2 sec
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 13); // 3 sec
cmpTimer.OnUpdate({ "turnLength": 7 });
TS_ASSERT_EQUALS(dealtDamage, 23); // 10 sec
// Test removing a status removes effects.
reset();
statusName = "Poison";
// Damage scheduled: 0, 10, 20 seconds.
cmpStatusReceiver.AddStatus(statusName, {
"Duration": 20000,
"Interval": 10000,
"Damage": {
[statusName]: 1
}
},
{
"entity": enemyEntity,
"owner": enemy,
});
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 1); // 1 sec
cmpStatusReceiver.RemoveStatus(statusName);
cmpTimer.OnUpdate({ "turnLength": 10 });
TS_ASSERT_EQUALS(dealtDamage, 1); // 11 sec
// Test that a status effect with modifications modifies.
reset();
AddMock(target, IID_Identity, {
"GetClassesList": () => ["AffectedClass"]
});
let cmpModifiersManager = ConstructComponent(SYSTEM_ENTITY, "ModifiersManager");
let maxHealth = 100;
AddMock(target, IID_Health, {
"GetMaxHitpoints": () => ApplyValueModificationsToEntity("Health/Max", maxHealth, target)
});
statusName = "Haste";
let factor = 0.5;
cmpStatusReceiver.AddStatus(statusName, {
"Duration": 5000,
"Modifiers": {
[statusName]: {
"Paths": {
"_string": "Health/Max"
},
"Affects": {
"_string": "AffectedClass"
},
"Multiply": factor
}
}
},
{
"entity": enemyEntity,
"owner": enemy,
});
let cmpHealth = Engine.QueryInterface(target, IID_Health);
// Test that the modification is applied.
TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), maxHealth * factor);
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), maxHealth * factor);
// Test that the modification is removed after the appropriate time.
cmpTimer.OnUpdate({ "turnLength": 4 });
TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), maxHealth);
// Test addition.
let addition = 50;
cmpStatusReceiver.AddStatus(statusName, {
"Duration": 5000,
"Modifiers": {
[statusName]: {
"Paths": {
"_string": "Health/Max"
},
"Affects": {
"_string": "AffectedClass"
},
"Add": addition
}
}
},
{
"entity": enemyEntity,
"owner": enemy,
});
// Test that the addition modification is applied.
TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), maxHealth + addition);
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), maxHealth + addition);
// Test that the modification is removed after the appropriate time.
cmpTimer.OnUpdate({ "turnLength": 4 });
TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), maxHealth);
// Test replacement.
let newValue = 50;
cmpStatusReceiver.AddStatus(statusName, {
"Duration": 5000,
"Modifiers": {
[statusName]: {
"Paths": {
"_string": "Health/Max"
},
"Affects": {
"_string": "AffectedClass"
},
"Replace": newValue
}
}
},
{
"entity": enemyEntity,
"owner": enemy,
});
// Test that the replacement modification is applied.
TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), newValue);
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), newValue);
// Test that the modification is removed after the appropriate time.
cmpTimer.OnUpdate({ "turnLength": 4 });
TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), maxHealth);
function applyStatus(stackability)
{
cmpStatusReceiver.ApplyStatus({
"randomName": {
"Duration": 3000,
"Interval": 1000,
"Damage": {
"randomName": 1
},
"Stackability": stackability
}
});
}
// Test different stackabilities.
// First ignoring, i.e. next time the same status is added it is just ignored.
reset();
applyStatus("Ignore");
// 1 Second: 1 update and lateness.
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 2);
applyStatus("Ignore");
// 2 Seconds.
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 3);
// 3 Seconds: finished in previous turn.
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 3);
// Extending, i.e. next time the same status is applied the times are added.
reset();
applyStatus("Extend");
// 1 Second: 1 update and lateness.
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 2);
// Add 3 seconds.
applyStatus("Extend");
// 2 Seconds.
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 3);
// 3 Seconds: extended in previous turn.
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 4);
// 4 Seconds.
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 5);
// 5 Seconds.
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 6);
// 6 Seconds: finished in previous turn (3 + 3).
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 6);
// Replacing, i.e. the next applied status replaces the former.
reset();
applyStatus("Replace");
// 1 Second: 1 update and lateness.
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 2);
applyStatus("Replace");
// 2 Seconds: 1 update and lateness of the new status.
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 4);
// 3 Seconds.
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 5);
// 4 Seconds: finished in previous turn.
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 5);
// Stacking, every new status just applies besides the rest.
reset();
applyStatus("Stack");
// 1 Second: 1 update and lateness.
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 2);
applyStatus("Stack");
// 2 Seconds: 1 damage from the previous status + 2 from the new (1 turn + lateness).
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 5);
// 3 Seconds: first one finished in the previous turn, +1 from the new.
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 6);
// 4 Seconds: new status finished in previous turn.
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 6);
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Attacking.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/Attacking.js (revision 25012)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/Attacking.js (revision 25013)
@@ -1,381 +1,383 @@
/**
* Provides attack and damage-related helpers under the Attacking umbrella (to avoid name ambiguity with the component).
*/
function Attacking() {}
const DirectEffectsSchema =
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
const StatusEffectsSchema =
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
DirectEffectsSchema +
"" +
"" +
"" +
"" +
"" +
ModificationsSchema +
"" +
"" +
"" +
"Ignore" +
"Extend" +
"Replace" +
"Stack" +
"" +
"" +
"" +
"" +
"" +
"";
/**
* Builds a RelaxRNG schema of possible attack effects.
* See globalscripts/AttackEffects.js for possible elements.
* Attacks may also have a "Bonuses" element.
*
* @return {string} - RelaxNG schema string.
*/
Attacking.prototype.BuildAttackEffectsSchema = function()
{
return "" +
"" +
"" +
DirectEffectsSchema +
StatusEffectsSchema +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
};
/**
* Returns a template-like object of attack effects.
*/
Attacking.prototype.GetAttackEffectsData = function(valueModifRoot, template, entity)
{
let ret = {};
if (template.Damage)
{
ret.Damage = {};
let applyMods = damageType =>
ApplyValueModificationsToEntity(valueModifRoot + "/Damage/" + damageType, +(template.Damage[damageType] || 0), entity);
for (let damageType in template.Damage)
ret.Damage[damageType] = applyMods(damageType);
}
if (template.Capture)
ret.Capture = ApplyValueModificationsToEntity(valueModifRoot + "/Capture", +(template.Capture || 0), entity);
if (template.ApplyStatus)
ret.ApplyStatus = this.GetStatusEffectsData(valueModifRoot, template.ApplyStatus, entity);
if (template.Bonuses)
ret.Bonuses = template.Bonuses;
return ret;
};
Attacking.prototype.GetStatusEffectsData = function(valueModifRoot, template, entity)
{
let result = {};
for (let effect in template)
{
let statusTemplate = template[effect];
result[effect] = {
"Duration": ApplyValueModificationsToEntity(valueModifRoot + "/ApplyStatus/" + effect + "/Duration", +(statusTemplate.Duration || 0), entity),
"Interval": ApplyValueModificationsToEntity(valueModifRoot + "/ApplyStatus/" + effect + "/Interval", +(statusTemplate.Interval || 0), entity),
"Stackability": statusTemplate.Stackability
};
Object.assign(result[effect], this.GetAttackEffectsData(valueModifRoot + "/ApplyStatus" + effect, statusTemplate, entity));
if (statusTemplate.Modifiers)
result[effect].Modifiers = this.GetStatusEffectsModifications(valueModifRoot, statusTemplate.Modifiers, entity, effect);
}
return result;
};
Attacking.prototype.GetStatusEffectsModifications = function(valueModifRoot, template, entity, effect)
{
let modifiers = {};
for (let modifier in template)
{
let modifierTemplate = template[modifier];
modifiers[modifier] = {
"Paths": modifierTemplate.Paths,
"Affects": modifierTemplate.Affects
};
if (modifierTemplate.Add !== undefined)
modifiers[modifier].Add = ApplyValueModificationsToEntity(valueModifRoot + "/ApplyStatus/" + effect + "/Modifiers/" + modifier + "/Add", +modifierTemplate.Add, entity);
if (modifierTemplate.Multiply !== undefined)
modifiers[modifier].Multiply = ApplyValueModificationsToEntity(valueModifRoot + "/ApplyStatus/" + effect + "/Modifiers/" + modifier + "/Multiply", +modifierTemplate.Multiply, entity);
if (modifierTemplate.Replace !== undefined)
modifiers[modifier].Replace = modifierTemplate.Replace;
}
return modifiers;
};
/**
* Calculate the total effect taking bonus and resistance into account.
*
* @param {number} target - The target of the attack.
* @param {Object} effectData - The effects calculate the effect for.
* @param {string} effectType - The type of effect to apply (e.g. Damage, Capture or ApplyStatus).
* @param {number} bonusMultiplier - The factor to multiply the total effect with.
* @param {Object} cmpResistance - Optionally the resistance component of the target.
*
* @return {number} - The total value of the effect.
*/
Attacking.prototype.GetTotalAttackEffects = function(target, effectData, effectType, bonusMultiplier, cmpResistance)
{
let total = 0;
if (!cmpResistance)
cmpResistance = Engine.QueryInterface(target, IID_Resistance);
let resistanceStrengths = cmpResistance ? cmpResistance.GetEffectiveResistanceAgainst(effectType) : {};
if (effectType == "Damage")
for (let type in effectData.Damage)
total += effectData.Damage[type] * Math.pow(0.9, resistanceStrengths.Damage ? resistanceStrengths.Damage[type] || 0 : 0);
else if (effectType == "Capture")
{
total = effectData.Capture * Math.pow(0.9, resistanceStrengths.Capture || 0);
// If Health is lower we are more susceptible to capture attacks.
let cmpHealth = Engine.QueryInterface(target, IID_Health);
if (cmpHealth)
total /= 0.1 + 0.9 * cmpHealth.GetHitpoints() / cmpHealth.GetMaxHitpoints();
}
if (effectType != "ApplyStatus")
return total * bonusMultiplier;
if (!resistanceStrengths.ApplyStatus)
return effectData[effectType];
let result = {};
for (let statusEffect in effectData[effectType])
{
if (!resistanceStrengths.ApplyStatus[statusEffect])
{
result[statusEffect] = effectData[effectType][statusEffect];
continue;
}
if (randBool(resistanceStrengths.ApplyStatus[statusEffect].blockChance))
continue;
result[statusEffect] = effectData[effectType][statusEffect];
if (effectData[effectType][statusEffect].Duration)
result[statusEffect].Duration = effectData[effectType][statusEffect].Duration *
resistanceStrengths.ApplyStatus[statusEffect].duration;
}
return result;
};
/**
* Get the list of players affected by the damage.
* @param {number} attackerOwner - The player id of the attacker.
* @param {boolean} friendlyFire - A flag indicating if allied entities are also damaged.
* @return {number[]} The ids of players need to be damaged.
*/
Attacking.prototype.GetPlayersToDamage = function(attackerOwner, friendlyFire)
{
if (!friendlyFire)
return QueryPlayerIDInterface(attackerOwner).GetEnemies();
return Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers();
};
/**
* Damages units around a given origin.
* @param {Object} data - The data sent by the caller.
* @param {string} data.type - The type of damage.
* @param {Object} data.attackData - The attack data.
* @param {number} data.attacker - The entity id of the attacker.
* @param {number} data.attackerOwner - The player id of the attacker.
* @param {Vector2D} data.origin - The origin of the projectile hit.
* @param {number} data.radius - The radius of the splash damage.
* @param {string} data.shape - The shape of the radius.
* @param {Vector3D} [data.direction] - The unit vector defining the direction. Needed for linear splash damage.
* @param {boolean} data.friendlyFire - A flag indicating if allied entities also ought to be damaged.
*/
Attacking.prototype.CauseDamageOverArea = function(data)
{
let nearEnts = PositionHelper.EntitiesNearPoint(data.origin, data.radius,
this.GetPlayersToDamage(data.attackerOwner, data.friendlyFire));
let damageMultiplier = 1;
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
// Cycle through all the nearby entities and damage it appropriately based on its distance from the origin.
for (let ent of nearEnts)
{
// Correct somewhat for the entity's obstruction radius.
// TODO: linear falloff should arguably use something cleverer.
let distance = cmpObstructionManager.DistanceToPoint(ent, data.origin.x, data.origin.y);
if (data.shape == 'Circular') // circular effect with quadratic falloff in every direction
damageMultiplier = 1 - distance * distance / (data.radius * data.radius);
else if (data.shape == 'Linear') // linear effect with quadratic falloff in two directions (only used for certain missiles)
{
// The entity has a position here since it was returned by the range manager.
let entityPosition = Engine.QueryInterface(ent, IID_Position).GetPosition2D();
let relativePos = entityPosition.sub(data.origin).normalize().mult(distance);
// Get the position relative to the missile direction.
let direction = Vector2D.from3D(data.direction);
let parallelPos = relativePos.dot(direction);
let perpPos = relativePos.cross(direction);
// The width of linear splash is one fifth of the normal splash radius.
let width = data.radius / 5;
// Check that the unit is within the distance splash width of the line starting at the missile's
// landing point which extends in the direction of the missile for length splash radius.
if (parallelPos >= 0 && Math.abs(perpPos) < width) // If in radius, quadratic falloff in both directions
damageMultiplier = (1 - parallelPos * parallelPos / (data.radius * data.radius)) *
(1 - perpPos * perpPos / (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!");
}
// The RangeManager can return units that are too far away (due to approximations there)
// so the multiplier can end up below 0.
damageMultiplier = Math.max(0, damageMultiplier);
- this.HandleAttackEffects(ent, data.type + ".Splash", data.attackData, data.attacker, data.attackerOwner, damageMultiplier);
+ data.type += ".Splash";
+ this.HandleAttackEffects(ent, data, damageMultiplier);
}
};
/**
* Handle an attack peformed on an entity.
*
* @param {number} target - The targetted entityID.
- * @param {string} attackType - The type of attack that was performed (e.g. "Melee" or "Capture").
- * @param {Object} effectData - The effects use.
- * @param {number} attacker - The entityID that attacked us.
- * @param {number} attackerOwner - The playerID that owned the attacker when the attack was performed.
+ * @param {Object} data - The data of the attack.
+ * @param {string} data.type - The type of attack that was performed (e.g. "Melee" or "Capture").
+ * @param {Object} data.effectData - The effects use.
+ * @param {number} data.attacker - The entityID that attacked us.
+ * @param {number} data.attackerOwner - The playerID that owned the attacker when the attack was performed.
* @param {number} bonusMultiplier - The factor to multiply the total effect with, defaults to 1.
*
* @return {boolean} - Whether we handled the attack.
*/
-Attacking.prototype.HandleAttackEffects = function(target, attackType, attackData, attacker, attackerOwner, bonusMultiplier = 1)
+Attacking.prototype.HandleAttackEffects = function(target, data, bonusMultiplier = 1)
{
let cmpResistance = Engine.QueryInterface(target, IID_Resistance);
if (cmpResistance && cmpResistance.IsInvulnerable())
return false;
- bonusMultiplier *= !attackData.Bonuses ? 1 : this.GetAttackBonus(attacker, target, attackType, attackData.Bonuses);
+ bonusMultiplier *= !data.attackData.Bonuses ? 1 : this.GetAttackBonus(data.attacker, target, data.type, data.attackData.Bonuses);
let targetState = {};
for (let receiver of g_AttackEffects.Receivers())
{
- if (!attackData[receiver.type])
+ if (!data.attackData[receiver.type])
continue;
let cmpReceiver = Engine.QueryInterface(target, global[receiver.IID]);
if (!cmpReceiver)
continue;
- Object.assign(targetState, cmpReceiver[receiver.method](this.GetTotalAttackEffects(target, attackData, receiver.type, bonusMultiplier, cmpResistance), attacker, attackerOwner));
+ Object.assign(targetState, cmpReceiver[receiver.method](this.GetTotalAttackEffects(target, data.attackData, receiver.type, bonusMultiplier, cmpResistance), data.attacker, data.attackerOwner));
}
if (!Object.keys(targetState).length)
return false;
Engine.PostMessage(target, MT_Attacked, {
- "type": attackType,
+ "type": data.type,
"target": target,
- "attacker": attacker,
- "attackerOwner": attackerOwner,
+ "attacker": data.attacker,
+ "attackerOwner": data.attackerOwner,
"damage": -(targetState.healthChange || 0),
"capture": targetState.captureChange || 0,
"statusEffects": targetState.inflictedStatuses || [],
- "fromStatusEffect": !!attackData.StatusEffect,
+ "fromStatusEffect": !!data.attackData.StatusEffect,
});
// We do not want an entity to get XP from active Status Effects.
- if (!!attackData.StatusEffect)
+ if (!!data.attackData.StatusEffect)
return true;
- let cmpPromotion = Engine.QueryInterface(attacker, IID_Promotion);
+ let cmpPromotion = Engine.QueryInterface(data.attacker, IID_Promotion);
if (cmpPromotion && targetState.xp)
cmpPromotion.IncreaseXp(targetState.xp);
return true;
};
/**
* Calculates the attack damage multiplier against a target.
* @param {number} source - The source entity's id.
* @param {number} target - The target entity's id.
* @param {string} type - The type of attack.
* @param {Object} template - The bonus' template.
* @return {number} - The source entity's attack bonus against the specified target.
*/
Attacking.prototype.GetAttackBonus = function(source, target, type, template)
{
let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
if (!cmpIdentity)
return 1;
let attackBonus = 1;
let targetClasses = cmpIdentity.GetClassesList();
let targetCiv = cmpIdentity.GetCiv();
// Multiply the bonuses for all matching classes.
for (let key in template)
{
let bonus = template[key];
if (bonus.Civ && bonus.Civ !== targetCiv)
continue;
if (!bonus.Classes || MatchesClassList(targetClasses, bonus.Classes))
attackBonus *= ApplyValueModificationsToEntity("Attack/" + type + "/Bonuses/" + key + "/Multiplier", +bonus.Multiplier, source);
}
return attackBonus;
};
var AttackingInstance = new Attacking();
Engine.RegisterGlobal("Attacking", AttackingInstance);
Engine.RegisterGlobal("g_AttackEffects", new AttackEffects());
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/tests/test_Attacking.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/tests/test_Attacking.js (revision 25012)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/tests/test_Attacking.js (revision 25013)
@@ -1,159 +1,199 @@
AttackEffects = class AttackEffects
{
constructor() {}
Receivers()
{
return [{
"type": "Damage",
"IID": "IID_Health",
"method": "TakeDamage"
},
{
"type": "Capture",
"IID": "IID_Capturable",
"method": "Capture"
},
{
"type": "ApplyStatus",
"IID": "IID_StatusEffectsReceiver",
"method": "ApplyStatus"
}];
}
};
Engine.LoadHelperScript("Attacking.js");
Engine.LoadComponentScript("interfaces/Capturable.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/Promotion.js");
Engine.LoadComponentScript("interfaces/Resistance.js");
Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js");
// Unit tests for the Attacking helper.
// TODO: Some of it is tested in components/test_Damage.js, which should be spliced and moved.
class testHandleAttackEffects {
constructor() {
this.resultString = "";
this.TESTED_ENTITY_ID = 5;
this.attackData = {
"Damage": "1",
"Capture": "2",
"ApplyStatus": {
"statusName": {}
}
};
}
/**
* This tests that we inflict multiple effect types.
*/
testMultipleEffects() {
AddMock(this.TESTED_ENTITY_ID, IID_Health, {
"TakeDamage": x => { this.resultString += x; },
"GetHitpoints": () => 1,
"GetMaxHitpoints": () => 1,
});
AddMock(this.TESTED_ENTITY_ID, IID_Capturable, {
"Capture": x => { this.resultString += x; },
});
- Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, "Test", this.attackData, INVALID_ENTITY, INVALID_PLAYER);
+ Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, {
+ "type": "Test",
+ "attackData": this.attackData,
+ "attacker": INVALID_ENTITY,
+ "attackerOwner": INVALID_PLAYER
+ });
TS_ASSERT(this.resultString.indexOf(this.attackData.Damage) !== -1);
TS_ASSERT(this.resultString.indexOf(this.attackData.Capture) !== -1);
}
/**
* This tests that we correctly handle effect types if one is not received.
*/
testSkippedEffect() {
AddMock(this.TESTED_ENTITY_ID, IID_Capturable, {
"Capture": x => { this.resultString += x; },
});
- Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, "Test", this.attackData, INVALID_ENTITY, INVALID_PLAYER);
+ Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, {
+ "type": "Test",
+ "attackData": this.attackData,
+ "attacker": INVALID_ENTITY,
+ "attackerOwner": INVALID_PLAYER
+ });
TS_ASSERT(this.resultString.indexOf(this.attackData.Damage) === -1);
TS_ASSERT(this.resultString.indexOf(this.attackData.Capture) !== -1);
this.resultString = "";
DeleteMock(this.TESTED_ENTITY_ID, IID_Capturable);
AddMock(this.TESTED_ENTITY_ID, IID_Health, {
"TakeDamage": x => { this.resultString += x; },
"GetHitpoints": () => 1,
"GetMaxHitpoints": () => 1,
});
- Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, "Test", this.attackData, INVALID_ENTITY, INVALID_PLAYER);
+ Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, {
+ "type": "Test",
+ "attackData": this.attackData,
+ "attacker": INVALID_ENTITY,
+ "attackerOwner": INVALID_PLAYER
+ });
TS_ASSERT(this.resultString.indexOf(this.attackData.Damage) !== -1);
TS_ASSERT(this.resultString.indexOf(this.attackData.Capture) === -1);
}
/**
* Check that the Attacked message is [not] sent if [no] receivers exist.
*/
testAttackedMessage() {
Engine.PostMessage = () => TS_ASSERT(false);
- Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, "Test", this.attackData, INVALID_ENTITY, INVALID_PLAYER);
+ Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, {
+ "type": "Test",
+ "attackData": this.attackData,
+ "attacker": INVALID_ENTITY,
+ "attackerOwner": INVALID_PLAYER
+ });
AddMock(this.TESTED_ENTITY_ID, IID_Capturable, {
"Capture": () => ({ "captureChange": 0 }),
});
let count = 0;
Engine.PostMessage = () => count++;
- Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, "Test", this.attackData, INVALID_ENTITY, INVALID_PLAYER);
+ Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, {
+ "type": "Test",
+ "attackData": this.attackData,
+ "attacker": INVALID_ENTITY,
+ "attackerOwner": INVALID_PLAYER
+ });
TS_ASSERT_EQUALS(count, 1);
AddMock(this.TESTED_ENTITY_ID, IID_Health, {
"TakeDamage": () => ({ "healthChange": 0 }),
"GetHitpoints": () => 1,
"GetMaxHitpoints": () => 1,
});
count = 0;
Engine.PostMessage = () => count++;
- Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, "Test", this.attackData, INVALID_ENTITY, INVALID_PLAYER);
+ Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, {
+ "type": "Test",
+ "attackData": this.attackData,
+ "attacker": INVALID_ENTITY,
+ "attackerOwner": INVALID_PLAYER
+ });
TS_ASSERT_EQUALS(count, 1);
}
/**
* Regression test that StatusEffects are handled correctly.
*/
testStatusEffects() {
let cmpStatusEffectsReceiver = AddMock(this.TESTED_ENTITY_ID, IID_StatusEffectsReceiver, {
"ApplyStatus": (effectData, __, ___) => {
TS_ASSERT_UNEVAL_EQUALS(effectData, this.attackData.ApplyStatus);
}
});
let spy = new Spy(cmpStatusEffectsReceiver, "ApplyStatus");
- Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, "Test", this.attackData, INVALID_ENTITY, INVALID_PLAYER, 2);
+ Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, {
+ "type": "Test",
+ "attackData": this.attackData,
+ "attacker": INVALID_ENTITY,
+ "attackerOwner": INVALID_PLAYER
+ }, 2);
TS_ASSERT_EQUALS(spy._called, 1);
}
/**
* Regression test that bonus multiplier is handled correctly.
*/
testBonusMultiplier() {
AddMock(this.TESTED_ENTITY_ID, IID_Health, {
"TakeDamage": (amount, __, ___) => {
TS_ASSERT_EQUALS(amount, this.attackData.Damage * 2);
},
"GetHitpoints": () => 1,
"GetMaxHitpoints": () => 1,
});
AddMock(this.TESTED_ENTITY_ID, IID_Capturable, {
"Capture": (amount, __, ___) => {
TS_ASSERT_EQUALS(amount, this.attackData.Capture * 2);
},
});
- Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, "Test", this.attackData, INVALID_ENTITY, INVALID_PLAYER, 2);
+ Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, {
+ "type": "Test",
+ "attackData": this.attackData,
+ "attacker": INVALID_ENTITY,
+ "attackerOwner": INVALID_PLAYER
+ }, 2);
}
}
new testHandleAttackEffects().testMultipleEffects();
new testHandleAttackEffects().testSkippedEffect();
new testHandleAttackEffects().testAttackedMessage();
new testHandleAttackEffects().testStatusEffects();
new testHandleAttackEffects().testBonusMultiplier();