Index: ps/trunk/binaries/data/mods/public/simulation/components/Cost.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Cost.js (revision 22963) +++ ps/trunk/binaries/data/mods/public/simulation/components/Cost.js (revision 22964) @@ -1,119 +1,117 @@ function Cost() {} Cost.prototype.Schema = "Specifies the construction/training costs of this entity." + "" + "1" + "15" + "20.0" + "" + "50" + "0" + "0" + "25" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + Resources.BuildSchema("nonNegativeInteger") + ""; Cost.prototype.Init = function() { this.populationCost = +this.template.Population; this.populationBonus = +this.template.PopulationBonus; }; Cost.prototype.GetPopCost = function() { return this.populationCost; }; Cost.prototype.GetPopBonus = function() { return this.populationBonus; }; Cost.prototype.GetBuildTime = function() { - var cmpPlayer = QueryOwnerInterface(this.entity); - var buildTime = (+this.template.BuildTime) * cmpPlayer.GetTimeMultiplier(); - return ApplyValueModificationsToEntity("Cost/BuildTime", buildTime, this.entity); + return ApplyValueModificationsToEntity("Cost/BuildTime", +this.template.BuildTime, this.entity); }; Cost.prototype.GetResourceCosts = function(owner) { if (!owner) { let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership) error("GetResourceCosts called without valid ownership"); else owner = cmpOwnership.GetOwner(); } let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let entityTemplateName = cmpTemplateManager.GetCurrentTemplateName(this.entity); let entityTemplate = cmpTemplateManager.GetTemplate(entityTemplateName); let costs = {}; for (let res in this.template.Resources) costs[res] = ApplyValueModificationsToTemplate("Cost/Resources/"+res, +this.template.Resources[res], owner, entityTemplate); return costs; }; Cost.prototype.OnOwnershipChanged = function(msg) { if (msg.from != INVALID_PLAYER) { let cmpPlayer = QueryPlayerIDInterface(msg.from); if (cmpPlayer) cmpPlayer.AddPopulationBonuses(-this.GetPopBonus()); } if (msg.to != INVALID_PLAYER) { let cmpPlayer = QueryPlayerIDInterface(msg.to); if (cmpPlayer) cmpPlayer.AddPopulationBonuses(this.GetPopBonus()); } }; Cost.prototype.OnValueModification = function(msg) { if (msg.component != "Cost") return; // foundations shouldn't give a pop bonus and a pop cost var cmpFoundation = Engine.QueryInterface(this.entity, IID_Foundation); if (cmpFoundation) return; // update the population costs var newPopCost = Math.round(ApplyValueModificationsToEntity("Cost/Population", +this.template.Population, this.entity)); var popCostDifference = newPopCost - this.populationCost; this.populationCost = newPopCost; // update the population bonuses var newPopBonus = Math.round(ApplyValueModificationsToEntity("Cost/PopulationBonus", +this.template.PopulationBonus, this.entity)); var popDifference = newPopBonus - this.populationBonus; this.populationBonus = newPopBonus; var cmpPlayer = QueryOwnerInterface(this.entity); if (!cmpPlayer) return; if (popCostDifference) cmpPlayer.AddPopulation(popCostDifference); if (popDifference) cmpPlayer.AddPopulationBonuses(popDifference); }; Engine.RegisterComponentType(IID_Cost, "Cost", Cost); Index: ps/trunk/binaries/data/mods/public/simulation/components/Pack.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Pack.js (revision 22963) +++ ps/trunk/binaries/data/mods/public/simulation/components/Pack.js (revision 22964) @@ -1,141 +1,139 @@ function Pack() {} const PACKING_INTERVAL = 250; Pack.prototype.Schema = "" + "" + "" + "" + "" + "" + "" + "" + "packed" + "unpacked" + "" + ""; Pack.prototype.Init = function() { this.packed = this.template.State == "packed"; this.packing = false; this.elapsedTime = 0; this.timer = undefined; }; Pack.prototype.OnDestroy = function() { this.CancelTimer(); }; Pack.prototype.CancelTimer = function() { if (this.timer) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); this.timer = undefined; } }; Pack.prototype.IsPacked = function() { return this.packed; }; Pack.prototype.IsPacking = function() { return this.packing; }; Pack.prototype.Pack = function() { if (this.IsPacked() || this.IsPacking()) return; this.packing = true; let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetInterval(this.entity, IID_Pack, "PackProgress", 0, PACKING_INTERVAL, { "packing": true }); let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) cmpVisual.SelectAnimation("packing", true, 1.0, "packing"); }; Pack.prototype.Unpack = function() { if (!this.IsPacked() || this.IsPacking()) return; this.packing = true; let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetInterval(this.entity, IID_Pack, "PackProgress", 0, PACKING_INTERVAL, { "packing": false }); let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) cmpVisual.SelectAnimation("unpacking", true, 1.0); }; Pack.prototype.CancelPack = function() { if (!this.IsPacking()) return; this.CancelTimer(); this.packing = false; this.SetElapsedTime(0); // Clear animation let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) cmpVisual.SelectAnimation("idle", false, 1.0); }; Pack.prototype.GetPackTime = function() { - let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); - - return ApplyValueModificationsToEntity("Pack/Time", +this.template.Time, this.entity) * cmpPlayer.GetTimeMultiplier(); + return ApplyValueModificationsToEntity("Pack/Time", +this.template.Time, this.entity); }; Pack.prototype.GetElapsedTime = function() { return this.elapsedTime; }; Pack.prototype.GetProgress = function() { return Math.min(this.elapsedTime / this.GetPackTime(), 1); }; Pack.prototype.SetElapsedTime = function(time) { this.elapsedTime = time; Engine.PostMessage(this.entity, MT_PackProgressUpdate, { "progress": this.elapsedTime }); }; Pack.prototype.PackProgress = function(data, lateness) { if (this.elapsedTime < this.GetPackTime()) { this.SetElapsedTime(this.GetElapsedTime() + PACKING_INTERVAL + lateness); return; } this.CancelTimer(); this.packed = !this.packed; this.packing = false; Engine.PostMessage(this.entity, MT_PackFinished, { "packed": this.packed }); let newEntity = ChangeEntityTemplate(this.entity, this.template.Entity); if (newEntity) PlaySound(this.packed ? "packed" : "unpacked", newEntity); }; Engine.RegisterComponentType(IID_Pack, "Pack", Pack); Index: ps/trunk/binaries/data/mods/public/simulation/components/Player.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Player.js (revision 22963) +++ ps/trunk/binaries/data/mods/public/simulation/components/Player.js (revision 22964) @@ -1,983 +1,937 @@ function Player() {} Player.prototype.Schema = "" + "" + "" + Resources.BuildSchema("positiveDecimal") + "" + "" + Resources.BuildSchema("positiveDecimal") + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; /** * Don't serialize diplomacyColor or displayDiplomacyColor since they're modified by the GUI. */ Player.prototype.Serialize = function() { let state = {}; for (let key in this) if (this.hasOwnProperty(key)) state[key] = this[key]; state.diplomacyColor = undefined; state.displayDiplomacyColor = false; return state; }; /** * Which units will be shown with special icons at the top. */ var panelEntityClasses = "Hero Relic"; Player.prototype.Init = function() { this.playerID = undefined; this.name = undefined; // define defaults elsewhere (supporting other languages) this.civ = undefined; this.color = undefined; this.diplomacyColor = undefined; this.displayDiplomacyColor = false; this.popUsed = 0; // population of units owned or trained by this player this.popBonuses = 0; // sum of population bonuses of player's entities this.maxPop = 300; // maximum population this.trainingBlocked = false; // indicates whether any training queue is currently blocked this.resourceCount = {}; this.tradingGoods = []; // goods for next trade-route and its proba in % (the sum of probas must be 100) this.team = -1; // team number of the player, players on the same team will always have ally diplomatic status - also this is useful for team emblems, scoring, etc. this.teamsLocked = false; this.state = "active"; // game state - one of "active", "defeated", "won" this.diplomacy = []; // array of diplomatic stances for this player with respect to other players (including gaia and self) this.sharedDropsites = false; this.formations = []; this.startCam = undefined; this.controlAllUnits = false; this.isAI = false; - this.timeMultiplier = 1; - this.gatherRateMultiplier = 1; - this.tradeRateMultiplier = 1; this.cheatsEnabled = false; - this.cheatTimeMultiplier = 1; this.panelEntities = []; this.resourceNames = {}; this.disabledTemplates = {}; this.disabledTechnologies = {}; this.startingTechnologies = []; this.spyCostMultiplier = +this.template.SpyCostMultiplier; this.barterMultiplier = { "buy": clone(this.template.BarterMultiplier.Buy), "sell": clone(this.template.BarterMultiplier.Sell) }; // Initial resources and trading goods probability in steps of 5 let resCodes = Resources.GetCodes(); let quotient = Math.floor(20 / resCodes.length); let remainder = 20 % resCodes.length; for (let i in resCodes) { let res = resCodes[i]; this.resourceCount[res] = 300; this.resourceNames[res] = Resources.GetResource(res).name; this.tradingGoods.push({ "goods": res, "proba": 5 * (quotient + (+i < remainder ? 1 : 0)) }); } }; Player.prototype.SetPlayerID = function(id) { this.playerID = id; }; Player.prototype.GetPlayerID = function() { return this.playerID; }; Player.prototype.SetName = function(name) { this.name = name; }; Player.prototype.GetName = function() { return this.name; }; Player.prototype.SetCiv = function(civcode) { var oldCiv = this.civ; this.civ = civcode; // Normally, the civ is only set once // But in Atlas, the map designers can change civs at any time var playerID = this.GetPlayerID(); if (oldCiv && playerID && oldCiv != civcode) Engine.BroadcastMessage(MT_CivChanged, { "player": playerID, "from": oldCiv, "to": civcode }); }; Player.prototype.GetCiv = function() { return this.civ; }; Player.prototype.SetColor = function(r, g, b) { var colorInitialized = !!this.color; this.color = { "r": r / 255, "g": g / 255, "b": b / 255, "a": 1 }; // Used in Atlas if (colorInitialized) Engine.BroadcastMessage(MT_PlayerColorChanged, { "player": this.playerID }); }; Player.prototype.SetDiplomacyColor = function(color) { this.diplomacyColor = { "r": color.r / 255, "g": color.g / 255, "b": color.b / 255, "a": 1 }; }; Player.prototype.SetDisplayDiplomacyColor = function(displayDiplomacyColor) { this.displayDiplomacyColor = displayDiplomacyColor; }; Player.prototype.GetColor = function() { return this.color; }; Player.prototype.GetDisplayedColor = function() { return this.displayDiplomacyColor ? this.diplomacyColor : this.color; }; // Try reserving num population slots. Returns 0 on success or number of missing slots otherwise. Player.prototype.TryReservePopulationSlots = function(num) { if (num != 0 && num > (this.GetPopulationLimit() - this.GetPopulationCount())) return num - (this.GetPopulationLimit() - this.GetPopulationCount()); this.popUsed += num; return 0; }; Player.prototype.UnReservePopulationSlots = function(num) { this.popUsed -= num; }; Player.prototype.GetPopulationCount = function() { return this.popUsed; }; Player.prototype.AddPopulation = function(num) { this.popUsed += num; }; Player.prototype.SetPopulationBonuses = function(num) { this.popBonuses = num; }; Player.prototype.AddPopulationBonuses = function(num) { this.popBonuses += num; }; Player.prototype.GetPopulationLimit = function() { return Math.min(this.GetMaxPopulation(), this.popBonuses); }; Player.prototype.SetMaxPopulation = function(max) { this.maxPop = max; }; Player.prototype.GetMaxPopulation = function() { return Math.round(ApplyValueModificationsToEntity("Player/MaxPopulation", this.maxPop, this.entity)); }; Player.prototype.GetBarterMultiplier = function() { return this.barterMultiplier; }; -Player.prototype.GetGatherRateMultiplier = function() -{ - return this.gatherRateMultiplier / this.cheatTimeMultiplier; -}; - -Player.prototype.GetTimeMultiplier = function() -{ - return this.timeMultiplier * this.cheatTimeMultiplier; -}; - -Player.prototype.GetTradeRateMultiplier = function() -{ - return this.tradeRateMultiplier; -}; - Player.prototype.GetSpyCostMultiplier = function() { return this.spyCostMultiplier; }; /** * Setters currently used by the AI to set the difficulty level */ -Player.prototype.SetGatherRateMultiplier = function(value) -{ - this.gatherRateMultiplier = value; - Engine.BroadcastMessage(MT_MultiplierChanged, { "player": this.playerID, "type": "gather" }); -}; - -Player.prototype.SetTimeMultiplier = function(value) -{ - this.timeMultiplier = value; - Engine.BroadcastMessage(MT_MultiplierChanged, { "player": this.playerID, "type": "time" }); -}; - -Player.prototype.SetTradeRateMultiplier = function(value) -{ - this.tradeRateMultiplier = value; -}; Player.prototype.GetPanelEntities = function() { return this.panelEntities; }; Player.prototype.IsTrainingBlocked = function() { return this.trainingBlocked; }; Player.prototype.BlockTraining = function() { this.trainingBlocked = true; }; Player.prototype.UnBlockTraining = function() { this.trainingBlocked = false; }; Player.prototype.SetResourceCounts = function(resources) { for (let res in resources) this.resourceCount[res] = resources[res]; }; Player.prototype.GetResourceCounts = function() { return this.resourceCount; }; /** * Add resource of specified type to player * @param type Generic type of resource (string) * @param amount Amount of resource, which should be added (integer) */ Player.prototype.AddResource = function(type, amount) { this.resourceCount[type] += +amount; }; /** * Add resources to player */ Player.prototype.AddResources = function(amounts) { for (var type in amounts) this.resourceCount[type] += +amounts[type]; }; Player.prototype.GetNeededResources = function(amounts) { // Check if we can afford it all var amountsNeeded = {}; for (var type in amounts) if (this.resourceCount[type] != undefined && amounts[type] > this.resourceCount[type]) amountsNeeded[type] = amounts[type] - Math.floor(this.resourceCount[type]); if (Object.keys(amountsNeeded).length == 0) return undefined; return amountsNeeded; }; Player.prototype.SubtractResourcesOrNotify = function(amounts) { var amountsNeeded = this.GetNeededResources(amounts); // If we don't have enough resources, send a notification to the player if (amountsNeeded) { var parameters = {}; var i = 0; for (var type in amountsNeeded) { ++i; parameters["resourceType"+i] = this.resourceNames[type]; parameters["resourceAmount"+i] = amountsNeeded[type]; } var msg = ""; // when marking strings for translations, you need to include the actual string, // not some way to derive the string if (i < 1) warn("Amounts needed but no amounts given?"); else if (i == 1) msg = markForTranslation("Insufficient resources - %(resourceAmount1)s %(resourceType1)s"); else if (i == 2) msg = markForTranslation("Insufficient resources - %(resourceAmount1)s %(resourceType1)s, %(resourceAmount2)s %(resourceType2)s"); else if (i == 3) msg = markForTranslation("Insufficient resources - %(resourceAmount1)s %(resourceType1)s, %(resourceAmount2)s %(resourceType2)s, %(resourceAmount3)s %(resourceType3)s"); else if (i == 4) msg = markForTranslation("Insufficient resources - %(resourceAmount1)s %(resourceType1)s, %(resourceAmount2)s %(resourceType2)s, %(resourceAmount3)s %(resourceType3)s, %(resourceAmount4)s %(resourceType4)s"); else warn("Localisation: Strings are not localised for more than 4 resources"); // Send as time-notification let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "players": [this.playerID], "message": msg, "parameters": parameters, "translateMessage": true, "translateParameters": { "resourceType1": "withinSentence", "resourceType2": "withinSentence", "resourceType3": "withinSentence", "resourceType4": "withinSentence", }, }); return false; } for (var type in amounts) this.resourceCount[type] -= amounts[type]; return true; }; Player.prototype.TrySubtractResources = function(amounts) { if (!this.SubtractResourcesOrNotify(amounts)) return false; var cmpStatisticsTracker = QueryPlayerIDInterface(this.playerID, IID_StatisticsTracker); if (cmpStatisticsTracker) for (var type in amounts) cmpStatisticsTracker.IncreaseResourceUsedCounter(type, amounts[type]); return true; }; Player.prototype.GetNextTradingGoods = function() { var value = randFloat(0, 100); var last = this.tradingGoods.length - 1; var sumProba = 0; for (var i = 0; i < last; ++i) { sumProba += this.tradingGoods[i].proba; if (value < sumProba) return this.tradingGoods[i].goods; } return this.tradingGoods[last].goods; }; Player.prototype.GetTradingGoods = function() { var tradingGoods = {}; for (let resource of this.tradingGoods) tradingGoods[resource.goods] = resource.proba; return tradingGoods; }; Player.prototype.SetTradingGoods = function(tradingGoods) { let resCodes = Resources.GetCodes(); let sumProba = 0; for (let resource in tradingGoods) { if (resCodes.indexOf(resource) == -1 || tradingGoods[resource] < 0) { error("Invalid trading goods: " + uneval(tradingGoods)); return; } sumProba += tradingGoods[resource]; } if (sumProba != 100) { error("Invalid trading goods: " + uneval(tradingGoods)); return; } this.tradingGoods = []; for (let resource in tradingGoods) this.tradingGoods.push({ "goods": resource, "proba": tradingGoods[resource] }); }; Player.prototype.GetState = function() { return this.state; }; /** * @param {string} newState - Either "defeated" or "won". * @param {string|undefined} message - A string to be shown in chat, for example * markForTranslation("%(player)s has been defeated (failed objective)."). * If it is undefined, the caller MUST send that GUI notification manually. */ Player.prototype.SetState = function(newState, message) { if (this.state != "active") return; if (newState != "won" && newState != "defeated") { warn("Can't change playerstate to " + this.state); return; } this.state = newState; let won = newState == "won"; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (won) cmpRangeManager.SetLosRevealAll(this.playerID, true); else { // Reassign all player's entities to Gaia let entities = cmpRangeManager.GetEntitiesByPlayer(this.playerID); // The ownership change is done in two steps so that entities don't hit idle // (and thus possibly look for "enemies" to attack) before nearby allies get // converted to Gaia as well. for (let entity of entities) { let cmpOwnership = Engine.QueryInterface(entity, IID_Ownership); cmpOwnership.SetOwnerQuiet(0); } // With the real ownership change complete, send OwnershipChanged messages. for (let entity of entities) Engine.PostMessage(entity, MT_OwnershipChanged, { "entity": entity, "from": this.playerID, "to": 0 }); } Engine.PostMessage(this.entity, won ? MT_PlayerWon : MT_PlayerDefeated, { "playerId": this.playerID }); if (message) { let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": won ? "won" : "defeat", "players": [this.playerID], "allies": [this.playerID], "message": message }); } }; Player.prototype.GetTeam = function() { return this.team; }; Player.prototype.SetTeam = function(team) { if (this.teamsLocked) return; this.team = team; // Set all team members as allies if (this.team != -1) { let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 0; i < numPlayers; ++i) { let cmpPlayer = QueryPlayerIDInterface(i); if (this.team != cmpPlayer.GetTeam()) continue; this.SetAlly(i); cmpPlayer.SetAlly(this.playerID); } } Engine.BroadcastMessage(MT_DiplomacyChanged, { "player": this.playerID, "otherPlayer": null }); }; Player.prototype.SetLockTeams = function(value) { this.teamsLocked = value; }; Player.prototype.GetLockTeams = function() { return this.teamsLocked; }; Player.prototype.GetDiplomacy = function() { return this.diplomacy.slice(); }; Player.prototype.SetDiplomacy = function(dipl) { this.diplomacy = dipl.slice(); Engine.BroadcastMessage(MT_DiplomacyChanged, { "player": this.playerID, "otherPlayer": null }); }; Player.prototype.SetDiplomacyIndex = function(idx, value) { let cmpPlayer = QueryPlayerIDInterface(idx); if (!cmpPlayer) return; if (this.state != "active" || cmpPlayer.state != "active") return; this.diplomacy[idx] = value; Engine.BroadcastMessage(MT_DiplomacyChanged, { "player": this.playerID, "otherPlayer": cmpPlayer.GetPlayerID() }); // Mutual worsening of relations if (cmpPlayer.diplomacy[this.playerID] > value) cmpPlayer.SetDiplomacyIndex(this.playerID, value); }; Player.prototype.UpdateSharedLos = function() { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let cmpTechnologyManager = Engine.QueryInterface(this.entity, IID_TechnologyManager); if (!cmpRangeManager || !cmpTechnologyManager) return; if (!cmpTechnologyManager.IsTechnologyResearched(this.template.SharedLosTech)) { cmpRangeManager.SetSharedLos(this.playerID, [this.playerID]); return; } cmpRangeManager.SetSharedLos(this.playerID, this.GetMutualAllies()); }; Player.prototype.GetFormations = function() { return this.formations; }; Player.prototype.SetFormations = function(formations) { this.formations = formations; }; Player.prototype.GetStartingCameraPos = function() { return this.startCam.position; }; Player.prototype.GetStartingCameraRot = function() { return this.startCam.rotation; }; Player.prototype.SetStartingCamera = function(pos, rot) { this.startCam = { "position": pos, "rotation": rot }; }; Player.prototype.HasStartingCamera = function() { return this.startCam !== undefined; }; Player.prototype.HasSharedLos = function() { let cmpTechnologyManager = Engine.QueryInterface(this.entity, IID_TechnologyManager); return cmpTechnologyManager && cmpTechnologyManager.IsTechnologyResearched(this.template.SharedLosTech); }; Player.prototype.HasSharedDropsites = function() { return this.sharedDropsites; }; Player.prototype.SetControlAllUnits = function(c) { this.controlAllUnits = c; }; Player.prototype.CanControlAllUnits = function() { return this.controlAllUnits; }; Player.prototype.SetAI = function(flag) { this.isAI = flag; }; Player.prototype.IsAI = function() { return this.isAI; }; Player.prototype.GetPlayersByDiplomacy = function(func) { var players = []; for (var i = 0; i < this.diplomacy.length; ++i) if (this[func](i)) players.push(i); return players; }; Player.prototype.SetAlly = function(id) { this.SetDiplomacyIndex(id, 1); }; /** * Check if given player is our ally */ Player.prototype.IsAlly = function(id) { return this.diplomacy[id] > 0; }; Player.prototype.GetAllies = function() { return this.GetPlayersByDiplomacy("IsAlly"); }; /** * Check if given player is our ally excluding ourself */ Player.prototype.IsExclusiveAlly = function(id) { return this.playerID != id && this.IsAlly(id); }; /** * Check if given player is our ally, and we are its ally */ Player.prototype.IsMutualAlly = function(id) { var cmpPlayer = QueryPlayerIDInterface(id); return this.IsAlly(id) && cmpPlayer && cmpPlayer.IsAlly(this.playerID); }; Player.prototype.GetMutualAllies = function() { return this.GetPlayersByDiplomacy("IsMutualAlly"); }; /** * Check if given player is our ally, and we are its ally, excluding ourself */ Player.prototype.IsExclusiveMutualAlly = function(id) { return this.playerID != id && this.IsMutualAlly(id); }; Player.prototype.SetEnemy = function(id) { this.SetDiplomacyIndex(id, -1); }; /** * Check if given player is our enemy */ Player.prototype.IsEnemy = function(id) { return this.diplomacy[id] < 0; }; Player.prototype.GetEnemies = function() { return this.GetPlayersByDiplomacy("IsEnemy"); }; Player.prototype.SetNeutral = function(id) { this.SetDiplomacyIndex(id, 0); }; /** * Check if given player is neutral */ Player.prototype.IsNeutral = function(id) { return this.diplomacy[id] == 0; }; /** * Do some map dependant initializations */ Player.prototype.OnGlobalInitGame = function(msg) { let cmpTechnologyManager = Engine.QueryInterface(this.entity, IID_TechnologyManager); if (cmpTechnologyManager) for (let tech of this.startingTechnologies) cmpTechnologyManager.ResearchTechnology(tech); // Replace the "{civ}" code with this civ ID let disabledTemplates = this.disabledTemplates; this.disabledTemplates = {}; for (let template in disabledTemplates) if (disabledTemplates[template]) this.disabledTemplates[template.replace(/\{civ\}/g, this.civ)] = true; }; /** * Keep track of population effects of all entities that * become owned or unowned by this player */ Player.prototype.OnGlobalOwnershipChanged = function(msg) { if (msg.from != this.playerID && msg.to != this.playerID) return; var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity); var cmpCost = Engine.QueryInterface(msg.entity, IID_Cost); if (msg.from == this.playerID) { if (cmpCost) this.popUsed -= cmpCost.GetPopCost(); if (cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), panelEntityClasses)) { let index = this.panelEntities.indexOf(msg.entity); if (index >= 0) this.panelEntities.splice(index, 1); } } if (msg.to == this.playerID) { if (cmpCost) this.popUsed += cmpCost.GetPopCost(); if (cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), panelEntityClasses)) this.panelEntities.push(msg.entity); } }; Player.prototype.OnResearchFinished = function(msg) { if (msg.tech == this.template.SharedLosTech) this.UpdateSharedLos(); else if (msg.tech == this.template.SharedDropsitesTech) this.sharedDropsites = true; }; Player.prototype.OnDiplomacyChanged = function() { this.UpdateSharedLos(); }; Player.prototype.OnValueModification = function(msg) { if (msg.component != "Player") return; if (msg.valueNames.indexOf("Player/SpyCostMultiplier") != -1) this.spyCostMultiplier = ApplyValueModificationsToEntity("Player/SpyCostMultiplier", +this.template.SpyCostMultiplier, this.entity); if (msg.valueNames.some(mod => mod.startsWith("Player/BarterMultiplier/"))) for (let res in this.template.BarterMultiplier.Buy) { this.barterMultiplier.buy[res] = ApplyValueModificationsToEntity("Player/BarterMultiplier/Buy/"+res, +this.template.BarterMultiplier.Buy[res], this.entity); this.barterMultiplier.sell[res] = ApplyValueModificationsToEntity("Player/BarterMultiplier/Sell/"+res, +this.template.BarterMultiplier.Sell[res], this.entity); } }; Player.prototype.SetCheatsEnabled = function(flag) { this.cheatsEnabled = flag; }; Player.prototype.GetCheatsEnabled = function() { return this.cheatsEnabled; }; -Player.prototype.SetCheatTimeMultiplier = function(time) -{ - this.cheatTimeMultiplier = time; - Engine.BroadcastMessage(MT_MultiplierChanged, { "player": this.playerID, "type": "cheat" }); -}; - -Player.prototype.GetCheatTimeMultiplier = function() -{ - return this.cheatTimeMultiplier; -}; - Player.prototype.TributeResource = function(player, amounts) { var cmpPlayer = QueryPlayerIDInterface(player); if (!cmpPlayer) return; if (this.state != "active" || cmpPlayer.state != "active") return; for (let resCode in amounts) if (Resources.GetCodes().indexOf(resCode) == -1 || !Number.isInteger(amounts[resCode]) || amounts[resCode] < 0) { warn("Invalid tribute amounts: " + uneval(amounts)); return; } if (!this.SubtractResourcesOrNotify(amounts)) return; cmpPlayer.AddResources(amounts); var total = Object.keys(amounts).reduce((sum, type) => sum + amounts[type], 0); var cmpOurStatisticsTracker = QueryPlayerIDInterface(this.playerID, IID_StatisticsTracker); if (cmpOurStatisticsTracker) cmpOurStatisticsTracker.IncreaseTributesSentCounter(total); var cmpTheirStatisticsTracker = QueryPlayerIDInterface(player, IID_StatisticsTracker); if (cmpTheirStatisticsTracker) cmpTheirStatisticsTracker.IncreaseTributesReceivedCounter(total); var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); if (cmpGUIInterface) cmpGUIInterface.PushNotification({ "type": "tribute", "players": [player], "donator": this.playerID, "amounts": amounts }); Engine.BroadcastMessage(MT_TributeExchanged, { "to": player, "from": this.playerID, "amounts": amounts }); }; Player.prototype.AddDisabledTemplate = function(template) { this.disabledTemplates[template] = true; Engine.BroadcastMessage(MT_DisabledTemplatesChanged, {}); var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "resetselectionpannel", "players": [this.GetPlayerID()] }); }; Player.prototype.RemoveDisabledTemplate = function(template) { this.disabledTemplates[template] = false; Engine.BroadcastMessage(MT_DisabledTemplatesChanged, {}); var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "resetselectionpannel", "players": [this.GetPlayerID()] }); }; Player.prototype.SetDisabledTemplates = function(templates) { this.disabledTemplates = {}; for (let template of templates) this.disabledTemplates[template] = true; Engine.BroadcastMessage(MT_DisabledTemplatesChanged, {}); var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "resetselectionpannel", "players": [this.GetPlayerID()] }); }; Player.prototype.GetDisabledTemplates = function() { return this.disabledTemplates; }; Player.prototype.AddDisabledTechnology = function(tech) { this.disabledTechnologies[tech] = true; Engine.BroadcastMessage(MT_DisabledTechnologiesChanged, {}); }; Player.prototype.RemoveDisabledTechnology = function(tech) { this.disabledTechnologies[tech] = false; Engine.BroadcastMessage(MT_DisabledTechnologiesChanged, {}); }; Player.prototype.SetDisabledTechnologies = function(techs) { this.disabledTechnologies = {}; for (let tech of techs) this.disabledTechnologies[tech] = true; Engine.BroadcastMessage(MT_DisabledTechnologiesChanged, {}); }; Player.prototype.GetDisabledTechnologies = function() { return this.disabledTechnologies; }; Player.prototype.AddStartingTechnology = function(tech) { if (this.startingTechnologies.indexOf(tech) == -1) this.startingTechnologies.push(tech); }; Player.prototype.SetStartingTechnologies = function(techs) { this.startingTechnologies = techs; }; Engine.RegisterComponentType(IID_Player, "Player", Player); Index: ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js (revision 22963) +++ ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js (revision 22964) @@ -1,833 +1,829 @@ var g_ProgressInterval = 1000; const MAX_QUEUE_SIZE = 16; function ProductionQueue() {} ProductionQueue.prototype.Schema = "Allows the building to train new units and research technologies" + "" + "0.7" + "" + "\n units/{civ}_support_female_citizen\n units/{native}_support_trader\n units/athen_infantry_spearman_b\n " + "" + "" + "" + "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + Resources.BuildSchema("nonNegativeDecimal", ["time"]) + ""; ProductionQueue.prototype.Init = function() { this.nextID = 1; this.queue = []; // Queue items are: // { // "id": 1, // "player": 1, // who paid for this batch; we need this to cope with refunds cleanly // "unitTemplate": "units/example", // "count": 10, // "neededSlots": 3, // number of population slots missing for production to begin // "resources": { "wood": 100, ... }, // resources per unit, multiply by count to get total // "population": 1, // population per unit, multiply by count to get total // "productionStarted": false, // true iff we have reserved population // "timeTotal": 15000, // msecs // "timeRemaining": 10000, // msecs // } // // { // "id": 1, // "player": 1, // who paid for this research; we need this to cope with refunds cleanly // "technologyTemplate": "example_tech", // "resources": { "wood": 100, ... }, // resources needed for research // "productionStarted": false, // true iff production has started // "timeTotal": 15000, // msecs // "timeRemaining": 10000, // msecs // } this.timer = undefined; // g_ProgressInterval msec timer, active while the queue is non-empty this.paused = false; this.entityCache = []; this.spawnNotified = false; }; /* * Returns list of entities that can be trained by this building. */ ProductionQueue.prototype.GetEntitiesList = function() { return this.entitiesList; }; ProductionQueue.prototype.CalculateEntitiesList = function() { this.entitiesList = []; if (!this.template.Entities) return; let string = this.template.Entities._string; if (!string) return; // Replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID. let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let cmpPlayer = QueryOwnerInterface(this.entity); if (!cmpPlayer) return; let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); if (cmpIdentity) string = string.replace(/\{native\}/g, cmpIdentity.GetCiv()); let entitiesList = string.replace(/\{civ\}/g, cmpPlayer.GetCiv()).split(/\s+/); // filter out disabled and invalid entities let disabledEntities = cmpPlayer.GetDisabledTemplates(); entitiesList = entitiesList.filter(ent => !disabledEntities[ent] && cmpTemplateManager.TemplateExists(ent)); // check if some templates need to show their advanced or elite version let upgradeTemplate = function(templateName) { let template = cmpTemplateManager.GetTemplate(templateName); while (template && template.Promotion !== undefined) { let requiredXp = ApplyValueModificationsToTemplate("Promotion/RequiredXp", +template.Promotion.RequiredXp, cmpPlayer.GetPlayerID(), template); if (requiredXp > 0) break; templateName = template.Promotion.Entity; template = cmpTemplateManager.GetTemplate(templateName); } return templateName; }; for (let templateName of entitiesList) this.entitiesList.push(upgradeTemplate(templateName)); for (let item of this.queue) if (item.unitTemplate) item.unitTemplate = upgradeTemplate(item.unitTemplate); }; /* * Returns list of technologies that can be researched by this building. */ ProductionQueue.prototype.GetTechnologiesList = function() { if (!this.template.Technologies) return []; var string = this.template.Technologies._string; if (!string) return []; var cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); if (!cmpTechnologyManager) return []; var cmpPlayer = QueryOwnerInterface(this.entity); var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); if (!cmpPlayer || !cmpIdentity) return []; var techs = string.split(/\s+/); // Replace the civ specific technologies for (let i = 0; i < techs.length; ++i) { let tech = techs[i]; if (tech.indexOf("{civ}") == -1) continue; let civTech = tech.replace("{civ}", cmpPlayer.GetCiv()); techs[i] = TechnologyTemplates.Has(civTech) ? civTech : tech.replace("{civ}", "generic"); } // Remove any technologies that can't be researched by this civ techs = techs.filter(tech => cmpTechnologyManager.CheckTechnologyRequirements( DeriveTechnologyRequirements(TechnologyTemplates.Get(tech), cmpPlayer.GetCiv()), true)); var techList = []; var superseded = {}; // Stores the tech which supersedes the key var disabledTechnologies = cmpPlayer.GetDisabledTechnologies(); // Add any top level technologies to an array which corresponds to the displayed icons // Also store what a technology is superceded by in the superceded object {"tech1":"techWhichSupercedesTech1", ...} for (var i in techs) { var tech = techs[i]; if (disabledTechnologies && disabledTechnologies[tech]) continue; let template = TechnologyTemplates.Get(tech); if (!template.supersedes || techs.indexOf(template.supersedes) === -1) techList.push(tech); else superseded[template.supersedes] = tech; } // Now make researched/in progress techs invisible for (var i in techList) { var tech = techList[i]; while (this.IsTechnologyResearchedOrInProgress(tech)) tech = superseded[tech]; techList[i] = tech; } var ret = []; // This inserts the techs into the correct positions to line up the technology pairs for (var i = 0; i < techList.length; i++) { var tech = techList[i]; if (!tech) { ret[i] = undefined; continue; } let template = TechnologyTemplates.Get(tech); if (template.top) ret[i] = { "pair": true, "top": template.top, "bottom": template.bottom }; else ret[i] = tech; } return ret; }; ProductionQueue.prototype.GetTechCostMultiplier = function() { let techCostMultiplier = {}; for (let res in this.template.TechCostMultiplier) techCostMultiplier[res] = ApplyValueModificationsToEntity("ProductionQueue/TechCostMultiplier/"+res, +this.template.TechCostMultiplier[res], this.entity); return techCostMultiplier; }; ProductionQueue.prototype.IsTechnologyResearchedOrInProgress = function(tech) { if (!tech) return false; var cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); let template = TechnologyTemplates.Get(tech); if (template.top) return cmpTechnologyManager.IsTechnologyResearched(template.top) || cmpTechnologyManager.IsInProgress(template.top) || cmpTechnologyManager.IsTechnologyResearched(template.bottom) || cmpTechnologyManager.IsInProgress(template.bottom); return cmpTechnologyManager.IsTechnologyResearched(tech) || cmpTechnologyManager.IsInProgress(tech); }; /* * Adds a new batch of identical units to train or a technology to research to the production queue. */ ProductionQueue.prototype.AddBatch = function(templateName, type, count, metadata) { // TODO: there should probably be a limit on the number of queued batches // TODO: there should be a way for the GUI to determine whether it's going // to be possible to add a batch (based on resource costs and length limits) let cmpPlayer = QueryOwnerInterface(this.entity); if (this.queue.length < MAX_QUEUE_SIZE) { if (type == "unit") { if (!Number.isInteger(count) || count <= 0) { error("Invalid batch count " + count); return; } // Find the template data so we can determine the build costs let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let template = cmpTemplateManager.GetTemplate(templateName); if (!template) return; if (template.Promotion && ApplyValueModificationsToTemplate("Promotion/RequiredXp", +template.Promotion.RequiredXp, cmpPlayer.GetPlayerID(), template) == 0) { this.AddBatch(template.Promotion.Entity, type, count, metadata); return; } // Apply a time discount to larger batches. let timeMult = this.GetBatchTime(count); // We need the costs after tech modifications // Obviously we don't have the entities yet, so we must use template data let costs = {}; let totalCosts = {}; let buildTime = ApplyValueModificationsToTemplate("Cost/BuildTime", +template.Cost.BuildTime, cmpPlayer.GetPlayerID(), template); let time = timeMult * buildTime; for (let res in template.Cost.Resources) { costs[res] = ApplyValueModificationsToTemplate("Cost/Resources/"+res, +template.Cost.Resources[res], cmpPlayer.GetPlayerID(), template); totalCosts[res] = Math.floor(count * costs[res]); } // TrySubtractResources should report error to player (they ran out of resources) if (!cmpPlayer.TrySubtractResources(totalCosts)) return; // Update entity count in the EntityLimits component if (template.TrainingRestrictions) { let unitCategory = template.TrainingRestrictions.Category; let cmpPlayerEntityLimits = QueryOwnerInterface(this.entity, IID_EntityLimits); cmpPlayerEntityLimits.ChangeCount(unitCategory, count); } this.queue.push({ "id": this.nextID++, "player": cmpPlayer.GetPlayerID(), "unitTemplate": templateName, "count": count, "metadata": metadata, "resources": costs, "population": ApplyValueModificationsToTemplate("Cost/Population", +template.Cost.Population, cmpPlayer.GetPlayerID(), template), "productionStarted": false, "timeTotal": time*1000, "timeRemaining": time*1000, }); // Call the related trigger event let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.CallEvent("TrainingQueued", { "playerid": cmpPlayer.GetPlayerID(), "unitTemplate": templateName, "count": count, "metadata": metadata, "trainerEntity": this.entity }); } else if (type == "technology") { if (!TechnologyTemplates.Has(templateName)) return; if (!this.GetTechnologiesList().some(tech => tech && (tech == templateName || tech.pair && (tech.top == templateName || tech.bottom == templateName)))) { error("This entity cannot research " + templateName); return; } let template = TechnologyTemplates.Get(templateName); let techCostMultiplier = this.GetTechCostMultiplier(); - let time = techCostMultiplier.time * template.researchTime * cmpPlayer.GetTimeMultiplier(); + let time = techCostMultiplier.time * template.researchTime; let cost = {}; for (let res in template.cost) cost[res] = Math.floor((techCostMultiplier[res] || 1) * template.cost[res]); // TrySubtractResources should report error to player (they ran out of resources) if (!cmpPlayer.TrySubtractResources(cost)) return; // Tell the technology manager that we have started researching this so that people can't research the same // thing twice. let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); cmpTechnologyManager.QueuedResearch(templateName, this.entity); if (this.queue.length == 0) cmpTechnologyManager.StartedResearch(templateName, false); this.queue.push({ "id": this.nextID++, "player": cmpPlayer.GetPlayerID(), "count": 1, "technologyTemplate": templateName, "resources": cost, "productionStarted": false, "timeTotal": time*1000, "timeRemaining": time*1000, }); // Call the related trigger event let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.CallEvent("ResearchQueued", { "playerid": cmpPlayer.GetPlayerID(), "technologyTemplate": templateName, "researcherEntity": this.entity }); } else { warn("Tried to add invalid item of type \"" + type + "\" and template \"" + templateName + "\" to a production queue"); return; } Engine.PostMessage(this.entity, MT_ProductionQueueChanged, { }); // If this is the first item in the queue, start the timer if (!this.timer) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetTimeout(this.entity, IID_ProductionQueue, "ProgressTimeout", g_ProgressInterval, {}); } } else { let notification = { "players": [cmpPlayer.GetPlayerID()], "message": markForTranslation("The production queue is full."), "translateMessage": true }; let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification(notification); } }; /* * Removes an existing batch of units from the production queue. * Refunds resource costs and population reservations. */ ProductionQueue.prototype.RemoveBatch = function(id) { // Destroy any cached entities (those which didn't spawn for some reason) for (let ent of this.entityCache) Engine.DestroyEntity(ent); this.entityCache = []; for (var i = 0; i < this.queue.length; ++i) { var item = this.queue[i]; if (item.id != id) continue; // Now we've found the item to remove var cmpPlayer = QueryPlayerIDInterface(item.player); // Update entity count in the EntityLimits component if (item.unitTemplate) { var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetTemplate(item.unitTemplate); if (template.TrainingRestrictions) { var unitCategory = template.TrainingRestrictions.Category; var cmpPlayerEntityLimits = QueryPlayerIDInterface(item.player, IID_EntityLimits); cmpPlayerEntityLimits.ChangeCount(unitCategory, -item.count); } } // Refund the resource cost for this batch var totalCosts = {}; var cmpStatisticsTracker = QueryPlayerIDInterface(item.player, IID_StatisticsTracker); for (let r in item.resources) { totalCosts[r] = Math.floor(item.count * item.resources[r]); if (cmpStatisticsTracker) cmpStatisticsTracker.IncreaseResourceUsedCounter(r, -totalCosts[r]); } cmpPlayer.AddResources(totalCosts); // Remove reserved population slots if necessary if (item.productionStarted && item.unitTemplate) cmpPlayer.UnReservePopulationSlots(item.population * item.count); // Mark the research as stopped if we cancel it if (item.technologyTemplate) { // item.player is used as this.entity's owner may be invalid (deletion, etc.) var cmpTechnologyManager = QueryPlayerIDInterface(item.player, IID_TechnologyManager); cmpTechnologyManager.StoppedResearch(item.technologyTemplate, true); } // Remove from the queue // (We don't need to remove the timer - it'll expire if it discovers the queue is empty) this.queue.splice(i, 1); Engine.PostMessage(this.entity, MT_ProductionQueueChanged, { }); return; } }; /* * Returns basic data from all batches in the production queue. */ ProductionQueue.prototype.GetQueue = function() { var out = []; for (var item of this.queue) { out.push({ "id": item.id, "unitTemplate": item.unitTemplate, "technologyTemplate": item.technologyTemplate, "count": item.count, "neededSlots": item.neededSlots, "progress": 1 - (item.timeRemaining / (item.timeTotal || 1)), "timeRemaining": item.timeRemaining, "metadata": item.metadata, }); } return out; }; /* * Removes all existing batches from the queue. */ ProductionQueue.prototype.ResetQueue = function() { // Empty the production queue and refund all the resource costs // to the player. (This is to avoid players having to micromanage their // buildings' queues when they're about to be destroyed or captured.) while (this.queue.length) this.RemoveBatch(this.queue[0].id); }; /* * Returns batch build time. */ ProductionQueue.prototype.GetBatchTime = function(batchSize) { - var cmpPlayer = QueryOwnerInterface(this.entity); - - var batchTimeModifier = ApplyValueModificationsToEntity("ProductionQueue/BatchTimeModifier", +this.template.BatchTimeModifier, this.entity); - // TODO: work out what equation we should use here. - return Math.pow(batchSize, batchTimeModifier) * cmpPlayer.GetTimeMultiplier(); + return Math.pow(batchSize, ApplyValueModificationsToEntity("ProductionQueue/BatchTimeModifier", +this.template.BatchTimeModifier, this.entity)); }; ProductionQueue.prototype.OnOwnershipChanged = function(msg) { if (msg.from != INVALID_PLAYER) { // Unset flag that previous owner's training may be blocked var cmpPlayer = QueryPlayerIDInterface(msg.from); if (cmpPlayer && this.queue.length > 0) cmpPlayer.UnBlockTraining(); } if (msg.to != INVALID_PLAYER) this.CalculateEntitiesList(); // Reset the production queue whenever the owner changes. // (This should prevent players getting surprised when they capture // an enemy building, and then loads of the enemy's civ's soldiers get // created from it. Also it means we don't have to worry about // updating the reserved pop slots.) this.ResetQueue(); }; ProductionQueue.prototype.OnCivChanged = function() { this.CalculateEntitiesList(); }; ProductionQueue.prototype.OnDestroy = function() { // Reset the queue to refund any resources this.ResetQueue(); if (this.timer) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); } }; /* * This function creates the entities and places them in world if possible * and returns the number of successfully created entities. * (some of these entities may be garrisoned directly if autogarrison, the others are spawned). */ ProductionQueue.prototype.SpawnUnits = function(templateName, count, metadata) { let cmpFootprint = Engine.QueryInterface(this.entity, IID_Footprint); let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); let cmpRallyPoint = Engine.QueryInterface(this.entity, IID_RallyPoint); let createdEnts = []; let spawnedEnts = []; // We need entities to test spawning, but we don't want to waste resources, // so only create them once and use as needed if (this.entityCache.length == 0) for (let i = 0; i < count; ++i) this.entityCache.push(Engine.AddEntity(templateName)); let cmpAutoGarrison; if (cmpRallyPoint) { let data = cmpRallyPoint.GetData()[0]; if (data && data.target && data.target == this.entity && data.command == "garrison") cmpAutoGarrison = Engine.QueryInterface(this.entity, IID_GarrisonHolder); } for (let i = 0; i < count; ++i) { let ent = this.entityCache[0]; let cmpNewOwnership = Engine.QueryInterface(ent, IID_Ownership); let garrisoned = false; if (cmpAutoGarrison) { // Temporary owner affectation needed for GarrisonHolder checks cmpNewOwnership.SetOwnerQuiet(cmpOwnership.GetOwner()); garrisoned = cmpAutoGarrison.PerformGarrison(ent); cmpNewOwnership.SetOwnerQuiet(INVALID_PLAYER); } if (garrisoned) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) cmpUnitAI.Autogarrison(this.entity); } else { let pos = cmpFootprint.PickSpawnPoint(ent); if (pos.y < 0) break; let cmpNewPosition = Engine.QueryInterface(ent, IID_Position); cmpNewPosition.JumpTo(pos.x, pos.z); let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (cmpPosition) cmpNewPosition.SetYRotation(cmpPosition.GetPosition().horizAngleTo(pos)); spawnedEnts.push(ent); } // Decrement entity count in the EntityLimits component // since it will be increased by EntityLimits.OnGlobalOwnershipChanged function, // i.e. we replace a 'trained' entity by 'alive' one // Must be done after spawn check so EntityLimits decrements only if unit spawns let cmpTrainingRestrictions = Engine.QueryInterface(ent, IID_TrainingRestrictions); if (cmpTrainingRestrictions) { let unitCategory = cmpTrainingRestrictions.GetCategory(); let cmpPlayerEntityLimits = QueryOwnerInterface(this.entity, IID_EntityLimits); cmpPlayerEntityLimits.ChangeCount(unitCategory, -1); } cmpNewOwnership.SetOwner(cmpOwnership.GetOwner()); let cmpPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker); if (cmpPlayerStatisticsTracker) cmpPlayerStatisticsTracker.IncreaseTrainedUnitsCounter(ent); // Play a sound, but only for the first in the batch (to avoid nasty phasing effects) if (createdEnts.length == 0) PlaySound("trained", ent); this.entityCache.shift(); createdEnts.push(ent); } if (spawnedEnts.length > 0 && !cmpAutoGarrison) { // If a rally point is set, walk towards it (in formation) using a suitable command based on where the // rally point is placed. if (cmpRallyPoint) { let rallyPos = cmpRallyPoint.GetPositions()[0]; if (rallyPos) { let commands = GetRallyPointCommands(cmpRallyPoint, spawnedEnts); for (let com of commands) ProcessCommand(cmpOwnership.GetOwner(), com); } } } if (createdEnts.length > 0) Engine.PostMessage(this.entity, MT_TrainingFinished, { "entities": createdEnts, "owner": cmpOwnership.GetOwner(), "metadata": metadata, }); return createdEnts.length; }; /* * Increments progress on the first batch in the production queue, and blocks the * queue if population limit is reached or some units failed to spawn. */ ProductionQueue.prototype.ProgressTimeout = function(data) { // Check if the production is paused (eg the entity is garrisoned) if (this.paused) return; // Allocate the 1000msecs to as many queue items as it takes // until we've used up all the time (so that we work accurately // with items that take fractions of a second) var time = g_ProgressInterval; var cmpPlayer = QueryOwnerInterface(this.entity); while (time > 0 && this.queue.length) { var item = this.queue[0]; if (!item.productionStarted) { // If the item is a unit then do population checks if (item.unitTemplate) { // If something change population cost var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(item.unitTemplate); item.population = ApplyValueModificationsToTemplate("Cost/Population", +template.Cost.Population, item.player, template); // Batch's training hasn't started yet. // Try to reserve the necessary population slots item.neededSlots = cmpPlayer.TryReservePopulationSlots(item.population * item.count); if (item.neededSlots) { // Not enough slots available - don't train this batch now // (we'll try again on the next timeout) // Set flag that training is blocked cmpPlayer.BlockTraining(); break; } // Unset flag that training is blocked cmpPlayer.UnBlockTraining(); } if (item.technologyTemplate) { // Mark the research as started. var cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); cmpTechnologyManager.StartedResearch(item.technologyTemplate, true); } item.productionStarted = true; if (item.unitTemplate) Engine.PostMessage(this.entity, MT_TrainingStarted, { "entity": this.entity }); } // If we won't finish the batch now, just update its timer if (item.timeRemaining > time) { item.timeRemaining -= time; // send a message for the AIs. Engine.PostMessage(this.entity, MT_ProductionQueueChanged, { }); break; } if (item.unitTemplate) { var numSpawned = this.SpawnUnits(item.unitTemplate, item.count, item.metadata); if (numSpawned == item.count) { // All entities spawned, this batch finished cmpPlayer.UnReservePopulationSlots(item.population * numSpawned); time -= item.timeRemaining; this.queue.shift(); // Unset flag that training is blocked cmpPlayer.UnBlockTraining(); this.spawnNotified = false; Engine.PostMessage(this.entity, MT_ProductionQueueChanged, { }); } else { if (numSpawned > 0) { // Only partially finished cmpPlayer.UnReservePopulationSlots(item.population * numSpawned); item.count -= numSpawned; Engine.PostMessage(this.entity, MT_ProductionQueueChanged, { }); } // Some entities failed to spawn // Set flag that training is blocked cmpPlayer.BlockTraining(); if (!this.spawnNotified) { var cmpPlayer = QueryOwnerInterface(this.entity); var notification = { "players": [cmpPlayer.GetPlayerID()], "message": markForTranslation("Can't find free space to spawn trained units"), "translateMessage": true }; var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification(notification); this.spawnNotified = true; } break; } } else if (item.technologyTemplate) { var cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); cmpTechnologyManager.ResearchTechnology(item.technologyTemplate); let template = TechnologyTemplates.Get(item.technologyTemplate); if (template && template.soundComplete) { var cmpSoundManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager); if (cmpSoundManager) cmpSoundManager.PlaySoundGroup(template.soundComplete, this.entity); } time -= item.timeRemaining; this.queue.shift(); Engine.PostMessage(this.entity, MT_ProductionQueueChanged, { }); } } // If the queue's empty, delete the timer, else repeat it if (this.queue.length == 0) { this.timer = undefined; // Unset flag that training is blocked // (This might happen when the player unqueues all batches) cmpPlayer.UnBlockTraining(); } else { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetTimeout(this.entity, IID_ProductionQueue, "ProgressTimeout", g_ProgressInterval, data); } }; ProductionQueue.prototype.PauseProduction = function() { this.timer = undefined; this.paused = true; }; ProductionQueue.prototype.UnpauseProduction = function() { this.paused = false; var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetTimeout(this.entity, IID_ProductionQueue, "ProgressTimeout", g_ProgressInterval, {}); }; ProductionQueue.prototype.OnValueModification = function(msg) { // if the promotion requirements of units is changed, // update the entities list so that automatically promoted units are shown // appropriately in the list if (msg.component == "Promotion") this.CalculateEntitiesList(); }; ProductionQueue.prototype.OnDisabledTemplatesChanged = function(msg) { // if the disabled templates of the player is changed, // update the entities list so that this is reflected there this.CalculateEntitiesList(); }; Engine.RegisterComponentType(IID_ProductionQueue, "ProductionQueue", ProductionQueue); Index: ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js (revision 22963) +++ ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js (revision 22964) @@ -1,352 +1,350 @@ 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 } }; /** * 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 (let resource of 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; 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.baseSpeed = 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); 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) let diminishingReturns = cmpResourceSupply.GetDiminishingReturns(); if (diminishingReturns) 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 (let type of 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 == INVALID_PLAYER) return; this.RecalculateGatherRatesAndCapacities(); }; ResourceGatherer.prototype.OnGlobalInitGame = function(msg) { this.RecalculateGatherRatesAndCapacities(); }; ResourceGatherer.prototype.OnMultiplierChanged = function(msg) { if (msg.player == QueryOwnerInterface(this.entity, IID_Player).GetPlayerID()) this.RecalculateGatherRatesAndCapacities(); }; Engine.RegisterComponentType(IID_ResourceGatherer, "ResourceGatherer", ResourceGatherer); Index: ps/trunk/binaries/data/mods/public/simulation/components/Upgrade.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Upgrade.js (revision 22963) +++ ps/trunk/binaries/data/mods/public/simulation/components/Upgrade.js (revision 22964) @@ -1,320 +1,318 @@ function Upgrade() {} const UPGRADING_PROGRESS_INTERVAL = 250; Upgrade.prototype.Schema = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + Resources.BuildSchema("nonNegativeInteger") + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; Upgrade.prototype.Init = function() { this.upgrading = false; this.completed = false; this.elapsedTime = 0; this.timer = undefined; this.expendedResources = {}; this.upgradeTemplates = {}; for (let choice in this.template) { let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); let name = this.template[choice].Entity; if (cmpIdentity) name = name.replace(/\{civ\}/g, cmpIdentity.GetCiv()); if (this.upgradeTemplates.name) warn("Upgrade Component: entity " + this.entity + " has two upgrades to the same entity, only the last will be used."); this.upgradeTemplates[name] = choice; } }; // This will also deal with the "OnDestroy" case. Upgrade.prototype.OnOwnershipChanged = function(msg) { if (!this.completed) this.CancelUpgrade(msg.from); if (msg.to != INVALID_PLAYER) this.owner = msg.to; }; Upgrade.prototype.ChangeUpgradedEntityCount = function(amount) { if (!this.IsUpgrading()) return; let cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let template = cmpTempMan.GetTemplate(this.upgrading); let categoryTo; if (template.TrainingRestrictions) categoryTo = template.TrainingRestrictions.Category; else if (template.BuildRestrictions) categoryTo = template.BuildRestrictions.Category; if (!categoryTo) return; let categoryFrom; let cmpTrainingRestrictions = Engine.QueryInterface(this.entity, IID_TrainingRestrictions); let cmpBuildRestrictions = Engine.QueryInterface(this.entity, IID_BuildRestrictions); if (cmpTrainingRestrictions) categoryFrom = cmpTrainingRestrictions.GetCategory(); else if (cmpBuildRestrictions) categoryFrom = cmpBuildRestrictions.GetCategory(); if (categoryTo == categoryFrom) return; let cmpEntityLimits = QueryPlayerIDInterface(this.owner, IID_EntityLimits); cmpEntityLimits.ChangeCount(categoryTo, amount); }; Upgrade.prototype.CanUpgradeTo = function(template) { return this.upgradeTemplates[template] !== undefined; }; Upgrade.prototype.GetUpgrades = function() { let ret = []; let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); for (let option in this.template) { let choice = this.template[option]; let templateName = cmpIdentity ? choice.Entity.replace(/\{civ\}/g, cmpIdentity.GetCiv()) : choice.Entity; let cost = {}; if (choice.Cost) cost = this.GetResourceCosts(templateName); if (choice.Time) cost.time = this.GetUpgradeTime(templateName); let hasCost = choice.Cost || choice.Time; ret.push({ "entity": templateName, "icon": choice.Icon || undefined, "cost": hasCost ? cost : undefined, "tooltip": choice.Tooltip || undefined, "requiredTechnology": this.GetRequiredTechnology(option), }); } return ret; }; Upgrade.prototype.CancelTimer = function() { if (!this.timer) return; let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); this.timer = undefined; }; Upgrade.prototype.IsUpgrading = function() { return !!this.upgrading; }; Upgrade.prototype.GetUpgradingTo = function() { return this.upgrading; }; Upgrade.prototype.WillCheckPlacementRestrictions = function(template) { if (!this.upgradeTemplates[template]) return undefined; // is undefined by default so use X in Y return "CheckPlacementRestrictions" in this.template[this.upgradeTemplates[template]]; }; Upgrade.prototype.GetRequiredTechnology = function(templateArg) { let choice = this.upgradeTemplates[templateArg] || templateArg; if (this.template[choice].RequiredTechnology) return this.template[choice].RequiredTechnology; if (!("RequiredTechnology" in this.template[choice])) return undefined; let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); let entType = this.template[choice].Entity; if (cmpIdentity) entType = entType.replace(/\{civ\}/g, cmpIdentity.GetCiv()); let template = cmpTemplateManager.GetTemplate(entType); return template.Identity.RequiredTechnology || undefined; }; Upgrade.prototype.GetResourceCosts = function(template) { if (!this.upgradeTemplates[template]) return undefined; if (this.IsUpgrading() && template == this.GetUpgradingTo()) return clone(this.expendedResources); let choice = this.upgradeTemplates[template]; if (!this.template[choice].Cost) return {}; let costs = {}; for (let r in this.template[choice].Cost) costs[r] = ApplyValueModificationsToEntity("Upgrade/Cost/"+r, +this.template[choice].Cost[r], this.entity); return costs; }; Upgrade.prototype.Upgrade = function(template) { if (this.IsUpgrading() || !this.upgradeTemplates[template]) return false; let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); this.expendedResources = this.GetResourceCosts(template); if (!cmpPlayer.TrySubtractResources(this.expendedResources)) { this.expendedResources = {}; return false; } this.upgrading = template; // Prevent cheating this.ChangeUpgradedEntityCount(1); if (this.GetUpgradeTime(template) !== 0) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetInterval(this.entity, IID_Upgrade, "UpgradeProgress", 0, UPGRADING_PROGRESS_INTERVAL, { "upgrading": template }); } else this.UpgradeProgress(); return true; }; Upgrade.prototype.CancelUpgrade = function(owner) { if (!this.IsUpgrading()) return; let cmpPlayer = QueryPlayerIDInterface(owner, IID_Player); if (cmpPlayer) cmpPlayer.AddResources(this.expendedResources); this.expendedResources = {}; this.ChangeUpgradedEntityCount(-1); this.upgrading = false; this.CancelTimer(); this.SetElapsedTime(0); }; Upgrade.prototype.GetUpgradeTime = function(templateArg) { let template = this.upgrading || templateArg; let choice = this.upgradeTemplates[template]; if (!choice) return undefined; if (!this.template[choice].Time) return 0; - let cmpPlayer = QueryPlayerIDInterface(this.owner, IID_Player); - return ApplyValueModificationsToEntity("Upgrade/Time", +this.template[choice].Time, this.entity) * - cmpPlayer.GetTimeMultiplier(); + return ApplyValueModificationsToEntity("Upgrade/Time", +this.template[choice].Time, this.entity); }; Upgrade.prototype.GetElapsedTime = function() { return this.elapsedTime; }; Upgrade.prototype.GetProgress = function() { if (!this.IsUpgrading()) return undefined; return this.GetUpgradeTime() == 0 ? 1 : Math.min(this.elapsedTime / 1000.0 / this.GetUpgradeTime(), 1.0); }; Upgrade.prototype.SetElapsedTime = function(time) { this.elapsedTime = time; }; Upgrade.prototype.UpgradeProgress = function(data, lateness) { if (this.elapsedTime/1000.0 < this.GetUpgradeTime()) { this.SetElapsedTime(this.GetElapsedTime() + UPGRADING_PROGRESS_INTERVAL + lateness); return; } this.CancelTimer(); this.completed = true; this.ChangeUpgradedEntityCount(-1); this.expendedResources = {}; let newEntity = ChangeEntityTemplate(this.entity, this.upgrading); if (newEntity) PlaySound("upgraded", newEntity); }; Engine.RegisterComponentType(IID_Upgrade, "Upgrade", Upgrade); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Pack.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Pack.js (revision 22963) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Pack.js (revision 22964) @@ -1,159 +1,155 @@ Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("Sound.js"); Engine.LoadHelperScript("Transform.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Engine.LoadComponentScript("interfaces/Capturable.js"); Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); Engine.LoadComponentScript("interfaces/Guard.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/Pack.js"); Engine.LoadComponentScript("interfaces/Player.js"); Engine.LoadComponentScript("interfaces/Promotion.js"); Engine.LoadComponentScript("interfaces/ResourceGatherer.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("Pack.js"); Engine.RegisterGlobal("MT_EntityRenamed", "entityRenamed"); const ent = 170; const newEnt = 171; const PACKING_INTERVAL = 250; let timerActivated = false; AddMock(ent, IID_Visual, { "SelectAnimation": (name, once, speed) => name }); AddMock(ent, IID_Ownership, { "GetOwner": () => 1 }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => 11 }); -AddMock(11, IID_Player, { - "GetTimeMultiplier": () => 1 -}); - AddMock(ent, IID_Sound, { "PlaySoundGroup": name => {} }); AddMock(SYSTEM_ENTITY, IID_Timer, { "CancelTimer": id => { timerActivated = false; return; }, "SetInterval": (ent, iid, funcname, time, repeattime, data) => { timerActivated = true; return 7; } }); Engine.AddEntity = function(template) { TS_ASSERT_EQUALS(template, "finalTemplate"); return true; }; // Test Packing let template = { "Entity": "finalTemplate", "Time": "2000", "State": "unpacked" }; let cmpPack = ConstructComponent(ent, "Pack", template); // Check internals TS_ASSERT(!cmpPack.packed); TS_ASSERT(!cmpPack.packing); TS_ASSERT_EQUALS(cmpPack.elapsedTime, 0); TS_ASSERT_EQUALS(cmpPack.timer, undefined); TS_ASSERT(!cmpPack.IsPacked()); TS_ASSERT(!cmpPack.IsPacking()); TS_ASSERT_EQUALS(cmpPack.GetProgress(), 0); // Pack cmpPack.Pack(); TS_ASSERT_EQUALS(cmpPack.timer, 7); TS_ASSERT(cmpPack.IsPacking()); // Listen to destroy message cmpPack.OnDestroy(); TS_ASSERT(!cmpPack.timer); TS_ASSERT(!timerActivated); // Test UnPacking template = { "Entity": "finalTemplate", "Time": "2000", "State": "packed" }; cmpPack = ConstructComponent(ent, "Pack", template); // Check internals TS_ASSERT(cmpPack.packed); TS_ASSERT(!cmpPack.packing); TS_ASSERT_EQUALS(cmpPack.elapsedTime, 0); TS_ASSERT_EQUALS(cmpPack.timer, undefined); TS_ASSERT(cmpPack.IsPacked()); TS_ASSERT(!cmpPack.IsPacking()); TS_ASSERT_EQUALS(cmpPack.GetProgress(), 0); // Unpack cmpPack.Unpack(); TS_ASSERT(cmpPack.IsPacking()); TS_ASSERT_EQUALS(cmpPack.timer, 7); // Unpack progress cmpPack.elapsedTime = 400; cmpPack.PackProgress({}, 100); TS_ASSERT(cmpPack.IsPacking()); TS_ASSERT_EQUALS(cmpPack.GetElapsedTime(), 400 + 100 + PACKING_INTERVAL); TS_ASSERT_EQUALS(cmpPack.GetProgress(), (400 + 100 + PACKING_INTERVAL) / 2000); // Try to Pack or Unpack while packing, nothing happen cmpPack.elapsedTime = 400; cmpPack.Unpack(); TS_ASSERT(cmpPack.IsPacking()); TS_ASSERT_EQUALS(cmpPack.GetElapsedTime(), 400); TS_ASSERT_EQUALS(cmpPack.timer, 7); TS_ASSERT(timerActivated); cmpPack.Pack(); TS_ASSERT(cmpPack.IsPacking()); TS_ASSERT_EQUALS(cmpPack.GetElapsedTime(), 400); TS_ASSERT_EQUALS(cmpPack.timer, 7); TS_ASSERT(timerActivated); // Cancel cmpPack.CancelPack(); TS_ASSERT(!cmpPack.IsPacking()); TS_ASSERT_EQUALS(cmpPack.GetElapsedTime(), 0); TS_ASSERT_EQUALS(cmpPack.GetProgress(), 0); TS_ASSERT_EQUALS(cmpPack.timer, undefined); TS_ASSERT(!timerActivated); // Progress until completing cmpPack.Unpack(); cmpPack.elapsedTime = 1800; cmpPack.PackProgress({}, 100); TS_ASSERT(cmpPack.IsPacking()); TS_ASSERT_EQUALS(cmpPack.GetElapsedTime(), 1800 + 100 + PACKING_INTERVAL); // Cap progress at 100% TS_ASSERT_EQUALS(cmpPack.GetProgress(), 1); TS_ASSERT_EQUALS(cmpPack.timer, 7); TS_ASSERT(timerActivated); // Unpack completing cmpPack.Unpack(); cmpPack.elapsedTime = 2100; cmpPack.PackProgress({}, 100); TS_ASSERT(!cmpPack.IsPacking()); TS_ASSERT(!cmpPack.timer); TS_ASSERT(!timerActivated); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UpgradeModification.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UpgradeModification.js (revision 22963) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UpgradeModification.js (revision 22964) @@ -1,162 +1,161 @@ Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("ValueModification.js"); Resources = { "BuildSchema": type => { let schema = ""; for (let res of ["food", "metal", "stone", "wood"]) schema += "" + "" + "" + "" + ""; return "" + schema + ""; } }; Engine.LoadComponentScript("interfaces/ModifiersManager.js"); // Provides `IID_ModifiersManager`, used below. Engine.LoadComponentScript("interfaces/Timer.js"); // Provides `IID_Timer`, used below. // What we're testing: Engine.LoadComponentScript("interfaces/Upgrade.js"); Engine.LoadComponentScript("Upgrade.js"); // Input (bare minimum needed for tests): let techs = { "alter_tower_upgrade_cost": { "modifications": [ { "value": "Upgrade/Cost/stone", "add": 60.0 }, { "value": "Upgrade/Cost/wood", "multiply": 0.5 }, { "value": "Upgrade/Time", "replace": 90 } ], "affects": ["Tower"] } }; let template = { "Identity": { "Classes": { '@datatype': "tokens", "_string": "Tower" }, "VisibleClasses": { '@datatype': "tokens", "_string": "" } }, "Upgrade": { "Tower": { "Cost": { "stone": "100", "wood": "50" }, "Entity": "structures/{civ}_defense_tower", "Time": "100" } } }; let civCode = "pony"; let playerID = 1; // Usually, the tech modifications would be worked out by the TechnologyManager // with assistance from globalscripts. This test is not about testing the // TechnologyManager, so the modifications (both with and without the technology // researched) are worked out before hand and placed here. let isResearched = false; let templateTechModifications = { "without": {}, "with": { "Upgrade/Cost/stone": [{ "affects": [["Tower"]], "add": 60 }], "Upgrade/Cost/wood": [{ "affects": [["Tower"]], "multiply": 0.5 }], "Upgrade/Time": [{ "affects": [["Tower"]], "replace": 90 }] } }; let entityTechModifications = { "without": { 'Upgrade/Cost/stone': { "20": { "origValue": 100, "newValue": 100 } }, 'Upgrade/Cost/wood': { "20": { "origValue": 50, "newValue": 50 } }, 'Upgrade/Time': { "20": { "origValue": 100, "newValue": 100 } } }, "with": { 'Upgrade/Cost/stone': { "20": { "origValue": 100, "newValue": 160 } }, 'Upgrade/Cost/wood': { "20": { "origValue": 50, "newValue": 25 } }, 'Upgrade/Time': { "20": { "origValue": 100, "newValue": 90 } } } }; /** * Initialise various bits. */ // System Entities: AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": pID => 10 // Called in helpers/player.js::QueryPlayerIDInterface(), as part of Tests T2 and T5. }); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "GetTemplate": () => template // Called in components/Upgrade.js::ChangeUpgradedEntityCount(). }); AddMock(SYSTEM_ENTITY, IID_Timer, { "SetInterval": () => 1, // Called in components/Upgrade.js::Upgrade(). "CancelTimer": () => {} // Called in components/Upgrade.js::CancelUpgrade(). }); AddMock(SYSTEM_ENTITY, IID_ModifiersManager, { "ApplyTemplateModifiers": (valueName, curValue, template, player) => { // Called in helpers/ValueModification.js::ApplyValueModificationsToTemplate() // as part of Tests T2 and T5 below. let mods = isResearched ? templateTechModifications.with : templateTechModifications.without; if (mods[valueName]) return GetTechModifiedProperty(mods[valueName], GetIdentityClasses(template.Identity), curValue); return curValue; }, "ApplyModifiers": (valueName, curValue, ent) => { // Called in helpers/ValueModification.js::ApplyValueModificationsToEntity() // as part of Tests T3, T6 and T7 below. let mods = isResearched ? entityTechModifications.with : entityTechModifications.without; return mods[valueName][ent].newValue; } }); // Init Player: AddMock(10, IID_Player, { "AddResources": () => {}, // Called in components/Upgrade.js::CancelUpgrade(). "GetPlayerID": () => playerID, // Called in helpers/Player.js::QueryOwnerInterface() (and several times below). - "GetTimeMultiplier": () => 1.0, // Called in components/Upgrade.js::GetUpgradeTime(). "TrySubtractResources": () => true // Called in components/Upgrade.js::Upgrade(). }); // Create an entity with an Upgrade component: AddMock(20, IID_Ownership, { "GetOwner": () => playerID // Called in helpers/Player.js::QueryOwnerInterface(). }); AddMock(20, IID_Identity, { "GetCiv": () => civCode // Called in components/Upgrade.js::init(). }); let cmpUpgrade = ConstructComponent(20, "Upgrade", template.Upgrade); cmpUpgrade.owner = playerID; /** * Now to start the test proper * To start with, no techs are researched... */ // T1: Check the cost of the upgrade without a player value being passed (as it would be in the structree). let parsed_template = GetTemplateDataHelper(template, null, {}); TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrades[0].cost, { "stone": 100, "wood": 50, "time": 100 }); // T2: Check the value, with a player ID (as it would be in-session). parsed_template = GetTemplateDataHelper(template, playerID, {}); TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrades[0].cost, { "stone": 100, "wood": 50, "time": 100 }); // T3: Check that the value is correct within the Update Component. TS_ASSERT_UNEVAL_EQUALS(cmpUpgrade.GetUpgrades()[0].cost, { "stone": 100, "wood": 50, "time": 100 }); /** * Tell the Upgrade component to start the Upgrade, * then mark the technology that alters the upgrade cost as researched. */ cmpUpgrade.Upgrade("structures/"+civCode+"_defense_tower"); isResearched = true; // T4: Check that the player-less value hasn't increased... parsed_template = GetTemplateDataHelper(template, null, {}); TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrades[0].cost, { "stone": 100, "wood": 50, "time": 100 }); // T5: ...but the player-backed value has. parsed_template = GetTemplateDataHelper(template, playerID, {}); TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrades[0].cost, { "stone": 160, "wood": 25, "time": 90 }); // T6: The upgrade component should still be using the old resource cost (but new time cost) for the upgrade in progress... TS_ASSERT_UNEVAL_EQUALS(cmpUpgrade.GetUpgrades()[0].cost, { "stone": 100, "wood": 50, "time": 90 }); // T7: ...but with the upgrade cancelled, it now uses the modified value. cmpUpgrade.CancelUpgrade(playerID); TS_ASSERT_UNEVAL_EQUALS(cmpUpgrade.GetUpgrades()[0].cost, { "stone": 160, "wood": 25, "time": 90 }); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Cheat.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Cheat.js (revision 22963) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Cheat.js (revision 22964) @@ -1,170 +1,177 @@ function Cheat(input) { var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (!cmpPlayerManager || input.player < 0) return; var playerEnt = cmpPlayerManager.GetPlayerByID(input.player); if (playerEnt == INVALID_ENTITY) return; var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player); if (!cmpPlayer) return; var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); if (!cmpPlayer.GetCheatsEnabled()) return; switch(input.action) { case "addresource": cmpPlayer.AddResource(input.text, input.parameter); return; case "revealmap": var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); cmpRangeManager.SetLosRevealAll(-1, true); return; case "maxpopulation": cmpPlayer.SetPopulationBonuses(500); return; case "changemaxpopulation": cmpPlayer.SetMaxPopulation(500); return; case "convertunit": for (let ent of input.selected) { let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) cmpOwnership.SetOwner(cmpPlayer.GetPlayerID()); } return; case "killunits": for (let ent of input.selected) { let cmpHealth = Engine.QueryInterface(ent, IID_Health); if (cmpHealth) cmpHealth.Kill(); else Engine.DestroyEntity(ent); } return; case "defeatplayer": cmpPlayer = QueryPlayerIDInterface(input.parameter); if (cmpPlayer) cmpPlayer.SetState("defeated", markForTranslation("%(player)s has been defeated (cheat).")); return; case "createunits": var cmpProductionQueue = input.selected.length && Engine.QueryInterface(input.selected[0], IID_ProductionQueue); if (!cmpProductionQueue) { cmpGuiInterface.PushNotification({ "type": "text", "players": [input.player], "message": markForTranslation("You need to select a building that trains units."), "translateMessage": true }); return; } for (let i = 0; i < Math.min(input.parameter, cmpPlayer.GetMaxPopulation() - cmpPlayer.GetPopulationCount()); ++i) cmpProductionQueue.SpawnUnits(input.templates[i % input.templates.length], 1, null); return; case "fastactions": - cmpPlayer.SetCheatTimeMultiplier((cmpPlayer.GetCheatTimeMultiplier() == 1) ? 0.01 : 1); - return; - case "changespeed": - cmpPlayer.SetCheatTimeMultiplier(input.parameter); + let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); + if (cmpModifiersManager.HasAnyModifier("cheat/fastactions", playerEnt)) + cmpModifiersManager.RemoveAllModifiers("cheat/fastactions", playerEnt); + else + cmpModifiersManager.AddModifiers("cheat/fastactions", { + "Cost/BuildTime": { "affects": [["Structure"], ["Unit"]], "multiply": 0.01 }, + "ResourceGatherer/BaseSpeed": { "affects": [["Structure"], ["Unit"]], "multiply": 1000 }, + "Pack/Time": { "affects": [["Structure"], ["Unit"]], "multiply": 0.01 }, + "Upgrade/Time": { "affects": [["Structure"], ["Unit"]], "multiply": 0.01 }, + "ProductionQueue/TechCostMultiplier/time": { "affects": [["Structure"], ["Unit"]], "multiply": 0.01 } + }, playerEnt); return; case "changephase": var cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager); if (!cmpTechnologyManager) return; // store the phase we want in the next input parameter var parameter; if (!cmpTechnologyManager.IsTechnologyResearched("phase_town")) parameter = "phase_town"; else if (!cmpTechnologyManager.IsTechnologyResearched("phase_city")) parameter = "phase_city"; else return; if (TechnologyTemplates.Has(parameter + "_" + cmpPlayer.civ)) parameter += "_" + cmpPlayer.civ; else parameter += "_generic"; Cheat({ "player": input.player, "action": "researchTechnology", "parameter": parameter, "selected": input.selected }); return; case "researchTechnology": if (!input.parameter.length) return; var techname = input.parameter; var cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager); if (!cmpTechnologyManager) return; // check, if building is selected if (input.selected[0]) { var cmpProductionQueue = Engine.QueryInterface(input.selected[0], IID_ProductionQueue); if (cmpProductionQueue) { // try to spilt the input var tmp = input.parameter.split(/\s+/); var number = +tmp[0]; var pair = tmp.length > 1 && (tmp[1] == "top" || tmp[1] == "bottom") ? tmp[1] : "top"; // use top as default value // check, if valid number was parsed. if (number || number === 0) { // get name of tech var techs = cmpProductionQueue.GetTechnologiesList(); if (number > 0 && number <= techs.length) { var tech = techs[number-1]; if (!tech) return; // get name of tech if (tech.pair) techname = tech[pair]; else techname = tech; } else return; } } } if (TechnologyTemplates.Has(techname) && !cmpTechnologyManager.IsTechnologyResearched(techname)) cmpTechnologyManager.ResearchTechnology(techname); return; case "metaCheat": for (let resource of Resources.GetCodes()) Cheat({ "player": input.player, "action": "addresource", "text": resource, "parameter": input.parameter }); Cheat({ "player": input.player, "action": "maxpopulation" }); Cheat({ "player": input.player, "action": "changemaxpopulation" }); Cheat({ "player": input.player, "action": "fastactions" }); for (let i=0; i<2; ++i) Cheat({ "player": input.player, "action": "changephase", "selected": input.selected }); return; case "playRetro": let play = input.parameter.toLowerCase() != "off"; cmpGuiInterface.PushNotification({ "type": "play-tracks", "tracks": play && input.parameter.split(" "), "lock": play, "players": [input.player] }); return; default: warn("Cheat '" + input.action + "' is not implemented"); return; } } Engine.RegisterGlobal("Cheat", Cheat); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/InitGame.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/InitGame.js (revision 22963) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/InitGame.js (revision 22964) @@ -1,84 +1,87 @@ /** * Called when the map has been loaded, but before the simulation has started. * Only called when a new game is started, not when loading a saved game. */ function PreInitGame() { // We need to replace skirmish "default" entities with real ones. // This needs to happen before AI initialization (in InitGame). // And we need to flush destroyed entities otherwise the AI gets the wrong game state in // the beginning and a bunch of "destroy" messages on turn 0, which just shouldn't happen. Engine.BroadcastMessage(MT_SkirmishReplace, {}); Engine.FlushDestroyedEntities(); let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 1; i < numPlayers; ++i) // ignore gaia { let cmpTechnologyManager = QueryPlayerIDInterface(i, IID_TechnologyManager); if (cmpTechnologyManager) cmpTechnologyManager.UpdateAutoResearch(); } // Explore the map inside the players' territory borders let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); cmpRangeManager.ExploreTerritories(); } function InitGame(settings) { // No settings when loading a map in Atlas, so do nothing if (!settings) { // Map dependent initialisations of components (i.e. garrisoned units) Engine.BroadcastMessage(MT_InitGame, {}); return; } if (settings.ExploreMap) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); for (let i = 1; i < settings.PlayerData.length; ++i) cmpRangeManager.ExploreAllTiles(i); } // Sandbox, Very Easy, Easy, Medium, Hard, Very Hard // rate apply on resource stockpiling as gathering and trading // time apply on building, upgrading, packing, training and technologies let rate = [ 0.42, 0.56, 0.75, 1.00, 1.25, 1.56 ]; let time = [ 1.40, 1.25, 1.10, 1.00, 1.00, 1.00 ]; + let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); let cmpAIManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIManager); for (let i = 0; i < settings.PlayerData.length; ++i) { let cmpPlayer = QueryPlayerIDInterface(i); cmpPlayer.SetCheatsEnabled(!!settings.CheatsEnabled); if (settings.PlayerData[i] && settings.PlayerData[i].AI && settings.PlayerData[i].AI != "") { let AIDiff = +settings.PlayerData[i].AIDiff; cmpAIManager.AddPlayer(settings.PlayerData[i].AI, i, AIDiff, settings.PlayerData[i].AIBehavior || "random"); cmpPlayer.SetAI(true); AIDiff = Math.min(AIDiff, rate.length - 1); - cmpPlayer.SetGatherRateMultiplier(rate[AIDiff]); - cmpPlayer.SetTradeRateMultiplier(rate[AIDiff]); - cmpPlayer.SetTimeMultiplier(time[AIDiff]); + cmpModifiersManager.AddModifiers("AI Bonus", { + "ResourceGatherer/BaseSpeed": { "affects": ["Unit", "Structure"], "multiply": rate[AIDiff] }, + "Trader/GainMultiplier": { "affects": ["Unit", "Structure"], "multiply": rate[AIDiff] }, + "Cost/BuildTime": { "affects": ["Unit", "Structure"], "multiply": time[AIDiff] }, + }, cmpPlayer.entity); } if (settings.PopulationCap) cmpPlayer.SetMaxPopulation(settings.PopulationCap); if (settings.mapType !== "scenario" && settings.StartingResources) { let resourceCounts = cmpPlayer.GetResourceCounts(); let newResourceCounts = {}; for (let resouces in resourceCounts) newResourceCounts[resouces] = settings.StartingResources; cmpPlayer.SetResourceCounts(newResourceCounts); } } // Map or player data (handicap...) dependent initialisations of components (i.e. garrisoned units) Engine.BroadcastMessage(MT_InitGame, {}); cmpAIManager.TryLoadSharedComponent(); cmpAIManager.RunGamestateInit(); } Engine.RegisterGlobal("PreInitGame", PreInitGame); Engine.RegisterGlobal("InitGame", InitGame); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Player.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Player.js (revision 22963) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Player.js (revision 22964) @@ -1,366 +1,361 @@ /** * Used to create player entities prior to reading the rest of a map, * all other initialization must be done after loading map (terrain/entities). * DO NOT use other components here, as they may fail unpredictably. * settings is the object containing settings for this map. * newPlayers if true will remove old player entities or add new ones until * the new number of player entities is obtained * (used when loading a map or when Atlas changes the number of players). */ function LoadPlayerSettings(settings, newPlayers) { var playerDefaults = Engine.ReadJSONFile("simulation/data/settings/player_defaults.json").PlayerData; // Default settings if (!settings) settings = {}; // Add gaia to simplify iteration // (if gaia is not already the first civ such as when called from Atlas' ActorViewer) if (settings.PlayerData && settings.PlayerData[0] && (!settings.PlayerData[0].Civ || settings.PlayerData[0].Civ != "gaia")) settings.PlayerData.unshift(null); var playerData = settings.PlayerData; // Disable the AIIinterface when no AI players are present if (playerData && !playerData.some(v => v && !!v.AI)) Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface).Disable(); var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var numPlayers = cmpPlayerManager.GetNumPlayers(); var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); // Remove existing players or add new ones if (newPlayers) { var settingsNumPlayers = 9; // default 8 players + gaia if (playerData) settingsNumPlayers = playerData.length; // includes gaia (see above) else warn("Player.js: Setup has no player data - using defaults"); while (settingsNumPlayers > numPlayers) { // Add player entity to engine var entID = Engine.AddEntity(GetPlayerTemplateName(getSetting(playerData, playerDefaults, numPlayers, "Civ"))); var cmpPlayer = Engine.QueryInterface(entID, IID_Player); if (!cmpPlayer) throw new Error("Player.js: Error creating player entity " + numPlayers); cmpPlayerManager.AddPlayer(entID); ++numPlayers; } while (settingsNumPlayers < numPlayers) { cmpPlayerManager.RemoveLastPlayer(); --numPlayers; } } // Even when no new player, we must check the template compatibility as player template may be civ dependent for (var i = 0; i < numPlayers; ++i) { var template = GetPlayerTemplateName(getSetting(playerData, playerDefaults, i, "Civ")); var entID = cmpPlayerManager.GetPlayerByID(i); if (cmpTemplateManager.GetCurrentTemplateName(entID) === template) continue; // We need to recreate this player to have the right template entID = Engine.AddEntity(template); cmpPlayerManager.ReplacePlayer(i, entID); } // Initialize the player data for (var i = 0; i < numPlayers; ++i) { let cmpPlayer = QueryPlayerIDInterface(i); cmpPlayer.SetName(getSetting(playerData, playerDefaults, i, "Name")); cmpPlayer.SetCiv(getSetting(playerData, playerDefaults, i, "Civ")); var color = getSetting(playerData, playerDefaults, i, "Color"); cmpPlayer.SetColor(color.r, color.g, color.b); // Special case for gaia if (i == 0) { // Gaia should be its own ally. cmpPlayer.SetAlly(0); // Gaia is everyone's enemy for (var j = 1; j < numPlayers; ++j) cmpPlayer.SetEnemy(j); continue; } - // Note: this is not yet implemented but I leave it commented to highlight it's easy - // If anyone ever adds handicap. - //if (getSetting(playerData, playerDefaults, i, "GatherRateMultiplier") !== undefined) - // cmpPlayer.SetGatherRateMultiplier(getSetting(playerData, playerDefaults, i, "GatherRateMultiplier")); - if (getSetting(playerData, playerDefaults, i, "PopulationLimit") !== undefined) cmpPlayer.SetMaxPopulation(getSetting(playerData, playerDefaults, i, "PopulationLimit")); if (getSetting(playerData, playerDefaults, i, "Resources") !== undefined) cmpPlayer.SetResourceCounts(getSetting(playerData, playerDefaults, i, "Resources")); if (getSetting(playerData, playerDefaults, i, "StartingTechnologies") !== undefined) cmpPlayer.SetStartingTechnologies(getSetting(playerData, playerDefaults, i, "StartingTechnologies")); if (getSetting(playerData, playerDefaults, i, "DisabledTechnologies") !== undefined) cmpPlayer.SetDisabledTechnologies(getSetting(playerData, playerDefaults, i, "DisabledTechnologies")); let disabledTemplates = []; if (settings.DisabledTemplates !== undefined) disabledTemplates = settings.DisabledTemplates; if (getSetting(playerData, playerDefaults, i, "DisabledTemplates") !== undefined) disabledTemplates = disabledTemplates.concat(getSetting(playerData, playerDefaults, i, "DisabledTemplates")); if (disabledTemplates.length) cmpPlayer.SetDisabledTemplates(disabledTemplates); if (settings.DisableSpies) { cmpPlayer.AddDisabledTechnology("unlock_spies"); cmpPlayer.AddDisabledTemplate("special/spy"); } // If diplomacy explicitly defined, use that; otherwise use teams if (getSetting(playerData, playerDefaults, i, "Diplomacy") !== undefined) cmpPlayer.SetDiplomacy(getSetting(playerData, playerDefaults, i, "Diplomacy")); else { // Init diplomacy var myTeam = getSetting(playerData, playerDefaults, i, "Team"); // Set all but self as enemies as SetTeam takes care of allies for (var j = 0; j < numPlayers; ++j) { if (i == j) cmpPlayer.SetAlly(j); else cmpPlayer.SetEnemy(j); } cmpPlayer.SetTeam(myTeam === undefined ? -1 : myTeam); } cmpPlayer.SetFormations( getSetting(playerData, playerDefaults, i, "Formations") || Engine.ReadJSONFile("simulation/data/civs/" + cmpPlayer.GetCiv() + ".json").Formations); var startCam = getSetting(playerData, playerDefaults, i, "StartingCamera"); if (startCam !== undefined) cmpPlayer.SetStartingCamera(startCam.Position, startCam.Rotation); } // NOTE: We need to do the team locking here, as otherwise // SetTeam can't ally the players. if (settings.LockTeams) for (let i = 0; i < numPlayers; ++i) QueryPlayerIDInterface(i).SetLockTeams(true); } // Get a setting if it exists or return default function getSetting(settings, defaults, idx, property) { if (settings && settings[idx] && (property in settings[idx])) return settings[idx][property]; // Use defaults if (defaults && defaults[idx] && (property in defaults[idx])) return defaults[idx][property]; return undefined; } function GetPlayerTemplateName(civ) { let path = "special/player/player"; if (Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).TemplateExists(path + "_" + civ)) return path + "_" + civ; return path; } /** * @param id An entity's ID * @returns The entity ID of the owner player (not his player ID) or ent if ent is a player entity. */ function QueryOwnerEntityID(ent) { let cmpPlayer = Engine.QueryInterface(ent, IID_Player); if (cmpPlayer) return ent; let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (!cmpOwnership) return null; let owner = cmpOwnership.GetOwner(); if (owner == INVALID_PLAYER) return null; let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (!cmpPlayerManager) return null; return cmpPlayerManager.GetPlayerByID(owner); } /** * Similar to Engine.QueryInterface but applies to the player entity * that owns the given entity. * iid is typically IID_Player. */ function QueryOwnerInterface(ent, iid = IID_Player) { var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (!cmpOwnership) return null; var owner = cmpOwnership.GetOwner(); if (owner == INVALID_PLAYER) return null; return QueryPlayerIDInterface(owner, iid); } /** * Similar to Engine.QueryInterface but applies to the player entity * with the given ID number. * iid is typically IID_Player. */ function QueryPlayerIDInterface(id, iid = IID_Player) { var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var playerEnt = cmpPlayerManager.GetPlayerByID(id); if (!playerEnt) return null; return Engine.QueryInterface(playerEnt, iid); } /** * Similar to Engine.QueryInterface but first checks if the entity * mirages the interface. */ function QueryMiragedInterface(ent, iid) { var cmp = Engine.QueryInterface(ent, IID_Mirage); if (cmp && !cmp.Mirages(iid)) return null; else if (!cmp) cmp = Engine.QueryInterface(ent, iid); return cmp; } /** * Similar to Engine.QueryInterface, but checks for all interfaces * implementing a builder list (currently Foundation and Repairable) * TODO Foundation and Repairable could both implement a BuilderList component */ function QueryBuilderListInterface(ent) { return Engine.QueryInterface(ent, IID_Foundation) || Engine.QueryInterface(ent, IID_Repairable); } /** * Returns true if the entity 'target' is owned by an ally of * the owner of 'entity'. */ function IsOwnedByAllyOfEntity(entity, target) { return IsOwnedByEntityHelper(entity, target, "IsAlly"); } function IsOwnedByMutualAllyOfEntity(entity, target) { return IsOwnedByEntityHelper(entity, target, "IsMutualAlly"); } function IsOwnedByEntityHelper(entity, target, check) { // Figure out which player controls us let owner = 0; let cmpOwnership = Engine.QueryInterface(entity, IID_Ownership); if (cmpOwnership) owner = cmpOwnership.GetOwner(); // Figure out which player controls the target entity let targetOwner = 0; let cmpOwnershipTarget = Engine.QueryInterface(target, IID_Ownership); if (cmpOwnershipTarget) targetOwner = cmpOwnershipTarget.GetOwner(); let cmpPlayer = QueryPlayerIDInterface(owner); return cmpPlayer && cmpPlayer[check](targetOwner); } /** * Returns true if the entity 'target' is owned by player */ function IsOwnedByPlayer(player, target) { var cmpOwnershipTarget = Engine.QueryInterface(target, IID_Ownership); return cmpOwnershipTarget && player == cmpOwnershipTarget.GetOwner(); } function IsOwnedByGaia(target) { return IsOwnedByPlayer(0, target); } /** * Returns true if the entity 'target' is owned by an ally of player */ function IsOwnedByAllyOfPlayer(player, target) { return IsOwnedByHelper(player, target, "IsAlly"); } function IsOwnedByMutualAllyOfPlayer(player, target) { return IsOwnedByHelper(player, target, "IsMutualAlly"); } function IsOwnedByNeutralOfPlayer(player,target) { return IsOwnedByHelper(player, target, "IsNeutral"); } function IsOwnedByEnemyOfPlayer(player, target) { return IsOwnedByHelper(player, target, "IsEnemy"); } function IsOwnedByHelper(player, target, check) { let targetOwner = 0; let cmpOwnershipTarget = Engine.QueryInterface(target, IID_Ownership); if (cmpOwnershipTarget) targetOwner = cmpOwnershipTarget.GetOwner(); let cmpPlayer = QueryPlayerIDInterface(player); return cmpPlayer && cmpPlayer[check](targetOwner); } Engine.RegisterGlobal("LoadPlayerSettings", LoadPlayerSettings); Engine.RegisterGlobal("QueryOwnerEntityID", QueryOwnerEntityID); Engine.RegisterGlobal("QueryOwnerInterface", QueryOwnerInterface); Engine.RegisterGlobal("QueryPlayerIDInterface", QueryPlayerIDInterface); Engine.RegisterGlobal("QueryMiragedInterface", QueryMiragedInterface); Engine.RegisterGlobal("QueryBuilderListInterface", QueryBuilderListInterface); Engine.RegisterGlobal("IsOwnedByAllyOfEntity", IsOwnedByAllyOfEntity); Engine.RegisterGlobal("IsOwnedByMutualAllyOfEntity", IsOwnedByMutualAllyOfEntity); Engine.RegisterGlobal("IsOwnedByPlayer", IsOwnedByPlayer); Engine.RegisterGlobal("IsOwnedByGaia", IsOwnedByGaia); Engine.RegisterGlobal("IsOwnedByAllyOfPlayer", IsOwnedByAllyOfPlayer); Engine.RegisterGlobal("IsOwnedByMutualAllyOfPlayer", IsOwnedByMutualAllyOfPlayer); Engine.RegisterGlobal("IsOwnedByNeutralOfPlayer", IsOwnedByNeutralOfPlayer); Engine.RegisterGlobal("IsOwnedByEnemyOfPlayer", IsOwnedByEnemyOfPlayer); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/TraderGain.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/TraderGain.js (revision 22963) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/TraderGain.js (revision 22964) @@ -1,70 +1,66 @@ function CalculateTraderGain(firstMarket, secondMarket, traderTemplate, trader) { let cmpMarket1 = QueryMiragedInterface(firstMarket, IID_Market); let cmpMarket2 = QueryMiragedInterface(secondMarket, IID_Market); if (!cmpMarket1 || !cmpMarket2) return null; let cmpMarket1Player = QueryOwnerInterface(firstMarket); let cmpMarket2Player = QueryOwnerInterface(secondMarket); if (!cmpMarket1Player || !cmpMarket2Player) return null; let cmpFirstMarketPosition = Engine.QueryInterface(firstMarket, IID_Position); let cmpSecondMarketPosition = Engine.QueryInterface(secondMarket, IID_Position); if (!cmpFirstMarketPosition || !cmpFirstMarketPosition.IsInWorld() || !cmpSecondMarketPosition || !cmpSecondMarketPosition.IsInWorld()) return null; let firstMarketPosition = cmpFirstMarketPosition.GetPosition2D(); let secondMarketPosition = cmpSecondMarketPosition.GetPosition2D(); let mapSize = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain).GetMapSize(); let gainMultiplier = TradeGainNormalization(mapSize); if (trader) { let cmpTrader = Engine.QueryInterface(trader, IID_Trader); if (!cmpTrader) return null; gainMultiplier *= cmpTrader.GetTraderGainMultiplier(); } - else //called from the gui, modifications already applied + else // Called from the gui, modifications already applied. { if (!traderTemplate || !traderTemplate.GainMultiplier) return null; gainMultiplier *= traderTemplate.GainMultiplier; } let gain = {}; // Calculate ordinary Euclidean distance between markets. // We don't use pathfinder, because ordinary distance looks more fair. let distanceSq = firstMarketPosition.distanceToSquared(secondMarketPosition); // We calculate gain as square of distance to encourage trading between remote markets // and gainMultiplier corresponds to the gain for a 100m distance gain.traderGain = gainMultiplier * TradeGain(distanceSq, mapSize); gain.market1Owner = cmpMarket1Player.GetPlayerID(); gain.market2Owner = cmpMarket2Player.GetPlayerID(); // If trader undefined, the trader owner is supposed to be the same as the first market let cmpPlayer = trader ? QueryOwnerInterface(trader) : cmpMarket1Player; if (!cmpPlayer) return null; gain.traderOwner = cmpPlayer.GetPlayerID(); - // Add potential player trade multipliers - let playerBonus = cmpPlayer.GetTradeRateMultiplier(); + // If markets belong to different players, add gain from international trading if (gain.market1Owner != gain.market2Owner) { - let market1PlayerBonus = cmpMarket1Player.GetTradeRateMultiplier(); - let market2PlayerBonus = cmpMarket2Player.GetTradeRateMultiplier(); let internationalBonus1 = cmpMarket1.GetInternationalBonus(); let internationalBonus2 = cmpMarket2.GetInternationalBonus(); - gain.market1Gain = Math.round(gain.traderGain * internationalBonus1 * market1PlayerBonus); - gain.market2Gain = Math.round(gain.traderGain * internationalBonus2 * market2PlayerBonus); + gain.market1Gain = Math.round(gain.traderGain * internationalBonus1); + gain.market2Gain = Math.round(gain.traderGain * internationalBonus2); } - gain.traderGain = Math.round(gain.traderGain * playerBonus); return gain; } Engine.RegisterGlobal("CalculateTraderGain", CalculateTraderGain);