Index: ps/trunk/binaries/data/mods/public/simulation/components/Attack.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Attack.js (revision 22768)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Attack.js (revision 22769)
@@ -1,611 +1,615 @@
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." +
"" +
"" +
"" +
"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" +
"1000" +
"" +
"" +
"Cavalry" +
"2" +
"" +
"" +
"" +
"50.0" +
"2.5" +
"props/units/weapons/rock_flaming.xml" +
"props/units/weapons/rock_explosion.xml" +
"0.1" +
"" +
"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 targetClass of targetClasses)
{
let pref = preferredClasses.indexOf(targetClass);
if (pref === 0)
return pref;
if (pref != -1 && (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 tp = this.template[type];
if (splash)
tp = tp.Splash;
return Attacking.GetAttackEffectsData("Attack/" + type + splash ? "/Splash" : "", tp, 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;
- let targetClasses = cmpIdentity.GetClassesList();
- let isTargetClass = className => targetClasses.indexOf(className) != -1;
-
// Always slaughter domestic animals instead of using a normal attack
- if (isTargetClass("Domestic") && this.template.Slaughter)
+ 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 isPreferred = className => this.GetPreferredClasses(className).some(isTargetClass);
+ 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.GetTimers = function(type)
{
let prepare = +(this.template[type].PrepareTime || 0);
prepare = ApplyValueModificationsToEntity("Attack/" + type + "/PrepareTime", prepare, this.entity);
let repeat = +(this.template[type].RepeatTime || 1000);
repeat = ApplyValueModificationsToEntity("Attack/" + type + "/RepeatTime", repeat, this.entity);
return { "prepare": prepare, "repeat": repeat };
};
Attack.prototype.GetSplashDamage = function(type)
{
if (!this.template[type].Splash)
return false;
return {
"attackData": this.GetAttackEffectsData(type, true),
"friendlyFire": this.template[type].Splash.FriendlyFire != "false",
"shape": this.template[type].Splash.Shape,
};
};
Attack.prototype.GetRange = function(type)
{
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();
// 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
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 previousTargetPosition = Engine.QueryInterface(target, IID_Position).GetPreviousPosition();
let targetVelocity = Vector3D.sub(targetPosition, previousTargetPosition).div(turnLength);
let timeToTarget = this.PredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity);
let predictedPosition = (timeToTarget !== false) ? Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition) : targetPosition;
// 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, targetPosition.y, 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.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
};
if (this.template[type].Splash)
data.splash = {
"friendlyFire": this.template[type].Splash.FriendlyFire != "false",
"radius": +this.template[type].Splash.Range,
"shape": this.template[type].Splash.Shape,
"attackData": this.GetAttackEffectsData(type, true),
};
cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_DelayedDamage, "MissileHit", +this.template[type].Delay + timeToTarget * 1000, data);
}
else
Attacking.HandleAttackEffects(type, this.GetAttackEffectsData(type), target, this.entity, attackerOwner);
};
/**
* Get the predicted time of collision between a projectile (or a chaser)
* and its target, assuming they both move in straight line at a constant speed.
* Vertical component of movement is ignored.
* @param {Vector3D} selfPosition - the 3D position of the projectile (or chaser).
* @param {number} horizSpeed - the horizontal speed of the projectile (or chaser).
* @param {Vector3D} targetPosition - the 3D position of the target.
* @param {Vector3D} targetVelocity - the 3D velocity vector of the target.
* @return {Vector3D|boolean} - the 3D predicted position or false if the collision will not happen.
*/
Attack.prototype.PredictTimeToTarget = function(selfPosition, horizSpeed, targetPosition, targetVelocity)
{
let relativePosition = new Vector3D.sub(targetPosition, selfPosition);
let a = targetVelocity.x * targetVelocity.x + targetVelocity.z * targetVelocity.z - horizSpeed * horizSpeed;
let b = relativePosition.x * targetVelocity.x + relativePosition.z * targetVelocity.z;
let c = relativePosition.x * relativePosition.x + relativePosition.z * relativePosition.z;
// The predicted time to reach the target is the smallest non negative solution
// (when it exists) of the equation a t^2 + 2 b t + c = 0.
// Using c>=0, we can straightly compute the right solution.
if (c == 0)
return 0;
let disc = b * b - a * c;
if (a < 0 || b < 0 && disc >= 0)
return c / (Math.sqrt(disc) - b);
return false;
};
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/GarrisonHolder.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js (revision 22768)
+++ ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js (revision 22769)
@@ -1,725 +1,729 @@
function GarrisonHolder() {}
GarrisonHolder.prototype.Schema =
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
/**
* Initialize GarrisonHolder Component
* Garrisoning when loading a map is set in the script of the map, by setting initGarrison
* which should contain the array of garrisoned entities.
*/
GarrisonHolder.prototype.Init = function()
{
// Garrisoned Units
this.entities = [];
this.timer = undefined;
this.allowGarrisoning = new Map();
this.visibleGarrisonPoints = [];
if (this.template.VisibleGarrisonPoints)
{
let points = this.template.VisibleGarrisonPoints;
for (let point in points)
this.visibleGarrisonPoints.push({
"offset": {
"x": +points[point].X,
"y": +points[point].Y,
"z": +points[point].Z
},
"angle": points[point].Angle ? +points[point].Angle * Math.PI / 180 : null,
"entity": null
});
}
};
/**
* @return {Object} max and min range at which entities can garrison the holder.
*/
GarrisonHolder.prototype.GetLoadingRange = function()
{
return { "max": +this.template.LoadingRange, "min": 0 };
};
GarrisonHolder.prototype.CanPickup = function(ent)
{
if (!this.template.Pickup || this.IsFull())
return false;
let cmpOwner = Engine.QueryInterface(this.entity, IID_Ownership);
return !!cmpOwner && IsOwnedByPlayer(cmpOwner.GetOwner(), ent);
};
GarrisonHolder.prototype.GetEntities = function()
{
return this.entities;
};
/**
* @return {Array} unit classes which can be garrisoned inside this
* particular entity. Obtained from the entity's template.
*/
GarrisonHolder.prototype.GetAllowedClasses = function()
{
return this.template.List._string;
};
GarrisonHolder.prototype.GetCapacity = function()
{
return ApplyValueModificationsToEntity("GarrisonHolder/Max", +this.template.Max, this.entity);
};
GarrisonHolder.prototype.IsFull = function()
{
return this.GetGarrisonedEntitiesCount() >= this.GetCapacity();
};
GarrisonHolder.prototype.GetHealRate = function()
{
return ApplyValueModificationsToEntity("GarrisonHolder/BuffHeal", +this.template.BuffHeal, this.entity);
};
/**
* Set this entity to allow or disallow garrisoning in the entity.
* Every component calling this function should do it with its own ID, and as long as one
* component doesn't allow this entity to garrison, it can't be garrisoned
* When this entity already contains garrisoned soldiers,
* these will not be able to ungarrison until the flag is set to true again.
*
* This more useful for modern-day features. For example you can't garrison or ungarrison
* a driving vehicle or plane.
* @param {boolean} allow - Whether the entity should be garrisonable.
*/
GarrisonHolder.prototype.AllowGarrisoning = function(allow, callerID)
{
this.allowGarrisoning.set(callerID, allow);
};
GarrisonHolder.prototype.IsGarrisoningAllowed = function()
{
return Array.from(this.allowGarrisoning.values()).every(allow => allow);
};
GarrisonHolder.prototype.GetGarrisonedEntitiesCount = function()
{
let count = this.entities.length;
for (let ent of this.entities)
{
let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
if (cmpGarrisonHolder)
count += cmpGarrisonHolder.GetGarrisonedEntitiesCount();
}
return count;
};
GarrisonHolder.prototype.IsAllowedToGarrison = function(ent)
{
if (!this.IsGarrisoningAllowed())
return false;
if (!IsOwnedByMutualAllyOfEntity(ent, this.entity))
return false;
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (!cmpIdentity)
return false;
let entityClasses = cmpIdentity.GetClassesList();
return MatchesClassList(entityClasses, this.template.List._string) && !!Engine.QueryInterface(ent, IID_Garrisonable);
};
/**
* Garrison a unit inside. The timer for AutoHeal is started here.
* @param {number} vgpEntity - The visual garrison point that will be used.
* If vgpEntity is given, this visualGarrisonPoint will be used for the entity.
* @return {boolean} Whether the entity was garrisonned.
*/
GarrisonHolder.prototype.Garrison = function(entity, vgpEntity)
{
let cmpPosition = Engine.QueryInterface(entity, IID_Position);
if (!cmpPosition)
return false;
if (!this.PerformGarrison(entity))
return false;
let visibleGarrisonPoint = vgpEntity;
if (!visibleGarrisonPoint)
for (let vgp of this.visibleGarrisonPoints)
{
if (vgp.entity)
continue;
visibleGarrisonPoint = vgp;
break;
}
if (visibleGarrisonPoint)
{
visibleGarrisonPoint.entity = entity;
// Angle of turrets:
// Renamed entities (vgpEntity != undefined) should keep their angle.
// Otherwise if an angle is given in the visibleGarrisonPoint, use it.
// If no such angle given (usually walls for which outside/inside not well defined), we keep
// the current angle as it was used for garrisoning and thus quite often was from inside to
// outside, except when garrisoning from outWorld where we take as default PI.
let cmpTurretPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!vgpEntity && visibleGarrisonPoint.angle != null)
cmpPosition.SetYRotation(cmpTurretPosition.GetRotation().y + visibleGarrisonPoint.angle);
else if (!vgpEntity && !cmpPosition.IsInWorld())
cmpPosition.SetYRotation(cmpTurretPosition.GetRotation().y + Math.PI);
let cmpUnitMotion = Engine.QueryInterface(entity, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetFacePointAfterMove(false);
cmpPosition.SetTurretParent(this.entity, visibleGarrisonPoint.offset);
let cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI);
if (cmpUnitAI)
cmpUnitAI.SetTurretStance();
}
else
cmpPosition.MoveOutOfWorld();
return true;
};
/**
* @return {boolean} Whether the entity was garrisonned.
*/
GarrisonHolder.prototype.PerformGarrison = function(entity)
{
if (!this.HasEnoughHealth())
return false;
// Check if the unit is allowed to be garrisoned inside the building
if (!this.IsAllowedToGarrison(entity))
return false;
// Check the capacity
let extraCount = 0;
let cmpGarrisonHolder = Engine.QueryInterface(entity, IID_GarrisonHolder);
if (cmpGarrisonHolder)
extraCount += cmpGarrisonHolder.GetGarrisonedEntitiesCount();
if (this.GetGarrisonedEntitiesCount() + extraCount >= this.GetCapacity())
return false;
if (!this.timer && this.GetHealRate() > 0)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.timer = cmpTimer.SetTimeout(this.entity, IID_GarrisonHolder, "HealTimeout", 1000, {});
}
// Actual garrisoning happens here
this.entities.push(entity);
this.UpdateGarrisonFlag();
let cmpProductionQueue = Engine.QueryInterface(entity, IID_ProductionQueue);
if (cmpProductionQueue)
cmpProductionQueue.PauseProduction();
let cmpAura = Engine.QueryInterface(entity, IID_Auras);
if (cmpAura && cmpAura.HasGarrisonAura())
cmpAura.ApplyGarrisonAura(this.entity);
Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added": [entity], "removed": [] });
return true;
};
/**
* Simply eject the unit from the garrisoning entity without moving it
* @param {number} entity - Id of the entity to be ejected.
* @param {boolean} forced - Whether eject is forced (i.e. if building is destroyed).
* @return {boolean} Whether the entity was ejected.
*/
GarrisonHolder.prototype.Eject = function(entity, forced)
{
let entityIndex = this.entities.indexOf(entity);
// Error: invalid entity ID, usually it's already been ejected
if (entityIndex == -1)
return false;
// Find spawning location
let cmpFootprint = Engine.QueryInterface(this.entity, IID_Footprint);
let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
// If the garrisonHolder is a sinking ship, restrict the location to the intersection of both passabilities
// TODO: should use passability classes to be more generic
let pos;
if ((!cmpHealth || cmpHealth.GetHitpoints() == 0) && cmpIdentity && cmpIdentity.HasClass("Ship"))
pos = cmpFootprint.PickSpawnPointBothPass(entity);
else
pos = cmpFootprint.PickSpawnPoint(entity);
if (pos.y < 0)
{
// Error: couldn't find space satisfying the unit's passability criteria
if (!forced)
return false;
// If ejection is forced, we need to continue, so use center of the building
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
pos = cmpPosition.GetPosition();
}
this.entities.splice(entityIndex, 1);
let cmpEntPosition = Engine.QueryInterface(entity, IID_Position);
let cmpEntUnitAI = Engine.QueryInterface(entity, IID_UnitAI);
for (let vgp of this.visibleGarrisonPoints)
{
if (vgp.entity != entity)
continue;
cmpEntPosition.SetTurretParent(INVALID_ENTITY, new Vector3D());
let cmpEntUnitMotion = Engine.QueryInterface(entity, IID_UnitMotion);
if (cmpEntUnitMotion)
cmpEntUnitMotion.SetFacePointAfterMove(true);
if (cmpEntUnitAI)
cmpEntUnitAI.ResetTurretStance();
vgp.entity = null;
break;
}
if (cmpEntUnitAI)
cmpEntUnitAI.Ungarrison();
let cmpEntProductionQueue = Engine.QueryInterface(entity, IID_ProductionQueue);
if (cmpEntProductionQueue)
cmpEntProductionQueue.UnpauseProduction();
let cmpEntAura = Engine.QueryInterface(entity, IID_Auras);
if (cmpEntAura && cmpEntAura.HasGarrisonAura())
cmpEntAura.RemoveGarrisonAura(this.entity);
cmpEntPosition.JumpTo(pos.x, pos.z);
cmpEntPosition.SetHeightOffset(0);
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (cmpPosition)
cmpEntPosition.SetYRotation(cmpPosition.GetPosition().horizAngleTo(pos));
Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added": [], "removed": [entity] });
return true;
};
/**
* Ejects units and orders them to move to the rally point. If an ejection
* with a given obstruction radius has failed, we won't try anymore to eject
* entities with a bigger obstruction as that is compelled to also fail.
* @param {Array} entities - An array containing the ids of the entities to eject.
* @param {boolean} forced - Whether eject is forced (ie: if building is destroyed).
* @return {boolean} Whether the entities were ejected.
*/
GarrisonHolder.prototype.PerformEject = function(entities, forced)
{
if (!this.IsGarrisoningAllowed() && !forced)
return false;
let ejectedEntities = [];
let success = true;
let failedRadius;
let radius;
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
for (let entity of entities)
{
if (failedRadius !== undefined)
{
let cmpObstruction = Engine.QueryInterface(entity, IID_Obstruction);
radius = cmpObstruction ? cmpObstruction.GetUnitRadius() : 0;
if (radius >= failedRadius)
continue;
}
if (this.Eject(entity, forced))
{
let cmpEntOwnership = Engine.QueryInterface(entity, IID_Ownership);
if (cmpOwnership && cmpEntOwnership && cmpOwnership.GetOwner() == cmpEntOwnership.GetOwner())
ejectedEntities.push(entity);
}
else
{
success = false;
if (failedRadius !== undefined)
failedRadius = Math.min(failedRadius, radius);
else
{
let cmpObstruction = Engine.QueryInterface(entity, IID_Obstruction);
failedRadius = cmpObstruction ? cmpObstruction.GetUnitRadius() : 0;
}
}
}
this.OrderWalkToRallyPoint(ejectedEntities);
this.UpdateGarrisonFlag();
return success;
};
/**
* Order entities to walk to the rally point.
* @param {Array} entities - An array containing all the ids of the entities.
*/
GarrisonHolder.prototype.OrderWalkToRallyPoint = function(entities)
{
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
let cmpRallyPoint = Engine.QueryInterface(this.entity, IID_RallyPoint);
if (!cmpRallyPoint || !cmpRallyPoint.GetPositions()[0])
return;
let commands = GetRallyPointCommands(cmpRallyPoint, entities);
// Ignore the rally point if it is autogarrison
if (commands[0].type == "garrison" && commands[0].target == this.entity)
return;
for (let command of commands)
ProcessCommand(cmpOwnership.GetOwner(), command);
};
/**
* Unload unit from the garrisoning entity and order them
* to move to the rally point.
* @return {boolean} Whether the command was successful.
*/
GarrisonHolder.prototype.Unload = function(entity, forced)
{
return this.PerformEject([entity], forced);
};
/**
* Unload one or all units that match a template and owner from
* the garrisoning entity and order them to move to the rally point.
* @param {string} template - Type of units that should be ejected.
* @param {number} owner - Id of the player whose units should be ejected.
* @param {boolean} all - Whether all units should be ejected.
* @param {boolean} forced - Whether unload is forced.
* @return {boolean} Whether the unloading was successful.
*/
GarrisonHolder.prototype.UnloadTemplate = function(template, owner, all, forced)
{
let entities = [];
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
for (let entity of this.entities)
{
let cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
// Units with multiple ranks are grouped together.
let name = cmpIdentity.GetSelectionGroupName() || cmpTemplateManager.GetCurrentTemplateName(entity);
if (name != template || owner != Engine.QueryInterface(entity, IID_Ownership).GetOwner())
continue;
entities.push(entity);
// If 'all' is false, only ungarrison the first matched unit.
if (!all)
break;
}
return this.PerformEject(entities, forced);
};
/**
* Unload all units, that belong to certain player
* and order all own units to move to the rally point.
* @param {boolean} forced - Whether unload is forced.
* @param {number} owner - Id of the player whose units should be ejected.
* @return {boolean} Whether the unloading was successful.
*/
GarrisonHolder.prototype.UnloadAllByOwner = function(owner, forced)
{
let entities = this.entities.filter(ent => {
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
return cmpOwnership && cmpOwnership.GetOwner() == owner;
});
return this.PerformEject(entities, forced);
};
/**
* Unload all units from the entity and order them to move to the rally point.
* @param {boolean} forced - Whether unload is forced.
* @return {boolean} Whether the unloading was successful.
*/
GarrisonHolder.prototype.UnloadAll = function(forced)
{
return this.PerformEject(this.entities.slice(), forced);
};
/**
* Used to check if the garrisoning entity's health has fallen below
* a certain limit after which all garrisoned units are unloaded.
*/
GarrisonHolder.prototype.OnHealthChanged = function(msg)
{
if (!this.HasEnoughHealth() && this.entities.length)
this.EjectOrKill(this.entities.slice());
};
GarrisonHolder.prototype.HasEnoughHealth = function()
{
let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
return cmpHealth.GetHitpoints() > Math.floor(+this.template.EjectHealth * cmpHealth.GetMaxHitpoints());
};
/**
* Called every second. Heals garrisoned units.
*/
GarrisonHolder.prototype.HealTimeout = function(data)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
if (!this.entities.length)
{
cmpTimer.CancelTimer(this.timer);
this.timer = undefined;
return;
}
for (let entity of this.entities)
{
let cmpHealth = Engine.QueryInterface(entity, IID_Health);
if (cmpHealth && !cmpHealth.IsUnhealable())
cmpHealth.Increase(this.GetHealRate());
}
this.timer = cmpTimer.SetTimeout(this.entity, IID_GarrisonHolder, "HealTimeout", 1000, {});
};
/**
* Updates the garrison flag depending whether something is garrisoned in the entity.
*/
GarrisonHolder.prototype.UpdateGarrisonFlag = function()
{
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpVisual)
return;
cmpVisual.SetVariant("garrison", this.entities.length ? "garrisoned" : "ungarrisoned");
};
/**
* Cancel timer when destroyed.
*/
GarrisonHolder.prototype.OnDestroy = function()
{
if (this.timer)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
}
};
/**
* If a garrisoned entity is captured, or about to be killed (so its owner changes to '-1'),
* remove it from the building so we only ever contain valid entities.
*/
GarrisonHolder.prototype.OnGlobalOwnershipChanged = function(msg)
{
// The ownership change may be on the garrisonholder
if (this.entity == msg.entity)
{
let entities = this.entities.filter(ent => msg.to == INVALID_PLAYER || !IsOwnedByMutualAllyOfEntity(this.entity, ent));
if (entities.length)
this.EjectOrKill(entities);
return;
}
// or on some of its garrisoned units
let entityIndex = this.entities.indexOf(msg.entity);
if (entityIndex != -1)
{
// If the entity is dead, remove it directly instead of ejecting the corpse
let cmpHealth = Engine.QueryInterface(msg.entity, IID_Health);
if (cmpHealth && cmpHealth.GetHitpoints() == 0)
{
this.entities.splice(entityIndex, 1);
Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added": [], "removed": [msg.entity] });
this.UpdateGarrisonFlag();
for (let point of this.visibleGarrisonPoints)
if (point.entity == msg.entity)
point.entity = null;
}
else if (msg.to == INVALID_PLAYER || !IsOwnedByMutualAllyOfEntity(this.entity, msg.entity))
this.EjectOrKill([msg.entity]);
}
};
/**
* Update list of garrisoned entities if one gets renamed (e.g. by promotion).
*/
GarrisonHolder.prototype.OnGlobalEntityRenamed = function(msg)
{
let entityIndex = this.entities.indexOf(msg.entity);
if (entityIndex != -1)
{
let vgpRenamed;
for (let vgp of this.visibleGarrisonPoints)
{
if (vgp.entity != msg.entity)
continue;
vgpRenamed = vgp;
break;
}
this.Eject(msg.entity, true);
this.Garrison(msg.newentity, vgpRenamed);
}
if (!this.initGarrison)
return;
// Update the pre-game garrison because of SkirmishReplacement
if (msg.entity == this.entity)
{
let cmpGarrisonHolder = Engine.QueryInterface(msg.newentity, IID_GarrisonHolder);
if (cmpGarrisonHolder)
cmpGarrisonHolder.initGarrison = this.initGarrison;
}
else
{
entityIndex = this.initGarrison.indexOf(msg.entity);
if (entityIndex != -1)
this.initGarrison[entityIndex] = msg.newentity;
}
};
/**
* Eject all foreign garrisoned entities which are no more allied.
*/
GarrisonHolder.prototype.OnDiplomacyChanged = function()
{
this.EjectOrKill(this.entities.filter(ent => !IsOwnedByMutualAllyOfEntity(this.entity, ent)));
};
/**
* Eject or kill a garrisoned unit which can no more be garrisoned
* (garrisonholder's health too small or ownership changed).
*/
GarrisonHolder.prototype.EjectOrKill = function(entities)
{
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
// Eject the units which can be ejected (if not in world, it generally means this holder
// is inside a holder which kills its entities, so do not eject)
if (cmpPosition && cmpPosition.IsInWorld())
{
let ejectables = entities.filter(ent => this.IsEjectable(ent));
if (ejectables.length)
this.PerformEject(ejectables, false);
}
// And destroy all remaining entities
let killedEntities = [];
for (let entity of entities)
{
let entityIndex = this.entities.indexOf(entity);
if (entityIndex == -1)
continue;
let cmpHealth = Engine.QueryInterface(entity, IID_Health);
if (cmpHealth)
cmpHealth.Kill();
this.entities.splice(entityIndex, 1);
killedEntities.push(entity);
}
if (killedEntities.length)
Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added": [], "removed": killedEntities });
this.UpdateGarrisonFlag();
};
+/**
+ * Whether an entity is ejectable.
+ * @param {number} entity - The entity-ID to be tested.
+ * @return {boolean} - Whether the entity is ejectable.
+ */
GarrisonHolder.prototype.IsEjectable = function(entity)
{
if (!this.entities.find(ent => ent == entity))
return false;
let ejectableClasses = this.template.EjectClassesOnDestroy._string;
- ejectableClasses = ejectableClasses ? ejectableClasses.split(/\s+/) : [];
let entityClasses = Engine.QueryInterface(entity, IID_Identity).GetClassesList();
- return ejectableClasses.some(ejectableClass => entityClasses.indexOf(ejectableClass) != -1);
+ return MatchesClassList(entityClasses, ejectableClasses);
};
/**
* Initialise the garrisoned units.
*/
GarrisonHolder.prototype.OnGlobalInitGame = function(msg)
{
if (!this.initGarrison)
return;
for (let ent of this.initGarrison)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI && cmpUnitAI.CanGarrison(this.entity) && this.Garrison(ent))
cmpUnitAI.Autogarrison(this.entity);
}
this.initGarrison = undefined;
};
GarrisonHolder.prototype.OnValueModification = function(msg)
{
if (msg.component != "GarrisonHolder" || msg.valueNames.indexOf("GarrisonHolder/BuffHeal") == -1)
return;
if (this.timer && this.GetHealRate() == 0)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
this.timer = undefined;
}
else if (!this.timer && this.GetHealRate() > 0)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.timer = cmpTimer.SetTimeout(this.entity, IID_GarrisonHolder, "HealTimeout", 1000, {});
}
};
Engine.RegisterComponentType(IID_GarrisonHolder, "GarrisonHolder", GarrisonHolder);
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 22768)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Damage.js (revision 22769)
@@ -1,547 +1,547 @@
Engine.LoadHelperScript("DamageBonus.js");
Engine.LoadHelperScript("Attacking.js");
Engine.LoadHelperScript("Player.js");
Engine.LoadHelperScript("Sound.js");
Engine.LoadHelperScript("ValueModification.js");
Engine.LoadComponentScript("interfaces/Attack.js");
Engine.LoadComponentScript("interfaces/AttackDetection.js");
Engine.LoadComponentScript("interfaces/DelayedDamage.js");
Engine.LoadComponentScript("interfaces/Resistance.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/Loot.js");
Engine.LoadComponentScript("interfaces/Player.js");
Engine.LoadComponentScript("interfaces/Promotion.js");
Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js");
Engine.LoadComponentScript("interfaces/ModifiersManager.js");
Engine.LoadComponentScript("interfaces/Timer.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,
"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),
"IsInWorld": () => true,
});
AddMock(target, IID_Health, {
"TakeDamage": (effectData, __, ___, bonusMultiplier) => {
damageTaken = true;
return { "killed": false, "HPchange": -bonusMultiplier * effectData.Crush };
},
});
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": [] }, 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(data.type, data.attackData, data.target, data.attacker, data.attackerOwner);
TestDamage();
data.type = "Ranged";
type = data.type;
Attacking.HandleAttackEffects(data.type, data.attackData, data.target, data.attacker, data.attackerOwner);
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),
"playersToDamage": [2],
};
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(SYSTEM_ENTITY, IID_RangeManager, {
"ExecuteQueryAroundPos": () => [60, 61, 62],
});
AddMock(60, IID_Position, {
"GetPosition2D": () => new Vector2D(2.2, -0.4),
});
AddMock(61, IID_Position, {
"GetPosition2D": () => new Vector2D(0, 0),
});
AddMock(62, IID_Position, {
"GetPosition2D": () => new Vector2D(5, 2),
});
AddMock(60, IID_Health, {
"TakeDamage": (effectData, __, ___, mult) => {
hitEnts.add(60);
TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(2.2, -0.4));
return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
AddMock(61, IID_Health, {
"TakeDamage": (effectData, __, ___, mult) => {
hitEnts.add(61);
TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(0, 0));
return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
AddMock(62, IID_Health, {
"TakeDamage": (effectData, __, ___, mult) => {
hitEnts.add(62);
TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 0);
return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
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": (effectData, __, ___, mult) => {
hitEnts.add(60);
TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(1, 2));
return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
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 fallOff = function(r)
{
return 1 - r * r / (radius * radius);
};
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"ExecuteQueryAroundPos": () => [60, 61, 62, 64],
});
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
AddMock(64, IID_Position, {
"GetPosition2D": () => new Vector2D(9, -4),
});
AddMock(60, IID_Resistance, {
"TakeDamage": (effectData, __, ___, mult) => {
TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(0));
return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
AddMock(61, IID_Resistance, {
"TakeDamage": (effectData, __, ___, mult) => {
TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(5));
return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
AddMock(62, IID_Resistance, {
"TakeDamage": (effectData, __, ___, mult) => {
TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(1));
return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
AddMock(63, IID_Resistance, {
"TakeDamage": (effectData, __, ___, mult) => {
TS_ASSERT(false);
}
});
AddMock(64, IID_Resistance, {
"TakeDamage": (effectData, __, ___, mult) => {
TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 0);
return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
Attacking.CauseDamageOverArea({
"type": "Ranged",
"attackData": { "Damage": { "Hack": 100, "Pierce": 0, "Crush": 0 } },
"attacker": 50,
"attackerOwner": 1,
"origin": new Vector2D(3, 4),
"radius": radius,
"shape": "Circular",
"playersToDamage": [2],
});
}
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,
};
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": (effectData, __, ___, mult) => {
hitEnts.add(60);
TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100);
return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
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();
// 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": (effectData, __, ___, mult) => {
TS_ASSERT_EQUALS(false);
return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"ExecuteQueryAroundPos": () => [61]
});
AddMock(61, IID_Position, {
"GetPosition": () => targetPos,
"GetPreviousPosition": () => targetPos,
"GetPosition2D": () => Vector2D.from3D(targetPos),
"IsInWorld": () => true,
});
AddMock(61, IID_Health, {
"TakeDamage": (effectData, __, ___, mult) => {
hitEnts.add(61);
TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100);
return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
AddMock(61, IID_Footprint, {
"GetShape": () => ({ "type": "circle", "radius": 20 }),
});
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]
});
let dealtDamage = 0;
AddMock(61, IID_Health, {
"TakeDamage": (effectData, __, ___, mult) => {
hitEnts.add(61);
dealtDamage += mult * (effectData.Hack + effectData.Pierce + effectData.Crush);
return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
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": (effectData, __, ___, mult) => {
hitEnts.add(62);
TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 200 * 0.75);
return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
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]
});
let bonus= { "BonusCav": { "Classes": "Cavalry", "Multiplier": 400 } };
let splashBonus = { "BonusCav": { "Classes": "Cavalry", "Multiplier": 10000 } };
AddMock(61, IID_Identity, {
- "HasClass": cl => cl == "Cavalry"
+ "GetClassesList": () => ["Cavalry"]
});
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_MissileHit();
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/DamageBonus.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/DamageBonus.js (revision 22768)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/DamageBonus.js (revision 22769)
@@ -1,31 +1,30 @@
/**
* Calculates the attack damage multiplier against a target.
* @param {entity_id_t} source - The source entity's id.
* @param {entity_id_t} 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.
*/
function GetAttackBonus(source, target, type, template)
{
let attackBonus = 1;
let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
if (!cmpIdentity)
return 1;
// Multiply the bonuses for all matching classes
for (let key in template)
{
let bonus = template[key];
if (bonus.Civ && bonus.Civ !== cmpIdentity.GetCiv())
continue;
- if (bonus.Classes && bonus.Classes.split(/\s+/).some(cls => !cmpIdentity.HasClass(cls)))
- continue;
- attackBonus *= ApplyValueModificationsToEntity("Attack/" + type + "/Bonuses/" + key + "/Multiplier", +bonus.Multiplier, source);
+ if (!bonus.Classes || MatchesClassList(cmpIdentity.GetClassesList(), bonus.Classes))
+ attackBonus *= ApplyValueModificationsToEntity("Attack/" + type + "/Bonuses/" + key + "/Multiplier", +bonus.Multiplier, source);
}
return attackBonus;
}
Engine.RegisterGlobal("GetAttackBonus", GetAttackBonus);