/**
* @class
*/
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()
{
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()
{
let ret = [];
for (let 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 (let 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 (let 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 (let r in this.template.Rates)
{
let type = r.split(".");
if (!Resources.GetResource(type[0]).subtypes[type[1]])
{
error("Resource subtype not found: " + type[0] + "." + type[1]);
continue;
}
let rate = ApplyValueModificationsToEntity("ResourceGatherer/Rates/" + r, +this.template.Rates[r], this.entity);
this.rates[r] = rate * this.baseSpeed;
}
};
ResourceGatherer.prototype.RecalculateCapacities = function()
{
this.capacities = {};
for (let 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 { "max": +this.template.MaxDistance, "min": 0 };
};
/**
* @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();
let rate = this.GetTargetGatherRate(target);
if (!rate)
return false;
let cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply);
if (!cmpResourceSupply || !cmpResourceSupply.AddActiveGatherer(this.entity))
return false;
let 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();
let 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.
let timing = 1000 / rate;
this.target = target;
this.callerIID = callerIID;
let 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;
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
delete this.timer;
let cmpResourceSupply = Engine.QueryInterface(this.target, IID_ResourceSupply);
if (cmpResourceSupply)
cmpResourceSupply.RemoveGatherer(this.entity);
delete this.target;
let 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.
let callerIID = this.callerIID;
delete this.callerIID;
if (reason && callerIID)
{
let 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)
{
let 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);
let type = cmpResourceSupply.GetType();
if (!this.carrying[type.generic])
this.carrying[type.generic] = 0;
let maxGathered = this.GetCapacity(type.generic) - this.carrying[type.generic];
let 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.)
let 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)
{
let cmpResourceSupply = QueryMiragedInterface(target, IID_ResourceSupply);
if (!cmpResourceSupply || cmpResourceSupply.GetCurrentAmount() <= 0)
return 0;
let 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);
let 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)
{
let amount = this.carrying[type] || 0;
return amount < this.GetCapacity(type);
};
ResourceGatherer.prototype.IsCarrying = function(type)
{
let 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 (let 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)
{
let cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite);
if (!cmpResourceDropsite)
return false;
if (checkCarriedResource)
{
let type = this.GetMainCarryingType();
if (!type || !cmpResourceDropsite.AcceptsType(type))
return false;
}
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (cmpOwnership && IsOwnedByPlayer(cmpOwnership.GetOwner(), target))
return true;
let cmpPlayer = QueryOwnerInterface(this.entity);
return cmpPlayer && cmpPlayer.HasSharedDropsites() && cmpResourceDropsite.IsShared() &&
cmpOwnership && IsOwnedByMutualAllyOfPlayer(cmpOwnership.GetOwner(), 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)
{
let cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite);
if (!cmpResourceDropsite)
return;
let change = cmpResourceDropsite.ReceiveResources(this.carrying, this.entity);
for (let 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;
let 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;
let 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, 0, +this.template.MaxDistance, 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;
// 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)
{
let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
if (cmpPlayer && msg.player == cmpPlayer.GetPlayerID())
this.RecalculateGatherRates();
};
Engine.RegisterComponentType(IID_ResourceGatherer, "ResourceGatherer", ResourceGatherer);