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);