mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-20 15:23:56 -07:00
306 lines
12 KiB
JavaScript
306 lines
12 KiB
JavaScript
function Damage() {}
|
|
|
|
Damage.prototype.Schema =
|
|
"<a:component type='system'/><empty/>";
|
|
|
|
Damage.prototype.Init = function()
|
|
{
|
|
};
|
|
|
|
/**
|
|
* Gives the position of the given entity, taking the lateness into account.
|
|
* @param {number} ent - entity id of the entity we are finding the location for.
|
|
* @param {number} lateness - the time passed since the expected time to fire the function.
|
|
* @return {Vector3D} - the location of the entity.
|
|
*/
|
|
Damage.prototype.InterpolatedLocation = function(ent, lateness)
|
|
{
|
|
let cmpTargetPosition = Engine.QueryInterface(ent, IID_Position);
|
|
if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) // TODO: handle dead target properly
|
|
return undefined;
|
|
let curPos = cmpTargetPosition.GetPosition();
|
|
let prevPos = cmpTargetPosition.GetPreviousPosition();
|
|
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
|
|
let turnLength = cmpTimer.GetLatestTurnLength();
|
|
return new Vector3D(
|
|
(curPos.x * (turnLength - lateness) + prevPos.x * lateness) / turnLength,
|
|
0,
|
|
(curPos.z * (turnLength - lateness) + prevPos.z * lateness) / turnLength);
|
|
};
|
|
|
|
/**
|
|
* Test if a point is inside of an entity's footprint.
|
|
* @param {number} ent - id of the entity we are checking with.
|
|
* @param {Vector3D} point - the point we are checking with.
|
|
* @param {number} lateness - the time passed since the expected time to fire the function.
|
|
* @return {boolean} - true if the point is inside of the entity's footprint.
|
|
*/
|
|
Damage.prototype.TestCollision = function(ent, point, lateness)
|
|
{
|
|
let targetPosition = this.InterpolatedLocation(ent, lateness);
|
|
if (!targetPosition)
|
|
return false;
|
|
|
|
let cmpFootprint = Engine.QueryInterface(ent, IID_Footprint);
|
|
if (!cmpFootprint)
|
|
return false;
|
|
|
|
let targetShape = cmpFootprint.GetShape();
|
|
|
|
if (!targetShape)
|
|
return false;
|
|
|
|
if (targetShape.type == "circle")
|
|
return targetPosition.horizDistanceToSquared(point) < targetShape.radius * targetShape.radius;
|
|
|
|
if (targetShape.type == "square")
|
|
{
|
|
let angle = Engine.QueryInterface(ent, IID_Position).GetRotation().y;
|
|
let distance = Vector2D.from3D(Vector3D.sub(point, targetPosition)).rotate(-angle);
|
|
return Math.abs(distance.x) < targetShape.width / 2 && Math.abs(distance.y) < targetShape.depth / 2;
|
|
}
|
|
|
|
warn("TestCollision called with an invalid footprint shape");
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
Damage.prototype.GetPlayersToDamage = function(attackerOwner, friendlyFire)
|
|
{
|
|
if (!friendlyFire)
|
|
return QueryPlayerIDInterface(attackerOwner).GetEnemies();
|
|
|
|
return Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers();
|
|
};
|
|
|
|
/**
|
|
* Handles hit logic after the projectile travel time has passed.
|
|
* @param {Object} data - the data sent by the caller.
|
|
* @param {number} data.attacker - the entity id of the attacker.
|
|
* @param {number} data.target - the entity id of the target.
|
|
* @param {Vector2D} data.origin - the origin of the projectile hit.
|
|
* @param {Object} data.strengths - data of the form { 'hack': number, 'pierce': number, 'crush': number }.
|
|
* @param {string} data.type - the type of damage.
|
|
* @param {number} data.attackerOwner - the player id of the owner of the attacker.
|
|
* @param {boolean} data.isSplash - a flag indicating if it's splash damage.
|
|
* @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 {Object} data.bonus - the attack bonus template from the attacker.
|
|
* @param {string} data.attackImpactSound - the name of the sound emited on impact.
|
|
* ***When splash damage***
|
|
* @param {boolean} data.friendlyFire - a flag indicating if allied entities are also damaged.
|
|
* @param {number} data.radius - the radius of the splash damage.
|
|
* @param {string} data.shape - the shape of the splash range.
|
|
* @param {Object} data.splashBonus - the attack bonus template from the attacker.
|
|
* @param {Object} data.splashStrengths - data of the form { 'hack': number, 'pierce': number, 'crush': number }.
|
|
*/
|
|
Damage.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.isSplash)
|
|
{
|
|
this.CauseSplashDamage({
|
|
"attacker": data.attacker,
|
|
"origin": Vector2D.from3D(data.position),
|
|
"radius": data.radius,
|
|
"shape": data.shape,
|
|
"strengths": data.splashStrengths,
|
|
"splashBonus": data.splashBonus,
|
|
"direction": data.direction,
|
|
"playersToDamage": this.GetPlayersToDamage(data.attackerOwner, data.friendlyFire),
|
|
"type": data.type,
|
|
"attackerOwner": data.attackerOwner
|
|
});
|
|
}
|
|
|
|
let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager);
|
|
|
|
// Deal direct damage if we hit the main target
|
|
// and if the target has DamageReceiver (not the case for a mirage for example)
|
|
let cmpDamageReceiver = Engine.QueryInterface(data.target, IID_DamageReceiver);
|
|
if (cmpDamageReceiver && this.TestCollision(data.target, data.position, lateness))
|
|
{
|
|
data.multiplier = GetDamageBonus(data.target, data.bonus);
|
|
this.CauseDamage(data);
|
|
cmpProjectileManager.RemoveProjectile(data.projectileId);
|
|
return;
|
|
}
|
|
|
|
let targetPosition = this.InterpolatedLocation(data.target, lateness);
|
|
if (!targetPosition)
|
|
return;
|
|
|
|
// If we didn't hit the main target look for nearby units
|
|
let cmpPlayer = QueryPlayerIDInterface(data.attackerOwner);
|
|
let ents = this.EntitiesNearPoint(Vector2D.from3D(data.position), targetPosition.horizDistanceTo(data.position) * 2, cmpPlayer.GetEnemies());
|
|
for (let ent of ents)
|
|
{
|
|
if (!this.TestCollision(ent, data.position, lateness))
|
|
continue;
|
|
|
|
this.CauseDamage({
|
|
"strengths": data.strengths,
|
|
"target": ent,
|
|
"attacker": data.attacker,
|
|
"multiplier": GetDamageBonus(ent, data.bonus),
|
|
"type": data.type,
|
|
"attackerOwner": data.attackerOwner
|
|
});
|
|
cmpProjectileManager.RemoveProjectile(data.projectileId);
|
|
break;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Damages units around a given origin.
|
|
* @param {Object} data - the data sent by the caller.
|
|
* @param {number} data.attacker - the entity 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 {Object} data.strengths - data of the form { 'hack': number, 'pierce': number, 'crush': number }.
|
|
* @param {string} data.type - the type of damage.
|
|
* @param {number} data.attackerOwner - the player id of the attacker.
|
|
* @param {Vector3D} [data.direction] - the unit vector defining the direction. Needed for linear splash damage.
|
|
* @param {Object} data.splashBonus - the attack bonus template from the attacker.
|
|
* @param {number[]} data.playersToDamage - the array of player id's to damage.
|
|
*/
|
|
Damage.prototype.CauseSplashDamage = function(data)
|
|
{
|
|
// Get nearby entities and define variables
|
|
let nearEnts = this.EntitiesNearPoint(data.origin, data.radius, data.playersToDamage);
|
|
let damageMultiplier = 1;
|
|
|
|
// Cycle through all the nearby entities and damage it appropriately based on its distance from the origin.
|
|
for (let ent of nearEnts)
|
|
{
|
|
let entityPosition = Engine.QueryInterface(ent, IID_Position).GetPosition2D();
|
|
if (data.shape == 'Circular') // circular effect with quadratic falloff in every direction
|
|
damageMultiplier = 1 - data.origin.distanceToSquared(entityPosition) / (data.radius * data.radius);
|
|
else if (data.shape == 'Linear') // linear effect with quadratic falloff in two directions (only used for certain missiles)
|
|
{
|
|
// Get position of entity relative to splash origin.
|
|
let relativePos = entityPosition.sub(data.origin);
|
|
|
|
// 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!");
|
|
}
|
|
|
|
if (data.splashBonus)
|
|
damageMultiplier *= GetDamageBonus(ent, data.splashBonus);
|
|
|
|
// Call CauseDamage which reduces the hitpoints, posts network command, plays sounds....
|
|
this.CauseDamage({
|
|
"strengths": data.strengths,
|
|
"target": ent,
|
|
"attacker": data.attacker,
|
|
"multiplier": damageMultiplier,
|
|
"type": data.type + ".Splash",
|
|
"attackerOwner": data.attackerOwner
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Causes damage on a given unit.
|
|
* @param {Object} data - the data passed by the caller.
|
|
* @param {Object} data.strengths - data in the form of { 'hack': number, 'pierce': number, 'crush': number }.
|
|
* @param {number} data.target - the entity id of the target.
|
|
* @param {number} data.attacker - the entity id og the attacker.
|
|
* @param {number} data.multiplier - the damage multiplier.
|
|
* @param {string} data.type - the type of damage.
|
|
* @param {number} data.attackerOwner - the player id of the attacker.
|
|
*/
|
|
Damage.prototype.CauseDamage = function(data)
|
|
{
|
|
let cmpDamageReceiver = Engine.QueryInterface(data.target, IID_DamageReceiver);
|
|
if (!cmpDamageReceiver)
|
|
return;
|
|
|
|
let targetState = cmpDamageReceiver.TakeDamage(data.strengths, data.multiplier);
|
|
|
|
let cmpPromotion = Engine.QueryInterface(data.attacker, IID_Promotion);
|
|
let cmpLoot = Engine.QueryInterface(data.target, IID_Loot);
|
|
let cmpHealth = Engine.QueryInterface(data.target, IID_Health);
|
|
if (cmpPromotion && cmpLoot && cmpLoot.GetXp() > 0)
|
|
cmpPromotion.IncreaseXp(cmpLoot.GetXp() * -targetState.change / cmpHealth.GetMaxHitpoints());
|
|
|
|
if (targetState.killed)
|
|
this.TargetKilled(data.attacker, data.target, data.attackerOwner);
|
|
|
|
Engine.PostMessage(data.target, MT_Attacked, { "attacker": data.attacker, "target": data.target, "type": data.type, "damage": -targetState.change, "attackerOwner": data.attackerOwner });
|
|
};
|
|
|
|
/**
|
|
* Gets entities near a give point for given players.
|
|
* @param {Vector2D} origin - the point to check around.
|
|
* @param {number} radius - the radius around the point to check.
|
|
* @param {number[]} players - the players of which we need to check entities.
|
|
* @return {number[]} - the id's of the entities in range of the given point.
|
|
*/
|
|
Damage.prototype.EntitiesNearPoint = function(origin, radius, players)
|
|
{
|
|
// If there is insufficient data return an empty array.
|
|
if (!origin || !radius || !players)
|
|
return [];
|
|
|
|
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).ExecuteQueryAroundPos(origin, 0, radius, players, IID_DamageReceiver);
|
|
};
|
|
|
|
/**
|
|
* Called when a unit kills something (another unit, building, animal etc)
|
|
* @param {number} attacker - the entity id of the killer.
|
|
* @param {number} target - the entity id of the target.
|
|
* @param {number} attackerOwner - the player id of the attacker.
|
|
*/
|
|
Damage.prototype.TargetKilled = function(attacker, target, attackerOwner)
|
|
{
|
|
let cmpAttackerOwnership = Engine.QueryInterface(attacker, IID_Ownership);
|
|
let atkOwner = cmpAttackerOwnership && cmpAttackerOwnership.GetOwner() != INVALID_PLAYER ? cmpAttackerOwnership.GetOwner() : attackerOwner;
|
|
|
|
// Add to killer statistics.
|
|
let cmpKillerPlayerStatisticsTracker = QueryPlayerIDInterface(atkOwner, IID_StatisticsTracker);
|
|
if (cmpKillerPlayerStatisticsTracker)
|
|
cmpKillerPlayerStatisticsTracker.KilledEntity(target);
|
|
// Add to loser statistics.
|
|
let cmpTargetPlayerStatisticsTracker = QueryOwnerInterface(target, IID_StatisticsTracker);
|
|
if (cmpTargetPlayerStatisticsTracker)
|
|
cmpTargetPlayerStatisticsTracker.LostEntity(target);
|
|
|
|
// If killer can collect loot, let's try to collect it.
|
|
let cmpLooter = Engine.QueryInterface(attacker, IID_Looter);
|
|
if (cmpLooter)
|
|
cmpLooter.Collect(target);
|
|
};
|
|
|
|
Engine.RegisterSystemComponentType(IID_Damage, "Damage", Damage);
|