Source: EntityLimits.js

/**
 * @class
 */
function EntityLimits() {}

EntityLimits.prototype.Schema =
	"<a:help>Specifies per category limits on number of entities (buildings or units) that can be created for each player</a:help>" +
	"<a:example>" +
		"<Limits>" +
			"<Apadana>1</Apadana>" +
			"<Fortress>10</Fortress>" +
			"<Hero>1</Hero>" +
			"<Monument>5</Monument>" +
			"<Tower>25</Tower>" +
			"<Wonder>1</Wonder>" +
		"</Limits>" +
		"<LimitChangers>" +
			"<Monument>" +
				"<CivilCentre>2</CivilCentre>" +
			"</Monument>" +
		"</LimitChangers>" +
		"<LimitRemovers>" +
			"<CivilCentre>" +
				"<RequiredTechs datatype=\"tokens\">town_phase</RequiredTechs>" +
			"</CivilCentre>" +
		"</LimitRemovers>" +
	"</a:example>" +
	"<element name='Limits'>" +
		"<zeroOrMore>" +
			"<element a:help='Specifies a category of building/unit on which to apply this limit. See BuildRestrictions/TrainingRestrictions for possible categories'>" +
				"<anyName />" +
				"<data type='integer'/>" +
			"</element>" +
		"</zeroOrMore>" +
	"</element>" +
	"<element name='LimitChangers'>" +
		"<zeroOrMore>" +
			"<element a:help='Specifies a category of building/unit on which to apply this limit. See BuildRestrictions/TrainingRestrictions for possible categories'>" +
				"<anyName />" +
				"<zeroOrMore>" +
					"<element a:help='Specifies the class that changes the entity limit'>" +
						"<anyName />" +
						"<data type='integer'/>" +
					"</element>" +
				"</zeroOrMore>" +
			"</element>" +
		"</zeroOrMore>" +
	"</element>" +
	"<element name='LimitRemovers'>" +
		"<zeroOrMore>" +
			"<element a:help='Specifies a category of building/unit on which to remove this limit. The limit will be removed if all the followings requirements are satisfied'>" +
				"<anyName />" +
				"<oneOrMore>" +
					"<element a:help='Possible requirements are: RequiredTechs and RequiredClasses'>" +
						"<anyName />" +
						"<attribute name='datatype'>" +
							"<value>tokens</value>" +
						"</attribute>" +
						"<text/>" +
					"</element>" +
				"</oneOrMore>" +
			"</element>" +
		"</zeroOrMore>" +
	"</element>";


const TRAINING = "training";
const BUILD = "build";

EntityLimits.prototype.Init = function()
{
	this.limit = {};
	// Counts entities which change the limit of the given category.
	this.count = {};
	this.changers = {};
	this.removers = {};
	// Counts entities with the given class, used in the limit removal.
	this.classCount = {};
	this.removedLimit = {};
	this.matchTemplateCount = {};
	for (var category in this.template.Limits)
	{
		this.limit[category] = +this.template.Limits[category];
		this.count[category] = 0;
		if (category in this.template.LimitChangers)
		{
			this.changers[category] = {};
			for (var c in this.template.LimitChangers[category])
				this.changers[category][c] = +this.template.LimitChangers[category][c];
		}
		if (category in this.template.LimitRemovers)
		{
			// Keep a copy of removable limits for possible restoration.
			this.removedLimit[category] = this.limit[category];
			this.removers[category] = {};
			for (var c in this.template.LimitRemovers[category])
			{
				this.removers[category][c] = this.template.LimitRemovers[category][c]._string.split(/\s+/);
				if (c === "RequiredClasses")
					for (var cls of this.removers[category][c])
						this.classCount[cls] = 0;
			}
		}
	}
};

EntityLimits.prototype.ChangeCount = function(category, value)
{
	if (this.count[category] !== undefined)
		this.count[category] += value;
};

EntityLimits.prototype.ChangeMatchCount = function(template, value)
{
	if (!this.matchTemplateCount[template])
		this.matchTemplateCount[template] = 0;

	this.matchTemplateCount[template] += value;
};

EntityLimits.prototype.GetLimits = function()
{
	return this.limit;
};

EntityLimits.prototype.GetCounts = function()
{
	return this.count;
};

EntityLimits.prototype.GetMatchCounts = function()
{
	return this.matchTemplateCount;
};

EntityLimits.prototype.GetLimitChangers = function()
{
	return this.changers;
};

EntityLimits.prototype.UpdateLimitsFromTech = function(tech)
{
	for (var category in this.removers)
		if ("RequiredTechs" in this.removers[category] && this.removers[category].RequiredTechs.indexOf(tech) !== -1)
			this.removers[category].RequiredTechs.splice(this.removers[category].RequiredTechs.indexOf(tech), 1);

	this.UpdateLimitRemoval();
};

EntityLimits.prototype.UpdateLimitRemoval = function()
{
	for (var category in this.removers)
	{
		var nolimit = true;
		if ("RequiredTechs" in this.removers[category])
			nolimit = !this.removers[category].RequiredTechs.length;
		if (nolimit && "RequiredClasses" in this.removers[category])
			for (var cls of this.removers[category].RequiredClasses)
				nolimit = nolimit && this.classCount[cls] > 0;

		if (nolimit && this.limit[category] !== undefined)
			this.limit[category] = undefined;
		else if (!nolimit && this.limit[category] === undefined)
			this.limit[category] = this.removedLimit[category];
	}
};

