Index: ps/trunk/binaries/data/mods/public/simulation/components/Player.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Player.js (revision 16505) +++ ps/trunk/binaries/data/mods/public/simulation/components/Player.js (revision 16506) @@ -1,792 +1,799 @@ function Player() {} Player.prototype.Schema = ""; Player.prototype.Init = function() { this.playerID = undefined; this.name = undefined; // define defaults elsewhere (supporting other languages) this.civ = undefined; this.color = { "r": 0.0, "g": 0.0, "b": 0.0, "a": 1.0 }; 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 = { "food": 300, "wood": 300, "metal": 300, "stone": 300 }; this.tradingGoods = [ // goods for next trade-route and its proba in % (the sum of probas must be 100) { "goods": "wood", "proba": 30 }, { "goods": "stone", "proba": 35 }, { "goods": "metal", "proba": 35 } ]; 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.conquestCriticalEntitiesCount = 0; // number of owned units with ConquestCritical class this.formations = []; this.startCam = undefined; this.controlAllUnits = false; this.isAI = false; this.gatherRateMultiplier = 1; this.cheatsEnabled = false; this.cheatTimeMultiplier = 1; this.heroes = []; this.resourceNames = { "food": markForTranslation("Food"), "wood": markForTranslation("Wood"), "metal": markForTranslation("Metal"), "stone": markForTranslation("Stone"), } this.disabledTemplates = {}; this.disabledTechnologies = {}; this.startingTechnologies = []; }; 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 == undefined || playerID == undefined) + return; // not initialised + Engine.BroadcastMessage(MT_CivChanged, {"player": playerID, "from": oldCiv, "to": civcode}); }; Player.prototype.GetCiv = function() { return this.civ; }; Player.prototype.SetColor = function(r, g, b) { this.color = { "r": r/255.0, "g": g/255.0, "b": b/255.0, "a": 1.0 }; }; Player.prototype.GetColor = function() { return 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.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(ApplyValueModificationsToPlayer("Player/MaxPopulation", this.maxPop, this.entity)); }; Player.prototype.SetGatherRateMultiplier = function(value) { this.gatherRateMultiplier = value; }; Player.prototype.GetGatherRateMultiplier = function() { return this.gatherRateMultiplier; }; Player.prototype.GetHeroes = function() { return this.heroes; }; 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) { if (resources.food !== undefined) this.resourceCount.food = resources.food; if (resources.wood !== undefined) this.resourceCount.wood = resources.wood; if (resources.stone !== undefined) this.resourceCount.stone = resources.stone; if (resources.metal !== undefined) this.resourceCount.metal = resources.metal; }; 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"); var notification = { "players": [this.playerID], "message": msg, "parameters": parameters, "translateMessage": true, "translateParameters": { "resourceType1": "withinSentence", "resourceType2": "withinSentence", "resourceType3": "withinSentence", "resourceType4": "withinSentence", }, }; var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification(notification); return false; } // Subtract the resources 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 = 100*Math.random(); 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 each (var resource in this.tradingGoods) tradingGoods[resource.goods] = resource.proba; return tradingGoods; }; Player.prototype.SetTradingGoods = function(tradingGoods) { var sumProba = 0; for (var resource in tradingGoods) sumProba += tradingGoods[resource]; if (sumProba != 100) // consistency check { error("Player.js SetTradingGoods: " + uneval(tradingGoods)); tradingGoods = { "food": 20, "wood":20, "stone":30, "metal":30 }; } this.tradingGoods = []; for (var resource in tradingGoods) this.tradingGoods.push( {"goods": resource, "proba": tradingGoods[resource]} ); }; Player.prototype.GetState = function() { return this.state; }; Player.prototype.SetState = function(newState) { this.state = newState; }; Player.prototype.GetConquestCriticalEntitiesCount = function() { return this.conquestCriticalEntitiesCount; }; Player.prototype.GetTeam = function() { return this.team; }; Player.prototype.SetTeam = function(team) { if (!this.teamsLocked) { this.team = team; var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (cmpPlayerManager && this.team != -1) { // Set all team members as allies for (var i = 0; i < cmpPlayerManager.GetNumPlayers(); ++i) { var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(i), IID_Player); if (this.team == cmpPlayer.GetTeam()) { this.SetAlly(i); cmpPlayer.SetAlly(this.playerID); } } } Engine.BroadcastMessage(MT_DiplomacyChanged, {"player": this.playerID}); } }; Player.prototype.SetLockTeams = function(value) { this.teamsLocked = value; }; Player.prototype.GetLockTeams = function() { return this.teamsLocked; }; Player.prototype.GetDiplomacy = function() { return this.diplomacy; }; Player.prototype.SetDiplomacy = function(dipl) { // Should we check for teamsLocked here? this.diplomacy = dipl; Engine.BroadcastMessage(MT_DiplomacyChanged, {"player": this.playerID}); }; Player.prototype.SetDiplomacyIndex = function(idx, value) { var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (!cmpPlayerManager) return; var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(idx), IID_Player); if (!cmpPlayer) return; if (this.state != "active" || cmpPlayer.state != "active") return; // You can have alliances with other players, if (this.teamsLocked) { // but can't stab your team members in the back if (this.team == -1 || this.team != cmpPlayer.GetTeam()) { // Break alliance or declare war if (Math.min(this.diplomacy[idx],cmpPlayer.diplomacy[this.playerID]) > value) { this.diplomacy[idx] = value; cmpPlayer.SetDiplomacyIndex(this.playerID, value); } else { this.diplomacy[idx] = value; } Engine.BroadcastMessage(MT_DiplomacyChanged, {"player": this.playerID}); } } else { // Break alliance or declare war (worsening of relations is mutual) if (Math.min(this.diplomacy[idx],cmpPlayer.diplomacy[this.playerID]) > value) { // This is duplicated because otherwise we get too much recursion this.diplomacy[idx] = value; cmpPlayer.SetDiplomacyIndex(this.playerID, value); } else { this.diplomacy[idx] = value; } Engine.BroadcastMessage(MT_DiplomacyChanged, {"player": this.playerID}); } }; Player.prototype.UpdateSharedLos = function() { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (!cmpRangeManager) return; var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (!cmpPlayerManager) return; var sharedLos = []; for (var i = 0; i < cmpPlayerManager.GetNumPlayers(); ++i) if (this.IsMutualAlly(i)) sharedLos.push(i); cmpRangeManager.SetSharedLos(this.playerID, sharedLos); }; 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.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.SetAlly = function(id) { this.SetDiplomacyIndex(id, 1); }; /** * Check if given player is our ally */ Player.prototype.IsAlly = function(id) { return this.diplomacy[id] > 0; }; /** * Check if given player is our ally, and we are its ally */ Player.prototype.IsMutualAlly = function(id) { var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (!cmpPlayerManager) return false; var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(id), IID_Player); return this.IsAlly(id) && cmpPlayer && cmpPlayer.IsAlly(this.playerID); }; Player.prototype.SetEnemy = function(id) { this.SetDiplomacyIndex(id, -1); }; /** * Get all enemies of a given player. */ Player.prototype.GetEnemies = function() { var enemies = []; for (var i = 0; i < this.diplomacy.length; i++) if (this.diplomacy[i] < 0) enemies.push(i); return enemies; }; /** * Check if given player is our enemy */ Player.prototype.IsEnemy = function(id) { return this.diplomacy[id] < 0; }; 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); var cmpFoundation = Engine.QueryInterface(msg.entity, IID_Foundation); if (msg.from == this.playerID) { if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("ConquestCritical")) this.conquestCriticalEntitiesCount--; if (cmpCost) this.popUsed -= cmpCost.GetPopCost(); if (cmpIdentity && cmpIdentity.HasClass("Hero")) { //Remove from Heroes list var index = this.heroes.indexOf(msg.entity); if (index >= 0) this.heroes.splice(index, 1); } } if (msg.to == this.playerID) { if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("ConquestCritical")) this.conquestCriticalEntitiesCount++; if (cmpCost) this.popUsed += cmpCost.GetPopCost(); if (cmpIdentity && cmpIdentity.HasClass("Hero")) this.heroes.push(msg.entity); } }; Player.prototype.OnPlayerDefeated = function(msg) { this.state = "defeated"; // TODO: Tribute all resources to this player's active allies (if any) // Reassign all player's entities to Gaia var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var 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 each (var entity in entities) { var cmpOwnership = Engine.QueryInterface(entity, IID_Ownership); cmpOwnership.SetOwnerQuiet(0); } // With the real ownership change complete, send OwnershipChanged messages. for each (var entity in entities) Engine.PostMessage(entity, MT_OwnershipChanged, { "entity": entity, "from": this.playerID, "to": 0 }); // Reveal the map for this player. cmpRangeManager.SetLosRevealAll(this.playerID, true); // Send a chat message notifying of the player's defeat. var notification = {"type": "defeat", "players": [this.playerID]}; var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification(notification); }; Player.prototype.OnDiplomacyChanged = function() { this.UpdateSharedLos(); }; Player.prototype.SetCheatsEnabled = function(flag) { this.cheatsEnabled = flag; }; Player.prototype.GetCheatsEnabled = function() { return this.cheatsEnabled; }; Player.prototype.SetCheatTimeMultiplier = function(time) { this.cheatTimeMultiplier = time; }; Player.prototype.GetCheatTimeMultiplier = function() { return this.cheatTimeMultiplier; }; Player.prototype.TributeResource = function(player, amounts) { var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (!cmpPlayerManager) return; var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(player), IID_Player); if (!cmpPlayer) return; if (this.state != "active" || cmpPlayer.state != "active") return; if (!this.SubtractResourcesOrNotify(amounts)) return; cmpPlayer.AddResources(amounts); var total = Object.keys(amounts).reduce(function (sum, type){ return 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 notification = {"type": "tribute", "players": [player], "donator": this.playerID, "amounts": amounts}; var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); if (cmpGUIInterface) cmpGUIInterface.PushNotification(notification); 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 16505) +++ ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js (revision 16506) @@ -1,808 +1,813 @@ 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/{civ}_support_trader\n units/athen_infantry_spearman_b\n " + "" + "" + "" + "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + ""; 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; this.alertRaiser = undefined; }; ProductionQueue.prototype.PutUnderAlert = function(raiser) { this.alertRaiser = raiser; }; ProductionQueue.prototype.ResetAlert = function() { this.alertRaiser = undefined; }; /* * 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; - + var string = this.template.Entities._string; if (!string) return; - + // Replace the "{civ}" codes with this entity's civ ID - var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); - if (cmpIdentity) - string = string.replace(/\{civ\}/g, cmpIdentity.GetCiv()); - - var entitiesList = string.split(/\s+/); - - // Remove disabled entities - var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); + var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); + var cmpPlayer = QueryOwnerInterface(this.entity); + if (!cmpPlayer) + return; + + var entitiesList = string.replace(/\{civ\}/g, cmpPlayer.GetCiv()).split(/\s+/); + + // filter out disabled and invalid entities var disabledEntities = cmpPlayer.GetDisabledTemplates(); - - for (var i = entitiesList.length - 1; i >= 0; --i) - if (disabledEntities[entitiesList[i]]) - entitiesList.splice(i, 1); - + entitiesList = entitiesList.filter(function(v) + { + return !disabledEntities[v] && cmpTemplateManager.TemplateExists(v); + }); + // check if some templates need to show their advanced or elite version var upgradeTemplate = function(templateName) { var template = cmpTemplateManager.GetTemplate(templateName); while (template && template.Promotion !== undefined) { - var requiredXp = ApplyValueModificationsToTemplate("Promotion/RequiredXp", +template.Promotion.RequiredXp, playerID, template); + var requiredXp = ApplyValueModificationsToTemplate("Promotion/RequiredXp", +template.Promotion.RequiredXp, cmpPlayer.GetPlayerID(), template); if (requiredXp > 0) break; templateName = template.Promotion.Entity; template = cmpTemplateManager.GetTemplate(templateName); } return templateName; }; - var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); - var playerID = cmpPlayer.GetPlayerID(); - for each (var templateName in entitiesList) + for (let templateName of entitiesList) this.entitiesList.push(upgradeTemplate(templateName)); - for each (var item in this.queue) - { + + 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 || cmpPlayer.GetCiv() != cmpIdentity.GetCiv()) + return []; + var techs = string.split(/\s+/); var techList = []; var superseded = {}; // Stores the tech which supersedes the key - var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); - if (cmpPlayer) - var disabledTechnologies = cmpPlayer.GetDisabledTechnologies(); + 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; var template = cmpTechnologyManager.GetTechnologyTemplate(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; } var template = cmpTechnologyManager.GetTechnologyTemplate(tech); if (template.top) ret[i] = {"pair": true, "top": template.top, "bottom": template.bottom}; else ret[i] = tech; } return ret; }; ProductionQueue.prototype.IsTechnologyResearchedOrInProgress = function(tech) { if (!tech) return false; var cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); var template = cmpTechnologyManager.GetTechnologyTemplate(tech); if (template.top) { return (cmpTechnologyManager.IsTechnologyResearched(template.top) || cmpTechnologyManager.IsInProgress(template.top) || cmpTechnologyManager.IsTechnologyResearched(template.bottom) || cmpTechnologyManager.IsInProgress(template.bottom)) } else { 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) - var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); + var cmpPlayer = QueryOwnerInterface(this.entity); if (this.queue.length < MAX_QUEUE_SIZE) { if (type == "unit") { // Find the template data so we can determine the build costs var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTempMan.GetTemplate(templateName); if (!template) return; if (template.Promotion) { var requiredXp = ApplyValueModificationsToTemplate("Promotion/RequiredXp", +template.Promotion.RequiredXp, cmpPlayer.GetPlayerID(), template); if (requiredXp == 0) { this.AddBatch(template.Promotion.Entity, type, count, metadata); return; } } // Apply a time discount to larger batches. var 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 var costs = {}; var totalCosts = {}; var buildTime = ApplyValueModificationsToTemplate("Cost/BuildTime", +template.Cost.BuildTime, cmpPlayer.GetPlayerID(), template); var time = timeMult * buildTime; for (var r in template.Cost.Resources) { costs[r] = ApplyValueModificationsToTemplate("Cost/Resources/"+r, +template.Cost.Resources[r], cmpPlayer.GetPlayerID(), template); totalCosts[r] = Math.floor(count * costs[r]); } var population = +template.Cost.Population; // 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) { var unitCategory = template.TrainingRestrictions.Category; var 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": population, "productionStarted": false, "timeTotal": time*1000, "timeRemaining": time*1000, }); // Call the related trigger event var 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") { // Load the technology template var cmpTechTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TechnologyTemplateManager); var template = cmpTechTempMan.GetTemplate(templateName); if (!template) return; - var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); + var cmpPlayer = QueryOwnerInterface(this.entity); var time = template.researchTime * cmpPlayer.GetCheatTimeMultiplier(); var cost = {}; for each (var r in ["food", "wood", "stone", "metal"]) cost[r] = Math.floor(template.cost[r]); // 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. var cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); cmpTechnologyManager.QueuedResearch(templateName, this.entity); if (this.queue.length == 0) cmpTechnologyManager.StartedResearch(templateName); this.queue.push({ "id": this.nextID++, "player": cmpPlayer.GetPlayerID(), "count": 1, "technologyTemplate": templateName, "resources": deepcopy(template.cost), // need to copy to avoid serialization problems "productionStarted": false, "timeTotal": time*1000, "timeRemaining": time*1000, }); // Call the related trigger event var 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) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetTimeout(this.entity, IID_ProductionQueue, "ProgressTimeout", g_ProgressInterval, {}); } } else { var notification = {"players": [cmpPlayer.GetPlayerID()], "message": markForTranslation("The production queue is full."), "translateMessage": true }; var 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 (var i = 0; i < this.entityCache.length; ++i) { Engine.DestroyEntity(this.entityCache[i]); } 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, IID_Player); + var cmpPlayer = QueryPlayerIDInterface(item.player); // Update entity count in the EntityLimits component if (item.unitTemplate) { var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTempMan.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 each (var r in ["food", "wood", "stone", "metal"]) { 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); } // 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 each (var item in 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, IID_Player); + 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.GetCheatTimeMultiplier(); }; ProductionQueue.prototype.OnOwnershipChanged = function(msg) { if (msg.from != -1) { // Unset flag that previous owner's training may be blocked - var cmpPlayer = QueryPlayerIDInterface(msg.from, IID_Player); + var cmpPlayer = QueryPlayerIDInterface(msg.from); if (cmpPlayer && this.queue.length > 0) cmpPlayer.UnBlockTraining(); } if (msg.to != -1) 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) { var cmpFootprint = Engine.QueryInterface(this.entity, IID_Footprint); var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); var cmpRallyPoint = Engine.QueryInterface(this.entity, IID_RallyPoint); var createdEnts = []; var spawnedEnts = []; if (this.entityCache.length == 0) { // We need entities to test spawning, but we don't want to waste resources, // so only create them once and use as needed for (var i = 0; i < count; ++i) { var ent = Engine.AddEntity(templateName); this.entityCache.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 to an 'alive' one var cmpTrainingRestrictions = Engine.QueryInterface(ent, IID_TrainingRestrictions); if (cmpTrainingRestrictions) { var unitCategory = cmpTrainingRestrictions.GetCategory(); var cmpPlayerEntityLimits = QueryOwnerInterface(this.entity, IID_EntityLimits); cmpPlayerEntityLimits.ChangeCount(unitCategory,-1); } } } var cmpAutoGarrison = undefined; if (cmpRallyPoint) { var data = cmpRallyPoint.GetData()[0]; if (data && data.target && data.target == this.entity && data.command == "garrison") cmpAutoGarrison = Engine.QueryInterface(this.entity, IID_GarrisonHolder); } for (var i = 0; i < count; ++i) { var ent = this.entityCache[0]; var cmpNewOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpNewOwnership.SetOwner(cmpOwnership.GetOwner()); if (cmpAutoGarrison && cmpAutoGarrison.PerformGarrison(ent)) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); cmpUnitAI.Autogarrison(this.entity); } else { var pos = cmpFootprint.PickSpawnPoint(ent); if (pos.y < 0) { // Fail: there wasn't any space to spawn the unit break; } else { // Successfully spawned var cmpNewPosition = Engine.QueryInterface(ent, IID_Position); cmpNewPosition.JumpTo(pos.x, pos.z); // TODO: what direction should they face in? spawnedEnts.push(ent); } } var cmpPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker); 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) { var rallyPos = cmpRallyPoint.GetPositions()[0]; if (rallyPos) { var commands = GetRallyPointCommands(cmpRallyPoint, spawnedEnts); for each(var com in commands) ProcessCommand(cmpOwnership.GetOwner(), com); } } } if (createdEnts.length > 0) { Engine.PostMessage(this.entity, MT_TrainingFinished, { "entities": createdEnts, "owner": cmpOwnership.GetOwner(), "metadata": metadata, }); if(this.alertRaiser && spawnedEnts.length > 0) { var cmpAlertRaiser = Engine.QueryInterface(this.alertRaiser, IID_AlertRaiser); if(cmpAlertRaiser) cmpAlertRaiser.UpdateUnits(spawnedEnts); } } 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, IID_Player); + 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) { // 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); } 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, IID_Player); + var cmpPlayer = QueryOwnerInterface(this.entity); var notification = {"players": [cmpPlayer.GetPlayerID()], "message": "Can't find free space to spawn trained units" }; 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); var template = cmpTechnologyManager.GetTechnologyTemplate(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/interfaces/Player.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Player.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Player.js (revision 16506) @@ -0,0 +1,5 @@ +/** + * Warn other components when a player changed civs + * This should only happen in Atlas + */ +Engine.RegisterMessageType("CivChanged");