source: ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js@ 25243

Last change on this file since 25243 was 25243, checked in by Freagarach, 3 years ago

Minor fixes after Gathering refactoring.

  • initPos was used wrong.
  • There were two CanGather functions in cmpResourceGatherer.
  • Reduced some duplication in the ReturnResource order.

Differential revision: D3838.

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