GUI support for Status Effects and extend their functionality to all attack effects following 16b452cf91

This fixes status effects following 16b452cf91 where the GiveStatus name
change broke them.

This lets status effects deal all types of damage, capture, or inflict
other status effects, like any attack.
This further adds some basic GUI spport to status effects, by showing up
to 5 icons in the top-right of the portrait, and including status
effects in the tooltips.

Status effects can specify a custom icon.

Differential Revision: https://code.wildfiregames.com/D2218
This was SVN commit r22901.
This commit is contained in:
wraitii 2019-09-15 09:24:52 +00:00
parent 89e511def9
commit 2333b1814e
9 changed files with 135 additions and 42 deletions

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:db2f0b822606a94a0769680265a043c40ac51a1306652dee5d04164d419acbae
size 15527

View file

@ -185,15 +185,16 @@ function getArmorTooltip(template)
function attackRateDetails(interval, projectiles)
{
// ToDo: Get the name of a projectile from the template.
if (!interval || projectiles == 0)
if (!interval)
return "";
if (projectiles === 0)
return translate("Garrison to fire arrows");
let attackRateString = getSecondsString(interval / 1000);
let header = headerFont(translate("Interval:"));
if (+projectiles > 1)
if (projectiles && +projectiles > 1)
{
header = headerFont(translate("Rate:"));
let projectileString = sprintf(translatePlural("%(projectileCount)s %(projectileName)s", "%(projectileCount)s %(projectileName)s", projectiles), {
@ -258,7 +259,7 @@ function damageDetails(damageTemplate)
return Object.keys(damageTemplate).filter(dmgType => damageTemplate[dmgType]).map(
dmgType => sprintf(translate("%(damage)s %(damageType)s"), {
"damage": damageTemplate[dmgType].toFixed(1),
"damage": (+damageTemplate[dmgType]).toFixed(1),
"damageType": unitFont(translateWithContext("damage type", dmgType))
})).join(commaFont(translate(", ")));
}
@ -269,11 +270,21 @@ function captureDetails(captureTemplate)
return "";
return sprintf(translate("%(amount)s %(name)s"), {
"amount": captureTemplate.toFixed(1),
"amount": (+captureTemplate).toFixed(1),
"name": unitFont(translateWithContext("damage type", "Capture"))
});
}
function giveStatusDetails(giveStatusTemplate)
{
if (!giveStatusTemplate)
return "";
return sprintf(translate("gives %(name)s"), {
"name": Object.keys(giveStatusTemplate).map(x => unitFont(translateWithContext("status effect", x))).join(', '),
});
}
function attackEffectsDetails(attackTypeTemplate)
{
if (!attackTypeTemplate)
@ -281,7 +292,8 @@ function attackEffectsDetails(attackTypeTemplate)
let effects = [
captureDetails(attackTypeTemplate.Capture || undefined),
damageDetails(attackTypeTemplate.Damage || undefined)
damageDetails(attackTypeTemplate.Damage || undefined),
giveStatusDetails(attackTypeTemplate.GiveStatus || undefined)
];
return effects.filter(effect => effect).join(commaFont(translate(", ")));
}
@ -309,11 +321,19 @@ function getAttackTooltip(template)
if (template.buildingAI)
projectiles = template.buildingAI.arrowCount || template.buildingAI.defaultArrowCount;
tooltips.push(sprintf(translate("%(attackLabel)s: %(effects)s, %(range)s, %(rate)s"), {
// Show the effects of status effects below
let statusEffectsDetails = [];
if (attackTypeTemplate.GiveStatus)
for (let status in attackTypeTemplate.GiveStatus)
statusEffectsDetails.push("\n " + getStatusEffectsTooltip(status, attackTypeTemplate.GiveStatus[status]));
statusEffectsDetails = statusEffectsDetails.join("");
tooltips.push(sprintf(translate("%(attackLabel)s: %(effects)s, %(range)s, %(rate)s%(statusEffects)s"), {
"attackLabel": attackLabel,
"effects": attackEffectsDetails(attackTypeTemplate),
"range": rangeDetails(attackTypeTemplate),
"rate": attackRateDetails(attackTypeTemplate.repeatTime, projectiles)
"rate": attackRateDetails(attackTypeTemplate.repeatTime, projectiles),
"statusEffects": statusEffectsDetails
}));
}
return tooltips.join("\n");
@ -351,6 +371,23 @@ function getSplashDamageTooltip(template)
return tooltips.join("\n");
}
function getStatusEffectsTooltip(name, template)
{
let durationString = "";
if (template.Duration)
durationString = sprintf(translate(", %(durName)s: %(duration)s"), {
"durName": headerFont(translate("Duration")),
"duration": getSecondsString((template.TimeElapsed ? +template.Duration - template.TimeElapsed : +template.Duration) / 1000),
});
return sprintf(translate("%(statusName)s: %(effects)s, %(rate)s%(durationString)s"), {
"statusName": headerFont(translateWithContext("status effect", name)),
"effects": attackEffectsDetails(template),
"rate": attackRateDetails(+template.Interval),
"durationString": durationString
});
}
function getGarrisonTooltip(template)
{
if (!template.garrisonHolder)

View file

@ -93,6 +93,26 @@ function displaySingle(entState)
Engine.GetGUIObjectByName("rankIcon").tooltip = "";
}
if (entState.statusEffects)
{
let statusIcons = Engine.GetGUIObjectByName("statusEffectsIcons").children;
let i = 0;
for (let effectName in entState.statusEffects)
{
let effect = entState.statusEffects[effectName];
statusIcons[i].hidden = false;
statusIcons[i].sprite = "stretched:session/icons/status_effects/" + (effect.Icon || "default") + ".png";
statusIcons[i].tooltip = getStatusEffectsTooltip(effectName, effect);
let size = statusIcons[i].size;
size.top = i * 18;
size.bottom = i * 18 + 16;
statusIcons[i].size = size;
i++;
}
for (; i < statusIcons.length; ++i)
statusIcons[i].hidden = true;
}
let showHealth = entState.hitpoints;
let showResource = entState.resourceSupply;

View file

@ -82,9 +82,18 @@
<object z="20" size="4 4 20 20" name="rankIcon" type="image" tooltip_style="sessionToolTip">
<translatableAttribute id="tooltip">Rank</translatableAttribute>
</object>
<!-- Status Effecst icons -->
<object name="statusEffectsIcons" size="100%-20 4 100%-4 100%">
<repeat count="5">
<object type="image" size="0 0 16 16" z="200" tooltip_style="sessionToolTip"/>
</repeat>
</object>
</object>
</object>
<!-- Names (this must come before the attack and armor icon to avoid clipping issues) -->
<object size="2 96 100%-2 100%-36" name="statsArea" type="image" sprite="edgedPanelShader">

View file

@ -291,6 +291,10 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
"template": cmpUpgrade.GetUpgradingTo()
};
let cmpStatusEffects = Engine.QueryInterface(ent, IID_StatusEffectsReceiver);
if (cmpStatusEffects)
ret.statusEffects = cmpStatusEffects.GetActiveStatuses();
let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
if (cmpProductionQueue)
ret.production = {

View file

@ -5,6 +5,11 @@ StatusEffectsReceiver.prototype.Init = function()
this.activeStatusEffects = {};
};
StatusEffectsReceiver.prototype.GetActiveStatuses = function()
{
return this.activeStatusEffects;
};
// Called by attacking effects.
StatusEffectsReceiver.prototype.GiveStatus = function(effectData, attacker, attackerOwner, bonusMultiplier)
{
@ -23,23 +28,23 @@ StatusEffectsReceiver.prototype.AddStatus = function(statusName, data)
this.activeStatusEffects[statusName] = {};
let status = this.activeStatusEffects[statusName];
status.duration = +data.Duration;
status.interval = +data.Interval;
status.damage = +data.Damage;
status.timeElapsed = 0;
status.firstTime = true;
Object.assign(status, data);
status.Interval = +data.Interval;
status.TimeElapsed = 0;
status.FirstTime = true;
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
status.timer = cmpTimer.SetInterval(this.entity, IID_StatusEffectsReceiver, "ExecuteEffect", 0, +status.interval, statusName);
status.Timer = cmpTimer.SetInterval(this.entity, IID_StatusEffectsReceiver, "ExecuteEffect", 0, +status.Interval, statusName);
};
StatusEffectsReceiver.prototype.RemoveStatus = function(statusName) {
StatusEffectsReceiver.prototype.RemoveStatus = function(statusName)
{
if (!this.activeStatusEffects[statusName])
return;
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.activeStatusEffects[statusName].timer);
this.activeStatusEffects[statusName] = undefined;
cmpTimer.CancelTimer(this.activeStatusEffects[statusName].Timer);
delete this.activeStatusEffects[statusName];
};
StatusEffectsReceiver.prototype.ExecuteEffect = function(statusName, lateness)
@ -48,17 +53,17 @@ StatusEffectsReceiver.prototype.ExecuteEffect = function(statusName, lateness)
if (!status)
return;
if (status.firstTime)
if (status.FirstTime)
{
status.firstTime = false;
status.timeElapsed += lateness;
status.FirstTime = false;
status.TimeElapsed += lateness;
}
else
status.timeElapsed += status.interval + lateness;
status.TimeElapsed += status.Interval + lateness;
Attacking.HandleAttackEffects(statusName, { "Damage": { [statusName]: status.damage } }, this.entity, -1, -1);
Attacking.HandleAttackEffects(statusName, status, this.entity, -1, -1);
if (status.timeElapsed >= status.duration)
if (status.Duration && status.TimeElapsed >= +status.Duration)
this.RemoveStatus(statusName);
};

View file

@ -31,6 +31,7 @@ Engine.LoadComponentScript("interfaces/TechnologyManager.js");
Engine.LoadComponentScript("interfaces/Trader.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");
Engine.LoadComponentScript("interfaces/Upgrade.js");
Engine.LoadComponentScript("interfaces/BuildingAI.js");

View file

@ -28,7 +28,9 @@ function testInflictEffects()
cmpStatusReceiver.AddStatus(statusName, {
"Duration": 20000,
"Interval": 10000,
"Damage": 1
"Damage": {
[statusName]: 1
}
});
cmpTimer.OnUpdate({ "turnLength": 1 });
@ -65,12 +67,16 @@ function testMultipleEffects()
"Burn": {
"Duration": 20000,
"Interval": 10000,
"Damage": 10
"Damage": {
"Burn": 10
}
},
"Poison": {
"Duration": 3000,
"Interval": 1000,
"Damage": 1
"Damage": {
"Poison": 1
}
}
});
@ -102,7 +108,9 @@ function testRemoveStatus()
cmpStatusReceiver.AddStatus(statusName, {
"Duration": 20000,
"Interval": 10000,
"Damage": 1
"Damage": {
[statusName]: 1
}
});
cmpTimer.OnUpdate({ "turnLength": 1 });