EntityLimits.prototype.AllowedToCreate = function(limitType, category, count, templateName, matchLimit)
{
	if (this.count[category] !== undefined && this.limit[category] !== undefined &&
		this.count[category] + count > this.limit[category])
	{
		this.NotifyLimit(limitType, category, this.limit[category]);
		return false;
	}

	if (this.matchTemplateCount[templateName] !== undefined && matchLimit !== undefined &&
		this.matchTemplateCount[templateName] + count > matchLimit)
	{
		this.NotifyLimit(limitType, category, matchLimit);
		return false;
	}

	return true;
};

EntityLimits.prototype.NotifyLimit = function(limitType, category, limit)
{
	let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
	let notification = {
		"players": [cmpPlayer.GetPlayerID()],
		"translateMessage": true,
		"translateParameters": ["category"],
		"parameters": { "category": category, "limit": limit },
	};

	if (limitType == BUILD)
		notification.message = markForTranslation("%(category)s build limit of %(limit)s reached");
	else if (limitType == TRAINING)
		notification.message = markForTranslation("%(category)s training limit of %(limit)s reached");
	else
	{
		warn("EntityLimits.js: Unknown LimitType " + limitType);
		notification.message = markForTranslation("%(category)s limit of %(limit)s reached");
	}
	let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
	cmpGUIInterface.PushNotification(notification);
};

EntityLimits.prototype.AllowedToBuild = function(category)
{
	// We pass count 0 as the creation of the building has already taken place and
	// the ownership has been set (triggering OnGlobalOwnershipChanged)
	return this.AllowedToCreate(BUILD, category, 0);
};

EntityLimits.prototype.AllowedToTrain = function(category, count, templateName, matchLimit)
{
	return this.AllowedToCreate(TRAINING, category, count, templateName, matchLimit);
};

/**
 * @param {number} ent - id of the entity which would be replaced.
 * @param {string} template - name of the new template.
 * @return {boolean} - whether we can replace ent.
 */
EntityLimits.prototype.AllowedToReplace = function(ent, template)
{
	let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
	let templateFrom = cmpTemplateManager.GetTemplate(cmpTemplateManager.GetCurrentTemplateName(ent));
	let templateTo = cmpTemplateManager.GetTemplate(template);

	if (templateTo.TrainingRestrictions)
	{
		let category = templateTo.TrainingRestrictions.Category;
		return this.AllowedToCreate(TRAINING, category, templateFrom.TrainingRestrictions && templateFrom.TrainingRestrictions.Category == category ? 0 : 1);
	}

	if (templateTo.BuildRestrictions)
	{
		let category = templateTo.BuildRestrictions.Category;
		return this.AllowedToCreate(BUILD, category, templateFrom.BuildRestrictions && templateFrom.BuildRestrictions.Category == category ? 0 : 1);
	}

	return true;
};

EntityLimits.prototype.OnGlobalOwnershipChanged = function(msg)
{
	// check if we are adding or removing an entity from this player
	var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
	if (!cmpPlayer)
	{
		error("EntityLimits component is defined on a non-player entity");
		return;
	}
	if (msg.from == cmpPlayer.GetPlayerID())
		var modifier = -1;
	else if (msg.to == cmpPlayer.GetPlayerID())
		var modifier = 1;
	else
		return;

	// Update entity counts
	var category = null;
	var cmpBuildRestrictions = Engine.QueryInterface(msg.entity, IID_BuildRestrictions);
	if (cmpBuildRestrictions)
		category = cmpBuildRestrictions.GetCategory();
	var cmpTrainingRestrictions = Engine.QueryInterface(msg.entity, IID_TrainingRestrictions);
	if (cmpTrainingRestrictions)
		category = cmpTrainingRestrictions.GetCategory();
	if (category)
		this.ChangeCount(category, modifier);

	// Update entity limits
	var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity);
	if (!cmpIdentity)
		return;

	// foundations shouldn't change the entity limits until they're completed
	var cmpFoundation = Engine.QueryInterface(msg.entity, IID_Foundation);
	if (cmpFoundation)
		return;
	var classes = cmpIdentity.GetClassesList();
	for (var category in this.changers)
		for (var c in this.changers[category])
			if (classes.indexOf(c) >= 0)
			{
				if (this.limit[category] != undefined)
					this.limit[category] += modifier * this.changers[category][c];
				if (this.removedLimit[category] != undefined)	// update removed limits in case we want to restore it
					this.removedLimit[category] += modifier * this.changers[category][c];
			}

	for (var category in this.removers)
		if ("RequiredClasses" in this.removers[category])
			for (var cls of this.removers[category].RequiredClasses)
				if (classes.indexOf(cls) !== -1)
					this.classCount[cls] += modifier;

	this.UpdateLimitRemoval();
};

Engine.RegisterComponentType(IID_EntityLimits, "EntityLimits", EntityLimits);