Line data Source code
1 : function ResourceSupply() {} 2 : 3 2 : ResourceSupply.prototype.Schema = 4 : "<a:help>Provides a supply of one particular type of resource.</a:help>" + 5 : "<a:example>" + 6 : "<Max>1000</Max>" + 7 : "<Initial>1000</Initial>" + 8 : "<Type>food.meat</Type>" + 9 : "<KillBeforeGather>false</KillBeforeGather>" + 10 : "<MaxGatherers>25</MaxGatherers>" + 11 : "<DiminishingReturns>0.8</DiminishingReturns>" + 12 : "<Change>" + 13 : "<AnyName>" + 14 : "<Value>2</Value>" + 15 : "<Interval>1000</Interval>" + 16 : "</AnyName>" + 17 : "<Growth>" + 18 : "<State>alive</State>" + 19 : "<Value>2</Value>" + 20 : "<Interval>1000</Interval>" + 21 : "<UpperLimit>500</UpperLimit>" + 22 : "</Growth>" + 23 : "<Rotting>" + 24 : "<State>dead notGathered</State>" + 25 : "<Value>-2</Value>" + 26 : "<Interval>1000</Interval>" + 27 : "</Rotting>" + 28 : "<Decay>" + 29 : "<State>dead</State>" + 30 : "<Value>-1</Value>" + 31 : "<Interval>1000</Interval>" + 32 : "<LowerLimit>500</LowerLimit>" + 33 : "</Decay>" + 34 : "</Change>" + 35 : "</a:example>" + 36 : "<element name='KillBeforeGather' a:help='Whether this entity must be killed (health reduced to 0) before its resources can be gathered'>" + 37 : "<data type='boolean'/>" + 38 : "</element>" + 39 : "<element name='Max' a:help='Max amount of resources available from this entity.'>" + 40 : "<choice><data type='nonNegativeInteger'/><value>Infinity</value></choice>" + 41 : "</element>" + 42 : "<optional>" + 43 : "<element name='Initial' a:help='Initial amount of resources available from this entity, if this is not specified, Max is used.'>" + 44 : "<choice><data type='nonNegativeInteger'/><value>Infinity</value></choice>" + 45 : "</element>" + 46 : "</optional>" + 47 : "<element name='Type' a:help='Type and Subtype of resource available from this entity'>" + 48 : Resources.BuildChoicesSchema(true) + 49 : "</element>" + 50 : "<element name='MaxGatherers' a:help='Amount of gatherers who can gather resources from this entity at the same time'>" + 51 : "<data type='nonNegativeInteger'/>" + 52 : "</element>" + 53 : "<optional>" + 54 : "<element name='DiminishingReturns' a:help='The relative rate of any new gatherer compared to the previous one (geometric sequence). Leave the element out for no diminishing returns.'>" + 55 : "<ref name='positiveDecimal'/>" + 56 : "</element>" + 57 : "</optional>" + 58 : "<optional>" + 59 : "<element name='Change' a:help='Optional element containing all the modifications that affects a resource supply'>" + 60 : "<oneOrMore>" + 61 : "<element a:help='Element defining whether and how a resource supply regenerates or decays'>" + 62 : "<anyName/>" + 63 : "<interleave>" + 64 : "<element name='Value' a:help='The amount of resource added per interval.'>" + 65 : "<data type='integer'/>" + 66 : "</element>" + 67 : "<element name='Interval' a:help='The interval in milliseconds.'>" + 68 : "<data type='positiveInteger'/>" + 69 : "</element>" + 70 : "<optional>" + 71 : "<element name='UpperLimit' a:help='The upper limit of the value after which the Change has no effect.'>" + 72 : "<data type='nonNegativeInteger'/>" + 73 : "</element>" + 74 : "</optional>" + 75 : "<optional>" + 76 : "<element name='LowerLimit' a:help='The bottom limit of the value after which the Change has no effect.'>" + 77 : "<data type='nonNegativeInteger'/>" + 78 : "</element>" + 79 : "</optional>" + 80 : "<optional>" + 81 : "<element name='State' a:help='What state the entity must be in for the effect to occur.'>" + 82 : "<list>" + 83 : "<oneOrMore>" + 84 : "<choice>" + 85 : "<value>alive</value>" + 86 : "<value>dead</value>" + 87 : "<value>gathered</value>" + 88 : "<value>notGathered</value>" + 89 : "</choice>" + 90 : "</oneOrMore>" + 91 : "</list>" + 92 : "</element>" + 93 : "</optional>" + 94 : "</interleave>" + 95 : "</element>" + 96 : "</oneOrMore>" + 97 : "</element>" + 98 : "</optional>"; 99 : 100 2 : ResourceSupply.prototype.Init = function() 101 : { 102 29 : this.amount = +(this.template.Initial || this.template.Max); 103 : 104 : // Includes the ones that are tasked but not here yet, i.e. approaching. 105 29 : this.gatherers = []; 106 29 : this.activeGatherers = []; 107 : 108 29 : let [type, subtype] = this.template.Type.split('.'); 109 29 : this.cachedType = { "generic": type, "specific": subtype }; 110 : 111 29 : if (this.template.Change) 112 : { 113 26 : this.timers = {}; 114 26 : this.cachedChanges = {}; 115 : } 116 : }; 117 : 118 2 : ResourceSupply.prototype.IsInfinite = function() 119 : { 120 148 : return !isFinite(+this.template.Max); 121 : }; 122 : 123 2 : ResourceSupply.prototype.GetKillBeforeGather = function() 124 : { 125 1 : return this.template.KillBeforeGather == "true"; 126 : }; 127 : 128 2 : ResourceSupply.prototype.GetMaxAmount = function() 129 : { 130 70 : return this.maxAmount; 131 : }; 132 : 133 2 : ResourceSupply.prototype.GetCurrentAmount = function() 134 : { 135 105 : return this.amount; 136 : }; 137 : 138 2 : ResourceSupply.prototype.GetMaxGatherers = function() 139 : { 140 19 : return +this.template.MaxGatherers; 141 : }; 142 : 143 2 : ResourceSupply.prototype.GetNumGatherers = function() 144 : { 145 11 : return this.gatherers.length; 146 : }; 147 : 148 : /** 149 : * @return {number} - The number of currently active gatherers. 150 : */ 151 2 : ResourceSupply.prototype.GetNumActiveGatherers = function() 152 : { 153 43 : return this.activeGatherers.length; 154 : }; 155 : 156 : /** 157 : * @return {{ "generic": string, "specific": string }} An object containing the subtype and the generic type. All resources must have both. 158 : */ 159 2 : ResourceSupply.prototype.GetType = function() 160 : { 161 6 : return this.cachedType; 162 : }; 163 : 164 : /** 165 : * @param {number} gathererID - The gatherer's entity id. 166 : * @return {boolean} - Whether the ResourceSupply can have this additional gatherer or it is already gathering. 167 : */ 168 2 : ResourceSupply.prototype.IsAvailableTo = function(gathererID) 169 : { 170 5 : return this.IsAvailable() || this.IsGatheringUs(gathererID); 171 : }; 172 : 173 : /** 174 : * @return {boolean} - Whether this entity can have an additional gatherer. 175 : */ 176 2 : ResourceSupply.prototype.IsAvailable = function() 177 : { 178 19 : return this.amount && this.gatherers.length < this.GetMaxGatherers(); 179 : }; 180 : 181 : /** 182 : * @param {number} entity - The entityID to check for. 183 : * @return {boolean} - Whether the given entity is already gathering at us. 184 : */ 185 2 : ResourceSupply.prototype.IsGatheringUs = function(entity) 186 : { 187 14 : return this.gatherers.indexOf(entity) !== -1; 188 : }; 189 : 190 : /** 191 : * Each additional gatherer decreases the rate following a geometric sequence, with diminishingReturns as ratio. 192 : * @return {number} The diminishing return if any, null otherwise. 193 : */ 194 2 : ResourceSupply.prototype.GetDiminishingReturns = function() 195 : { 196 3 : if (!this.template.DiminishingReturns) 197 3 : return null; 198 : 199 0 : let diminishingReturns = ApplyValueModificationsToEntity("ResourceSupply/DiminishingReturns", +this.template.DiminishingReturns, this.entity); 200 0 : if (!diminishingReturns) 201 0 : return null; 202 : 203 0 : let numGatherers = this.GetNumGatherers(); 204 0 : if (numGatherers > 1) 205 0 : return diminishingReturns == 1 ? 1 : (1 - Math.pow(diminishingReturns, numGatherers)) / (1 - diminishingReturns) / numGatherers; 206 : 207 0 : return null; 208 : }; 209 : 210 : /** 211 : * @param {number} amount The amount of resources that should be taken from the resource supply. The amount must be positive. 212 : * @return {{ "amount": number, "exhausted": boolean }} The current resource amount in the entity and whether it's exhausted or not. 213 : */ 214 2 : ResourceSupply.prototype.TakeResources = function(amount) 215 : { 216 7 : if (this.IsInfinite()) 217 1 : return { "amount": amount, "exhausted": false }; 218 : 219 6 : return { 220 : "amount": Math.abs(this.Change(-amount)), 221 : "exhausted": this.amount == 0 222 : }; 223 : }; 224 : 225 : /** 226 : * @param {number} change - The amount to change the resources with (can be negative). 227 : * @return {number} - The actual change in resourceSupply. 228 : */ 229 2 : ResourceSupply.prototype.Change = function(change) 230 : { 231 : // Before changing the amount, activate Fogging if necessary to hide changes 232 80 : let cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging); 233 80 : if (cmpFogging) 234 4 : cmpFogging.Activate(); 235 : 236 80 : let oldAmount = this.amount; 237 80 : this.amount = Math.min(Math.max(oldAmount + change, 0), this.maxAmount); 238 : 239 : // Remove entities that have been exhausted. 240 80 : if (this.amount == 0) 241 1 : Engine.DestroyEntity(this.entity); 242 : 243 80 : let actualChange = this.amount - oldAmount; 244 80 : if (actualChange != 0) 245 : { 246 79 : Engine.PostMessage(this.entity, MT_ResourceSupplyChanged, { 247 : "from": oldAmount, 248 : "to": this.amount 249 : }); 250 79 : this.CheckTimers(); 251 : } 252 80 : return actualChange; 253 : }; 254 : 255 : /** 256 : * @param {number} newValue - The value to set the current amount to. 257 : */ 258 2 : ResourceSupply.prototype.SetAmount = function(newValue) 259 : { 260 : // We currently don't support changing to or fro Infinity. 261 1 : if (this.IsInfinite() || newValue === Infinity) 262 1 : return; 263 0 : this.Change(newValue - this.amount); 264 : }; 265 : 266 : /** 267 : * @param {number} gathererID - The gatherer to add. 268 : * @return {boolean} - Whether the gatherer was successfully added to the entity's gatherers list 269 : * or the entity was already gathering us. 270 : */ 271 2 : ResourceSupply.prototype.AddGatherer = function(gathererID) 272 : { 273 13 : if (!this.IsAvailable()) 274 2 : return false; 275 : 276 11 : if (this.IsGatheringUs(gathererID)) 277 0 : return true; 278 : 279 11 : this.gatherers.push(gathererID); 280 : 281 11 : return true; 282 : }; 283 : 284 : /** 285 : * @param {number} player - The playerID owning the gatherer. 286 : * @param {number} entity - The entityID gathering. 287 : * 288 : * @return {boolean} - Whether the gatherer was successfully added to the active-gatherers list 289 : * or the entity was already in that list. 290 : */ 291 2 : ResourceSupply.prototype.AddActiveGatherer = function(entity) 292 : { 293 9 : if (!this.AddGatherer(entity)) 294 0 : return false; 295 : 296 9 : if (this.activeGatherers.indexOf(entity) == -1) 297 : { 298 9 : this.activeGatherers.push(entity); 299 9 : this.CheckTimers(); 300 : } 301 9 : return true; 302 : }; 303 : 304 : /** 305 : * @param {number} gathererID - The gatherer's entity id. 306 : * @todo: Should this return false if the gatherer didn't gather from said resource? 307 : */ 308 2 : ResourceSupply.prototype.RemoveGatherer = function(gathererID) 309 : { 310 11 : let index = this.gatherers.indexOf(gathererID); 311 11 : if (index != -1) 312 11 : this.gatherers.splice(index, 1); 313 : 314 11 : index = this.activeGatherers.indexOf(gathererID); 315 11 : if (index == -1) 316 2 : return; 317 9 : this.activeGatherers.splice(index, 1); 318 9 : this.CheckTimers(); 319 : }; 320 : 321 : /** 322 : * Checks whether a timer ought to be added or removed. 323 : */ 324 2 : ResourceSupply.prototype.CheckTimers = function() 325 : { 326 124 : if (!this.template.Change || this.IsInfinite()) 327 12 : return; 328 : 329 112 : for (let changeKey in this.template.Change) 330 : { 331 135 : if (!this.CheckState(changeKey)) 332 : { 333 16 : this.StopTimer(changeKey); 334 16 : continue; 335 : } 336 119 : let template = this.template.Change[changeKey]; 337 119 : if (this.amount < +(template.LowerLimit || -1) || 338 : this.amount > +(template.UpperLimit || this.GetMaxAmount())) 339 : { 340 18 : this.StopTimer(changeKey); 341 18 : continue; 342 : } 343 : 344 101 : if (this.cachedChanges[changeKey] == 0) 345 : { 346 0 : this.StopTimer(changeKey); 347 0 : continue; 348 : } 349 : 350 101 : if (!this.timers[changeKey]) 351 26 : this.StartTimer(changeKey); 352 : } 353 : }; 354 : 355 : /** 356 : * This verifies whether the current state of the supply matches the ones needed 357 : * for the specific timer to run. 358 : * 359 : * @param {string} changeKey - The name of the Change to verify the state for. 360 : * @return {boolean} - Whether the timer may run. 361 : */ 362 2 : ResourceSupply.prototype.CheckState = function(changeKey) 363 : { 364 135 : let template = this.template.Change[changeKey]; 365 135 : if (!template.State) 366 81 : return true; 367 : 368 54 : let states = template.State; 369 54 : let cmpHealth = Engine.QueryInterface(this.entity, IID_Health); 370 54 : if (states.indexOf("alive") != -1 && !cmpHealth && states.indexOf("dead") == -1 || 371 : states.indexOf("dead") != -1 && cmpHealth && states.indexOf("alive") == -1) 372 11 : return false; 373 : 374 43 : let activeGatherers = this.GetNumActiveGatherers(); 375 43 : if (states.indexOf("gathered") != -1 && activeGatherers == 0 && states.indexOf("notGathered") == -1 || 376 : states.indexOf("notGathered") != -1 && activeGatherers > 0 && states.indexOf("gathered") == -1) 377 5 : return false; 378 : 379 38 : return true; 380 : }; 381 : 382 : /** 383 : * @param {string} changeKey - The name of the Change to apply to the entity. 384 : */ 385 2 : ResourceSupply.prototype.StartTimer = function(changeKey) 386 : { 387 26 : if (this.timers[changeKey]) 388 0 : return; 389 : 390 26 : let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); 391 26 : let interval = ApplyValueModificationsToEntity("ResourceSupply/Change/" + changeKey + "/Interval", +(this.template.Change[changeKey].Interval || 1000), this.entity); 392 26 : this.timers[changeKey] = cmpTimer.SetInterval(this.entity, IID_ResourceSupply, "TimerTick", interval, interval, changeKey); 393 : }; 394 : 395 : /** 396 : * @param {string} changeKey - The name of the change to stop the timer for. 397 : */ 398 2 : ResourceSupply.prototype.StopTimer = function(changeKey) 399 : { 400 35 : if (!this.timers[changeKey]) 401 21 : return; 402 : 403 14 : let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); 404 14 : cmpTimer.CancelTimer(this.timers[changeKey]); 405 14 : delete this.timers[changeKey]; 406 : }; 407 : 408 : /** 409 : * @param {string} changeKey - The name of the change to apply to the entity. 410 : */ 411 2 : ResourceSupply.prototype.TimerTick = function(changeKey) 412 : { 413 74 : let template = this.template.Change[changeKey]; 414 74 : if (!template || !this.Change(this.cachedChanges[changeKey])) 415 1 : this.StopTimer(changeKey); 416 : }; 417 : 418 : /** 419 : * Since the supposed changes can be affected by modifications, and applying those 420 : * are slow, do not calculate them every timer tick. 421 : */ 422 2 : ResourceSupply.prototype.RecalculateValues = function() 423 : { 424 29 : this.maxAmount = ApplyValueModificationsToEntity("ResourceSupply/Max", +this.template.Max, this.entity); 425 29 : if (!this.template.Change || this.IsInfinite()) 426 5 : return; 427 : 428 24 : for (let changeKey in this.template.Change) 429 28 : this.cachedChanges[changeKey] = ApplyValueModificationsToEntity("ResourceSupply/Change/" + changeKey + "/Value", +this.template.Change[changeKey].Value, this.entity); 430 : 431 24 : this.CheckTimers(); 432 : }; 433 : 434 : /** 435 : * @param {{ "component": string, "valueNames": string[] }} msg - Message containing a list of values that were changed. 436 : */ 437 2 : ResourceSupply.prototype.OnValueModification = function(msg) 438 : { 439 0 : if (msg.component != "ResourceSupply") 440 0 : return; 441 : 442 0 : this.RecalculateValues(); 443 : }; 444 : 445 : /** 446 : * @param {{ "from": number, "to": number }} msg - Message containing the old new owner. 447 : */ 448 2 : ResourceSupply.prototype.OnOwnershipChanged = function(msg) 449 : { 450 28 : if (msg.to == INVALID_PLAYER) 451 : { 452 0 : let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); 453 0 : for (let changeKey in this.timers) 454 0 : cmpTimer.CancelTimer(this.timers[changeKey]); 455 : } 456 : else 457 28 : this.RecalculateValues(); 458 : }; 459 : 460 : /** 461 : * @param {{ "entity": number, "newentity": number }} msg - Message to what the entity has been renamed. 462 : */ 463 2 : ResourceSupply.prototype.OnEntityRenamed = function(msg) 464 : { 465 1 : let cmpResourceSupplyNew = Engine.QueryInterface(msg.newentity, IID_ResourceSupply); 466 1 : if (cmpResourceSupplyNew) 467 1 : cmpResourceSupplyNew.SetAmount(this.GetCurrentAmount()); 468 : }; 469 : 470 : function ResourceSupplyMirage() {} 471 2 : ResourceSupplyMirage.prototype.Init = function(cmpResourceSupply) 472 : { 473 0 : this.maxAmount = cmpResourceSupply.GetMaxAmount(); 474 0 : this.amount = cmpResourceSupply.GetCurrentAmount(); 475 0 : this.type = cmpResourceSupply.GetType(); 476 0 : this.isInfinite = cmpResourceSupply.IsInfinite(); 477 0 : this.killBeforeGather = cmpResourceSupply.GetKillBeforeGather(); 478 0 : this.maxGatherers = cmpResourceSupply.GetMaxGatherers(); 479 0 : this.numGatherers = cmpResourceSupply.GetNumGatherers(); 480 : }; 481 : 482 2 : ResourceSupplyMirage.prototype.GetMaxAmount = function() { return this.maxAmount; }; 483 2 : ResourceSupplyMirage.prototype.GetCurrentAmount = function() { return this.amount; }; 484 2 : ResourceSupplyMirage.prototype.GetType = function() { return this.type; }; 485 2 : ResourceSupplyMirage.prototype.IsInfinite = function() { return this.isInfinite; }; 486 2 : ResourceSupplyMirage.prototype.GetKillBeforeGather = function() { return this.killBeforeGather; }; 487 2 : ResourceSupplyMirage.prototype.GetMaxGatherers = function() { return this.maxGatherers; }; 488 2 : ResourceSupplyMirage.prototype.GetNumGatherers = function() { return this.numGatherers; }; 489 : 490 : // Apply diminishing returns with more gatherers, for e.g. infinite farms. For most resources this has no effect 491 : // (GetDiminishingReturns will return null). We can assume that for resources that are miraged this is the case. 492 2 : ResourceSupplyMirage.prototype.GetDiminishingReturns = function() { return null; }; 493 : 494 2 : Engine.RegisterGlobal("ResourceSupplyMirage", ResourceSupplyMirage); 495 : 496 2 : ResourceSupply.prototype.Mirage = function() 497 : { 498 0 : let mirage = new ResourceSupplyMirage(); 499 0 : mirage.Init(this); 500 0 : return mirage; 501 : }; 502 : 503 2 : Engine.RegisterComponentType(IID_ResourceSupply, "ResourceSupply", ResourceSupply);