View file

@ -5,38 +5,44 @@ function Attacking() {}
/**
* Builds a RelaxRNG schema of possible attack effects.
* Currently harcoded to "Damage", "Capture" and "StatusEffects".
* See globalscripts/AttackEffects.js for possible elements.
* Attacks may also have a "Bonuses" element.
*
* @return {string} - RelaxNG schema string
*/
const DamageSchema = "" +
"<oneOrMore>" +
"<element a:help='One or more elements describing damage types'>" +
"<anyName>" +
// Armour requires Foundation to not be a damage type.
"<except><name>Foundation</name></except>" +
"</anyName>" +
"<ref name='nonNegativeDecimal' />" +
"</element>" +
"</oneOrMore>";
Attacking.prototype.BuildAttackEffectsSchema = function()
{
return "" +
"<oneOrMore>" +
"<choice>" +
"<element name='Damage'>" +
"<oneOrMore>" +
"<element a:help='One or more elements describing damage types'>" +
"<anyName>" +
// Armour requires Foundation to not be a damage type.
"<except><name>Foundation</name></except>" +
"</anyName>" +
"<ref name='nonNegativeDecimal' />" +
"</element>" +
"</oneOrMore>" +
DamageSchema +
"</element>" +
"<element name='Capture' a:help='Capture points value'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
"<element name='StatusEffects' a:help='Effects like poisoning or burning a unit.'>" +
"<element name='GiveStatus' a:help='Effects like poisoning or burning a unit.'>" +
"<oneOrMore>" +
"<element>" +
"<anyName/>" +
"<interleave>" +
"<optional>" +
"<element name='Icon' a:help='Icon for the status effect'><text/></element>" +
"</optional>" +
"<element name='Duration' a:help='The duration of the status while the effect occurs.'><ref name='nonNegativeDecimal'/></element>" +
"<element name='Interval' a:help='Interval between the occurances of the effect.'><ref name='nonNegativeDecimal'/></element>" +
"<element name='Damage' a:help='Damage caused by the effect.'><ref name='nonNegativeDecimal'/></element>" +
"<element name='Damage' a:help='Damage caused by the effect.'>" + DamageSchema + "</element>" +
"</interleave>" +
"</element>" +
"</oneOrMore>" +
@ -79,8 +85,8 @@ Attacking.prototype.GetAttackEffectsData = function(valueModifRoot, template, en
if (template.Capture)
ret.Capture = ApplyValueModificationsToEntity(valueModifRoot + "/Capture", +(template.Capture || 0), entity);
if (template.StatusEffects)
ret.StatusEffects = template.StatusEffects;
if (template.GiveStatus)
ret.GiveStatus = template.GiveStatus;
if (template.Bonuses)
ret.Bonuses = template.Bonuses;