mirror of
https://gitea.wildfiregames.com/0ad/0ad.git
synced 2026-07-27 02:13:45 +00:00
86289a89fd
Sometimes the same object was serialized.
382 lines
15 KiB
JavaScript
382 lines
15 KiB
JavaScript
/**
|
|
* Provides attack and damage-related helpers.
|
|
*/
|
|
function AttackHelper() {}
|
|
|
|
const DirectEffectsSchema =
|
|
"<element name='Damage'>" +
|
|
"<oneOrMore>" +
|
|
"<element a:help='One or more elements describing damage types'>" +
|
|
"<anyName/>" +
|
|
"<ref name='nonNegativeDecimal' />" +
|
|
"</element>" +
|
|
"</oneOrMore>" +
|
|
"</element>" +
|
|
"<element name='Capture' a:help='Capture points value'>" +
|
|
"<ref name='nonNegativeDecimal'/>" +
|
|
"</element>";
|
|
|
|
const StatusEffectsSchema =
|
|
"<element name='ApplyStatus' a:help='Effects like poisoning or burning a unit.'>" +
|
|
"<oneOrMore>" +
|
|
"<element>" +
|
|
"<anyName a:help='The name must have a matching JSON file in data/status_effects.'/>" +
|
|
"<interleave>" +
|
|
"<optional>" +
|
|
"<element name='Duration' a:help='The duration of the status while the effect occurs.'><ref name='nonNegativeDecimal'/></element>" +
|
|
"</optional>" +
|
|
"<optional>" +
|
|
"<interleave>" +
|
|
"<element name='Interval' a:help='Interval between the occurrences of the effect.'><ref name='nonNegativeDecimal'/></element>" +
|
|
"<oneOrMore>" +
|
|
"<choice>" +
|
|
DirectEffectsSchema +
|
|
"</choice>" +
|
|
"</oneOrMore>" +
|
|
"</interleave>" +
|
|
"</optional>" +
|
|
"<optional>" +
|
|
ModificationsSchema +
|
|
"</optional>" +
|
|
"<element name='Stackability' a:help='Defines how this status effect stacks, i.e. how subsequent status effects of the same kind are handled. Choices are: “Ignore”, which means a new one is ignored, “Extend”, which means the duration of a new one is added to the already active status effect, “Replace”, which means the currently active status effect is removed and the new one is put in place and “Stack”, which means that the status effect can be added multiple times.'>" +
|
|
"<choice>" +
|
|
"<value>Ignore</value>" +
|
|
"<value>Extend</value>" +
|
|
"<value>Replace</value>" +
|
|
"<value>Stack</value>" +
|
|
"</choice>" +
|
|
"</element>" +
|
|
"</interleave>" +
|
|
"</element>" +
|
|
"</oneOrMore>" +
|
|
"</element>";
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
AttackHelper.prototype.BuildAttackEffectsSchema = function()
|
|
{
|
|
return "" +
|
|
"<oneOrMore>" +
|
|
"<choice>" +
|
|
DirectEffectsSchema +
|
|
StatusEffectsSchema +
|
|
"</choice>" +
|
|
"</oneOrMore>" +
|
|
"<optional>" +
|
|
"<element name='Bonuses'>" +
|
|
"<zeroOrMore>" +
|
|
"<element>" +
|
|
"<anyName/>" +
|
|
"<interleave>" +
|
|
"<optional>" +
|
|
"<element name='Civ' a:help='If an entity has this civ then the bonus is applied'><text/></element>" +
|
|
"</optional>" +
|
|
"<element name='Classes' a:help='If an entity has all these classes then the bonus is applied'><text/></element>" +
|
|
"<element name='Multiplier' a:help='The effect strength is multiplied by this'><ref name='nonNegativeDecimal'/></element>" +
|
|
"</interleave>" +
|
|
"</element>" +
|
|
"</zeroOrMore>" +
|
|
"</element>" +
|
|
"</optional>";
|
|
};
|
|
|
|
/**
|
|
* Returns a template-like object of attack effects.
|
|
*/
|
|
AttackHelper.prototype.GetAttackEffectsData = function(valueModifRoot, template, entity)
|
|
{
|
|
const ret = {};
|
|
|
|
if (template.Damage)
|
|
{
|
|
ret.Damage = {};
|
|
const applyMods = damageType =>
|
|
ApplyValueModificationsToEntity(valueModifRoot + "/Damage/" + damageType, +(template.Damage[damageType] || 0), entity);
|
|
for (const 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 = clone(template.Bonuses);
|
|
|
|
return ret;
|
|
};
|
|
|
|
AttackHelper.prototype.GetStatusEffectsData = function(valueModifRoot, template, entity)
|
|
{
|
|
const result = {};
|
|
for (const effect in template)
|
|
{
|
|
const 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;
|
|
};
|
|
|
|
AttackHelper.prototype.GetStatusEffectsModifications = function(valueModifRoot, template, entity, effect)
|
|
{
|
|
const modifiers = {};
|
|
for (const modifier in template)
|
|
{
|
|
const 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.
|
|
*/
|
|
AttackHelper.prototype.GetTotalAttackEffects = function(target, effectData, effectType, bonusMultiplier, cmpResistance)
|
|
{
|
|
let total = 0;
|
|
if (!cmpResistance)
|
|
cmpResistance = Engine.QueryInterface(target, IID_Resistance);
|
|
|
|
const resistanceStrengths = cmpResistance ? cmpResistance.GetEffectiveResistanceAgainst(effectType) : {};
|
|
|
|
if (effectType == "Damage")
|
|
for (const 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.
|
|
const 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];
|
|
|
|
const result = {};
|
|
for (const 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.
|
|
*/
|
|
AttackHelper.prototype.GetPlayersToDamage = function(attackerOwner, friendlyFire)
|
|
{
|
|
if (!friendlyFire)
|
|
return QueryPlayerIDInterface(attackerOwner, IID_Diplomacy).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.
|
|
*/
|
|
AttackHelper.prototype.CauseDamageOverArea = function(data)
|
|
{
|
|
const nearEnts = PositionHelper.EntitiesNearPoint(data.origin, data.radius,
|
|
this.GetPlayersToDamage(data.attackerOwner, data.friendlyFire));
|
|
let damageMultiplier = 1;
|
|
|
|
const 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 (const ent of nearEnts)
|
|
{
|
|
// Correct somewhat for the entity's obstruction radius.
|
|
// TODO: linear falloff should arguably use something cleverer.
|
|
const 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.
|
|
const entityPosition = Engine.QueryInterface(ent, IID_Position).GetPosition2D();
|
|
const relativePos = entityPosition.sub(data.origin).normalize().mult(distance);
|
|
|
|
// Get the position relative to the missile direction.
|
|
const direction = Vector2D.from3D(data.direction);
|
|
const parallelPos = relativePos.dot(direction);
|
|
const perpPos = relativePos.cross(direction);
|
|
|
|
// The width of linear splash is one fifth of the normal splash radius.
|
|
const 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);
|
|
|
|
data.type += ".Splash";
|
|
this.HandleAttackEffects(ent, data, damageMultiplier);
|
|
}
|
|
};
|
|
/**
|
|
* Handle an attack peformed on an entity.
|
|
*
|
|
* @param {number} target - The targetted entityID.
|
|
* @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.
|
|
*/
|
|
AttackHelper.prototype.HandleAttackEffects = function(target, data, bonusMultiplier = 1)
|
|
{
|
|
const cmpResistance = Engine.QueryInterface(target, IID_Resistance);
|
|
if (cmpResistance && cmpResistance.IsInvulnerable())
|
|
return false;
|
|
|
|
bonusMultiplier *= !data.attackData.Bonuses ? 1 : this.GetAttackBonus(data.attacker, target, data.type, data.attackData.Bonuses);
|
|
|
|
const targetState = {};
|
|
for (const receiver of g_AttackEffects.Receivers())
|
|
{
|
|
if (!data.attackData[receiver.type])
|
|
continue;
|
|
|
|
const cmpReceiver = Engine.QueryInterface(target, global[receiver.IID]);
|
|
if (!cmpReceiver)
|
|
continue;
|
|
|
|
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": data.type,
|
|
"target": target,
|
|
"attacker": data.attacker,
|
|
"attackerOwner": data.attackerOwner,
|
|
"damage": -(targetState.healthChange || 0),
|
|
"capture": targetState.captureChange || 0,
|
|
"statusEffects": targetState.inflictedStatuses || [],
|
|
"fromStatusEffect": !!data.attackData.StatusEffect,
|
|
});
|
|
|
|
// We do not want an entity to get XP from active Status Effects.
|
|
if (data.attackData.StatusEffect)
|
|
return true;
|
|
|
|
const 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.
|
|
*/
|
|
AttackHelper.prototype.GetAttackBonus = function(source, target, type, template)
|
|
{
|
|
const cmpIdentity = Engine.QueryInterface(target, IID_Identity);
|
|
if (!cmpIdentity)
|
|
return 1;
|
|
|
|
let attackBonus = 1;
|
|
const targetClasses = cmpIdentity.GetClassesList();
|
|
const targetCiv = cmpIdentity.GetCiv();
|
|
|
|
// Multiply the bonuses for all matching classes.
|
|
for (const key in template)
|
|
{
|
|
const 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;
|
|
};
|
|
|
|
Engine.RegisterGlobal("AttackHelper", new AttackHelper());
|
|
Engine.RegisterGlobal("g_AttackEffects", new AttackEffects());
|