Line data Source code
1 : function ResourceGatherer() {}
2 :
3 2 : ResourceGatherer.prototype.Schema =
4 : "<a:help>Lets the unit gather resources from entities that have the ResourceSupply component.</a:help>" +
5 : "<a:example>" +
6 : "<MaxDistance>2.0</MaxDistance>" +
7 : "<BaseSpeed>1.0</BaseSpeed>" +
8 : "<Rates>" +
9 : "<food.fish>1</food.fish>" +
10 : "<metal.ore>3</metal.ore>" +
11 : "<stone.rock>3</stone.rock>" +
12 : "<wood.tree>2</wood.tree>" +
13 : "</Rates>" +
14 : "<Capacities>" +
15 : "<food>10</food>" +
16 : "<metal>10</metal>" +
17 : "<stone>10</stone>" +
18 : "<wood>10</wood>" +
19 : "</Capacities>" +
20 : "</a:example>" +
21 : "<element name='MaxDistance' a:help='Max resource-gathering distance'>" +
22 : "<ref name='positiveDecimal'/>" +
23 : "</element>" +
24 : "<element name='BaseSpeed' a:help='Base resource-gathering rate (in resource units per second)'>" +
25 : "<ref name='positiveDecimal'/>" +
26 : "</element>" +
27 : "<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'>" +
28 : Resources.BuildSchema("positiveDecimal", [], true) +
29 : "</element>" +
30 : "<element name='Capacities' a:help='Per-resource-type maximum carrying capacity'>" +
31 : Resources.BuildSchema("positiveDecimal") +
32 : "</element>";
33 :
34 : /*
35 : * Call interval will be determined by gather rate,
36 : * so always gather integer amount.
37 : */
38 2 : ResourceGatherer.prototype.GATHER_AMOUNT = 1;
39 :
40 2 : ResourceGatherer.prototype.Init = function()
41 : {
42 7 : this.capacities = {};
43 7 : this.carrying = {}; // { generic type: integer amount currently carried }
44 : // (Note that this component supports carrying multiple types of resources,
45 : // each with an independent capacity, but the rest of the game currently
46 : // ensures and assumes we'll only be carrying one type at once)
47 :
48 : // The last exact type gathered, so we can render appropriate props
49 7 : this.lastCarriedType = undefined; // { generic, specific }
50 : };
51 :
52 : /**
53 : * Returns data about what resources the unit is currently carrying,
54 : * in the form [ {"type":"wood", "amount":7, "max":10} ]
55 : */
56 2 : ResourceGatherer.prototype.GetCarryingStatus = function()
57 : {
58 22 : let ret = [];
59 22 : for (let type in this.carrying)
60 : {
61 17 : ret.push({
62 : "type": type,
63 : "amount": this.carrying[type],
64 : "max": +this.GetCapacity(type)
65 : });
66 : }
67 22 : return ret;
68 : };
69 :
70 : /**
71 : * Used to instantly give resources to unit
72 : * @param resources The same structure as returned form GetCarryingStatus
73 : */
74 2 : ResourceGatherer.prototype.GiveResources = function(resources)
75 : {
76 4 : for (let resource of resources)
77 5 : this.carrying[resource.type] = +resource.amount;
78 : };
79 :
80 : /**
81 : * Returns the generic type of one particular resource this unit is
82 : * currently carrying, or undefined if none.
83 : */
84 2 : ResourceGatherer.prototype.GetMainCarryingType = function()
85 : {
86 : // Return the first key, if any
87 1 : for (let type in this.carrying)
88 1 : return type;
89 :
90 0 : return undefined;
91 : };
92 :
93 : /**
94 : * Returns the exact resource type we last picked up, as long as
95 : * we're still carrying something similar enough, in the form
96 : * { generic, specific }
97 : */
98 2 : ResourceGatherer.prototype.GetLastCarriedType = function()
99 : {
100 1 : if (this.lastCarriedType && this.lastCarriedType.generic in this.carrying)
101 1 : return this.lastCarriedType;
102 :
103 0 : return undefined;
104 : };
105 :
106 2 : ResourceGatherer.prototype.SetLastCarriedType = function(lastCarriedType)
107 : {
108 0 : this.lastCarriedType = lastCarriedType;
109 : };
110 :
111 : // Since this code is very performancecritical and applying technologies quite slow, cache it.
112 2 : ResourceGatherer.prototype.RecalculateGatherRates = function()
113 : {
114 7 : this.baseSpeed = ApplyValueModificationsToEntity("ResourceGatherer/BaseSpeed", +this.template.BaseSpeed, this.entity);
115 :
116 7 : this.rates = {};
117 7 : for (let r in this.template.Rates)
118 : {
119 13 : let type = r.split(".");
120 :
121 13 : if (!Resources.GetResource(type[0]).subtypes[type[1]])
122 : {
123 0 : error("Resource subtype not found: " + type[0] + "." + type[1]);
124 0 : continue;
125 : }
126 :
127 13 : let rate = ApplyValueModificationsToEntity("ResourceGatherer/Rates/" + r, +this.template.Rates[r], this.entity);
128 13 : this.rates[r] = rate * this.baseSpeed;
129 : }
130 : };
131 :
132 2 : ResourceGatherer.prototype.RecalculateCapacities = function()
133 : {
134 7 : this.capacities = {};
135 7 : for (let r in this.template.Capacities)
136 13 : this.capacities[r] = ApplyValueModificationsToEntity("ResourceGatherer/Capacities/" + r, +this.template.Capacities[r], this.entity);
137 : };
138 :
139 2 : ResourceGatherer.prototype.RecalculateCapacity = function(type)
140 : {
141 0 : if (type in this.capacities)
142 0 : this.capacities[type] = ApplyValueModificationsToEntity("ResourceGatherer/Capacities/" + type, +this.template.Capacities[type], this.entity);
143 : };
144 :
145 2 : ResourceGatherer.prototype.GetGatherRates = function()
146 : {
147 0 : return this.rates;
148 : };
149 :
150 2 : ResourceGatherer.prototype.GetGatherRate = function(resourceType)
151 : {
152 12 : if (!this.template.Rates[resourceType])
153 3 : return 0;
154 :
155 9 : return this.rates[resourceType];
156 : };
157 :
158 2 : ResourceGatherer.prototype.GetCapacity = function(resourceType)
159 : {
160 64 : if (!this.template.Capacities[resourceType])
161 1 : return 0;
162 63 : return this.capacities[resourceType];
163 : };
164 :
165 2 : ResourceGatherer.prototype.GetRange = function()
166 : {
167 0 : return { "max": +this.template.MaxDistance, "min": 0 };
168 : };
169 :
170 : /**
171 : * @param {number} target - The target to gather from.
172 : * @param {number} callerIID - The IID to notify on specific events.
173 : * @return {boolean} - Whether we started gathering.
174 : */
175 2 : ResourceGatherer.prototype.StartGathering = function(target, callerIID)
176 : {
177 12 : if (this.target)
178 1 : this.StopGathering();
179 :
180 12 : let rate = this.GetTargetGatherRate(target);
181 12 : if (!rate)
182 3 : return false;
183 :
184 9 : let cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply);
185 9 : if (!cmpResourceSupply || !cmpResourceSupply.AddActiveGatherer(this.entity))
186 0 : return false;
187 :
188 9 : let resourceType = cmpResourceSupply.GetType();
189 :
190 : // If we've already got some resources but they're the wrong type,
191 : // drop them first to ensure we're only ever carrying one type.
192 9 : if (this.IsCarryingAnythingExcept(resourceType.generic))
193 1 : this.DropResources();
194 :
195 9 : let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
196 9 : if (cmpVisual)
197 0 : cmpVisual.SelectAnimation("gather_" + resourceType.specific, false, 1.0);
198 :
199 : // Calculate timing based on gather rates.
200 : // This allows the gather rate to control how often we gather, instead of how much.
201 9 : let timing = 1000 / rate;
202 :
203 9 : this.target = target;
204 9 : this.callerIID = callerIID;
205 :
206 9 : let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
207 9 : this.timer = cmpTimer.SetInterval(this.entity, IID_ResourceGatherer, "PerformGather", timing, timing, null);
208 :
209 9 : return true;
210 : };
211 :
212 : /**
213 : * @param {string} reason - The reason why we stopped gathering used to notify the caller.
214 : */
215 2 : ResourceGatherer.prototype.StopGathering = function(reason)
216 : {
217 7 : if (!this.target)
218 0 : return;
219 :
220 7 : let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
221 7 : cmpTimer.CancelTimer(this.timer);
222 7 : delete this.timer;
223 :
224 7 : let cmpResourceSupply = Engine.QueryInterface(this.target, IID_ResourceSupply);
225 7 : if (cmpResourceSupply)
226 7 : cmpResourceSupply.RemoveGatherer(this.entity);
227 :
228 7 : delete this.target;
229 :
230 7 : let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
231 7 : if (cmpVisual)
232 0 : cmpVisual.SelectAnimation("idle", false, 1.0);
233 :
234 : // The callerIID component may start again,
235 : // replacing the callerIID, hence save that.
236 7 : let callerIID = this.callerIID;
237 7 : delete this.callerIID;
238 :
239 7 : if (reason && callerIID)
240 : {
241 0 : let component = Engine.QueryInterface(this.entity, callerIID);
242 0 : if (component)
243 0 : component.ProcessMessage(reason, null);
244 : }
245 : };
246 :
247 : /**
248 : * Gather from our target entity.
249 : * @params - data and lateness are unused.
250 : */
251 2 : ResourceGatherer.prototype.PerformGather = function(data, lateness)
252 : {
253 22 : let cmpResourceSupply = Engine.QueryInterface(this.target, IID_ResourceSupply);
254 22 : if (!cmpResourceSupply || cmpResourceSupply.GetCurrentAmount() <= 0)
255 : {
256 0 : this.StopGathering("TargetInvalidated");
257 0 : return;
258 : }
259 :
260 22 : if (!this.IsTargetInRange(this.target))
261 : {
262 0 : this.StopGathering("OutOfRange");
263 0 : return;
264 : }
265 :
266 : // ToDo: Enable entities to keep facing a target.
267 22 : Engine.QueryInterface(this.entity, IID_UnitAI)?.FaceTowardsTarget(this.target);
268 :
269 22 : let type = cmpResourceSupply.GetType();
270 22 : if (!this.carrying[type.generic])
271 8 : this.carrying[type.generic] = 0;
272 :
273 22 : let maxGathered = this.GetCapacity(type.generic) - this.carrying[type.generic];
274 22 : let status = cmpResourceSupply.TakeResources(Math.min(this.GATHER_AMOUNT, maxGathered));
275 22 : this.carrying[type.generic] += status.amount;
276 22 : this.lastCarriedType = type;
277 :
278 : // Update stats of how much the player collected.
279 : // (We have to do it here rather than at the dropsite, because we
280 : // need to know what subtype it was.)
281 22 : let cmpStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
282 22 : if (cmpStatisticsTracker)
283 0 : cmpStatisticsTracker.IncreaseResourceGatheredCounter(type.generic, status.amount, type.specific);
284 :
285 22 : if (!this.CanCarryMore(type.generic))
286 1 : this.StopGathering("InventoryFilled");
287 21 : else if (status.exhausted)
288 1 : this.StopGathering("TargetInvalidated");
289 : };
290 :
291 : /**
292 : * Compute the amount of resources collected per second from the target.
293 : * Returns 0 if resources cannot be collected (e.g. the target doesn't
294 : * exist, or is the wrong type).
295 : */
296 2 : ResourceGatherer.prototype.GetTargetGatherRate = function(target)
297 : {
298 12 : let cmpResourceSupply = QueryMiragedInterface(target, IID_ResourceSupply);
299 12 : if (!cmpResourceSupply || cmpResourceSupply.GetCurrentAmount() <= 0)
300 0 : return 0;
301 :
302 12 : let type = cmpResourceSupply.GetType();
303 :
304 12 : let rate = 0;
305 12 : if (type.specific)
306 9 : rate = this.GetGatherRate(type.generic + "." + type.specific);
307 12 : if (rate == 0 && type.generic)
308 3 : rate = this.GetGatherRate(type.generic);
309 :
310 12 : let diminishingReturns = cmpResourceSupply.GetDiminishingReturns();
311 12 : if (diminishingReturns)
312 10 : rate *= diminishingReturns;
313 :
314 12 : return rate;
315 : };
316 :
317 : /**
318 : * @param {number} target - The entity ID of the target to check.
319 : * @return {boolean} - Whether we can gather from the target.
320 : */
321 2 : ResourceGatherer.prototype.CanGather = function(target)
322 : {
323 0 : return this.GetTargetGatherRate(target) > 0;
324 : };
325 :
326 : /**
327 : * Returns whether this unit can carry more of the given type of resource.
328 : * (This ignores whether the unit is actually able to gather that
329 : * resource type or not.)
330 : */
331 2 : ResourceGatherer.prototype.CanCarryMore = function(type)
332 : {
333 25 : let amount = this.carrying[type] || 0;
334 25 : return amount < this.GetCapacity(type);
335 : };
336 :
337 :
338 2 : ResourceGatherer.prototype.IsCarrying = function(type)
339 : {
340 3 : let amount = this.carrying[type] || 0;
341 3 : return amount > 0;
342 : };
343 :
344 : /**
345 : * Returns whether this unit is carrying any resources of a type that is
346 : * not the requested type. (This is to support cases where the unit is
347 : * only meant to be able to carry one type at once.)
348 : */
349 2 : ResourceGatherer.prototype.IsCarryingAnythingExcept = function(exceptedType)
350 : {
351 9 : for (let type in this.carrying)
352 2 : if (type != exceptedType)
353 1 : return true;
354 :
355 8 : return false;
356 : };
357 :
358 : /**
359 : * @param {number} target - The entity to check.
360 : * @param {boolean} checkCarriedResource - Whether we need to check the resource we are carrying.
361 : * @return {boolean} - Whether we can return carried resources.
362 : */
363 2 : ResourceGatherer.prototype.CanReturnResource = function(target, checkCarriedResource)
364 : {
365 0 : let cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite);
366 0 : if (!cmpResourceDropsite)
367 0 : return false;
368 :
369 0 : if (checkCarriedResource)
370 : {
371 0 : let type = this.GetMainCarryingType();
372 0 : if (!type || !cmpResourceDropsite.AcceptsType(type))
373 0 : return false;
374 : }
375 :
376 0 : let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
377 0 : if (cmpOwnership && IsOwnedByPlayer(cmpOwnership.GetOwner(), target))
378 0 : return true;
379 0 : let cmpPlayer = QueryOwnerInterface(this.entity);
380 0 : return cmpPlayer && cmpPlayer.HasSharedDropsites() && cmpResourceDropsite.IsShared() &&
381 : cmpOwnership && IsOwnedByMutualAllyOfPlayer(cmpOwnership.GetOwner(), target);
382 : };
383 :
384 : /**
385 : * Transfer our carried resources to our owner immediately.
386 : * Only resources of the appropriate types will be transferred.
387 : * (This should typically be called after reaching a dropsite.)
388 : *
389 : * @param {number} target - The target entity ID to drop resources at.
390 : */
391 2 : ResourceGatherer.prototype.CommitResources = function(target)
392 : {
393 4 : let cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite);
394 4 : if (!cmpResourceDropsite)
395 0 : return;
396 :
397 4 : let change = cmpResourceDropsite.ReceiveResources(this.carrying, this.entity);
398 4 : for (let type in change)
399 : {
400 4 : this.carrying[type] -= change[type];
401 4 : if (this.carrying[type] == 0)
402 4 : delete this.carrying[type];
403 : }
404 : };
405 :
406 : /**
407 : * Drop all currently-carried resources.
408 : * (Currently they just vanish after being dropped - we don't bother depositing
409 : * them onto the ground.)
410 : */
411 2 : ResourceGatherer.prototype.DropResources = function()
412 : {
413 3 : this.carrying = {};
414 : };
415 :
416 : /**
417 : * @return {string} - A generic resource type if we were tasked to gather.
418 : */
419 2 : ResourceGatherer.prototype.GetTaskedResourceType = function()
420 : {
421 0 : return this.taskedResourceType;
422 : };
423 :
424 : /**
425 : * @param {string} type - A generic resource type.
426 : */
427 2 : ResourceGatherer.prototype.AddToPlayerCounter = function(type)
428 : {
429 : // We need to be removed from the player counter first.
430 0 : if (this.taskedResourceType)
431 0 : return;
432 :
433 0 : let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
434 0 : if (cmpPlayer)
435 0 : cmpPlayer.AddResourceGatherer(type);
436 :
437 0 : this.taskedResourceType = type;
438 : };
439 :
440 : /**
441 : * @param {number} playerid - Optionally a player ID.
442 : */
443 2 : ResourceGatherer.prototype.RemoveFromPlayerCounter = function(playerid)
444 : {
445 0 : if (!this.taskedResourceType)
446 0 : return;
447 :
448 0 : let cmpPlayer = playerid != undefined ?
449 : QueryPlayerIDInterface(playerid) :
450 : QueryOwnerInterface(this.entity, IID_Player);
451 :
452 0 : if (cmpPlayer)
453 0 : cmpPlayer.RemoveResourceGatherer(this.taskedResourceType);
454 :
455 0 : delete this.taskedResourceType;
456 : };
457 :
458 : /**
459 : * @param {number} - The entity ID of the target to check.
460 : * @return {boolean} - Whether this entity is in range of its target.
461 : */
462 2 : ResourceGatherer.prototype.IsTargetInRange = function(target)
463 : {
464 22 : return Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).
465 : IsInTargetRange(this.entity, target, 0, +this.template.MaxDistance, false);
466 : };
467 :
468 : // Since we cache gather rates, we need to make sure we update them when tech changes.
469 : // and when our owner change because owners can had different techs.
470 2 : ResourceGatherer.prototype.OnValueModification = function(msg)
471 : {
472 0 : if (msg.component != "ResourceGatherer")
473 0 : return;
474 :
475 : // NB: at the moment, 0 A.D. always uses the fast path, the other is mod support.
476 0 : if (msg.valueNames.length === 1)
477 : {
478 0 : if (msg.valueNames[0].indexOf("Capacities") !== -1)
479 0 : this.RecalculateCapacity(msg.valueNames[0].substr(28));
480 : else
481 0 : this.RecalculateGatherRates();
482 : }
483 : else
484 : {
485 0 : this.RecalculateGatherRates();
486 0 : this.RecalculateCapacities();
487 : }
488 : };
489 :
490 2 : ResourceGatherer.prototype.OnOwnershipChanged = function(msg)
491 : {
492 0 : if (msg.to == INVALID_PLAYER)
493 : {
494 0 : this.RemoveFromPlayerCounter(msg.from);
495 0 : return;
496 : }
497 0 : if (this.lastGathered && msg.from !== INVALID_PLAYER)
498 : {
499 0 : const resource = this.taskedResourceType;
500 0 : this.RemoveFromPlayerCounter(msg.from);
501 0 : this.AddToPlayerCounter(resource);
502 : }
503 :
504 0 : this.RecalculateGatherRates();
505 0 : this.RecalculateCapacities();
506 : };
507 :
508 2 : ResourceGatherer.prototype.OnGlobalInitGame = function(msg)
509 : {
510 6 : this.RecalculateGatherRates();
511 6 : this.RecalculateCapacities();
512 : };
513 :
514 2 : ResourceGatherer.prototype.OnMultiplierChanged = function(msg)
515 : {
516 0 : let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
517 0 : if (cmpPlayer && msg.player == cmpPlayer.GetPlayerID())
518 0 : this.RecalculateGatherRates();
519 : };
520 :
521 2 : Engine.RegisterComponentType(IID_ResourceGatherer, "ResourceGatherer", ResourceGatherer);
|