0ad/binaries/data/mods/public/simulation/components/Capturable.js
wraitii 16b452cf91 Generalise Attack effects. All attacks, including death damage and splash, can deal any number of attack effects (damaging, capture, giving status effects.)
This moves most of what was in the Damage system component to a helper,
and renames that component DelayedDamage.
It also introduces a new global script with all possible attack effects.

Comments Taken From: Freagarach, Stan, bb

Differential Revision: https://code.wildfiregames.com/D2092
This was SVN commit r22754.
2019-08-22 18:00:33 +00:00

344 lines
9.8 KiB
JavaScript

function Capturable() {}
Capturable.prototype.Schema =
"<element name='CapturePoints' a:help='Maximum capture points'>" +
"<ref name='positiveDecimal'/>" +
"</element>" +
"<element name='RegenRate' a:help='Number of capture are regenerated per second in favour of the owner'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
"<element name='GarrisonRegenRate' a:help='Number of capture are regenerated per second and per garrisoned unit in favour of the owner'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>";
Capturable.prototype.Init = function()
{
// Cache this value
this.maxCp = +this.template.CapturePoints;
this.cp = [];
};
//// Interface functions ////
/**
* Returns the current capture points array
*/
Capturable.prototype.GetCapturePoints = function()
{
return this.cp;
};
Capturable.prototype.GetMaxCapturePoints = function()
{
return this.maxCp;
};
Capturable.prototype.GetGarrisonRegenRate = function()
{
return ApplyValueModificationsToEntity("Capturable/GarrisonRegenRate", +this.template.GarrisonRegenRate, this.entity);
};
/**
* Set the new capture points, used for cloning entities
* The caller should assure that the sum of capture points
* matches the max.
*/
Capturable.prototype.SetCapturePoints = function(capturePointsArray)
{
this.cp = capturePointsArray;
};
Capturable.prototype.Capture = function(effectData, attacker, attackerOwner, bonusMultiplier)
{
let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
if (attackerOwner == INVALID_PLAYER || !this.CanCapture(attackerOwner) ||
!cmpHealth || cmpHealth.GetHitpoints() == 0)
return {};
bonusMultiplier *= cmpHealth.GetMaxHitpoints() / (0.1 * cmpHealth.GetMaxHitpoints() + 0.9 * cmpHealth.GetHitpoints());
let total = Attacking.GetTotalAttackEffects({ "Capture": effectData }, "Capture") * bonusMultiplier;
let change = this.Reduce(total, attackerOwner);
// TODO: implement loot
return { "captureChange": change };
};
/**
* Reduces the amount of capture points of an entity,
* in favour of the player of the source
* Returns the number of capture points actually taken
*/
Capturable.prototype.Reduce = function(amount, playerID)
{
if (amount <= 0)
return 0;
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER)
return 0;
var cmpPlayerSource = QueryPlayerIDInterface(playerID);
if (!cmpPlayerSource)
return 0;
// Before changing the value, activate Fogging if necessary to hide changes
var cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging);
if (cmpFogging)
cmpFogging.Activate();
var numberOfEnemies = this.cp.filter((v, i) => v > 0 && cmpPlayerSource.IsEnemy(i)).length;
if (numberOfEnemies == 0)
return 0;
// Distribute the capture points over all enemies.
let distributedAmount = amount / numberOfEnemies;
let removedAmount = 0;
while (distributedAmount > 0.0001)
{
numberOfEnemies = 0;
for (let i in this.cp)
{
if (!this.cp[i] || !cmpPlayerSource.IsEnemy(i))
continue;
if (this.cp[i] > distributedAmount)
{
removedAmount += distributedAmount;
this.cp[i] -= distributedAmount;
++numberOfEnemies;
}
else
{
removedAmount += this.cp[i];
this.cp[i] = 0;
}
}
distributedAmount = numberOfEnemies ? (amount - removedAmount) / numberOfEnemies : 0;
}
// give all cp taken to the player
var takenCp = this.maxCp - this.cp.reduce((a, b) => a + b);
this.cp[playerID] += takenCp;
this.CheckTimer();
this.RegisterCapturePointsChanged();
return takenCp;
};
/**
* Check if the source can (re)capture points from this building
*/
Capturable.prototype.CanCapture = function(playerID)
{
var cmpPlayerSource = QueryPlayerIDInterface(playerID);
if (!cmpPlayerSource)
warn(playerID + " has no player component defined on its id");
var cp = this.GetCapturePoints();
var sourceEnemyCp = 0;
for (let i in this.GetCapturePoints())
if (cmpPlayerSource.IsEnemy(i))
sourceEnemyCp += cp[i];
return sourceEnemyCp > 0;
};
//// Private functions ////
/**
* This has to be called whenever the capture points are changed.
* It notifies other components of the change, and switches ownership when needed.
*/
Capturable.prototype.RegisterCapturePointsChanged = function()
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership)
return;
Engine.PostMessage(this.entity, MT_CapturePointsChanged, { "capturePoints": this.cp });
var owner = cmpOwnership.GetOwner();
if (owner == INVALID_PLAYER || this.cp[owner] > 0)
return;
// If all cp has been taken from the owner, convert it to the best player.
var bestPlayer = 0;
for (let i in this.cp)
if (this.cp[i] >= this.cp[bestPlayer])
bestPlayer = +i;
let cmpLostPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
if (cmpLostPlayerStatisticsTracker)
cmpLostPlayerStatisticsTracker.LostEntity(this.entity);
cmpOwnership.SetOwner(bestPlayer);
let cmpCapturedPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
if (cmpCapturedPlayerStatisticsTracker)
cmpCapturedPlayerStatisticsTracker.CapturedEntity(this.entity);
};
Capturable.prototype.GetRegenRate = function()
{
var regenRate = +this.template.RegenRate;
regenRate = ApplyValueModificationsToEntity("Capturable/RegenRate", regenRate, this.entity);
var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
if (cmpGarrisonHolder)
var garrisonRegenRate = this.GetGarrisonRegenRate() * cmpGarrisonHolder.GetEntities().length;
else
var garrisonRegenRate = 0;
return regenRate + garrisonRegenRate;
};
Capturable.prototype.TimerTick = function()
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER)
return;
var owner = cmpOwnership.GetOwner();
var modifiedCp = 0;
// Special handle for the territory decay.
// Reduce cp from the owner in favour of all neighbours (also allies).
var cmpTerritoryDecay = Engine.QueryInterface(this.entity, IID_TerritoryDecay);
if (cmpTerritoryDecay && cmpTerritoryDecay.IsDecaying())
{
var neighbours = cmpTerritoryDecay.GetConnectedNeighbours();
var totalNeighbours = neighbours.reduce((a, b) => a + b);
var decay = Math.min(cmpTerritoryDecay.GetDecayRate(), this.cp[owner]);
this.cp[owner] -= decay;
if (totalNeighbours)
for (let p in neighbours)
this.cp[p] += decay * neighbours[p] / totalNeighbours;
else // decay to gaia as default
this.cp[0] += decay;
modifiedCp += decay;
this.RegisterCapturePointsChanged();
}
var regenRate = this.GetRegenRate();
if (regenRate < 0)
modifiedCp += this.Reduce(-regenRate, 0);
else if (regenRate > 0)
modifiedCp += this.Reduce(regenRate, owner);
if (modifiedCp)
return;
// Nothing changed, stop the timer.
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
this.timer = 0;
Engine.PostMessage(this.entity, MT_CaptureRegenStateChanged, { "regenerating": false, "regenRate": 0, "territoryDecay": 0 });
};
/**
* Start the regeneration timer when no timer exists.
* When nothing can be modified (f.e. because it is fully regenerated), the
* timer stops automatically after one execution.
*/
Capturable.prototype.CheckTimer = function()
{
if (this.timer)
return;
var regenRate = this.GetRegenRate();
var cmpDecay = Engine.QueryInterface(this.entity, IID_TerritoryDecay);
var decay = cmpDecay && cmpDecay.IsDecaying() ? cmpDecay.GetDecayRate() : 0;
if (regenRate == 0 && decay == 0)
return;
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.timer = cmpTimer.SetInterval(this.entity, IID_Capturable, "TimerTick", 1000, 1000, null);
Engine.PostMessage(this.entity, MT_CaptureRegenStateChanged, { "regenerating": true, "regenRate": regenRate, "territoryDecay": decay });
};
//// Message Listeners ////
Capturable.prototype.OnValueModification = function(msg)
{
if (msg.component != "Capturable")
return;
var oldMaxCp = this.GetMaxCapturePoints();
this.maxCp = ApplyValueModificationsToEntity("Capturable/CapturePoints", +this.template.CapturePoints, this.entity);
if (oldMaxCp == this.maxCp)
return;
var scale = this.maxCp / oldMaxCp;
for (let i in this.cp)
this.cp[i] *= scale;
Engine.PostMessage(this.entity, MT_CapturePointsChanged, { "capturePoints": this.cp });
this.CheckTimer();
};
Capturable.prototype.OnGarrisonedUnitsChanged = function(msg)
{
this.CheckTimer();
};
Capturable.prototype.OnTerritoryDecayChanged = function(msg)
{
if (msg.to)
this.CheckTimer();
};
Capturable.prototype.OnDiplomacyChanged = function(msg)
{
this.CheckTimer();
};
Capturable.prototype.OnOwnershipChanged = function(msg)
{
if (msg.to == INVALID_PLAYER)
return; // we're dead
if (this.cp.length)
{
if (!this.cp[msg.from])
return; // nothing to change
// Was already initialised, this happens on defeat or wololo
// transfer the points of the old owner to the new one
this.cp[msg.to] += this.cp[msg.from];
this.cp[msg.from] = 0;
this.RegisterCapturePointsChanged();
}
else
{
// Initialise the capture points when created.
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 0; i < numPlayers; ++i)
if (i == msg.to)
this.cp[i] = this.maxCp;
else
this.cp[i] = 0;
}
this.CheckTimer();
};
/**
* When a player is defeated, reassign the cp of non-owned entities to gaia.
* Those owned by the defeated player are dealt with onOwnershipChanged.
*/
Capturable.prototype.OnGlobalPlayerDefeated = function(msg)
{
if (!this.cp[msg.playerId])
return;
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (cmpOwnership && cmpOwnership.GetOwner() == msg.playerId)
return;
this.cp[0] += this.cp[msg.playerId];
this.cp[msg.playerId] = 0;
this.RegisterCapturePointsChanged();
this.CheckTimer();
};
Engine.RegisterComponentType(IID_Capturable, "Capturable", Capturable);