Index: ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js (revision 18977) +++ ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js (revision 18978) @@ -1,369 +1,354 @@ function ResourceGatherer() {} ResourceGatherer.prototype.Schema = "Lets the unit gather resources from entities that have the ResourceSupply component." + "" + "2.0" + "1.0" + "" + "1" + "3" + "3" + "2" + "" + "" + "10" + "10" + "10" + "10" + "" + "" + "" + "" + "" + "" + "" + "" + "" + Resources.BuildSchema("positiveDecimal", ["treasure"], true) + "" + "" + Resources.BuildSchema("positiveDecimal") + ""; ResourceGatherer.prototype.Init = function() { 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 } this.RecalculateGatherRatesAndCapacities(); }; /** * 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 each (let resource in resources) { this.carrying[resource.type] = +(resource.amount); } Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() }); }; /** * 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; else return undefined; }; // Since this code is very performancecritical and applying technologies quite slow, cache it. ResourceGatherer.prototype.RecalculateGatherRatesAndCapacities = function() { let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); let multiplier = cmpPlayer ? cmpPlayer.GetGatherRateMultiplier() : 1; this.baseSpeed = multiplier * ApplyValueModificationsToEntity("ResourceGatherer/BaseSpeed", +this.template.BaseSpeed, this.entity); this.rates = {}; for (let r in this.template.Rates) { let type = r.split("."); if (type[0] != "treasure" && type.length > 1 && !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; } this.capacities = {}; for (let r in this.template.Capacities) this.capacities[r] = ApplyValueModificationsToEntity("ResourceGatherer/Capacities/" + r, +this.template.Capacities[r], 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 }; // maybe this should depend on the unit or target or something? }; /** * Try to gather treasure * @return 'true' if treasure is successfully gathered, otherwise 'false' */ ResourceGatherer.prototype.TryInstantGather = function(target) { let cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply); let type = cmpResourceSupply.GetType(); if (type.generic != "treasure") return false; let status = cmpResourceSupply.TakeResources(cmpResourceSupply.GetCurrentAmount()); let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); if (cmpPlayer) cmpPlayer.AddResource(type.specific, status.amount); let cmpStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker); if (cmpStatisticsTracker) cmpStatisticsTracker.IncreaseTreasuresCollectedCounter(); let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); if (cmpTrigger && cmpPlayer) cmpTrigger.CallEvent("TreasureCollected", {"player": cmpPlayer.GetPlayerID(), "type": type.specific, "amount": status.amount }); return true; }; /** * Gather from the target entity. This should only be called after a successful range check, * and if the target has a compatible ResourceSupply. * Call interval will be determined by gather rate, so always gather 1 amount when called. */ ResourceGatherer.prototype.PerformGather = function(target) { if (!this.GetTargetGatherRate(target)) return { "exhausted": true }; let gatherAmount = 1; let cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply); let type = cmpResourceSupply.GetType(); // Initialise the carried count if necessary if (!this.carrying[type.generic]) this.carrying[type.generic] = 0; // Find the maximum so we won't exceed our capacity let maxGathered = this.GetCapacity(type.generic) - this.carrying[type.generic]; let status = cmpResourceSupply.TakeResources(Math.min(gatherAmount, 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); Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() }); return { "amount": status.amount, "exhausted": status.exhausted, "filled": (this.carrying[type.generic] >= this.GetCapacity(type.generic)) }; }; /** * 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) 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 cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); let cheatMultiplier = cmpPlayer ? cmpPlayer.GetCheatTimeMultiplier() : 1; rate = rate / cheatMultiplier; if ("Mirages" in cmpResourceSupply) return rate; - // Apply diminishing returns with more gatherers, for e.g. infinite farms. For most resources this has no effect. (GetDiminishingReturns will return null.) - // We can assume that for resources that are miraged this is the case. (else just add the diminishing returns data to the mirage data and remove the - // early return above) - // Note to people looking to change in a template: This is a bit complicated. Basically, the lower that number is - // the steeper diminishing returns will be. I suggest playing around with Wolfram Alpha or a graphing calculator a bit. - // In each of the following links, replace 0.65 with the gather rate of your worker for the resource with diminishing returns and - // 14 with the constant you wish to use to control the diminishing returns. - // (In this case 0.65 is the women farming rate, in resources/second, and 14 is a good constant for farming.) - // This is the gather rate in resources/second of each individual worker as the total number of workers goes up: - // http://www.wolframalpha.com/input/?i=plot+%281%2F2+cos%28%28x-1%29*pi%2F14%29+%2B+1%2F2%29+*+0.65+from+1+to+5 - // This is the total output of the resource in resources/second: - // http://www.wolframalpha.com/input/?i=plot+x%281%2F2+cos%28%28x-1%29*pi%2F14%29+%2B+1%2F2%29+*+0.65+from+1+to+5 - // This is the fraction of a worker each new worker is worth (the 5th worker in this example is only producing about half as much as the first one): - // http://www.wolframalpha.com/input/?i=plot+x%281%2F2+cos%28%28x-1%29*pi%2F14%29+%2B+1%2F2%29+-++%28x-1%29%281%2F2+cos%28%28x-2%29*pi%2F14%29+%2B+1%2F2%29+from+x%3D1+to+5+and+y%3D0+to+1 - // Here's how this technically works: - // The cosine function is an oscillating curve, normally between -1 and 1. Multiplying by 0.5 squishes that down to - // between -0.5 and 0.5. Adding 0.5 to that changes the range to 0 to 1. The diminishingReturns constant - // adjusts the period of the curve. + // Apply diminishing returns with more gatherers, for e.g. infinite farms. For most resources this has no effect + // (GetDiminishingReturns will return null). We can assume that for resources that are miraged this is the case + // (else just add the diminishing returns data to the mirage data and remove the early return above) let diminishingReturns = cmpResourceSupply.GetDiminishingReturns(); if (diminishingReturns) - rate = (0.5 * Math.cos((cmpResourceSupply.GetNumGatherers() - 1) * Math.PI / diminishingReturns) + 0.5) * rate; + rate *= diminishingReturns; return rate; }; /** * 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; }; /** * Transfer our carried resources to our owner immediately. * Only resources of the given types will be transferred. * (This should typically be called after reaching a dropsite). */ ResourceGatherer.prototype.CommitResources = function(types) { let cmpPlayer = QueryOwnerInterface(this.entity); if (cmpPlayer) for each (let type in types) if (type in this.carrying) { cmpPlayer.AddResource(type, this.carrying[type]); delete this.carrying[type]; } Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() }); }; /** * 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 = {}; Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() }); }; // 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; this.RecalculateGatherRatesAndCapacities(); }; ResourceGatherer.prototype.OnOwnershipChanged = function(msg) { if (msg.to === -1) return; this.RecalculateGatherRatesAndCapacities(); }; ResourceGatherer.prototype.OnGlobalInitGame = function(msg) { this.RecalculateGatherRatesAndCapacities(); }; Engine.RegisterComponentType(IID_ResourceGatherer, "ResourceGatherer", ResourceGatherer); Index: ps/trunk/binaries/data/mods/public/simulation/components/ResourceSupply.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/ResourceSupply.js (revision 18977) +++ ps/trunk/binaries/data/mods/public/simulation/components/ResourceSupply.js (revision 18978) @@ -1,166 +1,186 @@ function ResourceSupply() {} ResourceSupply.prototype.Schema = "Provides a supply of one particular type of resource." + "" + "1000" + "food.meat" + "" + "" + "" + "" + "" + "Infinity" + "" + "" + Resources.BuildChoicesSchema(true, true) + "" + "" + "" + "" + "" + "" + "" + "" + ""; ResourceSupply.prototype.Init = function() { // Current resource amount (non-negative) this.amount = this.GetMaxAmount(); this.gatherers = []; // list of IDs for each players let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); // system component so that's safe. let numPlayers = cmpPlayerManager.GetNumPlayers(); for (let i = 0; i <= numPlayers; ++i) // use "<=" because we want Gaia too. this.gatherers.push([]); this.infinite = !isFinite(+this.template.Amount); let [type, subtype] = this.template.Type.split('.'); let resData = type === "treasure" ? { "subtypes": Resources.GetNames() } : Resources.GetResource(type); if (!resData || !resData.subtypes[subtype]) { error("ResourceSupply with invalid resource: " + uneval(resData)); Engine.DestroyEntity(this.entity); } this.cachedType = { "generic": type, "specific": subtype }; }; ResourceSupply.prototype.IsInfinite = function() { return this.infinite; }; ResourceSupply.prototype.GetKillBeforeGather = function() { return (this.template.KillBeforeGather == "true"); }; ResourceSupply.prototype.GetMaxAmount = function() { return +this.template.Amount; }; ResourceSupply.prototype.GetCurrentAmount = function() { return this.amount; }; ResourceSupply.prototype.GetMaxGatherers = function() { return +this.template.MaxGatherers; }; ResourceSupply.prototype.GetNumGatherers = function() { return this.gatherers.reduce((a, b) => a + b.length, 0); }; +/** Note to people looking to change in a template: This is a bit complicated. Basically, the lower that number is + * the steeper diminishing returns will be. I suggest playing around with Wolfram Alpha or a graphing calculator a bit. + * In each of the following links, replace 0.65 with the gather rate of your worker for the resource with diminishing returns and + * 14 with the constant you wish to use to control the diminishing returns. + * (In this case 0.65 is the women farming rate, in resources/second, and 14 is a good constant for farming.) + * This is the gather rate in resources/second of each individual worker as the total number of workers goes up: + * http://www.wolframalpha.com/input/?i=plot+%281%2F2+cos%28%28x-1%29*pi%2F14%29+%2B+1%2F2%29+*+0.65+from+1+to+5 + * This is the total output of the resource in resources/second: + * http://www.wolframalpha.com/input/?i=plot+x%281%2F2+cos%28%28x-1%29*pi%2F14%29+%2B+1%2F2%29+*+0.65+from+1+to+5 + * This is the fraction of a worker each new worker is worth (the 5th worker in this example is only producing about half as much as the first one): + * http://www.wolframalpha.com/input/?i=plot+x%281%2F2+cos%28%28x-1%29*pi%2F14%29+%2B+1%2F2%29+-++%28x-1%29%281%2F2+cos%28%28x-2%29*pi%2F14%29+%2B+1%2F2%29+from+x%3D1+to+5+and+y%3D0+to+1 + * Here's how this technically works: + * The cosine function is an oscillating curve, normally between -1 and 1. Multiplying by 0.5 squishes that down to + * between -0.5 and 0.5. Adding 0.5 to that changes the range to 0 to 1. The diminishingReturns constant + * adjusts the period of the curve. + */ ResourceSupply.prototype.GetDiminishingReturns = function() { if ("DiminishingReturns" in this.template) - return ApplyValueModificationsToEntity("ResourceSupply/DiminishingReturns", +this.template.DiminishingReturns, this.entity); + { + let diminishingReturns = ApplyValueModificationsToEntity("ResourceSupply/DiminishingReturns", +this.template.DiminishingReturns, this.entity); + if (diminishingReturns) + return (0.5 * Math.cos((this.GetNumGatherers() - 1) * Math.PI / diminishingReturns) + 0.5); + } return null; }; ResourceSupply.prototype.TakeResources = function(rate) { // Before changing the amount, activate Fogging if necessary to hide changes let cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging); if (cmpFogging) cmpFogging.Activate(); if (this.infinite) return { "amount": rate, "exhausted": false }; // 'rate' should be a non-negative integer var old = this.amount; this.amount = Math.max(0, old - rate); var change = old - this.amount; // Remove entities that have been exhausted if (this.amount === 0) Engine.DestroyEntity(this.entity); Engine.PostMessage(this.entity, MT_ResourceSupplyChanged, { "from": old, "to": this.amount }); return { "amount": change, "exhausted": (this.amount === 0) }; }; ResourceSupply.prototype.GetType = function() { // All resources must have both type and subtype return this.cachedType; }; ResourceSupply.prototype.IsAvailable = function(player, gathererID) { if (this.GetNumGatherers() < this.GetMaxGatherers() || this.gatherers[player].indexOf(gathererID) !== -1) return true; return false; }; ResourceSupply.prototype.AddGatherer = function(player, gathererID) { if (!this.IsAvailable(player, gathererID)) return false; if (this.gatherers[player].indexOf(gathererID) === -1) { this.gatherers[player].push(gathererID); // broadcast message, mainly useful for the AIs. Engine.PostMessage(this.entity, MT_ResourceSupplyNumGatherersChanged, { "to": this.GetNumGatherers() }); } return true; }; // should this return false if the gatherer didn't gather from said resource? ResourceSupply.prototype.RemoveGatherer = function(gathererID, player) { // this can happen if the unit is dead if (player === undefined || player === -1) { for (var i = 0; i < this.gatherers.length; ++i) this.RemoveGatherer(gathererID, i); } else { var index = this.gatherers[player].indexOf(gathererID); if (index !== -1) { this.gatherers[player].splice(index,1); // broadcast message, mainly useful for the AIs. Engine.PostMessage(this.entity, MT_ResourceSupplyNumGatherersChanged, { "to": this.GetNumGatherers() }); return; } } }; Engine.RegisterComponentType(IID_ResourceSupply, "ResourceSupply", ResourceSupply);