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

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

Attack using cmpAttack instead of UnitAI.

Keep responsibilities separated, allow easier modding of attacking behaviour.
Ideally the BuildingAI attacking routine would be merged in here as well, but not done for now.

Part of #5810
Differential revision: D3816
Comments by: @smiley, @Stan

  • Property svn:eol-style set to native
File size: 15.3 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 * @param {number} target - The entity ID of the target to check.
334 * @return {boolean} - Whether we can gather from the target.
335 */
336ResourceGatherer.prototype.CanGather = function(target)
337{
338 return this.GetTargetGatherRate(target) > 0;
339};
340
341/**
342 * Returns whether this unit can carry more of the given type of resource.
343 * (This ignores whether the unit is actually able to gather that
344 * resource type or not.)
345 */
346ResourceGatherer.prototype.CanCarryMore = function(type)
347{
348 let amount = this.carrying[type] || 0;
349 return amount < this.GetCapacity(type);
350};
351
352
353ResourceGatherer.prototype.IsCarrying = function(type)
354{
355 let amount = this.carrying[type] || 0;
356 return amount > 0;
357};
358
359/**
360 * Returns whether this unit is carrying any resources of a type that is
361 * not the requested type. (This is to support cases where the unit is
362 * only meant to be able to carry one type at once.)
363 */
364ResourceGatherer.prototype.IsCarryingAnythingExcept = function(exceptedType)
365{
366 for (let type in this.carrying)
367 if (type != exceptedType)
368 return true;
369
370 return false;
371};
372
373/**
374 * @param {number} target - The entity to check.
375 * @param {boolean} checkCarriedResource - Whether we need to check the resource we are carrying.
376 * @return {boolean} - Whether we can return carried resources.
377 */
378ResourceGatherer.prototype.CanReturnResource = function(target, checkCarriedResource)
379{
380 let cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite);
381 if (!cmpResourceDropsite)
382 return false;
383
384 if (checkCarriedResource)
385 {
386 let type = this.GetMainCarryingType();
387 if (!type || !cmpResourceDropsite.AcceptsType(type))
388 return false;
389 }
390
391 let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
392 if (cmpOwnership && IsOwnedByPlayer(cmpOwnership.GetOwner(), target))
393 return true;
394 let cmpPlayer = QueryOwnerInterface(this.entity);
395 return cmpPlayer && cmpPlayer.HasSharedDropsites() && cmpResourceDropsite.IsShared() &&
396 cmpOwnership && IsOwnedByMutualAllyOfPlayer(cmpOwnership.GetOwner(), target);
397};
398
399/**
400 * Transfer our carried resources to our owner immediately.
401 * Only resources of the appropriate types will be transferred.
402 * (This should typically be called after reaching a dropsite.)
403 *
404 * @param {number} target - The target entity ID to drop resources at.
405 */
406ResourceGatherer.prototype.CommitResources = function(target)
407{
408 let cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite);
409 if (!cmpResourceDropsite)
410 return;
411
412 let change = cmpResourceDropsite.ReceiveResources(this.carrying, this.entity);
413 let changed = false;
414 for (let type in change)
415 {
416 this.carrying[type] -= change[type];
417 if (this.carrying[type] == 0)
418 delete this.carrying[type];
419 changed = true;
420 }
421
422 if (changed)
423 Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() });
424};
425
426/**
427 * Drop all currently-carried resources.
428 * (Currently they just vanish after being dropped - we don't bother depositing
429 * them onto the ground.)
430 */
431ResourceGatherer.prototype.DropResources = function()
432{
433 this.carrying = {};
434
435 Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() });
436};
437
438
439/**
440 * @param {string} type - A generic resource type.
441 */
442ResourceGatherer.prototype.AddToPlayerCounter = function(type)
443{
444 // We need to be removed from the player counter first.
445 if (this.lastGathered)
446 return;
447
448 let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
449 if (cmpPlayer)
450 cmpPlayer.AddResourceGatherer(type);
451
452 this.lastGathered = type;
453};
454
455/**
456 * @param {number} playerid - Optionally a player ID.
457 */
458ResourceGatherer.prototype.RemoveFromPlayerCounter = function(playerid)
459{
460 if (!this.lastGathered)
461 return;
462
463 let cmpPlayer = playerid != undefined ?
464 QueryPlayerIDInterface(playerid) :
465 QueryOwnerInterface(this.entity, IID_Player);
466
467 if (cmpPlayer)
468 cmpPlayer.RemoveResourceGatherer(this.lastGathered);
469
470 delete this.lastGathered;
471};
472
473/**
474 * @param {number} - The entity ID of the target to check.
475 * @return {boolean} - Whether this entity is in range of its target.
476 */
477ResourceGatherer.prototype.IsTargetInRange = function(target)
478{
479 return Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).
480 IsInTargetRange(this.entity, target, 0, +this.template.MaxDistance, false);
481};
482
483// Since we cache gather rates, we need to make sure we update them when tech changes.
484// and when our owner change because owners can had different techs.
485ResourceGatherer.prototype.OnValueModification = function(msg)
486{
487 if (msg.component != "ResourceGatherer")
488 return;
489
490 // NB: at the moment, 0 A.D. always uses the fast path, the other is mod support.
491 if (msg.valueNames.length === 1)
492 {
493 if (msg.valueNames[0].indexOf("Capacities") !== -1)
494 this.RecalculateCapacity(msg.valueNames[0].substr(28));
495 else
496 this.RecalculateGatherRates();
497 }
498 else
499 {
500 this.RecalculateGatherRates();
501 this.RecalculateCapacities();
502 }
503};
504
505ResourceGatherer.prototype.OnOwnershipChanged = function(msg)
506{
507 if (msg.to == INVALID_PLAYER)
508 {
509 this.RemoveFromPlayerCounter(msg.from);
510 return;
511 }
512
513 this.RecalculateGatherRates();
514 this.RecalculateCapacities();
515};
516
517ResourceGatherer.prototype.OnGlobalInitGame = function(msg)
518{
519 this.RecalculateGatherRates();
520 this.RecalculateCapacities();
521};
522
523ResourceGatherer.prototype.OnMultiplierChanged = function(msg)
524{
525 let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
526 if (cmpPlayer && msg.player == cmpPlayer.GetPlayerID())
527 this.RecalculateGatherRates();
528};
529
530Engine.RegisterComponentType(IID_ResourceGatherer, "ResourceGatherer", ResourceGatherer);
Note: See TracBrowser for help on using the repository browser.