Index: ps/trunk/binaries/data/mods/public/simulation/components/Auras.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Auras.js (revision 22966) +++ ps/trunk/binaries/data/mods/public/simulation/components/Auras.js (revision 22967) @@ -1,514 +1,504 @@ function Auras() {} Auras.prototype.Schema = "" + "tokens" + "" + ""; Auras.prototype.Init = function() { this.affectedPlayers = {}; for (let name of this.GetAuraNames()) this.affectedPlayers[name] = []; // In case of autogarrisoning, this component can be called before ownership is set. // So it needs to be completely initialised from the start. this.Clean(); }; // We can modify identifier if we want stackable auras in some case. Auras.prototype.GetModifierIdentifier = function(name) { if (AuraTemplates.Get(name).stackable) return "aura/" + name + this.entity; return "aura/" + name; }; Auras.prototype.GetDescriptions = function() { var ret = {}; for (let auraID of this.GetAuraNames()) { let aura = AuraTemplates.Get(auraID); ret[auraID] = { "name": aura.auraName, "description": aura.auraDescription || null, "radius": this.GetRange(auraID) || null }; } return ret; }; Auras.prototype.GetAuraNames = function() { return this.template._string.split(/\s+/); }; Auras.prototype.GetOverlayIcon = function(name) { return AuraTemplates.Get(name).overlayIcon || ""; }; Auras.prototype.GetAffectedEntities = function(name) { return this[name].targetUnits; }; Auras.prototype.GetRange = function(name) { if (this.IsRangeAura(name)) return +AuraTemplates.Get(name).radius; return undefined; }; Auras.prototype.GetClasses = function(name) { return AuraTemplates.Get(name).affects; }; Auras.prototype.GetModifications = function(name) { return AuraTemplates.Get(name).modifications; }; Auras.prototype.GetAffectedPlayers = function(name) { return this.affectedPlayers[name]; }; Auras.prototype.GetRangeOverlays = function() { let rangeOverlays = []; for (let name of this.GetAuraNames()) { if (!this.IsRangeAura(name) || !this[name].isApplied) continue; let rangeOverlay = AuraTemplates.Get(name).rangeOverlay; rangeOverlays.push( rangeOverlay ? { "radius": this.GetRange(name), "texture": rangeOverlay.lineTexture, "textureMask": rangeOverlay.lineTextureMask, "thickness": rangeOverlay.lineThickness } : // Specify default in order not to specify it in about 40 auras { "radius": this.GetRange(name), "texture": "outline_border.png", "textureMask": "outline_border_mask.png", "thickness": 0.2 }); } return rangeOverlays; }; Auras.prototype.CalculateAffectedPlayers = function(name) { var affectedPlayers = AuraTemplates.Get(name).affectedPlayers || ["Player"]; this.affectedPlayers[name] = []; var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (!cmpPlayer) cmpPlayer = QueryOwnerInterface(this.entity); if (!cmpPlayer || cmpPlayer.GetState() == "defeated") return; let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); for (let i of cmpPlayerManager.GetAllPlayers()) { let cmpAffectedPlayer = QueryPlayerIDInterface(i); if (!cmpAffectedPlayer || cmpAffectedPlayer.GetState() == "defeated") continue; if (affectedPlayers.some(p => p == "Player" ? cmpPlayer.GetPlayerID() == i : cmpPlayer["Is" + p](i))) this.affectedPlayers[name].push(i); } }; Auras.prototype.CanApply = function(name) { if (!AuraTemplates.Get(name).requiredTechnology) return true; let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); if (!cmpTechnologyManager) return false; return cmpTechnologyManager.IsTechnologyResearched(AuraTemplates.Get(name).requiredTechnology); }; Auras.prototype.HasFormationAura = function() { return this.GetAuraNames().some(n => this.IsFormationAura(n)); }; Auras.prototype.HasGarrisonAura = function() { return this.GetAuraNames().some(n => this.IsGarrisonAura(n)); }; Auras.prototype.HasGarrisonedUnitsAura = function() { return this.GetAuraNames().some(n => this.IsGarrisonedUnitsAura(n)); }; Auras.prototype.GetType = function(name) { return AuraTemplates.Get(name).type; }; Auras.prototype.IsFormationAura = function(name) { return this.GetType(name) == "formation"; }; Auras.prototype.IsGarrisonAura = function(name) { return this.GetType(name) == "garrison"; }; Auras.prototype.IsGarrisonedUnitsAura = function(name) { return this.GetType(name) == "garrisonedUnits"; }; Auras.prototype.IsRangeAura = function(name) { return this.GetType(name) == "range"; }; Auras.prototype.IsGlobalAura = function(name) { return this.GetType(name) == "global"; }; Auras.prototype.IsPlayerAura = function(name) { return this.GetType(name) == "player"; }; /** * clean all bonuses. Remove the old ones and re-apply the new ones */ Auras.prototype.Clean = function() { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var auraNames = this.GetAuraNames(); let targetUnitsClone = {}; let needVisualizationUpdate = false; // remove all bonuses for (let name of auraNames) { targetUnitsClone[name] = []; if (!this[name]) continue; if (this.IsRangeAura(name)) needVisualizationUpdate = true; if (this[name].targetUnits) targetUnitsClone[name] = this[name].targetUnits.slice(); if (this.IsGlobalAura(name)) this.RemoveTemplateAura(name); this.RemoveAura(name, this[name].targetUnits); if (this[name].rangeQuery) cmpRangeManager.DestroyActiveQuery(this[name].rangeQuery); } for (let name of auraNames) { // only calculate the affected players on re-applying the bonuses // this makes sure the template bonuses are removed from the correct players this.CalculateAffectedPlayers(name); // initialise range query this[name] = {}; this[name].targetUnits = []; this[name].isApplied = this.CanApply(name); var affectedPlayers = this.GetAffectedPlayers(name); if (!affectedPlayers.length) continue; if (this.IsGlobalAura(name)) { this.ApplyTemplateAura(name, affectedPlayers); // Only need to call ApplyAura for the aura icons, so skip it if there are none. if (this.GetOverlayIcon(name)) for (let player of affectedPlayers) this.ApplyAura(name, cmpRangeManager.GetEntitiesByPlayer(player)); continue; } if (this.IsPlayerAura(name)) { let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); this.ApplyAura(name, affectedPlayers.map(p => cmpPlayerManager.GetPlayerByID(p))); continue; } if (!this.IsRangeAura(name)) { this.ApplyAura(name, targetUnitsClone[name]); continue; } needVisualizationUpdate = true; if (this[name].isApplied && (this.IsRangeAura(name) || this.IsGlobalAura(name) && !!this.GetOverlayIcon(name))) { this[name].rangeQuery = cmpRangeManager.CreateActiveQuery( this.entity, 0, this.GetRange(name), affectedPlayers, IID_Identity, cmpRangeManager.GetEntityFlagMask("normal") ); cmpRangeManager.EnableActiveQuery(this[name].rangeQuery); } } if (needVisualizationUpdate) { let cmpRangeOverlayManager = Engine.QueryInterface(this.entity, IID_RangeOverlayManager); if (cmpRangeOverlayManager) { cmpRangeOverlayManager.UpdateRangeOverlays("Auras"); cmpRangeOverlayManager.RegenerateRangeOverlays(false); } } }; Auras.prototype.GiveMembersWithValidClass = function(auraName, entityList) { var match = this.GetClasses(auraName); return entityList.filter(ent => { let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); return cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), match); }); }; Auras.prototype.OnRangeUpdate = function(msg) { for (let name of this.GetAuraNames().filter(n => this[n] && msg.tag == this[n].rangeQuery)) { this.ApplyAura(name, msg.added); this.RemoveAura(name, msg.removed); } }; Auras.prototype.OnGarrisonedUnitsChanged = function(msg) { for (let name of this.GetAuraNames().filter(n => this.IsGarrisonedUnitsAura(n))) { this.ApplyAura(name, msg.added); this.RemoveAura(name, msg.removed); } }; Auras.prototype.ApplyFormationAura = function(memberList) { for (let name of this.GetAuraNames().filter(n => this.IsFormationAura(n))) this.ApplyAura(name, memberList); }; Auras.prototype.ApplyGarrisonAura = function(structure) { for (let name of this.GetAuraNames().filter(n => this.IsGarrisonAura(n))) this.ApplyAura(name, [structure]); }; Auras.prototype.ApplyTemplateAura = function(name, players) { if (!this[name].isApplied) return; if (!this.IsGlobalAura(name)) return; let derivedModifiers = DeriveModificationsFromTech({ "modifications": this.GetModifications(name), "affects": this.GetClasses(name) }); let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); let modifName = this.GetModifierIdentifier(name); for (let player of players) - { - let playerId = cmpPlayerManager.GetPlayerByID(player); - for (let modifierPath in derivedModifiers) - for (let modifier of derivedModifiers[modifierPath]) - cmpModifiersManager.AddModifier(modifierPath, modifName, modifier, playerId); - } + cmpModifiersManager.AddModifiers(modifName, derivedModifiers, cmpPlayerManager.GetPlayerByID(player)); }; Auras.prototype.RemoveFormationAura = function(memberList) { for (let name of this.GetAuraNames().filter(n => this.IsFormationAura(n))) this.RemoveAura(name, memberList); }; Auras.prototype.RemoveGarrisonAura = function(structure) { for (let name of this.GetAuraNames().filter(n => this.IsGarrisonAura(n))) this.RemoveAura(name, [structure]); }; Auras.prototype.RemoveTemplateAura = function(name) { if (!this[name].isApplied) return; if (!this.IsGlobalAura(name)) return; let derivedModifiers = DeriveModificationsFromTech({ "modifications": this.GetModifications(name), "affects": this.GetClasses(name) }); let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); let modifName = this.GetModifierIdentifier(name); for (let player of this.GetAffectedPlayers(name)) { let playerId = cmpPlayerManager.GetPlayerByID(player); for (let modifierPath in derivedModifiers) - for (let modifier of derivedModifiers[modifierPath]) - cmpModifiersManager.RemoveModifier(modifierPath, modifName, playerId); + cmpModifiersManager.RemoveModifier(modifierPath, modifName, playerId); } }; Auras.prototype.ApplyAura = function(name, ents) { var validEnts = this.GiveMembersWithValidClass(name, ents); if (!validEnts.length) return; this[name].targetUnits = this[name].targetUnits.concat(validEnts); if (!this[name].isApplied) return; // update status bars if this has an icon if (this.GetOverlayIcon(name)) for (let ent of validEnts) { let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars); if (cmpStatusBars) cmpStatusBars.AddAuraSource(this.entity, name); } // Global aura modifications are handled at the player level by the modification manager, // so stop after icons have been applied. if (this.IsGlobalAura(name)) return; let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); let derivedModifiers = DeriveModificationsFromTech({ "modifications": this.GetModifications(name), "affects": this.GetClasses(name) }); let modifName = this.GetModifierIdentifier(name); for (let ent of validEnts) - for (let modifierPath in derivedModifiers) - for (let modifier of derivedModifiers[modifierPath]) - cmpModifiersManager.AddModifier(modifierPath, modifName, modifier, ent); - + cmpModifiersManager.AddModifiers(modifName, derivedModifiers, ent); }; Auras.prototype.RemoveAura = function(name, ents, skipModifications = false) { var validEnts = this.GiveMembersWithValidClass(name, ents); if (!validEnts.length) return; this[name].targetUnits = this[name].targetUnits.filter(v => validEnts.indexOf(v) == -1); if (!this[name].isApplied) return; // update status bars if this has an icon if (this.GetOverlayIcon(name)) for (let ent of validEnts) { let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars); if (cmpStatusBars) cmpStatusBars.RemoveAuraSource(this.entity, name); } // Global aura modifications are handled at the player level by the modification manager, // so stop after icons have been removed. if (this.IsGlobalAura(name)) return; let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); let derivedModifiers = DeriveModificationsFromTech({ "modifications": this.GetModifications(name), "affects": this.GetClasses(name) }); let modifName = this.GetModifierIdentifier(name); for (let ent of ents) for (let modifierPath in derivedModifiers) - for (let modifier of derivedModifiers[modifierPath]) - cmpModifiersManager.RemoveModifier(modifierPath, modifName, ent); + cmpModifiersManager.RemoveModifier(modifierPath, modifName, ent); }; Auras.prototype.OnOwnershipChanged = function(msg) { this.Clean(); }; Auras.prototype.OnDiplomacyChanged = function(msg) { var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (cmpPlayer && (cmpPlayer.GetPlayerID() == msg.player || cmpPlayer.GetPlayerID() == msg.otherPlayer) || IsOwnedByPlayer(msg.player, this.entity) || IsOwnedByPlayer(msg.otherPlayer, this.entity)) this.Clean(); }; Auras.prototype.OnGlobalResearchFinished = function(msg) { var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if ((!cmpPlayer || cmpPlayer.GetPlayerID() != msg.player) && !IsOwnedByPlayer(msg.player, this.entity)) return; for (let name of this.GetAuraNames()) { let requiredTech = AuraTemplates.Get(name).requiredTechnology; if (requiredTech && requiredTech == msg.tech) { this.Clean(); return; } } }; /** * Update auras of the player entity and entities affecting player entities that didn't change ownership. */ Auras.prototype.OnGlobalPlayerDefeated = function(msg) { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (cmpPlayer && cmpPlayer.GetPlayerID() == msg.playerId || this.GetAuraNames().some(name => this.GetAffectedPlayers(name).indexOf(msg.playerId) != -1)) this.Clean(); }; Engine.RegisterComponentType(IID_Auras, "Auras", Auras); Index: ps/trunk/binaries/data/mods/public/simulation/components/ModifiersManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/ModifiersManager.js (revision 22966) +++ ps/trunk/binaries/data/mods/public/simulation/components/ModifiersManager.js (revision 22967) @@ -1,289 +1,292 @@ function ModifiersManager() {} ModifiersManager.prototype.Schema = ""; ModifiersManager.prototype.Init = function() { // TODO: // - add a way to show an icon for a given modifier ID // > Note that aura code shows icons when the source is selected, so that's specific to them. // - support stacking modifiers (MultiKeyMap handles it but not this manager). // The cache computes values lazily when they are needed. // Helper functions remove items that have been changed to ensure we stay up-to-date. this.cachedValues = new Map(); // Keyed by property name, entity ID, original values. // When changing global modifiers, all entity-local caches are invalidated. This helps with that. // TODO: it might be worth keying by classes here. this.playerEntitiesCached = new Map(); // Keyed by player ID, property name, entity ID. this.modifiersStorage = new MultiKeyMap(); // Keyed by property name, entity. this.modifiersStorage._OnItemModified = (prim, sec, itemID) => this.ModifiersChanged.apply(this, [prim, sec, itemID]); }; ModifiersManager.prototype.Serialize = function() { // The value cache will be affected by property reads from the GUI and other places so we shouldn't serialize it. // Furthermore it is cyclically self-referencing. // We need to store the player for the Player-Entities cache. let players = []; this.playerEntitiesCached.forEach((_, player) => players.push(player)); return { "modifiersStorage": this.modifiersStorage.Serialize(), "players": players }; }; ModifiersManager.prototype.Deserialize = function(data) { this.Init(); this.modifiersStorage.Deserialize(data.modifiersStorage); data.players.forEach(player => this.playerEntitiesCached.set(player, new Map())); }; /** * Inform entities that we have changed possibly all values affected by that property. * It's not hugely efficient and would be nice to batch. * Invalidate caches where relevant. */ ModifiersManager.prototype.ModifiersChanged = function(propertyName, entity) { let playerCache = this.playerEntitiesCached.get(entity); this.InvalidateCache(propertyName, entity, playerCache); if (playerCache) { let cmpPlayer = Engine.QueryInterface(entity, IID_Player); if (cmpPlayer) this.SendPlayerModifierMessages(propertyName, cmpPlayer.GetPlayerID()); } else Engine.PostMessage(entity, MT_ValueModification, { "entities": [entity], "component": propertyName.split("/")[0], "valueNames": [propertyName] }); }; ModifiersManager.prototype.SendPlayerModifierMessages = function(propertyName, player) { // TODO: it would be preferable to be able to batch this (i.e. one message for several properties) Engine.PostMessage(SYSTEM_ENTITY, MT_TemplateModification, { "player": player, "component": propertyName.split("/")[0], "valueNames": [propertyName] }); // AIInterface wants the entities potentially affected. // TODO: improve on this let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let ents = cmpRangeManager.GetEntitiesByPlayer(player); Engine.BroadcastMessage(MT_ValueModification, { "entities": ents, "component": propertyName.split("/")[0], "valueNames": [propertyName] }); }; ModifiersManager.prototype.InvalidatePlayerEntCache = function(valueCache, propertyName, entsMap) { entsMap = entsMap.get(propertyName); if (entsMap) { // Invalidate all local caches directly (for simplicity in ApplyModifiers). entsMap.forEach(ent => valueCache.set(ent, new Map())); entsMap.clear(); } }; ModifiersManager.prototype.InvalidateCache = function(propertyName, entity, playerCache) { let valueCache = this.cachedValues.get(propertyName); if (!valueCache) return; if (playerCache) this.InvalidatePlayerEntCache(valueCache, propertyName, playerCache); valueCache.set(entity, new Map()); }; /** * @returns originalValue after modifiers. */ ModifiersManager.prototype.FetchModifiedProperty = function(classesList, propertyName, originalValue, target) { let modifs = this.modifiersStorage.GetItems(propertyName, target); if (!modifs.length) return originalValue; - return GetTechModifiedProperty(modifs, classesList, originalValue); + // Flatten the list of modifications + let modifications = []; + modifs.forEach(item => { modifications = modifications.concat(item.value); }); + return GetTechModifiedProperty(modifications, classesList, originalValue); }; /** * @returns originalValue after modifiers */ ModifiersManager.prototype.Cache = function(classesList, propertyName, originalValue, entity) { let cache = this.cachedValues.get(propertyName); if (!cache) cache = this.cachedValues.set(propertyName, new Map()).get(propertyName); let cache2 = cache.get(entity); if (!cache2) cache2 = cache.set(entity, new Map()).get(entity); let value = this.FetchModifiedProperty(classesList, propertyName, originalValue, entity); cache2.set(originalValue, value); return value; }; /** * Caching system in front of FetchModifiedProperty(), as calling that every time is quite slow. * This recomputes lazily. * Applies per-player modifiers before per-entity modifiers, so the latter take priority; * @param propertyName - Handle of a technology property (eg Attack/Ranged/Pierce) that was changed. * @param originalValue - template/raw/before-modifiers value. Note that if this is supposed to be a number (i.e. you call add/multiply on it) You must make sure to pass a number and not a string (by using + if necessary) * @param ent - ID of the target entity * @returns originalValue after the modifiers */ ModifiersManager.prototype.ApplyModifiers = function(propertyName, originalValue, entity) { let newValue = this.cachedValues.get(propertyName); if (newValue) { newValue = newValue.get(entity); if (newValue) { newValue = newValue.get(originalValue); if (newValue) return newValue; } } // Get the entity ID of the player / owner of the entity, since we use that to store per-player modifiers // (this prevents conflicts between player ID and entity ID). let ownerEntity = QueryOwnerEntityID(entity); if (ownerEntity == entity) ownerEntity = null; newValue = originalValue; let cmpIdentity = Engine.QueryInterface(entity, IID_Identity); if (!cmpIdentity) return originalValue; let classesList = cmpIdentity.GetClassesList(); // Apply player-wide modifiers before entity-local modifiers. if (ownerEntity) { let pc = this.playerEntitiesCached.get(ownerEntity).get(propertyName); if (!pc) pc = this.playerEntitiesCached.get(ownerEntity).set(propertyName, new Set()).get(propertyName); pc.add(entity); newValue = this.FetchModifiedProperty(classesList, propertyName, newValue, ownerEntity); } newValue = this.Cache(classesList, propertyName, newValue, entity); return newValue; }; /** * Alternative version of ApplyModifiers, applies to templates instead of entities. * Only needs to handle global modifiers. */ ModifiersManager.prototype.ApplyTemplateModifiers = function(propertyName, originalValue, template, player) { if (!template || !template.Identity) return originalValue; let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); return this.FetchModifiedProperty(GetIdentityClasses(template.Identity), propertyName, originalValue, cmpPlayerManager.GetPlayerByID(player)); }; /** * For efficiency in InvalidateCache, keep playerEntitiesCached updated. */ ModifiersManager.prototype.OnGlobalPlayerEntityChanged = function(msg) { if (msg.to != INVALID_PLAYER && !this.playerEntitiesCached.has(msg.to)) this.playerEntitiesCached.set(msg.to, new Map()); if (msg.from != INVALID_PLAYER && this.playerEntitiesCached.has(msg.from)) { this.playerEntitiesCached.get(msg.from).forEach(propName => this.InvalidateCache(propName, msg.from)); this.playerEntitiesCached.delete(msg.from); } }; /** * Handle modifiers when an entity changes owner. * We do not retain the original modifiers for now. */ ModifiersManager.prototype.OnGlobalOwnershipChanged = function(msg) { if (msg.from == INVALID_PLAYER || msg.to == INVALID_PLAYER) return; // Invalidate all caches. for (let propName of this.cachedValues.keys()) this.InvalidateCache(propName, msg.entity); let owner = QueryOwnerEntityID(msg.entity); if (!owner) return; let cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity); if (!cmpIdentity) return; let classes = cmpIdentity.GetClassesList(); // Warn entities that our values have changed. // Local modifiers will be added by the relevant components, so no need to check for them here. let modifiedComponents = {}; let playerModifs = this.modifiersStorage.GetAllItems(owner); for (let propertyName in playerModifs) { // We only need to find one one tech per component for a match. let component = propertyName.split("/")[0]; // Only inform if the modifier actually applies to the entity as an optimisation. // TODO: would it be better to call FetchModifiedProperty here and compare values? - playerModifs[propertyName].forEach(modif => { + playerModifs[propertyName].forEach(item => item.value.forEach(modif => { if (!DoesModificationApply(modif, classes)) return; if (!modifiedComponents[component]) modifiedComponents[component] = []; modifiedComponents[component].push(propertyName); - }); + })); } for (let component in modifiedComponents) Engine.PostMessage(msg.entity, MT_ValueModification, { "entities": [msg.entity], "component": component, "valueNames": modifiedComponents[component] }); }; /** * The following functions simply proxy MultiKeyMap's interface. */ ModifiersManager.prototype.AddModifier = function(propName, ModifID, Modif, entity, stackable = false) { return this.modifiersStorage.AddItem(propName, ModifID, Modif, entity, stackable); }; ModifiersManager.prototype.AddModifiers = function(ModifID, Modifs, entity, stackable = false) { return this.modifiersStorage.AddItems(ModifID, Modifs, entity, stackable); }; ModifiersManager.prototype.RemoveModifier = function(propName, ModifID, entity, stackable = false) { return this.modifiersStorage.RemoveItem(propName, ModifID, entity, stackable); }; ModifiersManager.prototype.RemoveAllModifiers = function(ModifID, entity, stackable = false) { return this.modifiersStorage.RemoveAllItems(ModifID, entity, stackable); }; ModifiersManager.prototype.HasModifier = function(propName, ModifID, entity) { return this.modifiersStorage.HasItem(propName, ModifID, entity); }; ModifiersManager.prototype.HasAnyModifier = function(ModifID, entity) { return this.modifiersStorage.HasAnyItem(ModifID, entity); }; ModifiersManager.prototype.GetModifiers = function(propName, entity, stackable = false) { return this.modifiersStorage.GetItems(propName, entity, stackable); }; ModifiersManager.prototype.GetAllModifiers = function(entity, stackable = false) { return this.modifiersStorage.GetAllItems(entity, stackable); }; Engine.RegisterSystemComponentType(IID_ModifiersManager, "ModifiersManager", ModifiersManager); Index: ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js (revision 22966) +++ ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js (revision 22967) @@ -1,366 +1,363 @@ function TechnologyManager() {} TechnologyManager.prototype.Schema = ""; TechnologyManager.prototype.Init = function() { // Holds names of technologies that have been researched. this.researchedTechs = new Set(); // Maps from technolgy name to the entityID of the researcher. this.researchQueued = new Map(); // Holds technologies which are being researched currently (non-queued). this.researchStarted = new Set(); this.classCounts = {}; // stores the number of entities of each Class this.typeCountsByClass = {}; // stores the number of entities of each type for each class i.e. // {"someClass": {"unit/spearman": 2, "unit/cav": 5} "someOtherClass":...} // Some technologies are automatically researched when their conditions are met. They have no cost and are // researched instantly. This allows civ bonuses and more complicated technologies. this.unresearchedAutoResearchTechs = new Set(); let allTechs = TechnologyTemplates.GetAll(); for (let key in allTechs) if (allTechs[key].autoResearch || allTechs[key].top) this.unresearchedAutoResearchTechs.add(key); }; TechnologyManager.prototype.OnUpdate = function() { this.UpdateAutoResearch(); }; // This function checks if the requirements of any autoresearch techs are met and if they are it researches them TechnologyManager.prototype.UpdateAutoResearch = function() { for (let key of this.unresearchedAutoResearchTechs) { let tech = TechnologyTemplates.Get(key); if ((tech.autoResearch && this.CanResearch(key)) || (tech.top && (this.IsTechnologyResearched(tech.top) || this.IsTechnologyResearched(tech.bottom)))) { this.unresearchedAutoResearchTechs.delete(key); this.ResearchTechnology(key); return; // We will have recursively handled any knock-on effects so can just return } } }; // Checks an entity template to see if its technology requirements have been met TechnologyManager.prototype.CanProduce = function (templateName) { var cmpTempManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTempManager.GetTemplate(templateName); if (template.Identity && template.Identity.RequiredTechnology) return this.IsTechnologyResearched(template.Identity.RequiredTechnology); // If there is no required technology then this entity can be produced return true; }; TechnologyManager.prototype.IsTechnologyQueued = function(tech) { return this.researchQueued.has(tech); }; TechnologyManager.prototype.IsTechnologyResearched = function(tech) { return this.researchedTechs.has(tech); }; TechnologyManager.prototype.IsTechnologyStarted = function(tech) { return this.researchStarted.has(tech); }; // Checks the requirements for a technology to see if it can be researched at the current time TechnologyManager.prototype.CanResearch = function(tech) { let template = TechnologyTemplates.Get(tech); if (!template) { warn("Technology \"" + tech + "\" does not exist"); return false; } if (template.top && this.IsInProgress(template.top) || template.bottom && this.IsInProgress(template.bottom)) return false; if (template.pair && !this.CanResearch(template.pair)) return false; if (this.IsInProgress(tech)) return false; if (this.IsTechnologyResearched(tech)) return false; return this.CheckTechnologyRequirements(DeriveTechnologyRequirements(template, Engine.QueryInterface(this.entity, IID_Player).GetCiv())); }; /** * Private function for checking a set of requirements is met * @param {object} reqs - Technology requirements as derived from the technology template by globalscripts * @param {boolean} civonly - True if only the civ requirement is to be checked * * @return true if the requirements pass, false otherwise */ TechnologyManager.prototype.CheckTechnologyRequirements = function(reqs, civonly = false) { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (!reqs) return false; if (civonly || !reqs.length) return true; return reqs.some(req => { return Object.keys(req).every(type => { switch (type) { case "techs": return req[type].every(this.IsTechnologyResearched, this); case "entities": return req[type].every(this.DoesEntitySpecPass, this); } return false; }); }); }; TechnologyManager.prototype.DoesEntitySpecPass = function(entity) { switch (entity.check) { case "count": if (!this.classCounts[entity.class] || this.classCounts[entity.class] < entity.number) return false; break; case "variants": if (!this.typeCountsByClass[entity.class] || Object.keys(this.typeCountsByClass[entity.class]).length < entity.number) return false; break; } return true; }; TechnologyManager.prototype.OnGlobalOwnershipChanged = function(msg) { // This automatically updates classCounts and typeCountsByClass var playerID = (Engine.QueryInterface(this.entity, IID_Player)).GetPlayerID(); if (msg.to == playerID) { var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetCurrentTemplateName(msg.entity); var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity); if (!cmpIdentity) return; var classes = cmpIdentity.GetClassesList(); // don't use foundations for the class counts but check if techs apply (e.g. health increase) if (!Engine.QueryInterface(msg.entity, IID_Foundation)) { for (let cls of classes) { this.classCounts[cls] = this.classCounts[cls] || 0; this.classCounts[cls] += 1; this.typeCountsByClass[cls] = this.typeCountsByClass[cls] || {}; this.typeCountsByClass[cls][template] = this.typeCountsByClass[cls][template] || 0; this.typeCountsByClass[cls][template] += 1; } } } if (msg.from == playerID) { var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetCurrentTemplateName(msg.entity); // don't use foundations for the class counts if (!Engine.QueryInterface(msg.entity, IID_Foundation)) { var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity); if (cmpIdentity) { var classes = cmpIdentity.GetClassesList(); for (let cls of classes) { this.classCounts[cls] -= 1; if (this.classCounts[cls] <= 0) delete this.classCounts[cls]; this.typeCountsByClass[cls][template] -= 1; if (this.typeCountsByClass[cls][template] <= 0) delete this.typeCountsByClass[cls][template]; } } } } }; // Marks a technology as researched. Note that this does not verify that the requirements are met. TechnologyManager.prototype.ResearchTechnology = function(tech) { this.StoppedResearch(tech, false); var modifiedComponents = {}; this.researchedTechs.add(tech); // store the modifications in an easy to access structure let template = TechnologyTemplates.Get(tech); if (template.modifications) { let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); - let derivedModifiers = DeriveModificationsFromTech(template); - for (let modifierPath in derivedModifiers) - for (let modifier of derivedModifiers[modifierPath]) - cmpModifiersManager.AddModifier(modifierPath, "tech/" + tech, modifier, this.entity); + cmpModifiersManager.AddModifiers("tech/" + tech, DeriveModificationsFromTech(template), this.entity); } if (template.replaces && template.replaces.length > 0) { for (var i of template.replaces) { if (!i || this.IsTechnologyResearched(i)) continue; this.researchedTechs.add(i); // Change the EntityLimit if any let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (cmpPlayer && cmpPlayer.GetPlayerID() !== undefined) { let playerID = cmpPlayer.GetPlayerID(); let cmpPlayerEntityLimits = QueryPlayerIDInterface(playerID, IID_EntityLimits); if (cmpPlayerEntityLimits) cmpPlayerEntityLimits.UpdateLimitsFromTech(i); } } } this.UpdateAutoResearch(); var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (!cmpPlayer || cmpPlayer.GetPlayerID() === undefined) return; var playerID = cmpPlayer.GetPlayerID(); var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var ents = cmpRangeManager.GetEntitiesByPlayer(playerID); ents.push(this.entity); // Change the EntityLimit if any var cmpPlayerEntityLimits = QueryPlayerIDInterface(playerID, IID_EntityLimits); if (cmpPlayerEntityLimits) cmpPlayerEntityLimits.UpdateLimitsFromTech(tech); // always send research finished message Engine.PostMessage(this.entity, MT_ResearchFinished, {"player": playerID, "tech": tech}); }; /** * Marks a technology as being queued for research at the given entityID. */ TechnologyManager.prototype.QueuedResearch = function(tech, researcher) { this.researchQueued.set(tech, researcher); }; // Marks a technology as actively being researched TechnologyManager.prototype.StartedResearch = function(tech, notification) { this.researchStarted.add(tech); if (notification && tech.startsWith("phase")) { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "phase", "players": [cmpPlayer.GetPlayerID()], "phaseName": tech, "phaseState": "started" }); } }; /** * Marks a technology as not being currently researched and optionally sends a GUI notification. */ TechnologyManager.prototype.StoppedResearch = function(tech, notification) { if (notification && tech.startsWith("phase") && this.researchStarted.has(tech)) { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "phase", "players": [cmpPlayer.GetPlayerID()], "phaseName": tech, "phaseState": "aborted" }); } this.researchQueued.delete(tech); this.researchStarted.delete(tech); }; /** * Checks whether a technology is set to be researched. */ TechnologyManager.prototype.IsInProgress = function(tech) { return this.researchQueued.has(tech); }; /** * Returns the names of technologies that are currently being researched (non-queued). */ TechnologyManager.prototype.GetStartedTechs = function() { return this.researchStarted; }; /** * Gets the entity currently researching the technology. */ TechnologyManager.prototype.GetResearcher = function(tech) { return this.researchQueued.get(tech); }; /** * Called by GUIInterface for PlayerData. AI use. */ TechnologyManager.prototype.GetQueuedResearch = function() { return this.researchQueued; }; /** * Returns the names of technologies that have already been researched. */ TechnologyManager.prototype.GetResearchedTechs = function() { return this.researchedTechs; }; TechnologyManager.prototype.GetClassCounts = function() { return this.classCounts; }; TechnologyManager.prototype.GetTypeCountsByClass = function() { return this.typeCountsByClass; }; Engine.RegisterComponentType(IID_TechnologyManager, "TechnologyManager", TechnologyManager); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ModifiersManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ModifiersManager.js (revision 22966) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ModifiersManager.js (revision 22967) @@ -1,148 +1,148 @@ Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Engine.LoadComponentScript("ModifiersManager.js"); Engine.LoadHelperScript("MultiKeyMap.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("ValueModification.js"); let cmpModifiersManager = ConstructComponent(SYSTEM_ENTITY, "ModifiersManager", {}); cmpModifiersManager.Init(); // These should be different as that is the general case. const PLAYER_ID_FOR_TEST = 2; const PLAYER_ENTITY_ID = 3; AddMock(SYSTEM_ENTITY, IID_RangeManager, { "GetEntitiesByPlayer": () => [], }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": (a) => PLAYER_ENTITY_ID }); AddMock(PLAYER_ENTITY_ID, IID_Player, { "GetPlayerID": () => PLAYER_ID_FOR_TEST }); let entitiesToTest = [5, 6, 7, 8]; for (let ent of entitiesToTest) AddMock(ent, IID_Ownership, { "GetOwner": () => PLAYER_ID_FOR_TEST }); AddMock(PLAYER_ENTITY_ID, IID_Identity, { "GetClassesList": () => "Player", }); AddMock(5, IID_Identity, { "GetClassesList": () => "Structure", }); AddMock(6, IID_Identity, { "GetClassesList": () => "Infantry", }); AddMock(7, IID_Identity, { "GetClassesList": () => "Unit", }); AddMock(8, IID_Identity, { "GetClassesList": () => "Structure Unit", }); // Sprinkle random serialisation cycles. function SerializationCycle() { let data = cmpModifiersManager.Serialize(); cmpModifiersManager = ConstructComponent(SYSTEM_ENTITY, "ModifiersManager", {}); cmpModifiersManager.Deserialize(data); } cmpModifiersManager.OnGlobalPlayerEntityChanged({ "player": PLAYER_ID_FOR_TEST, "from": -1, "to": PLAYER_ENTITY_ID }); -cmpModifiersManager.AddModifier("Test_A", "Test_A_0", { "affects": ["Structure"], "add": 10 }, 10, "testLol"); +cmpModifiersManager.AddModifier("Test_A", "Test_A_0", [{ "affects": ["Structure"], "add": 10 }], 10, "testLol"); -cmpModifiersManager.AddModifier("Test_A", "Test_A_0", { "affects": ["Structure"], "add": 10 }, PLAYER_ENTITY_ID); -cmpModifiersManager.AddModifier("Test_A", "Test_A_1", { "affects": ["Infantry"], "add": 5 }, PLAYER_ENTITY_ID); -cmpModifiersManager.AddModifier("Test_A", "Test_A_2", { "affects": ["Unit"], "add": 3 }, PLAYER_ENTITY_ID); +cmpModifiersManager.AddModifier("Test_A", "Test_A_0", [{ "affects": ["Structure"], "add": 10 }], PLAYER_ENTITY_ID); +cmpModifiersManager.AddModifier("Test_A", "Test_A_1", [{ "affects": ["Infantry"], "add": 5 }], PLAYER_ENTITY_ID); +cmpModifiersManager.AddModifier("Test_A", "Test_A_2", [{ "affects": ["Unit"], "add": 3 }], PLAYER_ENTITY_ID); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_A", 5, PLAYER_ENTITY_ID), 5); -cmpModifiersManager.AddModifier("Test_A", "Test_A_Player", { "affects": ["Player"], "add": 3 }, PLAYER_ENTITY_ID); +cmpModifiersManager.AddModifier("Test_A", "Test_A_Player", [{ "affects": ["Player"], "add": 3 }], PLAYER_ENTITY_ID); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_A", 5, PLAYER_ENTITY_ID), 8); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_A", 5, 5), 15); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_A", 5, 6), 10); SerializationCycle(); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_A", 5, 5), 15); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_A", 5, 6), 10); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_A", 5, 7), 8); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_A", 5, 8), 18); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_B", 5, 8), 5); cmpModifiersManager.RemoveAllModifiers("Test_A_0", PLAYER_ENTITY_ID); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_A", 5, 5), 5); cmpModifiersManager.AddModifiers("Test_A_0", { - "Test_A": { "affects": ["Structure"], "add": 10 }, - "Test_B": { "affects": ["Structure"], "add": 8 }, + "Test_A": [{ "affects": ["Structure"], "add": 10 }], + "Test_B": [{ "affects": ["Structure"], "add": 8 }], }, PLAYER_ENTITY_ID); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_A", 5, 5), 15); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_B", 5, 8), 13); // Add two local modifications, only the first should stick. -cmpModifiersManager.AddModifier("Test_C", "Test_C_0", { "affects": ["Structure"], "add": 10 }, 5); -cmpModifiersManager.AddModifier("Test_C", "Test_C_1", { "affects": ["Unit"], "add": 5 }, 5); +cmpModifiersManager.AddModifier("Test_C", "Test_C_0", [{ "affects": ["Structure"], "add": 10 }], 5); +cmpModifiersManager.AddModifier("Test_C", "Test_C_1", [{ "affects": ["Unit"], "add": 5 }], 5); SerializationCycle(); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_C", 5, 5), 15); // test that local modifications are indeed applied after global managers -cmpModifiersManager.AddModifier("Test_C", "Test_C_2", { "affects": ["Structure"], "replace": 0 }, 5); +cmpModifiersManager.AddModifier("Test_C", "Test_C_2", [{ "affects": ["Structure"], "replace": 0 }], 5); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_C", 5, 5), 0); TS_ASSERT(!cmpModifiersManager.HasAnyModifier("Test_C_3", PLAYER_ENTITY_ID)); SerializationCycle(); // check that things still work properly if we change global modifications -cmpModifiersManager.AddModifier("Test_C", "Test_C_3", { "affects": ["Structure"], "add": 10 }, PLAYER_ENTITY_ID); +cmpModifiersManager.AddModifier("Test_C", "Test_C_3", [{ "affects": ["Structure"], "add": 10 }], PLAYER_ENTITY_ID); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_C", 5, 5), 0); TS_ASSERT(cmpModifiersManager.HasAnyModifier("Test_C_3", PLAYER_ENTITY_ID)); TS_ASSERT(cmpModifiersManager.HasModifier("Test_C", "Test_C_3", PLAYER_ENTITY_ID)); TS_ASSERT(cmpModifiersManager.HasModifier("Test_C", "Test_C_2", 5)); // test removal cmpModifiersManager.RemoveModifier("Test_C", "Test_C_2", 5); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_C", 5, 5), 25); SerializationCycle(); TS_ASSERT(cmpModifiersManager.HasModifier("Test_C", "Test_C_3", PLAYER_ENTITY_ID)); TS_ASSERT(!cmpModifiersManager.HasModifier("Test_C", "Test_C_2", 5)); // Test that entities keep local modifications but not global ones when changing owner. AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": (a) => a == PLAYER_ID_FOR_TEST ? PLAYER_ENTITY_ID : PLAYER_ENTITY_ID + 1 }); AddMock(PLAYER_ENTITY_ID + 1, IID_Player, { "GetPlayerID": () => PLAYER_ID_FOR_TEST + 1 }); cmpModifiersManager = ConstructComponent(SYSTEM_ENTITY, "ModifiersManager", {}); cmpModifiersManager.Init(); -cmpModifiersManager.AddModifier("Test_D", "Test_D_0", { "affects": ["Structure"], "add": 10 }, PLAYER_ENTITY_ID); -cmpModifiersManager.AddModifier("Test_D", "Test_D_1", { "affects": ["Structure"], "add": 1 }, PLAYER_ENTITY_ID + 1); -cmpModifiersManager.AddModifier("Test_D", "Test_D_2", { "affects": ["Structure"], "add": 5 }, 5); +cmpModifiersManager.AddModifier("Test_D", "Test_D_0", [{ "affects": ["Structure"], "add": 10 }], PLAYER_ENTITY_ID); +cmpModifiersManager.AddModifier("Test_D", "Test_D_1", [{ "affects": ["Structure"], "add": 1 }], PLAYER_ENTITY_ID + 1); +cmpModifiersManager.AddModifier("Test_D", "Test_D_2", [{ "affects": ["Structure"], "add": 5 }], 5); cmpModifiersManager.OnGlobalPlayerEntityChanged({ "player": PLAYER_ID_FOR_TEST, "from": -1, "to": PLAYER_ENTITY_ID }); cmpModifiersManager.OnGlobalPlayerEntityChanged({ "player": PLAYER_ID_FOR_TEST + 1, "from": -1, "to": PLAYER_ENTITY_ID + 1 }); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_D", 10, 5), 25); cmpModifiersManager.OnGlobalOwnershipChanged({ "entity": 5, "from": PLAYER_ID_FOR_TEST, "to": PLAYER_ID_FOR_TEST + 1 }); AddMock(5, IID_Ownership, { "GetOwner": () => PLAYER_ID_FOR_TEST + 1 }); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_D", 10, 5), 16); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Cheat.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Cheat.js (revision 22966) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Cheat.js (revision 22967) @@ -1,177 +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": 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 } + "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 22966) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/InitGame.js (revision 22967) @@ -1,87 +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); 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] }, + "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/MultiKeyMap.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/MultiKeyMap.js (revision 22966) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/MultiKeyMap.js (revision 22967) @@ -1,225 +1,225 @@ // Convenient container abstraction for storing items referenced by a 3-tuple. // Used by the ModifiersManager to store items by (property Name, entity, item ID). // Methods starting with an underscore are private to the storage. // This supports stackable items as it stores count for each 3-tuple. // It is designed to be as fast as can be for a JS container. function MultiKeyMap() { this.items = new Map(); // Keys are referred to as 'primaryKey', 'secondaryKey', 'itemID'. } MultiKeyMap.prototype.Serialize = function() { let ret = []; for (let primary of this.items.keys()) { // Keys of a Map can be arbitrary types whereas objects only support string, so use a list. let vals = [primary, []]; ret.push(vals); for (let secondary of this.items.get(primary).keys()) vals[1].push([secondary, this.items.get(primary).get(secondary)]); } return ret; }; MultiKeyMap.prototype.Deserialize = function(data) { for (let primary in data) { this.items.set(data[primary][0], new Map()); for (let secondary in data[primary][1]) this.items.get(data[primary][0]).set(data[primary][1][secondary][0], data[primary][1][secondary][1]); } }; /** * Add a single item. * NB: if you add an item with a different value but the same itemID, the original value remains. * @param item - an object. * @param itemID - internal ID of this item, for later removal and/or updating * @param stackable - if stackable, changing the count of items invalides, otherwise not. * @returns true if the items list changed in such a way that cached values are possibly invalidated. */ MultiKeyMap.prototype.AddItem = function(primaryKey, itemID, item, secondaryKey, stackable = false) { if (!this._AddItem(primaryKey, itemID, item, secondaryKey, stackable)) return false; this._OnItemModified(primaryKey, secondaryKey, itemID); return true; }; /** * Add items to multiple properties at once (only one item per property) * @param items - Dictionnary of { primaryKey: item } * @returns true if the items list changed in such a way that cached values are possibly invalidated. */ MultiKeyMap.prototype.AddItems = function(itemID, items, secondaryKey, stackable = false) { let modified = false; for (let primaryKey in items) modified = this.AddItem(primaryKey, itemID, items[primaryKey], secondaryKey, stackable) || modified; return modified; }; /** * Removes a item on a property. * @param primaryKey - property to change (e.g. "Health/Max") * @param itemID - internal ID of the item to remove * @param secondaryKey - secondaryKey ID * @returns true if the items list changed in such a way that cached values are possibly invalidated. */ MultiKeyMap.prototype.RemoveItem = function(primaryKey, itemID, secondaryKey, stackable = false) { if (!this._RemoveItem(primaryKey, itemID, secondaryKey, stackable)) return false; this._OnItemModified(primaryKey, secondaryKey, itemID); return true; }; /** * Removes items with this ID for any property name. * Naively iterates all property names. * @returns true if the items list changed in such a way that cached values are possibly invalidated. */ MultiKeyMap.prototype.RemoveAllItems = function(itemID, secondaryKey, stackable = false) { let modified = false; // Map doesn't implement some so use a for-loop here. for (let primaryKey of this.items.keys()) modified = this.RemoveItem(primaryKey, itemID, secondaryKey, stackable) || modified; return modified; }; /** * @param itemID - internal ID of the item to try and find. * @returns true if there is at least one item with that itemID */ MultiKeyMap.prototype.HasItem = function(primaryKey, itemID, secondaryKey) { // some() returns false for an empty list which is wanted here. return this._getItems(primaryKey, secondaryKey).some(item => item._ID === itemID); }; /** * Check if we have a item for any property name. * Naively iterates all property names. * @returns true if there is at least one item with that itemID */ MultiKeyMap.prototype.HasAnyItem = function(itemID, secondaryKey) { // Map doesn't implement some so use for loops instead. for (let primaryKey of this.items.keys()) if (this.HasItem(primaryKey, itemID, secondaryKey)) return true; return false; }; /** * @returns A list of items (references to stored items to avoid copying) * (these need to be treated as constants to not break the map) */ MultiKeyMap.prototype.GetItems = function(primaryKey, secondaryKey) { return this._getItems(primaryKey, secondaryKey); }; /** * @returns A dictionary of { Property Name: items } for the secondary Key. * Naively iterates all property names. */ MultiKeyMap.prototype.GetAllItems = function(secondaryKey) { let items = {}; // Map doesn't implement filter so use a for loop. for (let primaryKey of this.items.keys()) { if (!this.items.get(primaryKey).has(secondaryKey)) continue; items[primaryKey] = this.GetItems(primaryKey, secondaryKey); } return items; }; /** * @returns a list of items. * This does not necessarily return a reference to items' list, use _getItemsOrInit for that. */ MultiKeyMap.prototype._getItems = function(primaryKey, secondaryKey) { let cache = this.items.get(primaryKey); if (cache) cache = cache.get(secondaryKey); return cache ? cache : []; }; /** * @returns a reference to the list of items for that property name and secondaryKey. */ MultiKeyMap.prototype._getItemsOrInit = function(primaryKey, secondaryKey) { let cache = this.items.get(primaryKey); if (!cache) cache = this.items.set(primaryKey, new Map()).get(primaryKey); let cache2 = cache.get(secondaryKey); if (!cache2) cache2 = cache.set(secondaryKey, []).get(secondaryKey); return cache2; }; /** * @returns true if the items list changed in such a way that cached values are possibly invalidated. */ MultiKeyMap.prototype._AddItem = function(primaryKey, itemID, item, secondaryKey, stackable) { let items = this._getItemsOrInit(primaryKey, secondaryKey); for (let it of items) if (it._ID == itemID) { it._count++; return stackable; } - items.push(Object.assign({ "_ID": itemID, "_count": 1 }, item)); + items.push({ "_ID": itemID, "_count": 1, "value": item }); return true; }; /** * @returns true if the items list changed in such a way that cached values are possibly invalidated. */ MultiKeyMap.prototype._RemoveItem = function(primaryKey, itemID, secondaryKey, stackable) { let items = this._getItems(primaryKey, secondaryKey); let existingItem = items.filter(item => { return item._ID == itemID; }); if (!existingItem.length) return false; if (--existingItem[0]._count > 0) return stackable; let stilValidItems = items.filter(item => item._count > 0); // Delete entries from the map if necessary to clean up. if (!stilValidItems.length) { this.items.get(primaryKey).delete(secondaryKey); if (!this.items.get(primaryKey).size) this.items.delete(primaryKey); return true; } this.items.get(primaryKey).set(secondaryKey, stilValidItems); return true; }; /** * Stub method, to overload. */ MultiKeyMap.prototype._OnItemModified = function(primaryKey, secondaryKey, itemID) {}; Engine.RegisterGlobal("MultiKeyMap", MultiKeyMap); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/tests/test_MultiKeyMap.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/tests/test_MultiKeyMap.js (revision 22966) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/tests/test_MultiKeyMap.js (revision 22967) @@ -1,124 +1,124 @@ Engine.LoadHelperScript("MultiKeyMap.js"); function setup_keys(map) { map.AddItem("prim_a", "item_a", null, "sec_a"); map.AddItem("prim_a", "item_b", null, "sec_a"); map.AddItem("prim_a", "item_c", null, "sec_a"); map.AddItem("prim_a", "item_a", null, "sec_b"); map.AddItem("prim_b", "item_a", null, "sec_a"); map.AddItem("prim_c", "item_a", null, "sec_a"); map.AddItem("prim_c", "item_a", null, 5); } // Check that key-related operations are correct. function test_keys(map) { TS_ASSERT(map.items.has("prim_a")); TS_ASSERT(map.items.has("prim_b")); TS_ASSERT(map.items.has("prim_c")); TS_ASSERT(map.items.get("prim_a").has("sec_a")); TS_ASSERT(map.items.get("prim_a").has("sec_b")); TS_ASSERT(!map.items.get("prim_a").has("sec_c")); TS_ASSERT(map.items.get("prim_b").has("sec_a")); TS_ASSERT(map.items.get("prim_c").has("sec_a")); TS_ASSERT(map.items.get("prim_c").has(5)); TS_ASSERT(map.items.get("prim_a").get("sec_a").length == 3); TS_ASSERT(map.items.get("prim_a").get("sec_b").length == 1); TS_ASSERT(map.items.get("prim_b").get("sec_a").length == 1); TS_ASSERT(map.items.get("prim_c").get("sec_a").length == 1); TS_ASSERT(map.items.get("prim_c").get(5).length == 1); TS_ASSERT(map.GetItems("prim_a", "sec_a").length == 3); TS_ASSERT(map.GetItems("prim_a", "sec_b").length == 1); TS_ASSERT(map.GetItems("prim_b", "sec_a").length == 1); TS_ASSERT(map.GetItems("prim_c", "sec_a").length == 1); TS_ASSERT(map.GetItems("prim_c", 5).length == 1); TS_ASSERT(map.HasItem("prim_a", "item_a", "sec_a")); TS_ASSERT(map.HasItem("prim_a", "item_b", "sec_a")); TS_ASSERT(map.HasItem("prim_a", "item_c", "sec_a")); TS_ASSERT(!map.HasItem("prim_a", "item_d", "sec_a")); TS_ASSERT(map.HasItem("prim_a", "item_a", "sec_b")); TS_ASSERT(!map.HasItem("prim_a", "item_b", "sec_b")); TS_ASSERT(!map.HasItem("prim_a", "item_c", "sec_b")); TS_ASSERT(map.HasItem("prim_b", "item_a", "sec_a")); TS_ASSERT(map.HasItem("prim_c", "item_a", "sec_a")); TS_ASSERT(map.HasAnyItem("item_a", "sec_b")); TS_ASSERT(map.HasAnyItem("item_b", "sec_a")); TS_ASSERT(!map.HasAnyItem("item_d", "sec_a")); TS_ASSERT(!map.HasAnyItem("item_b", "sec_b")); // Adding the same item increases its count. map.AddItem("prim_a", "item_b", 0, "sec_a"); TS_ASSERT_EQUALS(map.items.get("prim_a").get("sec_a").length, 3); TS_ASSERT_EQUALS(map.items.get("prim_a").get("sec_a").filter(item => item._ID == "item_b")[0]._count, 2); TS_ASSERT_EQUALS(map.GetItems("prim_a", "sec_a").length, 3); // Adding without stackable doesn't invalidate caches, adding with does. TS_ASSERT(!map.AddItem("prim_a", "item_b", 0, "sec_a")); TS_ASSERT(map.AddItem("prim_a", "item_b", 0, "sec_a", true)); TS_ASSERT(map.items.get("prim_a").get("sec_a").filter(item => item._ID == "item_b")[0]._count == 4); // Likewise removing, unless we now reach 0 TS_ASSERT(!map.RemoveItem("prim_a", "item_b", "sec_a")); TS_ASSERT(map.RemoveItem("prim_a", "item_b", "sec_a", true)); TS_ASSERT(!map.RemoveItem("prim_a", "item_b", "sec_a")); TS_ASSERT(map.RemoveItem("prim_a", "item_b", "sec_a")); // Check that cleanup is done TS_ASSERT(map.items.get("prim_a").get("sec_a").length == 2); TS_ASSERT(map.RemoveItem("prim_a", "item_a", "sec_a")); TS_ASSERT(map.RemoveItem("prim_a", "item_c", "sec_a")); TS_ASSERT(!map.items.get("prim_a").has("sec_a")); TS_ASSERT(map.items.get("prim_a").has("sec_b")); TS_ASSERT(map.RemoveItem("prim_a", "item_a", "sec_b")); TS_ASSERT(!map.items.has("prim_a")); } function setup_items(map) { map.AddItem("prim_a", "item_a", { "value": 1 }, "sec_a"); map.AddItem("prim_a", "item_b", { "value": 2 }, "sec_a"); map.AddItem("prim_a", "item_c", { "value": 3 }, "sec_a"); map.AddItem("prim_a", "item_c", { "value": 1000 }, "sec_a"); map.AddItem("prim_a", "item_a", { "value": 5 }, "sec_b"); map.AddItem("prim_b", "item_a", { "value": 6 }, "sec_a"); map.AddItem("prim_c", "item_a", { "value": 7 }, "sec_a"); } // Check that items returned are correct. function test_items(map) { let items = map.GetAllItems("sec_a"); TS_ASSERT("prim_a" in items); TS_ASSERT("prim_b" in items); TS_ASSERT("prim_c" in items); let sum = 0; for (let key in items) - items[key].forEach(item => (sum += item.value * item._count)); + items[key].forEach(item => { sum += item.value.value * item._count; }); TS_ASSERT(sum == 22); } // Test items, and test that deserialised versions still pass test (i.e. test serialisation). let map = new MultiKeyMap(); setup_keys(map); test_keys(map); map = new MultiKeyMap(); let map2 = new MultiKeyMap(); setup_keys(map); map2.Deserialize(map.Serialize()); test_keys(map2); map = new MultiKeyMap(); setup_items(map); test_items(map); map = new MultiKeyMap(); map2 = new MultiKeyMap(); setup_items(map); map2.Deserialize(map.Serialize()); test_items(map2);