Index: ps/trunk/binaries/data/mods/public/globalscripts/AttackEffects.js
===================================================================
--- ps/trunk/binaries/data/mods/public/globalscripts/AttackEffects.js (revision 24499)
+++ ps/trunk/binaries/data/mods/public/globalscripts/AttackEffects.js (revision 24500)
@@ -1,17 +1,61 @@
-// TODO: could be worth putting this in json files someday
-const g_EffectTypes = ["Damage", "Capture", "ApplyStatus"];
-const g_EffectReceiver = {
- "Damage": {
- "IID": "IID_Health",
- "method": "TakeDamage"
- },
- "Capture": {
- "IID": "IID_Capturable",
- "method": "Capture",
- "sound": "capture"
- },
- "ApplyStatus": {
- "IID": "IID_StatusEffectsReceiver",
- "method": "ApplyStatus"
+/**
+ * This class provides a cache for accessing attack effects stored in JSON files.
+ */
+class AttackEffects
+{
+ constructor()
+ {
+ let effectsDataObj = {};
+ this.effectReceivers = [];
+ this.effectSounds = {};
+
+ for (let filename of Engine.ListDirectoryFiles("simulation/data/attack_effects", "*.json", false))
+ {
+ let data = Engine.ReadJSONFile(filename);
+ if (!data)
+ continue;
+
+ if (effectsDataObj[data.code])
+ {
+ error("Encountered two effect types with the code " + data.name + ".");
+ continue;
+ }
+
+ effectsDataObj[data.code] = data;
+
+ this.effectReceivers.push({
+ "type": data.code,
+ "IID": data.IID,
+ "method": data.method
+ });
+ this.effectSounds[data.code] = data.sound || "";
+ }
+
+ let effDataSort = (a, b) => a.order < b.order ? -1 : a.order > b.order ? 1 : 0;
+ let effSort = (a, b) => effDataSort(
+ effectsDataObj[a.type],
+ effectsDataObj[b.type]
+ );
+ this.effectReceivers.sort(effSort);
+
+ deepfreeze(this.effectReceivers);
+ deepfreeze(this.effectSounds);
}
-};
+
+ /**
+ * @return {Object[]} - The effects possible with their data.
+ */
+ Receivers()
+ {
+ return this.effectReceivers;
+ }
+
+ /**
+ * @param {string} type - The type of effect to get the receiving sound for.
+ * @return {string} - The name of the soundgroup to play.
+ */
+ GetSound(type)
+ {
+ return this.effectSounds[type] || "";
+ }
+}
Property changes on: ps/trunk/binaries/data/mods/public/globalscripts/AttackEffects.js
___________________________________________________________________
Deleted: svn:executable
## -1 +0,0 ##
-*
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/globalscripts/tests/test_AttackEffects.js
===================================================================
--- ps/trunk/binaries/data/mods/public/globalscripts/tests/test_AttackEffects.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/globalscripts/tests/test_AttackEffects.js (revision 24500)
@@ -0,0 +1,31 @@
+let effects = {
+ "eff_A": {
+ "code": "a",
+ "name": "A",
+ "order": "2",
+ "IID": "IID_A",
+ "method": "doA"
+ },
+ "eff_B": {
+ "code": "b",
+ "name": "B",
+ "order": "1",
+ "IID": "IID_B",
+ "method": "doB"
+ }
+};
+
+Engine.ListDirectoryFiles = () => Object.keys(effects);
+Engine.ReadJSONFile = (file) => effects[file];
+
+let attackEffects = new AttackEffects();
+
+TS_ASSERT_UNEVAL_EQUALS(attackEffects.Receivers(), [{
+ "type": "b",
+ "IID": "IID_B",
+ "method": "doB"
+}, {
+ "type": "a",
+ "IID": "IID_A",
+ "method": "doA"
+}]);
Property changes on: ps/trunk/binaries/data/mods/public/globalscripts/tests/test_AttackEffects.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:mime-type
## -0,0 +1 ##
+text/plain
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/l10n/messages.json
===================================================================
--- ps/trunk/binaries/data/mods/public/l10n/messages.json (revision 24499)
+++ ps/trunk/binaries/data/mods/public/l10n/messages.json (revision 24500)
@@ -1,808 +1,821 @@
[
{
"output": "public-civilizations.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "json",
"filemasks": [
"simulation/data/civs/**.json"
],
"options": {
"keywords": [
"Name",
"Description",
"History",
"Special",
"AINames"
]
}
}
]
},
{
"output": "public-gui-ingame.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "javascript",
"filemasks": [
"gui/session/**.js"
],
"options": {
"format": "javascript-format",
"keywords": {
"translate": [1],
"translatePlural": [1, 2],
"translateWithContext": [[1], 2],
"translatePluralWithContext": [[1], 2, 3],
"markForTranslation": [1],
"markForTranslationWithContext": [[1], 2],
"markForPluralTranslation": [1, 2]
},
"commentTags": [
"Translation:"
]
}
},
{
"extractor": "xml",
"filemasks": [
"gui/session/**.xml"
],
"options": {
"keywords": {
"translatableAttribute": {
"locationAttributes": ["id"]
},
"translate": {}
}
}
}
]
},
{
"output": "public-gui-gamesetup.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "javascript",
"filemasks": [
"gui/gamesetup/**.js",
"gui/gamesetup_mp/**.js",
"gui/loading/**.js"
],
"options": {
"format": "javascript-format",
"keywords": {
"translate": [1],
"translatePlural": [1, 2],
"translateWithContext": [[1], 2],
"translatePluralWithContext": [[1], 2, 3],
"markForTranslation": [1],
"markForTranslationWithContext": [[1], 2],
"markForPluralTranslation": [1, 2]
},
"commentTags": [
"Translation:"
]
}
},
{
"extractor": "xml",
"filemasks": [
"gui/gamesetup/**.xml",
"gui/gamesetup_mp/**.xml",
"gui/loading/**.xml"
],
"options": {
"keywords": {
"translatableAttribute": {
"locationAttributes": ["id"]
},
"translate": {}
}
}
},
{
"extractor": "txt",
"filemasks": [
"gui/text/quotes.txt"
],
"options": {
}
}
]
},
{
"output": "public-gui-lobby.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "javascript",
"filemasks": [
"gui/lobby/**.js",
"gui/prelobby/**.js"
],
"options": {
"format": "javascript-format",
"keywords": {
"translate": [1],
"translatePlural": [1, 2],
"translateWithContext": [[1], 2],
"translatePluralWithContext": [[1], 2, 3],
"markForTranslation": [1],
"markForTranslationWithContext": [[1], 2],
"markForPluralTranslation": [1, 2]
},
"commentTags": [
"Translation:"
]
}
},
{
"extractor": "xml",
"filemasks": [
"gui/lobby/**.xml",
"gui/prelobby/**.xml"
],
"options": {
"keywords": {
"translatableAttribute": {
"locationAttributes": ["id"]
},
"translate": {}
}
}
},
{
"extractor": "txt",
"filemasks": [
"gui/prelobby/common/terms/*.txt"
],
"options": {
}
}
]
},
{
"output": "public-gui-manual.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "javascript",
"filemasks": [
"gui/manual/**.js"
],
"options": {
"format": "javascript-format",
"keywords": {
"translate": [1],
"translatePlural": [1, 2],
"translateWithContext": [[1], 2],
"translatePluralWithContext": [[1], 2, 3],
"markForTranslation": [1],
"markForTranslationWithContext": [[1], 2],
"markForPluralTranslation": [1, 2]
},
"commentTags": [
"Translation:"
]
}
},
{
"extractor": "xml",
"filemasks": [
"gui/manual/**.xml"
],
"options": {
"keywords": {
"translatableAttribute": {
"locationAttributes": ["id"]
},
"translate": {}
}
}
},
{
"extractor": "txt",
"filemasks": [
"gui/manual/intro.txt"
],
"options": {
}
}
]
},
{
"output": "public-gui-userreport.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "txt",
"filemasks": [
"gui/userreport/**.txt"
],
"options": {
}
}
]
},
{
"output": "public-gui-other.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "javascript",
"filemasks": [
"globalscripts/**.js",
"gui/common/**.js",
"gui/credits/**.js",
"gui/loadgame/**.js",
"gui/locale/**.js",
"gui/options/**.js",
"gui/pregame/**.js",
"gui/reference/civinfo/**.js",
"gui/reference/common/**.js",
"gui/reference/structree/**.js",
"gui/reference/viewer/**.js",
"gui/replaymenu/**.js",
"gui/splashscreen/**.js",
"gui/summary/**.js"
],
"options": {
"format": "javascript-format",
"keywords": {
"translate": [1],
"translatePlural": [1, 2],
"translateWithContext": [[1], 2],
"translatePluralWithContext": [[1], 2, 3],
"markForTranslation": [1],
"markForTranslationWithContext": [[1], 2],
"markForPluralTranslation": [1, 2]
},
"commentTags": [
"dennis-ignore:",
"Translation:"
]
}
},
{
"extractor": "xml",
"filemasks": [
"globalscripts/**.xml",
"gui/common/**.xml",
"gui/credits/**.xml",
"gui/loadgame/**.xml",
"gui/locale/**.xml",
"gui/options/**.xml",
"gui/pregame/**.xml",
"gui/reference/civinfo/**.xml",
"gui/reference/structree/**.xml",
"gui/reference/viewer/**.xml",
"gui/replaymenu/**.xml",
"gui/splashscreen/**.xml",
"gui/summary/**.xml"
],
"options": {
"keywords": {
"translatableAttribute": {
"locationAttributes": ["id"]
},
"translate": {}
}
}
},
{
"extractor": "json",
"filemasks": [
"gui/credits/texts/**.json"
],
"options": {
"keywords": [
"Title",
"Subtitle"
]
}
},
{
"extractor": "json",
"filemasks": [
"gui/options/**.json"
],
"options": {
"keywords": [
"label",
"tooltip"
]
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/resources/**.json"
],
"options": {
"keywords": [
"description"
]
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/resources/**.json"
],
"options": {
"keywords": [
"name",
"subtypes"
],
"comments": [
"Translation: Word as used at the beginning of a sentence or as a single-word sentence."
],
"context": "firstWord"
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/resources/**.json"
],
"options": {
"keywords": [
"name",
"subtypes"
],
"comments": [
"Translation: Word as used in the middle of a sentence (which may require using lowercase for your language)."
],
"context": "withinSentence"
}
},
{
"extractor": "txt",
"filemasks": [
"gui/gamesetup/**.txt",
"gui/splashscreen/splashscreen.txt",
"gui/text/tips/**.txt"
],
"options": {
}
}
]
},
{
"output": "public-templates-units.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "xml",
"filemasks": [
"simulation/templates/template_unit_*.xml",
"simulation/templates/units/**.xml"
],
"options": {
"keywords": {
"AttackName": {
"customContext": "Name of an attack, usually the weapon."
},
"StatusName": {
"customContext": "status effect"
},
"ApplierTooltip": {
"customContext": "status effect"
},
"ReceiverTooltip": {
"customContext": "status effect"
},
"GenericName": {},
"SpecificName": {},
"History": {},
"VisibleClasses": {
"splitOnWhitespace": true
},
"Tooltip": {},
"DisabledTooltip": {},
"FormationName": {},
"FromClass": {},
"Rank": {
"tagAsContext": true
}
}
}
}
]
},
{
"output": "public-templates-buildings.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "xml",
"filemasks": [
"simulation/templates/template_structure_*.xml",
"simulation/templates/structures/**.xml"
],
"options": {
"keywords": {
"AttackName": {
"customContext": "Name of an attack, usually the weapon."
},
"StatusName": {
"customContext": "status effect"
},
"ApplierTooltip": {
"customContext": "status effect"
},
"ReceiverTooltip": {
"customContext": "status effect"
},
"GenericName": {},
"SpecificName": {},
"History": {},
"VisibleClasses": {
"splitOnWhitespace": true
},
"Tooltip": {},
"DisabledTooltip": {},
"FormationName": {},
"FromClass": {},
"Rank": {
"tagAsContext": true
}
}
}
}
]
},
{
"output": "public-templates-other.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "xml",
"filemasks": {
"includeMasks": [
"simulation/templates/**.xml"
],
"excludeMasks": [
"simulation/templates/structures/**.xml",
"simulation/templates/template_structure_*.xml",
"simulation/templates/template_unit_*.xml",
"simulation/templates/units/**.xml"
]
},
"options": {
"keywords": {
"AttackName": {
"customContext": "Name of an attack, usually the weapon."
},
"GenericName": {},
"SpecificName": {},
"History": {},
"VisibleClasses": {
"splitOnWhitespace": true
},
"Tooltip": {},
"DisabledTooltip": {},
"FormationName": {},
"FromClass": {},
"Rank": {
"tagAsContext": true
}
}
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/template_helpers/damage_types/*.json"
],
"options": {
"keywords": [
"name",
"description"
],
"context": "damage type"
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/status_effects/*.json"
],
"options": {
"keywords": [
"statusName",
"applierTooltip",
"receiverTooltip"
],
"context": "status effect"
}
+ },
+ {
+ "extractor": "json",
+ "filemasks": [
+ "simulation/data/attack_effects/*.json"
+ ],
+ "options": {
+ "keywords": [
+ "name",
+ "description"
+ ],
+ "context": "effect caused by an attack"
+ }
}
]
},
{
"output": "public-simulation-auras.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "json",
"filemasks": [
"simulation/data/auras/**.json"
],
"options": {
"keywords": [
"auraName",
"auraDescription"
]
}
}
]
},
{
"output": "public-simulation-technologies.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "json",
"filemasks": [
"simulation/data/technologies/**.json"
],
"options": {
"keywords": [
"specificName",
"genericName",
"description",
"tooltip",
"requirementsTooltip"
]
}
}
]
},
{
"output": "public-simulation-other.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "javascript",
"filemasks": [
"simulation/ai/**.js",
"simulation/components/**.js",
"simulation/helpers/**.js"
],
"options": {
"format": "javascript-format",
"keywords": {
"translate": [1],
"translatePlural": [1, 2],
"translateWithContext": [[1], 2],
"translatePluralWithContext": [[1], 2, 3],
"markForTranslation": [1],
"markForTranslationWithContext": [[1], 2],
"markForPluralTranslation": [1, 2]
},
"commentTags": [
"Translation:"
]
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/settings/player_defaults.json"
],
"options": {
"keywords": [
"Name"
]
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/settings/game_speeds.json"
],
"options": {
"keywords": ["Title"]
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/settings/victory_conditions/*.json"
],
"options": {
"keywords": ["Title", "Description"]
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/settings/starting_resources.json"
],
"options": {
"keywords": ["Title"],
"context": "startingResources"
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/settings/trigger_difficulties.json"
],
"options": {
"keywords": ["Title", "Tooltip"]
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/settings/map_sizes.json"
],
"options": {
"keywords": [
"Name",
"Tooltip"
]
}
},
{
"extractor": "json",
"filemasks": [
"simulation/ai/**.json"
],
"options": {
"keywords": [
"name",
"description"
]
}
}
]
},
{
"output": "public-maps.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "json",
"filemasks": {
"includeMasks": [
"maps/random/**.json"
],
"excludeMasks": [
"maps/random/rmbiome/**.json"
]
},
"options": {
"keywords": [
"Name",
"Description"
]
}
},
{
"extractor": "javascript",
"filemasks": [
"maps/scenarios/**.js",
"maps/skirmishes/**.js",
"maps/random/**.js"
],
"options": {
"format": "javascript-format",
"keywords": {
"markForTranslation": [1],
"markForTranslationWithContext": [[1], 2],
"markForPluralTranslation": [1, 2]
},
"commentTags": [
"Translation:"
]
}
},
{
"extractor": "xml",
"filemasks": [
"maps/scenarios/**.xml",
"maps/skirmishes/**.xml"
],
"options": {
"keywords": {
"ScriptSettings": {
"extractJson": {
"keywords": [
"Name",
"Description"
]
}
}
}
}
},
{
"extractor": "json",
"filemasks": [
"maps/random/rmbiome/**.json"
],
"options": {
"keywords": ["Description"],
"context": "biome definition"
}
}
]
},
{
"output": "public-tutorials.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "javascript",
"filemasks": [
"maps/tutorials/**.js"
],
"options": {
"format": "javascript-format",
"keywords": {
"markForTranslation": [1],
"markForTranslationWithContext": [[1], 2],
"markForPluralTranslation": [1, 2]
},
"commentTags": [
"Translation:"
]
}
},
{
"extractor": "xml",
"filemasks": [
"maps/tutorials/**.xml"
],
"options": {
"keywords": {
"ScriptSettings": {
"extractJson": {
"keywords": [
"Name",
"Description"
]
}
}
}
}
}
]
}
]
Index: ps/trunk/binaries/data/mods/public/simulation/components/AttackDetection.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/AttackDetection.js (revision 24499)
+++ ps/trunk/binaries/data/mods/public/simulation/components/AttackDetection.js (revision 24500)
@@ -1,168 +1,165 @@
function AttackDetection() {}
AttackDetection.prototype.Schema =
"Detects incoming attacks." +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
AttackDetection.prototype.Init = function()
{
this.suppressionTime = +this.template.SuppressionTime;
// Use squared distance to avoid sqrts
this.suppressionTransferRangeSquared = +this.template.SuppressionTransferRange * +this.template.SuppressionTransferRange;
this.suppressionRangeSquared = +this.template.SuppressionRange * +this.template.SuppressionRange;
this.suppressedList = [];
};
AttackDetection.prototype.ActivateTimer = function()
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).SetTimeout(this.entity, IID_AttackDetection, "HandleTimeout", this.suppressionTime);
};
AttackDetection.prototype.AddSuppression = function(event)
{
this.suppressedList.push(event);
this.ActivateTimer();
};
AttackDetection.prototype.UpdateSuppressionEvent = function(index, event)
{
this.suppressedList[index] = event;
this.ActivateTimer();
};
//// Message handlers ////
AttackDetection.prototype.OnGlobalAttacked = function(msg)
{
var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
var cmpOwnership = Engine.QueryInterface(msg.target, IID_Ownership);
if (cmpOwnership.GetOwner() != cmpPlayer.GetPlayerID())
return;
Engine.PostMessage(msg.target, MT_MinimapPing);
this.AttackAlert(msg.target, msg.attacker, msg.type, msg.attackerOwner);
};
//// External interface ////
AttackDetection.prototype.AttackAlert = function(target, attacker, type, attackerOwner)
{
let playerID = Engine.QueryInterface(this.entity, IID_Player).GetPlayerID();
// Don't register attacks dealt against other players
if (Engine.QueryInterface(target, IID_Ownership).GetOwner() != playerID)
return;
let cmpAttackerOwnership = Engine.QueryInterface(attacker, IID_Ownership);
let atkOwner = cmpAttackerOwnership && cmpAttackerOwnership.GetOwner() != INVALID_PLAYER ? cmpAttackerOwnership.GetOwner() : attackerOwner;
// Don't register attacks dealt by myself
if (atkOwner == playerID)
return;
// Since livestock can be attacked/gathered by other players
// and generally are not so valuable as other units/buildings,
// we have a lower priority notification for it, which can be
// overriden by a regular one.
var cmpTargetIdentity = Engine.QueryInterface(target, IID_Identity);
var targetIsDomesticAnimal = cmpTargetIdentity && cmpTargetIdentity.HasClass("Animal") && cmpTargetIdentity.HasClass("Domestic");
var cmpPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
var event = {
"target": target,
"position": cmpPosition.GetPosition(),
"time": Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime(),
"targetIsDomesticAnimal": targetIsDomesticAnimal
};
// If we already have a low priority livestock event in suppressed list,
// and now a more important target is attacked, we want to upgrade the
// suppressed event and send the new notification
var isPriorityIncreased = false;
for (var i = 0; i < this.suppressedList.length; ++i)
{
var element = this.suppressedList[i];
// If the new attack is within suppression distance of this element,
// then check if the element should be updated and return
var dist = event.position.horizDistanceToSquared(element.position);
if (dist >= this.suppressionRangeSquared)
continue;
isPriorityIncreased = element.targetIsDomesticAnimal && !targetIsDomesticAnimal;
var isPriorityDescreased = !element.targetIsDomesticAnimal && targetIsDomesticAnimal;
if (isPriorityIncreased
|| (!isPriorityDescreased && dist < this.suppressionTransferRangeSquared))
this.UpdateSuppressionEvent(i, event);
// If priority has increased, exit the loop to send the upgraded notification below
if (isPriorityIncreased)
break;
return;
}
// If priority has increased for an existing event, then we already have it
// in the suppression list
if (!isPriorityIncreased)
this.AddSuppression(event);
Engine.PostMessage(this.entity, MT_AttackDetected, { "player": playerID, "event": event });
Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({
"type": "attack",
"target": target,
"players": [playerID],
"attacker": atkOwner,
"targetIsDomesticAnimal": targetIsDomesticAnimal
});
- let soundGroup = "attacked";
- if (g_EffectReceiver[type] && g_EffectReceiver[type].sound)
- soundGroup += '_' + g_EffectReceiver[type].sound;
-
+ let soundGroup = g_AttackEffects.GetSound(type);
if (attackerOwner === 0)
soundGroup += "_gaia";
PlaySound(soundGroup, target);
};
AttackDetection.prototype.GetSuppressionTime = function()
{
return this.suppressionTime;
};
AttackDetection.prototype.HandleTimeout = function()
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
var now = cmpTimer.GetTime();
for (var i = 0; i < this.suppressedList.length; ++i)
{
var event = this.suppressedList[i];
// Check if this event has timed out
if (now - event.time >= this.suppressionTime)
{
this.suppressedList.splice(i, 1);
return;
}
}
};
AttackDetection.prototype.GetIncomingAttacks = function()
{
return this.suppressedList;
};
Engine.RegisterComponentType(IID_AttackDetection, "AttackDetection", AttackDetection);
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Attack.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Attack.js (revision 24499)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Attack.js (revision 24500)
@@ -1,336 +1,359 @@
+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/Auras.js");
Engine.LoadComponentScript("interfaces/Capturable.js");
Engine.LoadComponentScript("interfaces/ModifiersManager.js");
Engine.LoadComponentScript("interfaces/Formation.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
Engine.LoadComponentScript("Attack.js");
let entityID = 903;
function attackComponentTest(defenderClass, isEnemy, test_function)
{
let playerEnt1 = 5;
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": () => playerEnt1
});
AddMock(playerEnt1, IID_Player, {
"GetPlayerID": () => 1,
"IsEnemy": () => isEnemy
});
let attacker = entityID;
AddMock(attacker, IID_Position, {
"IsInWorld": () => true,
"GetHeightOffset": () => 5,
"GetPosition2D": () => new Vector2D(1, 2)
});
AddMock(attacker, IID_Ownership, {
"GetOwner": () => 1
});
let cmpAttack = ConstructComponent(attacker, "Attack", {
"Melee": {
"Damage": {
"Hack": 11,
"Pierce": 5,
"Crush": 0
},
"MinRange": 3,
"MaxRange": 5,
"PreferredClasses": {
"_string": "FemaleCitizen"
},
"RestrictedClasses": {
"_string": "Elephant Archer"
},
"Bonuses":
{
"BonusCav": {
"Classes": "Cavalry",
"Multiplier": 2
}
}
},
"Ranged": {
"Damage": {
"Hack": 0,
"Pierce": 10,
"Crush": 0
},
"MinRange": 10,
"MaxRange": 80,
"PrepareTime": 300,
"RepeatTime": 500,
"Projectile": {
"Speed": 10,
"Spread": 2,
"Gravity": 1,
"FriendlyFire": "false"
},
"PreferredClasses": {
"_string": "Archer"
},
"RestrictedClasses": {
"_string": "Elephant"
},
"Splash": {
"Shape": "Circular",
"Range": 10,
"FriendlyFire": "false",
"Damage": {
"Hack": 0.0,
"Pierce": 15.0,
"Crush": 35.0
},
"Bonuses": {
"BonusCav": {
"Classes": "Cavalry",
"Multiplier": 3
}
}
}
},
"Capture": {
"Capture": 8,
"MaxRange": 10,
},
"Slaughter": {},
"StatusEffect": {
"ApplyStatus": {
"StatusInternalName": {
"StatusName": "StatusShownName",
"ApplierTooltip": "ApplierTooltip",
"ReceiverTooltip": "ReceiverTooltip",
"Duration": 5000,
"Stackability": "Stacks",
"Modifiers": {
"SE": {
"Paths": {
"_string": "Health/Max"
},
"Affects": {
"_string": "Unit"
},
"Add": 10
}
}
}
},
"MinRange": "10",
"MaxRange": "80"
}
});
let defender = ++entityID;
AddMock(defender, IID_Identity, {
"GetClassesList": () => [defenderClass],
"HasClass": className => className == defenderClass,
"GetCiv": () => "civ"
});
AddMock(defender, IID_Ownership, {
"GetOwner": () => 1
});
AddMock(defender, IID_Position, {
"IsInWorld": () => true,
"GetHeightOffset": () => 0
});
AddMock(defender, IID_Health, {
"GetHitpoints": () => 100
});
test_function(attacker, cmpAttack, defender);
}
// Validate template getter functions
attackComponentTest(undefined, true, (attacker, cmpAttack, defender) => {
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(), ["Melee", "Ranged", "Capture"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes([]), ["Melee", "Ranged", "Capture"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Melee", "Ranged", "Capture"]), ["Melee", "Ranged", "Capture"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Melee", "Ranged"]), ["Melee", "Ranged"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Capture"]), ["Capture"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Melee", "!Melee"]), []);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["!Melee"]), ["Ranged", "Capture"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["!Melee", "!Ranged"]), ["Capture"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Capture", "!Ranged"]), ["Capture"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Capture", "Melee", "!Ranged"]), ["Melee", "Capture"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetPreferredClasses("Melee"), ["FemaleCitizen"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRestrictedClasses("Melee"), ["Elephant", "Archer"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetFullAttackRange(), { "min": 0, "max": 80 });
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Capture"), { "Capture": 8 });
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Ranged"), {
"Damage": {
"Hack": 0,
"Pierce": 10,
"Crush": 0
}
});
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Ranged", true), {
"Damage": {
"Hack": 0.0,
"Pierce": 15.0,
"Crush": 35.0
},
"Bonuses": {
"BonusCav": {
"Classes": "Cavalry",
"Multiplier": 3
}
}
});
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("StatusEffect"), {
"ApplyStatus": {
"StatusInternalName": {
"Duration": 5000,
"Interval": 0,
"Stackability": "Stacks",
"Modifiers": {
"SE": {
"Paths": {
"_string": "Health/Max"
},
"Affects": {
"_string": "Unit"
},
"Add": 10
}
}
}
}
});
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetTimers("Ranged"), {
"prepare": 300,
"repeat": 500
});
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRepeatTime("Ranged"), 500);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetTimers("Capture"), {
"prepare": 0,
"repeat": 1000
});
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRepeatTime("Capture"), 1000);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetSplashData("Ranged"), {
"attackData": {
"Damage": {
"Hack": 0,
"Pierce": 15,
"Crush": 35,
},
"Bonuses": {
"BonusCav": {
"Classes": "Cavalry",
"Multiplier": 3
}
}
},
"friendlyFire": false,
"radius": 10,
"shape": "Circular"
});
});
for (let className of ["Infantry", "Cavalry"])
attackComponentTest(className, true, (attacker, cmpAttack, defender) => {
TS_ASSERT_EQUALS(cmpAttack.GetAttackEffectsData("Melee").Bonuses.BonusCav.Multiplier, 2);
TS_ASSERT_EQUALS(cmpAttack.GetAttackEffectsData("Capture").Bonuses || null, null);
let getAttackBonus = (s, t, e, splash) => Attacking.GetAttackBonus(s, e, t, cmpAttack.GetAttackEffectsData(t, splash).Bonuses || null);
TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Melee", defender), className == "Cavalry" ? 2 : 1);
TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Ranged", defender), 1);
TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Ranged", defender, true), className == "Cavalry" ? 3 : 1);
TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Capture", defender), 1);
TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Slaughter", defender), 1);
});
// CanAttack rejects elephant attack due to RestrictedClasses
attackComponentTest("Elephant", true, (attacker, cmpAttack, defender) => {
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), false);
});
function testGetBestAttackAgainst(defenderClass, bestAttack, bestAllyAttack, isBuilding = false)
{
attackComponentTest(defenderClass, true, (attacker, cmpAttack, defender) => {
if (isBuilding)
AddMock(defender, IID_Capturable, {
"CanCapture": playerID => {
TS_ASSERT_EQUALS(playerID, 1);
return true;
}
});
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), true);
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, []), true);
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged"]), true);
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Melee"]), true);
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Capture"]), isBuilding);
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "Capture"]), defenderClass != "Archer");
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged", "Capture"]), true);
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Ranged", "!Melee"]), isBuilding || defenderClass == "Domestic");
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "!Melee"]), false);
let allowCapturing = [true];
if (!isBuilding)
allowCapturing.push(false);
for (let ac of allowCapturing)
TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ac), bestAttack);
});
attackComponentTest(defenderClass, false, (attacker, cmpAttack, defender) => {
if (isBuilding)
AddMock(defender, IID_Capturable, {
"CanCapture": playerID => {
TS_ASSERT_EQUALS(playerID, 1);
return true;
}
});
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), isBuilding || defenderClass == "Domestic");
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, []), isBuilding || defenderClass == "Domestic");
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged"]), false);
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Melee"]), isBuilding || defenderClass == "Domestic");
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Capture"]), isBuilding);
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "Capture"]), isBuilding);
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged", "Capture"]), isBuilding);
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Ranged", "!Melee"]), isBuilding || defenderClass == "Domestic");
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "!Melee"]), false);
let allowCapturing = [true];
if (!isBuilding)
allowCapturing.push(false);
for (let ac of allowCapturing)
TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ac), bestAllyAttack);
});
}
testGetBestAttackAgainst("FemaleCitizen", "Melee", undefined);
testGetBestAttackAgainst("Archer", "Ranged", undefined);
testGetBestAttackAgainst("Domestic", "Slaughter", "Slaughter");
testGetBestAttackAgainst("Structure", "Capture", "Capture", true);
testGetBestAttackAgainst("Structure", "Ranged", undefined, false);
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 24499)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Damage.js (revision 24500)
@@ -1,692 +1,705 @@
+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("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),
"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);
TestDamage();
data.type = "Ranged";
type = data.type;
Attacking.HandleAttackEffects(target, data.type, data.attackData, 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),
"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 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_DeathDamage.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js (revision 24499)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js (revision 24500)
@@ -1,73 +1,77 @@
+AttackEffects = class AttackEffects
+{
+};
+
Engine.LoadHelperScript("Attacking.js");
Engine.LoadHelperScript("ValueModification.js");
Engine.LoadComponentScript("interfaces/DeathDamage.js");
Engine.LoadComponentScript("interfaces/ModifiersManager.js");
Engine.LoadComponentScript("DeathDamage.js");
let deadEnt = 60;
let player = 1;
ApplyValueModificationsToEntity = function(value, stat, ent)
{
if (value == "DeathDamage/Damage/Pierce" && ent == deadEnt)
return stat + 200;
return stat;
};
let template = {
"Shape": "Circular",
"Range": 10.7,
"FriendlyFire": "false",
"Damage": {
"Hack": 0.0,
"Pierce": 15.0,
"Crush": 35.0
}
};
let effects = {
"Damage": {
"Hack": 0.0,
"Pierce": 215.0,
"Crush": 35.0
}
};
let cmpDeathDamage = ConstructComponent(deadEnt, "DeathDamage", template);
let playersToDamage = [2, 3, 7];
let pos = new Vector2D(3, 4.2);
let result = {
"type": "Death",
"attackData": effects,
"attacker": deadEnt,
"attackerOwner": player,
"origin": pos,
"radius": template.Range,
"shape": template.Shape,
"friendlyFire": false
};
Attacking.CauseDamageOverArea = data => TS_ASSERT_UNEVAL_EQUALS(data, result);
Attacking.GetPlayersToDamage = () => playersToDamage;
AddMock(deadEnt, IID_Position, {
"GetPosition2D": () => pos,
"IsInWorld": () => true
});
AddMock(deadEnt, IID_Ownership, {
"GetOwner": () => player
});
TS_ASSERT_UNEVAL_EQUALS(cmpDeathDamage.GetDeathDamageEffects(), effects);
cmpDeathDamage.CauseDeathDamage();
// Test splash damage bonus
effects.Bonuses = { "BonusCav": { "Classes": "Cavalry", "Multiplier": 3 } };
template.Bonuses = effects.Bonuses;
cmpDeathDamage = ConstructComponent(deadEnt, "DeathDamage", template);
result.attackData.Bonuses = effects.Bonuses;
TS_ASSERT_UNEVAL_EQUALS(cmpDeathDamage.GetDeathDamageEffects(), effects);
cmpDeathDamage.CauseDeathDamage();
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 24499)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Resistance.js (revision 24500)
@@ -1,350 +1,373 @@
+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");
Attacking.HandleAttackEffects(this.EntityID, attackType, attackData, this.AttackerID, this.EnemyID);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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/data/attack_effects/applystatus.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/attack_effects/applystatus.json (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/data/attack_effects/applystatus.json (revision 24500)
@@ -0,0 +1,9 @@
+{
+ "code": "ApplyStatus",
+ "description": "Various (timed) effects.",
+ "IID": "IID_StatusEffectsReceiver",
+ "method": "ApplyStatus",
+ "name": "Apply Status",
+ "order": 3,
+ "sound": "attacked"
+}
Property changes on: ps/trunk/binaries/data/mods/public/simulation/data/attack_effects/applystatus.json
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:mime-type
## -0,0 +1 ##
+text/json
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/simulation/data/attack_effects/capture.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/attack_effects/capture.json (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/data/attack_effects/capture.json (revision 24500)
@@ -0,0 +1,9 @@
+{
+ "code": "Capture",
+ "description": "Reduces capture points of a target.",
+ "IID": "IID_Capturable",
+ "method": "Capture",
+ "name": "Capture",
+ "order": 2,
+ "sound": "attacked_capture"
+}
Property changes on: ps/trunk/binaries/data/mods/public/simulation/data/attack_effects/capture.json
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:mime-type
## -0,0 +1 ##
+text/json
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/simulation/data/attack_effects/damage.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/attack_effects/damage.json (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/data/attack_effects/damage.json (revision 24500)
@@ -0,0 +1,9 @@
+{
+ "code": "Damage",
+ "description": "Reduces the health of a target.",
+ "IID": "IID_Health",
+ "method": "TakeDamage",
+ "name": "Damage",
+ "order": 1,
+ "sound": "attacked"
+}
Property changes on: ps/trunk/binaries/data/mods/public/simulation/data/attack_effects/damage.json
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:mime-type
## -0,0 +1 ##
+text/json
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Attacking.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/Attacking.js (revision 24499)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/Attacking.js (revision 24500)
@@ -1,380 +1,381 @@
/**
* 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);
}
};
/**
* 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 {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)
{
let cmpResistance = Engine.QueryInterface(target, IID_Resistance);
if (cmpResistance && cmpResistance.IsInvulnerable())
return false;
bonusMultiplier *= !attackData.Bonuses ? 1 : this.GetAttackBonus(attacker, target, attackType, attackData.Bonuses);
let targetState = {};
- for (let effectType of g_EffectTypes)
+ for (let receiver of g_AttackEffects.Receivers())
{
- if (!attackData[effectType])
+ if (!attackData[receiver.type])
continue;
- let receiver = g_EffectReceiver[effectType];
let cmpReceiver = Engine.QueryInterface(target, global[receiver.IID]);
if (!cmpReceiver)
continue;
- Object.assign(targetState, cmpReceiver[receiver.method](this.GetTotalAttackEffects(target, attackData, effectType, bonusMultiplier, cmpResistance), attacker, attackerOwner));
+ Object.assign(targetState, cmpReceiver[receiver.method](this.GetTotalAttackEffects(target, attackData, receiver.type, bonusMultiplier, cmpResistance), attacker, attackerOwner));
}
if (!Object.keys(targetState).length)
return false;
Engine.PostMessage(target, MT_Attacked, {
"type": attackType,
"target": target,
"attacker": attacker,
"attackerOwner": attackerOwner,
"damage": -(targetState.healthChange || 0),
"capture": targetState.captureChange || 0,
"statusEffects": targetState.inflictedStatuses || [],
"fromStatusEffect": !!attackData.StatusEffect,
});
// We do not want an entity to get XP from active Status Effects.
if (!!attackData.StatusEffect)
return true;
let cmpPromotion = Engine.QueryInterface(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 24499)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/tests/test_Attacking.js (revision 24500)
@@ -1,136 +1,159 @@
+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);
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);
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);
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);
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);
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);
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);
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);
}
}
new testHandleAttackEffects().testMultipleEffects();
new testHandleAttackEffects().testSkippedEffect();
new testHandleAttackEffects().testAttackedMessage();
new testHandleAttackEffects().testStatusEffects();
new testHandleAttackEffects().testBonusMultiplier();