mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-19 23:03:56 -07:00
Manual fixes needed for: eslint --no-config-lookup --rule '"no-irregular-whitespace": 1' Ref: #7812 Signed-off-by: Ralph Sennhauser <ralph.sennhauser@gmail.com>
530 lines
15 KiB
JavaScript
530 lines
15 KiB
JavaScript
function ResourceGatherer() {}
|
||
|
||
ResourceGatherer.prototype.Schema =
|
||
"<a:help>Lets the unit gather resources from entities that have the ResourceSupply component.</a:help>" +
|
||
"<a:example>" +
|
||
"<MaxDistance>2.0</MaxDistance>" +
|
||
"<BaseSpeed>1.0</BaseSpeed>" +
|
||
"<Rates>" +
|
||
"<food.fish>1</food.fish>" +
|
||
"<metal.ore>3</metal.ore>" +
|
||
"<stone.rock>3</stone.rock>" +
|
||
"<wood.tree>2</wood.tree>" +
|
||
"</Rates>" +
|
||
"<Capacities>" +
|
||
"<food>10</food>" +
|
||
"<metal>10</metal>" +
|
||
"<stone>10</stone>" +
|
||
"<wood>10</wood>" +
|
||
"</Capacities>" +
|
||
"</a:example>" +
|
||
"<element name='MaxDistance' a:help='Max resource-gathering distance'>" +
|
||
"<ref name='positiveDecimal'/>" +
|
||
"</element>" +
|
||
"<element name='BaseSpeed' a:help='Base resource-gathering rate (in resource units per second)'>" +
|
||
"<ref name='positiveDecimal'/>" +
|
||
"</element>" +
|
||
"<element name='Rates' a:help='Per-resource-type gather rate multipliers. If a resource type is not specified then it cannot be gathered by this unit'>" +
|
||
Resources.BuildSchema("positiveDecimal", [], true) +
|
||
"</element>" +
|
||
"<element name='Capacities' a:help='Per-resource-type maximum carrying capacity'>" +
|
||
Resources.BuildSchema("positiveDecimal") +
|
||
"</element>";
|
||
|
||
/*
|
||
* Call interval will be determined by gather rate,
|
||
* so always gather integer amount.
|
||
*/
|
||
ResourceGatherer.prototype.GATHER_AMOUNT = 1;
|
||
|
||
ResourceGatherer.prototype.Init = function()
|
||
{
|
||
// Cached. Currently not a target of modifiers.
|
||
this.range = { "max": +this.template.MaxDistance, "min": 0 };
|
||
|
||
this.capacities = {};
|
||
this.carrying = {}; // { generic type: integer amount currently carried }
|
||
// (Note that this component supports carrying multiple types of resources,
|
||
// each with an independent capacity, but the rest of the game currently
|
||
// ensures and assumes we'll only be carrying one type at once)
|
||
|
||
// The last exact type gathered, so we can render appropriate props
|
||
this.lastCarriedType = undefined; // { generic, specific }
|
||
};
|
||
|
||
/**
|
||
* Returns data about what resources the unit is currently carrying,
|
||
* in the form [ {"type":"wood", "amount":7, "max":10} ]
|
||
*/
|
||
ResourceGatherer.prototype.GetCarryingStatus = function()
|
||
{
|
||
const ret = [];
|
||
for (const type in this.carrying)
|
||
{
|
||
ret.push({
|
||
"type": type,
|
||
"amount": this.carrying[type],
|
||
"max": +this.GetCapacity(type)
|
||
});
|
||
}
|
||
return ret;
|
||
};
|
||
|
||
/**
|
||
* Used to instantly give resources to unit
|
||
* @param resources The same structure as returned form GetCarryingStatus
|
||
*/
|
||
ResourceGatherer.prototype.GiveResources = function(resources)
|
||
{
|
||
for (const resource of resources)
|
||
this.carrying[resource.type] = +resource.amount;
|
||
};
|
||
|
||
/**
|
||
* Returns the generic type of one particular resource this unit is
|
||
* currently carrying, or undefined if none.
|
||
*/
|
||
ResourceGatherer.prototype.GetMainCarryingType = function()
|
||
{
|
||
// Return the first key, if any
|
||
for (const type in this.carrying)
|
||
return type;
|
||
|
||
return undefined;
|
||
};
|
||
|
||
/**
|
||
* Returns the exact resource type we last picked up, as long as
|
||
* we're still carrying something similar enough, in the form
|
||
* { generic, specific }
|
||
*/
|
||
ResourceGatherer.prototype.GetLastCarriedType = function()
|
||
{
|
||
if (this.lastCarriedType && this.lastCarriedType.generic in this.carrying)
|
||
return this.lastCarriedType;
|
||
|
||
return undefined;
|
||
};
|
||
|
||
ResourceGatherer.prototype.SetLastCarriedType = function(lastCarriedType)
|
||
{
|
||
this.lastCarriedType = lastCarriedType;
|
||
};
|
||
|
||
// Since this code is very performancecritical and applying technologies quite slow, cache it.
|
||
ResourceGatherer.prototype.RecalculateGatherRates = function()
|
||
{
|
||
this.baseSpeed = ApplyValueModificationsToEntity("ResourceGatherer/BaseSpeed", +this.template.BaseSpeed, this.entity);
|
||
|
||
this.rates = {};
|
||
for (const r in this.template.Rates)
|
||
{
|
||
const type = r.split(".");
|
||
|
||
if (!Resources.GetResource(type[0]).subtypes[type[1]])
|
||
{
|
||
error("Resource subtype not found: " + type[0] + "." + type[1]);
|
||
continue;
|
||
}
|
||
|
||
const rate = ApplyValueModificationsToEntity("ResourceGatherer/Rates/" + r, +this.template.Rates[r], this.entity);
|
||
this.rates[r] = rate * this.baseSpeed;
|
||
}
|
||
};
|
||
|
||
ResourceGatherer.prototype.RecalculateCapacities = function()
|
||
{
|
||
this.capacities = {};
|
||
for (const r in this.template.Capacities)
|
||
this.capacities[r] = ApplyValueModificationsToEntity("ResourceGatherer/Capacities/" + r, +this.template.Capacities[r], this.entity);
|
||
};
|
||
|
||
ResourceGatherer.prototype.RecalculateCapacity = function(type)
|
||
{
|
||
if (type in this.capacities)
|
||
this.capacities[type] = ApplyValueModificationsToEntity("ResourceGatherer/Capacities/" + type, +this.template.Capacities[type], this.entity);
|
||
};
|
||
|
||
ResourceGatherer.prototype.GetGatherRates = function()
|
||
{
|
||
return this.rates;
|
||
};
|
||
|
||
ResourceGatherer.prototype.GetGatherRate = function(resourceType)
|
||
{
|
||
if (!this.template.Rates[resourceType])
|
||
return 0;
|
||
|
||
return this.rates[resourceType];
|
||
};
|
||
|
||
ResourceGatherer.prototype.GetCapacity = function(resourceType)
|
||
{
|
||
if (!this.template.Capacities[resourceType])
|
||
return 0;
|
||
return this.capacities[resourceType];
|
||
};
|
||
|
||
ResourceGatherer.prototype.GetRange = function()
|
||
{
|
||
return this.range;
|
||
};
|
||
|
||
/**
|
||
* @param {number} target - The target to gather from.
|
||
* @param {number} callerIID - The IID to notify on specific events.
|
||
* @return {boolean} - Whether we started gathering.
|
||
*/
|
||
ResourceGatherer.prototype.StartGathering = function(target, callerIID)
|
||
{
|
||
if (this.target)
|
||
this.StopGathering();
|
||
|
||
const rate = this.GetTargetGatherRate(target);
|
||
if (!rate)
|
||
return false;
|
||
|
||
const cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply);
|
||
if (!cmpResourceSupply || !cmpResourceSupply.AddActiveGatherer(this.entity))
|
||
return false;
|
||
|
||
const resourceType = cmpResourceSupply.GetType();
|
||
|
||
// If we've already got some resources but they're the wrong type,
|
||
// drop them first to ensure we're only ever carrying one type.
|
||
if (this.IsCarryingAnythingExcept(resourceType.generic))
|
||
this.DropResources();
|
||
|
||
const cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
|
||
if (cmpVisual)
|
||
cmpVisual.SelectAnimation("gather_" + resourceType.specific, false, 1.0);
|
||
|
||
// Calculate timing based on gather rates.
|
||
// This allows the gather rate to control how often we gather, instead of how much.
|
||
const timing = 1000 / rate;
|
||
|
||
this.target = target;
|
||
this.callerIID = callerIID;
|
||
|
||
const cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
|
||
this.timer = cmpTimer.SetInterval(this.entity, IID_ResourceGatherer, "PerformGather", timing, timing, null);
|
||
|
||
return true;
|
||
};
|
||
|
||
/**
|
||
* @param {string} reason - The reason why we stopped gathering used to notify the caller.
|
||
*/
|
||
ResourceGatherer.prototype.StopGathering = function(reason)
|
||
{
|
||
if (!this.target)
|
||
return;
|
||
|
||
const cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
|
||
cmpTimer.CancelTimer(this.timer);
|
||
delete this.timer;
|
||
|
||
const cmpResourceSupply = Engine.QueryInterface(this.target, IID_ResourceSupply);
|
||
if (cmpResourceSupply)
|
||
cmpResourceSupply.RemoveGatherer(this.entity);
|
||
|
||
delete this.target;
|
||
|
||
const cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
|
||
if (cmpVisual)
|
||
cmpVisual.SelectAnimation("idle", false, 1.0);
|
||
|
||
// The callerIID component may start again,
|
||
// replacing the callerIID, hence save that.
|
||
const callerIID = this.callerIID;
|
||
delete this.callerIID;
|
||
|
||
if (reason && callerIID)
|
||
{
|
||
const component = Engine.QueryInterface(this.entity, callerIID);
|
||
if (component)
|
||
component.ProcessMessage(reason, null);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Gather from our target entity.
|
||
* @params - data and lateness are unused.
|
||
*/
|
||
ResourceGatherer.prototype.PerformGather = function(data, lateness)
|
||
{
|
||
const cmpResourceSupply = Engine.QueryInterface(this.target, IID_ResourceSupply);
|
||
if (!cmpResourceSupply || cmpResourceSupply.GetCurrentAmount() <= 0)
|
||
{
|
||
this.StopGathering("TargetInvalidated");
|
||
return;
|
||
}
|
||
|
||
if (!this.IsTargetInRange(this.target))
|
||
{
|
||
this.StopGathering("OutOfRange");
|
||
return;
|
||
}
|
||
|
||
// ToDo: Enable entities to keep facing a target.
|
||
Engine.QueryInterface(this.entity, IID_UnitAI)?.FaceTowardsTarget(this.target);
|
||
|
||
const type = cmpResourceSupply.GetType();
|
||
if (!this.carrying[type.generic])
|
||
this.carrying[type.generic] = 0;
|
||
|
||
const maxGathered = this.GetCapacity(type.generic) - this.carrying[type.generic];
|
||
const status = cmpResourceSupply.TakeResources(Math.min(this.GATHER_AMOUNT, maxGathered));
|
||
this.carrying[type.generic] += status.amount;
|
||
this.lastCarriedType = type;
|
||
|
||
// Update stats of how much the player collected.
|
||
// (We have to do it here rather than at the dropsite, because we
|
||
// need to know what subtype it was.)
|
||
const cmpStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
|
||
if (cmpStatisticsTracker)
|
||
cmpStatisticsTracker.IncreaseResourceGatheredCounter(type.generic, status.amount, type.specific);
|
||
|
||
if (!this.CanCarryMore(type.generic))
|
||
this.StopGathering("InventoryFilled");
|
||
else if (status.exhausted)
|
||
this.StopGathering("TargetInvalidated");
|
||
};
|
||
|
||
/**
|
||
* Compute the amount of resources collected per second from the target.
|
||
* Returns 0 if resources cannot be collected (e.g. the target doesn't
|
||
* exist, or is the wrong type).
|
||
*/
|
||
ResourceGatherer.prototype.GetTargetGatherRate = function(target)
|
||
{
|
||
const cmpResourceSupply = QueryMiragedInterface(target, IID_ResourceSupply);
|
||
if (!cmpResourceSupply || cmpResourceSupply.GetCurrentAmount() <= 0)
|
||
return 0;
|
||
|
||
const type = cmpResourceSupply.GetType();
|
||
|
||
let rate = 0;
|
||
if (type.specific)
|
||
rate = this.GetGatherRate(type.generic + "." + type.specific);
|
||
if (rate == 0 && type.generic)
|
||
rate = this.GetGatherRate(type.generic);
|
||
|
||
const diminishingReturns = cmpResourceSupply.GetDiminishingReturns();
|
||
if (diminishingReturns)
|
||
rate *= diminishingReturns;
|
||
|
||
return rate;
|
||
};
|
||
|
||
/**
|
||
* @param {number} target - The entity ID of the target to check.
|
||
* @return {boolean} - Whether we can gather from the target.
|
||
*/
|
||
ResourceGatherer.prototype.CanGather = function(target)
|
||
{
|
||
return this.GetTargetGatherRate(target) > 0;
|
||
};
|
||
|
||
/**
|
||
* Returns whether this unit can carry more of the given type of resource.
|
||
* (This ignores whether the unit is actually able to gather that
|
||
* resource type or not.)
|
||
*/
|
||
ResourceGatherer.prototype.CanCarryMore = function(type)
|
||
{
|
||
const amount = this.carrying[type] || 0;
|
||
return amount < this.GetCapacity(type);
|
||
};
|
||
|
||
|
||
ResourceGatherer.prototype.IsCarrying = function(type)
|
||
{
|
||
const amount = this.carrying[type] || 0;
|
||
return amount > 0;
|
||
};
|
||
|
||
/**
|
||
* Returns whether this unit is carrying any resources of a type that is
|
||
* not the requested type. (This is to support cases where the unit is
|
||
* only meant to be able to carry one type at once.)
|
||
*/
|
||
ResourceGatherer.prototype.IsCarryingAnythingExcept = function(exceptedType)
|
||
{
|
||
for (const type in this.carrying)
|
||
if (type != exceptedType)
|
||
return true;
|
||
|
||
return false;
|
||
};
|
||
|
||
/**
|
||
* @param {number} target - The entity to check.
|
||
* @param {boolean} checkCarriedResource - Whether we need to check the resource we are carrying.
|
||
* @return {boolean} - Whether we can return carried resources.
|
||
*/
|
||
ResourceGatherer.prototype.CanReturnResource = function(target, checkCarriedResource)
|
||
{
|
||
const cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite);
|
||
if (!cmpResourceDropsite)
|
||
return false;
|
||
|
||
if (checkCarriedResource)
|
||
{
|
||
const type = this.GetMainCarryingType();
|
||
if (!type || !cmpResourceDropsite.AcceptsType(type))
|
||
return false;
|
||
}
|
||
|
||
const cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
|
||
if (!cmpOwnership)
|
||
return false;
|
||
|
||
const playerID = cmpOwnership.GetOwner();
|
||
if (IsOwnedByPlayer(playerID, target))
|
||
return true;
|
||
|
||
return QueryPlayerIDInterface(playerID, IID_Diplomacy)?.HasSharedDropsites() &&
|
||
cmpResourceDropsite.IsShared() &&
|
||
IsOwnedByMutualAllyOfPlayer(playerID, target);
|
||
};
|
||
|
||
/**
|
||
* Transfer our carried resources to our owner immediately.
|
||
* Only resources of the appropriate types will be transferred.
|
||
* (This should typically be called after reaching a dropsite.)
|
||
*
|
||
* @param {number} target - The target entity ID to drop resources at.
|
||
*/
|
||
ResourceGatherer.prototype.CommitResources = function(target)
|
||
{
|
||
const cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite);
|
||
if (!cmpResourceDropsite)
|
||
return;
|
||
|
||
const change = cmpResourceDropsite.ReceiveResources(this.carrying, this.entity);
|
||
for (const type in change)
|
||
{
|
||
this.carrying[type] -= change[type];
|
||
if (this.carrying[type] == 0)
|
||
delete this.carrying[type];
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Drop all currently-carried resources.
|
||
* (Currently they just vanish after being dropped - we don't bother depositing
|
||
* them onto the ground.)
|
||
*/
|
||
ResourceGatherer.prototype.DropResources = function()
|
||
{
|
||
this.carrying = {};
|
||
};
|
||
|
||
/**
|
||
* @return {string} - A generic resource type if we were tasked to gather.
|
||
*/
|
||
ResourceGatherer.prototype.GetTaskedResourceType = function()
|
||
{
|
||
return this.taskedResourceType;
|
||
};
|
||
|
||
/**
|
||
* @param {string} type - A generic resource type.
|
||
*/
|
||
ResourceGatherer.prototype.AddToPlayerCounter = function(type)
|
||
{
|
||
// We need to be removed from the player counter first.
|
||
if (this.taskedResourceType)
|
||
return;
|
||
|
||
const cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
|
||
if (cmpPlayer)
|
||
cmpPlayer.AddResourceGatherer(type);
|
||
|
||
this.taskedResourceType = type;
|
||
};
|
||
|
||
/**
|
||
* @param {number} playerid - Optionally a player ID.
|
||
*/
|
||
ResourceGatherer.prototype.RemoveFromPlayerCounter = function(playerid)
|
||
{
|
||
if (!this.taskedResourceType)
|
||
return;
|
||
|
||
const cmpPlayer = playerid != undefined ?
|
||
QueryPlayerIDInterface(playerid) :
|
||
QueryOwnerInterface(this.entity, IID_Player);
|
||
|
||
if (cmpPlayer)
|
||
cmpPlayer.RemoveResourceGatherer(this.taskedResourceType);
|
||
|
||
delete this.taskedResourceType;
|
||
};
|
||
|
||
/**
|
||
* @param {number} - The entity ID of the target to check.
|
||
* @return {boolean} - Whether this entity is in range of its target.
|
||
*/
|
||
ResourceGatherer.prototype.IsTargetInRange = function(target)
|
||
{
|
||
return Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).
|
||
IsInTargetRange(this.entity, target, this.range.min, this.range.max, false);
|
||
};
|
||
|
||
// Since we cache gather rates, we need to make sure we update them when tech changes.
|
||
// and when our owner change because owners can had different techs.
|
||
ResourceGatherer.prototype.OnValueModification = function(msg)
|
||
{
|
||
if (msg.component != "ResourceGatherer")
|
||
return;
|
||
|
||
// eslint-disable-next-line no-irregular-whitespace
|
||
// NB: at the moment, 0 A.D. always uses the fast path, the other is mod support.
|
||
if (msg.valueNames.length === 1)
|
||
{
|
||
if (msg.valueNames[0].indexOf("Capacities") !== -1)
|
||
this.RecalculateCapacity(msg.valueNames[0].substr(28));
|
||
else
|
||
this.RecalculateGatherRates();
|
||
}
|
||
else
|
||
{
|
||
this.RecalculateGatherRates();
|
||
this.RecalculateCapacities();
|
||
}
|
||
};
|
||
|
||
ResourceGatherer.prototype.OnOwnershipChanged = function(msg)
|
||
{
|
||
if (msg.to == INVALID_PLAYER)
|
||
{
|
||
this.RemoveFromPlayerCounter(msg.from);
|
||
return;
|
||
}
|
||
if (this.lastGathered && msg.from !== INVALID_PLAYER)
|
||
{
|
||
const resource = this.taskedResourceType;
|
||
this.RemoveFromPlayerCounter(msg.from);
|
||
this.AddToPlayerCounter(resource);
|
||
}
|
||
|
||
this.RecalculateGatherRates();
|
||
this.RecalculateCapacities();
|
||
};
|
||
|
||
ResourceGatherer.prototype.OnGlobalInitGame = function(msg)
|
||
{
|
||
this.RecalculateGatherRates();
|
||
this.RecalculateCapacities();
|
||
};
|
||
|
||
ResourceGatherer.prototype.OnMultiplierChanged = function(msg)
|
||
{
|
||
const cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
|
||
if (cmpPlayer && msg.player == cmpPlayer.GetPlayerID())
|
||
this.RecalculateGatherRates();
|
||
};
|
||
|
||
Engine.RegisterComponentType(IID_ResourceGatherer, "ResourceGatherer", ResourceGatherer);
|