Allow to limit unit count per match.

This allows to limit the number of times a specific template can be
constructed/trained/created during a match.

Part of:
https://wildfiregames.com/forum/index.php?/topic/27214-borg-expansion-pack-mod-implementation-in-0ad-alpha-24-release/
Refs.
https://wildfiregames.com/forum/topic/24682-champions-and-civilisations-balance-mod-for-a23/

Differential revision: D2411
Reviewed by: @wraitii
Comments by: @Angen, @Stan
This was SVN commit r24468.
This commit is contained in:
Freagarach
2020-12-29 11:00:54 +00:00
parent 71a61d5f50
commit a79a47effe
13 changed files with 185 additions and 40 deletions
@@ -291,9 +291,13 @@ function GetTemplateDataHelper(template, player, auraTemplates, modifiers = {})
}
if (template.TrainingRestrictions)
{
ret.trainingRestrictions = {
"category": template.TrainingRestrictions.Category,
"category": template.TrainingRestrictions.Category
};
if (template.TrainingRestrictions.MatchLimit)
ret.trainingRestrictions.matchLimit = +template.TrainingRestrictions.MatchLimit;
}
if (template.Cost)
{
+25 -3
View File
@@ -1270,13 +1270,28 @@ function getEntityLimitAndCount(playerState, entType)
"entLimit": undefined,
"entCount": undefined,
"entLimitChangers": undefined,
"canBeAddedCount": undefined
"canBeAddedCount": undefined,
"matchLimit": undefined,
"matchCount": undefined,
"type": undefined
};
if (!playerState.entityLimits)
return ret;
let template = GetTemplateData(entType);
let entCategory = template.trainingRestrictions && template.trainingRestrictions.category ||
template.buildRestrictions && template.buildRestrictions.category;
let entCategory;
let matchLimit;
if (template.trainingRestrictions)
{
entCategory = template.trainingRestrictions.category;
matchLimit = template.trainingRestrictions.matchLimit;
ret.type = "training";
}
else if (template.buildRestrictions)
{
entCategory = template.buildRestrictions.category;
matchLimit = template.buildRestrictions.matchLimit;
ret.type = "build";
}
if (entCategory && playerState.entityLimits[entCategory] !== undefined)
{
@@ -1285,6 +1300,13 @@ function getEntityLimitAndCount(playerState, entType)
ret.entLimitChangers = playerState.entityLimitChangers[entCategory];
ret.canBeAddedCount = Math.max(ret.entLimit - ret.entCount, 0);
}
if (matchLimit)
{
ret.matchLimit = matchLimit;
ret.matchCount = playerState.matchEntityCounts[entType] || 0;
ret.canBeAddedCount = Math.min(Math.max(ret.entLimit - ret.entCount, 0), Math.max(ret.matchLimit - ret.matchCount, 0));
}
return ret;
}
@@ -257,6 +257,7 @@ g_SelectionPanels.Construction = {
let limits = getEntityLimitAndCount(data.playerState, data.item);
tooltips.push(
formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers),
formatMatchLimitString(limits.matchLimit, limits.matchCount, limits.type),
getRequiredTechnologyTooltip(technologyEnabled, template.requiredTechnology, GetSimState().players[data.player].civ),
getNeededResourcesTooltip(neededResources));
@@ -998,7 +999,8 @@ g_SelectionPanels.Training = {
getEntityCostTooltip(template, data.player, unitIds[0], buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch)
];
let limits = getEntityLimitAndCount(data.playerState, data.item);
tooltips.push(formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers));
tooltips.push(formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers),
formatMatchLimitString(limits.matchLimit, limits.matchCount, limits.type));
if (Engine.ConfigDB_GetValue("user", "showdetailedtooltips") === "true")
tooltips = tooltips.concat([
@@ -1111,6 +1113,7 @@ g_SelectionPanels.Upgrade = {
tooltips.push(
getEntityCostTooltip(data.item, undefined, undefined, data.unitEntStates.length),
formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers),
formatMatchLimitString(limits.matchLimit, limits.matchCount, limits.type),
getRequiredTechnologyTooltip(technologyEnabled, data.item.requiredTechnology, GetSimState().players[data.player].civ),
getNeededResourcesTooltip(neededResources),
showTemplateViewerOnRightClickTooltip());
@@ -115,6 +115,66 @@ function formatLimitString(trainEntLimit, trainEntCount, trainEntLimitChangers)
return text;
}
/**
* Format template match count/limit message for the tooltip.
*
* @param {number} matchEntLimit - The limit of the entity.
* @param {number} matchEntCount - The count of the entity.
* @param {string} type - The type of the action (i.e. "build" or "training").
*
* @return {string} - The string to show the user with information regarding the limit of this template.
*/
function formatMatchLimitString(matchEntLimit, matchEntCount, type)
{
if (matchEntLimit == undefined)
return "";
let passedLimit = matchEntCount >= matchEntLimit;
let count = matchEntLimit - matchEntCount;
let text;
if (type == "build")
{
if (passedLimit)
text = sprintf(translatePlural("Could be constructed merely once.", "Could be constructed merely %(limit)s times.", matchEntLimit), {
"limit": matchEntLimit
});
else if (matchEntLimit == 1)
text = translate("Can be constructed only once.");
else
text = sprintf(translatePlural("Can be constructed %(count)s more time.", "Can be constructed %(count)s more times.", count), {
"count": count
});
}
else if (type == "training")
{
if (passedLimit)
text = sprintf(translatePlural("Could be trained merely once.", "Could be trained merely %(limit)s times.", matchEntLimit), {
"limit": matchEntLimit
});
else if (matchEntLimit == 1)
text = translate("Can be trained only once.");
else
text = sprintf(translatePlural("Can be trained %(count)s more time.", "Can be trained %(count)s more times.", count), {
"count": count
});
}
else
{
if (passedLimit)
text = sprintf(translatePlural("Could be created merely once.", "Could be created merely %(limit)s times.", matchEntLimit), {
"limit": matchEntLimit
});
else if (matchEntLimit == 1)
text = translate("Can be created only once.");
else
text = sprintf(translatePlural("Can be created %(count)s more time.", "Can be created %(count)s more times.", count), {
"count": count
});
}
return passedLimit ? coloredText(text, "red") : text;
}
/**
* Format batch training string for the tooltip
* Examples:
@@ -35,6 +35,11 @@ BuildRestrictions.prototype.Schema =
"<element name='Category' a:help='Specifies the category of this building, for satisfying special constraints. Choices include: Apadana, CivilCentre, Council, Embassy, Fortress, Gladiator, Hall, Hero, Juggernaut, Library, Lighthouse, Monument, Pillar, PyramidLarge, PyramidSmall, Stoa, TempleOfAmun, Theater, Tower, UniqueBuilding, WarDog, Wonder'>" +
"<text/>" +
"</element>" +
"<optional>" +
"<element name='MatchLimit' a:help='Specifies how many times this entity can be created during a match.'>" +
"<data type='positiveInteger'/>" +
"</element>" +
"</optional>" +
"<optional>" +
"<element name='Distance' a:help='Specifies distance restrictions on this building, relative to buildings from the given category.'>" +
"<interleave>" +
@@ -72,6 +72,7 @@ EntityLimits.prototype.Init = function()
this.removers = {};
this.classCount = {}; // counts entities with the given class, used in the limit removal
this.removedLimit = {};
this.matchTemplateCount = {};
for (var category in this.template.Limits)
{
this.limit[category] = +this.template.Limits[category];
@@ -103,6 +104,14 @@ EntityLimits.prototype.ChangeCount = function(category, value)
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;
@@ -113,6 +122,11 @@ EntityLimits.prototype.GetCounts = function()
return this.count;
};
EntityLimits.prototype.GetMatchCounts = function()
{
return this.matchTemplateCount;
};
EntityLimits.prototype.GetLimitChangers = function()
{
return this.changers;
@@ -145,40 +159,48 @@ EntityLimits.prototype.UpdateLimitRemoval = function()
}
};
EntityLimits.prototype.AllowedToCreate = function(limitType, category, count)
EntityLimits.prototype.AllowedToCreate = function(limitType, category, count, templateName, matchLimit)
{
// Allow unspecified categories and those with no limit
if (this.count[category] === undefined || this.limit[category] === undefined)
return true;
if (this.count[category] + count > this.limit[category])
if (this.count[category] !== undefined && this.limit[category] !== undefined &&
this.count[category] + count > this.limit[category])
{
var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
var notification = {
"players": [cmpPlayer.GetPlayerID()],
"translateMessage": true,
"translateParameters": ["category"],
"parameters": {"category": category, "limit": this.limit[category]},
};
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");
}
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification(notification);
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
@@ -186,9 +208,9 @@ EntityLimits.prototype.AllowedToBuild = function(category)
return this.AllowedToCreate(BUILD, category, 0);
};
EntityLimits.prototype.AllowedToTrain = function(category, count)
EntityLimits.prototype.AllowedToTrain = function(category, count, templateName, matchLimit)
{
return this.AllowedToCreate(TRAINING, category, count);
return this.AllowedToCreate(TRAINING, category, count, templateName, matchLimit);
};
/**
@@ -119,6 +119,7 @@ GuiInterface.prototype.GetSimulationState = function()
"isEnemy": enemies,
"entityLimits": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimits() : null,
"entityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetCounts() : null,
"matchEntityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetMatchCounts() : null,
"entityLimitChangers": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimitChangers() : null,
"researchQueued": cmpTechnologyManager ? cmpTechnologyManager.GetQueuedResearch() : null,
"researchStarted": cmpTechnologyManager ? cmpTechnologyManager.GetStartedTechs() : null,
@@ -397,6 +397,8 @@ ProductionQueue.prototype.AddBatch = function(templateName, type, count, metadat
let cmpPlayerEntityLimits = QueryOwnerInterface(this.entity, IID_EntityLimits);
if (cmpPlayerEntityLimits)
cmpPlayerEntityLimits.ChangeCount(unitCategory, count);
if (template.TrainingRestrictions.MatchLimit)
cmpPlayerEntityLimits.ChangeMatchCount(templateName, count);
}
let buildTime = ApplyValueModificationsToTemplate(
@@ -545,6 +547,8 @@ ProductionQueue.prototype.RemoveBatch = function(id)
let cmpPlayerEntityLimits = QueryPlayerIDInterface(item.player, IID_EntityLimits);
if (cmpPlayerEntityLimits)
cmpPlayerEntityLimits.ChangeCount(template.TrainingRestrictions.Category, -item.count);
if (template.TrainingRestrictions.MatchLimit)
cmpPlayerEntityLimits.ChangeMatchCount(item.unitTemplate, -item.count);
}
}
@@ -9,7 +9,12 @@ TrainingRestrictions.prototype.Schema =
"</a:example>" +
"<element name='Category' a:help='Specifies the category of this unit, for satisfying special constraints. Choices include: Hero, Juggernaut, WarDog'>" +
"<text/>" +
"</element>";
"</element>" +
"<optional>" +
"<element name='MatchLimit' a:help='Specifies how many times this entity can be trained during a match.'>" +
"<data type='positiveInteger'/>" +
"</element>" +
"</optional>";
TrainingRestrictions.prototype.Init = function()
{
@@ -34,6 +34,7 @@ let cmpEntityLimits = ConstructComponent(10, "EntityLimits", template);
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 0, "Hero": 0, "Champion": 0 });
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimits(), { "Tower": 5, "Wonder": 1, "Hero": 2, "Champion": 1 });
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimitChangers(), { "Tower": { "Monument": 1 } });
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetMatchCounts(), {});
// Test training restrictions
TS_ASSERT(cmpEntityLimits.AllowedToTrain("Hero"));
@@ -44,7 +45,7 @@ for (let ent = 60; ent < 63; ++ent)
{
AddMock(ent, IID_TrainingRestrictions, {
"GetCategory": () => "Hero"
});
});
}
cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 60, "from": INVALID_PLAYER, "to": 1 });
@@ -124,7 +124,8 @@ AddMock(100, IID_Player, {
AddMock(100, IID_EntityLimits, {
"GetLimits": function() { return { "Foo": 10 }; },
"GetCounts": function() { return { "Foo": 5 }; },
"GetLimitChangers": function() {return { "Foo": {} }; }
"GetLimitChangers": function() { return { "Foo": {} }; },
"GetMatchCounts": function() { return { "Bar": 0 }; }
});
AddMock(100, IID_TechnologyManager, {
@@ -211,7 +212,8 @@ AddMock(101, IID_Player, {
AddMock(101, IID_EntityLimits, {
"GetLimits": function() { return { "Bar": 20 }; },
"GetCounts": function() { return { "Bar": 0 }; },
"GetLimitChangers": function() {return { "Bar": {} }; }
"GetLimitChangers": function() { return { "Bar": {} }; },
"GetMatchCounts": function() { return { "Foo": 0 }; }
});
AddMock(101, IID_TechnologyManager, {
@@ -299,6 +301,7 @@ TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), {
"isEnemy": [true, true],
"entityLimits": { "Foo": 10 },
"entityCounts": { "Foo": 5 },
"matchEntityCounts": { "Bar": 0 },
"entityLimitChangers": { "Foo": {} },
"researchQueued": new Map(),
"researchStarted": new Set(),
@@ -349,6 +352,7 @@ TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), {
"isEnemy": [false, false],
"entityLimits": { "Bar": 20 },
"entityCounts": { "Bar": 0 },
"matchEntityCounts": { "Foo": 0 },
"entityLimitChangers": { "Bar": {} },
"researchQueued": new Map(),
"researchStarted": new Set(),
@@ -409,6 +413,7 @@ TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedSimulationState(), {
"isEnemy": [true, true],
"entityLimits": { "Foo": 10 },
"entityCounts": { "Foo": 5 },
"matchEntityCounts": { "Bar": 0 },
"entityLimitChangers": { "Foo": {} },
"researchQueued": new Map(),
"researchStarted": new Set(),
@@ -482,6 +487,7 @@ TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedSimulationState(), {
"isEnemy": [false, false],
"entityLimits": { "Bar": 20 },
"entityCounts": { "Bar": 0 },
"matchEntityCounts": { "Foo": 0 },
"entityLimitChangers": { "Bar": {} },
"researchQueued": new Map(),
"researchStarted": new Set(),
@@ -227,7 +227,8 @@ function regression_test_d1879()
"Resources": {}
},
"TrainingRestrictions": {
"Category": "some_limit"
"Category": "some_limit",
"MatchLimit": "7"
}
})
});
@@ -244,6 +245,7 @@ function regression_test_d1879()
"UnBlockTraining": () => {},
"UnReservePopulationSlots": () => {},
"TrySubtractResources": () => true,
"AddResources": () => true,
"TryReservePopulationSlots": () => false // Always have pop space.
});
@@ -259,6 +261,8 @@ function regression_test_d1879()
let cmpEntLimits = QueryOwnerInterface(testEntity, IID_EntityLimits);
TS_ASSERT(cmpEntLimits.AllowedToTrain("some_limit", 8));
TS_ASSERT(!cmpEntLimits.AllowedToTrain("some_limit", 9));
TS_ASSERT(cmpEntLimits.AllowedToTrain("some_limit", 5, "some_template", 8));
TS_ASSERT(!cmpEntLimits.AllowedToTrain("some_limit", 10, "some_template", 8));
// Check that the entity limits do get updated if the spawn succeeds.
AddMock(testEntity, IID_Footprint, {
@@ -268,10 +272,12 @@ function regression_test_d1879()
cmpProdQueue.AddBatch("some_template", "unit", 3);
TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 3);
TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts().some_template, 3);
Engine.QueryInterface(testEntity, IID_ProductionQueue).ProgressTimeout();
cmpProdQueue.ProgressTimeout();
TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 3);
TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts().some_template, 3);
// Now check that it doesn't get updated when the spawn doesn't succeed.
AddMock(testEntity, IID_Footprint, {
@@ -283,10 +289,16 @@ function regression_test_d1879()
});
cmpProdQueue.AddBatch("some_template", "unit", 3);
Engine.QueryInterface(testEntity, IID_ProductionQueue).ProgressTimeout();
cmpProdQueue.ProgressTimeout();
TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1);
TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 6);
TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts().some_template, 6);
// Check that when the batch is removed the counts are subtracted again.
cmpProdQueue.RemoveBatch(cmpProdQueue.GetQueue()[0].id);
TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 3);
TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts().some_template, 3);
}
function test_batch_adding()
@@ -296,7 +296,7 @@ var g_Commands = {
if (unitCategory)
{
var cmpPlayerEntityLimits = QueryOwnerInterface(ent, IID_EntityLimits);
if (cmpPlayerEntityLimits && !cmpPlayerEntityLimits.AllowedToTrain(unitCategory, cmd.count))
if (cmpPlayerEntityLimits && !cmpPlayerEntityLimits.AllowedToTrain(unitCategory, cmd.count, cmd.template, template.TrainingRestrictions.MatchLimit))
{
if (g_DebugCommands)
warn(unitCategory + " train limit is reached: " + uneval(cmd));