Add a 'team population' gamesetting

Remove the world population setting from the game setup.
Add a dropdown for choosing the "Population Cap Type".
(-> containing Player Population, World Population, Team Population)

Use a single "Population Cap" dropdown adapting to the pop cap types.
Move all population data into a single .json file.

New system component "PopulationCapManager" for distributing pop caps.

Resolves: #6918
This commit is contained in:
Vantha 2024-11-12 13:47:08 +01:00
parent c448973398
commit 5741f77c6e
28 changed files with 531 additions and 308 deletions

View file

@ -5,6 +5,7 @@ GameSettings.prototype.Attributes.LockedTeams = class LockedTeams extends GameSe
this.enabled = false;
this.settings.map.watch(() => this.onMapChange(), ["map"]);
this.settings.rating.watch(() => this.onRatingChange(), ["enabled"]);
this.settings.population.watch(() => this.onPopCapTypeChange(), ["capType"]);
this.onRatingChange();
}
@ -20,20 +21,25 @@ GameSettings.prototype.Attributes.LockedTeams = class LockedTeams extends GameSe
onMapChange()
{
if (this.settings.map.type != "scenario")
return;
this.setAvailable(this.settings.map.type != "scenario");
this.setEnabled(!!this.getMapSetting("LockTeams"));
}
onRatingChange()
{
if (this.settings.rating.enabled)
{
this.available = false;
this.setEnabled(true);
}
else
this.available = true;
this.setAvailable(!this.settings.rating.enabled);
this.setEnabled(this.settings.rating.enabled);
}
onPopCapTypeChange()
{
this.setAvailable(this.settings.population.capType != "team");
this.setEnabled(this.settings.population.capType == "team");
}
setAvailable(available)
{
this.available = available;
}
setEnabled(enabled)

View file

@ -1,23 +1,21 @@
/**
* Combines the worldPopulation and regular population cap.
* At the moment those are incompatible so this makes sense.
* Manages the maximum population capacity.
* This includes the cap value itself and its type (determining how to distribute the set cap among players).
* TODO: Should there be a dialog allowing per-player pop limits?
*/
GameSettings.prototype.Attributes.Population = class Population extends GameSetting
{
init()
{
this.popDefault = this.getDefaultValue("PopulationCapacities", "Population") || 200;
this.worldPopDefault = this.getDefaultValue("WorldPopulationCapacities", "Population") || 800;
this.perPlayer = false;
this.useWorldPop = false;
this.cap = this.popDefault;
this.perPlayer = null;
this.capTypeDefault = this.getDefaultValue("PopulationCapacities", "Name");
this.setPopCapType(this.capTypeDefault);
this.settings.map.watch(() => this.onMapChange(), ["map"]);
}
toInitAttributes(attribs)
{
attribs.settings.PopulationCapType = this.capType;
if (this.perPlayer)
{
if (!attribs.settings.PlayerData)
@ -28,48 +26,45 @@ GameSettings.prototype.Attributes.Population = class Population extends GameSett
if (this.perPlayer[i])
attribs.settings.PlayerData[i].PopulationLimit = this.perPlayer[i];
}
if (this.useWorldPop)
{
attribs.settings.WorldPopulation = true;
attribs.settings.WorldPopulationCap = this.cap;
}
else
attribs.settings.PopulationCap = this.cap;
}
fromInitAttributes(attribs)
{
if (!!this.getLegacySetting(attribs, "WorldPopulation"))
this.setPopCap(true, this.getLegacySetting(attribs, "WorldPopulationCap"));
else if (!!this.getLegacySetting(attribs, "PopulationCap"))
this.setPopCap(false, this.getLegacySetting(attribs, "PopulationCap"));
if (this.getLegacySetting(attribs, "PopulationCapType") !== undefined)
this.setPopCapType(this.getLegacySetting(attribs, "PopulationCapType"));
if (this.getLegacySetting(attribs, "PopulationCap") !== undefined)
this.setPopCap(this.getLegacySetting(attribs, "PopulationCap"));
}
onMapChange()
{
this.perPlayer = undefined;
this.perPlayer = null;
if (this.settings.map.type != "scenario")
return;
if (this.getMapSetting("PlayerData")?.some(data => data.PopulationLimit))
{
this.perPlayer = this.getMapSetting("PlayerData").map(data => data.PopulationLimit || undefined);
else if (this.getMapSetting("WorldPopulation"))
this.setPopCap(true, +this.getMapSetting("WorldPopulationCap"));
else
this.setPopCap(false, +this.getMapSetting("PopulationCap"));
return;
}
this.setPopCapType(this.getMapSetting("PopulationCapType") || this.capTypeDefault);
if (this.getMapSetting("PopulationCap"))
this.setPopCap(this.getMapSetting("PopulationCap"));
}
setPopCap(worldPop, cap = undefined)
setPopCap(cap)
{
if (worldPop != this.useWorldPop)
this.cap = undefined;
this.cap = cap;
}
this.useWorldPop = worldPop;
if (!!cap)
this.cap = cap;
else if (!this.cap && !this.useWorldPop)
this.cap = this.popDefault;
else if (!this.cap && this.useWorldPop)
this.cap = this.worldPopDefault;
setPopCapType(capType)
{
this.capType = capType;
this.currentData = g_Settings.PopulationCapacities.find(type => type.Name == capType);
this.setPopCap(this.currentData.Options.Default);
}
};

View file

@ -360,25 +360,21 @@ function getGameDescription(initAttributes, mapCache)
})
});
if (initAttributes.settings.PopulationCap !== undefined)
if (initAttributes.settings.PopulationCapType !== undefined)
titles.push({
"label": translate("Population Limit"),
"value":
initAttributes.settings.PlayerData &&
initAttributes.settings.PlayerData.some(pData => pData && pData.PopulationLimit !== undefined) ?
translateWithContext("population limit", "Per Player") :
g_PopulationCapacities.Title[
g_PopulationCapacities.Population.indexOf(
initAttributes.settings.PopulationCap)]
"label": translate("Population Cap Type"),
"value": translate(g_PopulationCapacities.Title[g_PopulationCapacities.Name.indexOf(initAttributes.settings.PopulationCapType)])
});
if (initAttributes.settings.WorldPopulationCap !== undefined)
if (initAttributes.settings.PopulationCap !== undefined)
titles.push({
"label": translate("World Population Cap"),
"label": translate(g_PopulationCapacities.CapTitle[g_PopulationCapacities.Name.indexOf(initAttributes.settings.PopulationCapType)]),
"value":
g_WorldPopulationCapacities.Title[
g_WorldPopulationCapacities.Population.indexOf(
initAttributes.settings.WorldPopulationCap)]
initAttributes.settings.PlayerData?.some(pData => pData?.PopulationLimit !== undefined) ?
translateWithContext("population capacity", "Per Player") :
initAttributes.settings.PopulationCap < 10000 ?
initAttributes.settings.PopulationCap :
translateWithContext("population capacity", "Unlimited")
});
titles.push({

View file

@ -43,8 +43,7 @@ function loadSettingsValues()
"MapSizes": loadSettingValuesFile("map_sizes.json"),
"Biomes": loadBiomes(),
"PlayerDefaults": loadPlayerDefaults(),
"PopulationCapacities": loadPopulationCapacities(),
"WorldPopulationCapacities": loadWorldPopulationCapacities(),
"PopulationCapacities": loadSettingValuesFile("population_capacities.json"),
"StartingResources": loadSettingValuesFile("starting_resources.json"),
"VictoryConditions": loadVictoryConditions(),
"TriggerDifficulties": loadSettingValuesFile("trigger_difficulties.json")
@ -250,51 +249,6 @@ function loadPlayerDefaults()
return json.PlayerData;
}
/**
* Loads available population capacities.
*
* @returns {Array|undefined}
*/
function loadPopulationCapacities()
{
var json = Engine.ReadJSONFile(g_SettingsDirectory + "population_capacities.json");
if (!json || json.Default === undefined || !json.PopulationCapacities || !Array.isArray(json.PopulationCapacities))
{
error("Could not load population_capacities.json");
return undefined;
}
return json.PopulationCapacities.map(population => ({
"Population": population,
"Default": population == json.Default,
"Title": population < 10000 ? population : translate("Unlimited")
}));
}
/**
* Loads available world population capacities.
*
* @returns {Object[]|undefined} - An array of the world population capacities in the form:
* { "Population": number, "Default": number, "Title": number|String }.
*/
function loadWorldPopulationCapacities()
{
let json = Engine.ReadJSONFile(g_SettingsDirectory + "world_population_capacities.json");
if (!json || json.Default === undefined || !json.WorldPopulationCapacities || !Array.isArray(json.WorldPopulationCapacities))
{
error("Could not load population_capacities.json");
return undefined;
}
return json.WorldPopulationCapacities.map(population => ({
"Population": population,
"Default": population == json.Default,
"Title": population < 10000 ? population : translate("Unlimited")
}));
}
/**
* Creates an object with all values of that property of the given setting and
* finds the index of the default value.
@ -391,21 +345,22 @@ function translateMapSize(tiles)
/**
* Returns title or placeholder.
*
* @param {number} population
* @param {boolean} world - Whether the entry has world population enabled.
* @param {number} popCap
* @param {string} popCapType - "player", "team", or "world"
* @returns {string}
*/
function translatePopulationCapacity(population, world)
function translatePopulationCapacity(popCap, popCapType)
{
if (world)
{
let popCap = g_Settings.WorldPopulationCapacities.find(p => p.Population == population);
return popCap ? popCap.Title + " " + translateWithContext("population capacity addendum", "(world)") :
translateWithContext("population capacity", "Unknown");
}
const popCapTypeData = g_Settings.PopulationCapacities.find(type => type.Name == popCapType);
if (!popCapTypeData)
return translateWithContext("population capacity", "Unknown");
let popCap = g_Settings.PopulationCapacities.find(p => p.Population == population);
return popCap ? popCap.Title : translateWithContext("population capacity", "Unknown");
return popCap >= 10000 ?
translateWithContext("population capacity", "Unlimited") :
sprintf(translate("%(populationCapacity)s (%(populationCapacityType)s)"), {
"populationCapacity": popCap,
"populationCapacityType": popCapType
});
}
/**

View file

@ -29,9 +29,8 @@ var g_GameSettingsLayout = [
"label": translateWithContext("Match settings tab name", "Player"),
"settings": [
"PlayerCount",
"WorldPopulation",
"PopulationCapType",
"PopulationCap",
"WorldPopulationCap",
"StartingResources",
"Spies",
"Cheats"

View file

@ -5,6 +5,8 @@ GameSettingControls.LockedTeams = class LockedTeams extends GameSettingControlCh
super(...args);
g_GameSettings.map.watch(() => this.render(), ["type"]);
g_GameSettings.lockedTeams.watch(() => this.render(), ["available", "enabled"]);
g_GameSettings.population.watch(() => this.render(), ["capType"]);
g_GameSettings.rating.watch(() => this.render(), ["enabled"]);
this.render();
}
@ -15,7 +17,7 @@ GameSettingControls.LockedTeams = class LockedTeams extends GameSettingControlCh
render()
{
this.setEnabled(g_GameSettings.map.type != "scenario" && g_GameSettings.lockedTeams.available);
this.setEnabled(g_GameSettings.lockedTeams.available);
this.setChecked(g_GameSettings.lockedTeams.enabled);
}

View file

@ -1,28 +0,0 @@
GameSettingControls.WorldPopulation = class WorldPopulation extends GameSettingControlCheckbox
{
constructor(...args)
{
super(...args);
g_GameSettings.population.watch(() => this.render(), ["useWorldPop"]);
g_GameSettings.map.watch(() => this.render(), ["type"]);
this.render();
}
render()
{
this.setEnabled(g_GameSettings.map.type != "scenario");
this.setChecked(g_GameSettings.population.useWorldPop);
}
onPress(checked)
{
g_GameSettings.population.setPopCap(checked);
this.gameSettingsController.setNetworkInitAttributes();
}
};
GameSettingControls.WorldPopulation.prototype.TitleCaption =
translate("World Population");
GameSettingControls.WorldPopulation.prototype.Tooltip =
translate("When checked the Population Cap will be evenly distributed over all living players.");

View file

@ -1,66 +1,82 @@
const CAPTYPE_PLAYER_POPULATION = "player";
const CAPTYPE_TEAM_POPULATION = "team";
const CAPTYPE_WORLD_POPULATION = "world";
GameSettingControls.PopulationCap = class PopulationCap extends GameSettingControlDropdown
{
constructor(...args)
{
super(...args);
this.dropdown.list = g_PopulationCapacities.Title;
this.dropdown.list_data = g_PopulationCapacities.Population;
this.sprintfArgs = {};
g_GameSettings.population.watch(() => this.render(), ["useWorldPop", "cap", "perPlayer"]);
g_GameSettings.population.watch(() => this.render(), ["cap", "perPlayer"]);
g_GameSettings.map.watch(() => this.render(), ["type"]);
this.render();
}
render()
{
this.setHidden(g_GameSettings.population.useWorldPop);
this.setEnabled(g_GameSettings.map.type != "scenario" && !g_GameSettings.population.perPlayer);
this.title.caption = g_GameSettings.population.currentData.CapTitle;
if (g_GameSettings.population.perPlayer)
this.label.caption = this.PerPlayerCaption;
else
this.setSelectedValue(g_GameSettings.population.cap);
if (!this.enabled)
return;
this.dropdown.list_data = g_GameSettings.population.currentData.Options.List;
this.dropdown.list = this.dropdown.list_data.map(population =>
population < 10000 ? population : translate("Unlimited")
);
this.setSelectedValue(g_GameSettings.population.cap);
}
onHoverChange()
{
let tooltip = this.Tooltip;
if (this.dropdown.hovered != -1)
{
let popCap = g_PopulationCapacities.Population[this.dropdown.hovered];
let players = g_GameSettings.playerCount.nbPlayers;
if (popCap * players >= this.PopulationCapacityRecommendation)
{
this.sprintfArgs.players = players;
this.sprintfArgs.popCap = popCap;
tooltip = setStringTags(sprintf(this.HoverTooltip, this.sprintfArgs), this.HoverTags);
}
}
if (this.dropdown.hovered == -1)
return;
let tooltip = g_GameSettings.population.currentData.CapTooltip;
if (this.canTotalPopExceedRecommendedMax())
tooltip = setStringTags(this.WarningTooltip, this.WarningTags);
this.dropdown.tooltip = tooltip;
}
canTotalPopExceedRecommendedMax()
{
const popCap = g_GameSettings.population.currentData.Options.List[this.dropdown.hovered];
const nbPlayers = g_GameSettings.playerCount.nbPlayers;
const nbTeams = g_GameSettings.playerTeam.values.reduce((teamList, team) => {
if (!teamList.includes(team) || team == -1)
teamList.push(team);
return teamList;
}, []).length;
switch (g_GameSettings.population.capType)
{
case CAPTYPE_PLAYER_POPULATION: return nbPlayers * popCap > this.PopulationCapacityRecommendation;
case CAPTYPE_TEAM_POPULATION: return nbTeams * popCap > this.PopulationCapacityRecommendation;
case CAPTYPE_WORLD_POPULATION: return popCap > this.PopulationCapacityRecommendation;
default: return false;
}
}
onSelectionChange(itemIdx)
{
g_GameSettings.population.setPopCap(false, g_PopulationCapacities.Population[itemIdx]);
g_GameSettings.population.setPopCap(g_GameSettings.population.currentData.Options.List[itemIdx]);
this.gameSettingsController.setNetworkInitAttributes();
}
};
GameSettingControls.PopulationCap.prototype.TitleCaption =
translate("Population Cap");
GameSettingControls.PopulationCap.prototype.Tooltip =
translate("Select population limit.");
GameSettingControls.PopulationCap.prototype.PerPlayerCaption =
translateWithContext("population limit", "Per Player");
GameSettingControls.PopulationCap.prototype.HoverTooltip =
translate("Warning: There might be performance issues if all %(players)s players reach %(popCap)s population.");
GameSettingControls.PopulationCap.prototype.WarningTooltip =
translate("Warning: These settings can result in significant lag when all players reach their maximum population capacity.");
GameSettingControls.PopulationCap.prototype.HoverTags = {
GameSettingControls.PopulationCap.prototype.WarningTags = {
"color": "orange"
};

View file

@ -0,0 +1,38 @@
GameSettingControls.PopulationCapType = class PopulationCapType extends GameSettingControlDropdown
{
constructor(...args)
{
super(...args);
this.dropdown.list = g_PopulationCapacities.Title;
this.dropdown.list_data = g_PopulationCapacities.Name;
g_GameSettings.population.watch(() => this.render(), ["capType"]);
g_GameSettings.map.watch(() => this.render(), ["type"]);
this.render();
}
render()
{
this.setSelectedValue(g_GameSettings.population.capType);
this.setEnabled(g_GameSettings.map.type != "scenario");
}
onHoverChange()
{
this.dropdown.tooltip = g_PopulationCapacities.Tooltip[this.dropdown.hovered] || this.Tooltip;
}
onSelectionChange(itemIdx)
{
g_GameSettings.population.setPopCapType(g_PopulationCapacities.Name[itemIdx]);
this.gameSettingsController.setNetworkInitAttributes();
}
};
GameSettingControls.PopulationCapType.prototype.TitleCaption =
translate("Population Cap Type");
GameSettingControls.PopulationCapType.prototype.Tooltip =
translate("Select a population capacity type.");

View file

@ -1,62 +0,0 @@
GameSettingControls.WorldPopulationCap = class WorldPopulationCap extends GameSettingControlDropdown
{
constructor(...args)
{
super(...args);
this.dropdown.list = g_WorldPopulationCapacities.Title;
this.dropdown.list_data = g_WorldPopulationCapacities.Population;
this.sprintfArgs = {};
g_GameSettings.population.watch(() => this.render(), ["useWorldPop", "cap"]);
g_GameSettings.map.watch(() => this.render(), ["type"]);
this.render();
}
render()
{
this.setEnabled(g_GameSettings.map.type != "scenario");
this.setHidden(!g_GameSettings.population.useWorldPop);
this.setSelectedValue(g_GameSettings.population.cap);
}
onHoverChange()
{
let tooltip = this.Tooltip;
if (this.dropdown.hovered != -1)
{
let popCap = g_WorldPopulationCapacities.Population[this.dropdown.hovered];
if (popCap >= this.WorldPopulationCapacityRecommendation)
{
this.sprintfArgs.popCap = popCap;
tooltip = setStringTags(sprintf(this.HoverTooltip, this.sprintfArgs), this.HoverTags);
}
}
this.dropdown.tooltip = tooltip;
}
onSelectionChange(itemIdx)
{
g_GameSettings.population.setPopCap(true, g_WorldPopulationCapacities.Population[itemIdx]);
this.gameSettingsController.setNetworkInitAttributes();
}
};
GameSettingControls.WorldPopulationCap.prototype.TitleCaption =
translate("World Population Cap");
GameSettingControls.WorldPopulationCap.prototype.Tooltip =
translate("Select world population limit.");
GameSettingControls.WorldPopulationCap.prototype.HoverTooltip =
translate("Warning: There might be performance issues if %(popCap)s population is reached.");
GameSettingControls.WorldPopulationCap.prototype.HoverTags = {
"color": "orange"
};
/**
* Total number of units that the engine can run with smoothly.
*/
GameSettingControls.WorldPopulationCap.prototype.WorldPopulationCapacityRecommendation = 1200;

View file

@ -25,7 +25,7 @@ class GameDescription
g_GameSettings.mapExploration.watch(update, ["revealed"]);
g_GameSettings.mapExploration.watch(update, ["allied"]);
g_GameSettings.nomad.watch(update, ["enabled"]);
g_GameSettings.population.watch(update, ["perPlayer", "cap", "useWorldPop"]);
g_GameSettings.population.watch(update, ["perPlayer", "cap", "capType"]);
g_GameSettings.rating.watch(update, ["enabled"]);
g_GameSettings.regicideGarrison.watch(update, ["enabled"]);
g_GameSettings.relic.watch(update, ["count", "duration"]);

View file

@ -2,7 +2,6 @@
const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes);
const g_MapTypes = prepareForDropdown(g_Settings && g_Settings.MapTypes);
const g_PopulationCapacities = prepareForDropdown(g_Settings && g_Settings.PopulationCapacities);
const g_WorldPopulationCapacities = prepareForDropdown(g_Settings && g_Settings.WorldPopulationCapacities);
const g_StartingResources = prepareForDropdown(g_Settings && g_Settings.StartingResources);
const g_VictoryConditions = g_Settings && g_Settings.VictoryConditions;

View file

@ -16,7 +16,6 @@ var g_DurationFilterIntervals = [
* Allow to filter by population capacity.
*/
const g_PopulationCapacities = prepareForDropdown(g_Settings && g_Settings.PopulationCapacities);
const g_WorldPopulationCapacities = prepareForDropdown(g_Settings && g_Settings.WorldPopulationCapacities);
/**
* Reloads the selectable values in the filters. The filters depend on g_Settings and g_Replays
@ -97,10 +96,22 @@ function initMapNameFilter(filters)
function initPopCapFilter(filters)
{
var populationFilter = Engine.GetGUIObjectByName("populationFilter");
populationFilter.list = [translateWithContext("population capacity", "Any")].concat(g_PopulationCapacities.Title);
populationFilter.list_data = [""].concat(g_PopulationCapacities.Population);
if (filters && filters.popCap)
// Merge the pop cap options of all pop cap types into one single list.
const popCapOptions = g_PopulationCapacities.Options
.map(item => item.List)
.flat()
.reduce((list, cap) => {
if (!list.includes(cap))
list.push(cap);
return list;
}, [])
.sort((a, b) => a > b);
populationFilter.list = [translateWithContext("population capacity", "Any")].concat(popCapOptions.map(cap => cap >= 10000 ? "Unlimited" : cap));
populationFilter.list_data = [""].concat(popCapOptions);
if (filters?.popCap)
populationFilter.selected = populationFilter.list_data.indexOf(filters.popCap);
if (populationFilter.selected == -1 || populationFilter.selected >= populationFilter.list.length)
@ -191,7 +202,7 @@ function filterReplays()
let sortOrder = Engine.GetGUIObjectByName("replaySelection").selected_column_order;
g_ReplaysFiltered = g_Replays.filter(replay => filterReplay(replay)).sort((a, b) => {
let cmpA, cmpB;
let cmpA, cmpB, cmpA_secondary, cmpB_secondary;
switch (sortKey)
{
case 'months':
@ -217,6 +228,8 @@ function filterReplays()
case 'popCapacity':
cmpA = +a.attribs.settings.PopulationCap;
cmpB = +b.attribs.settings.PopulationCap;
cmpA_secondary = g_PopulationCapacities.Name.indexOf(a.attribs.settings.PopulationCapType);
cmpB_secondary = g_PopulationCapacities.Name.indexOf(b.attribs.settings.PopulationCapType);
break;
}
@ -225,6 +238,12 @@ function filterReplays()
else if (cmpA > cmpB)
return +sortOrder;
else if(cmpA_secondary && cmpB_secondary)
if (cmpA_secondary < cmpB_secondary)
return -sortOrder;
else if(cmpA_secondary > cmpB_secondary)
return +sortOrder;
return 0;
});
}

View file

@ -177,6 +177,12 @@ function sanitizeInitAttributes(attribs)
if (!attribs.settings.PopulationCap)
attribs.settings.PopulationCap = 300;
if (!attribs.settings.PopulationCapType)
attribs.settings.PopulationCapType =
attribs.settings.WorldPopulation ?
"world" :
"player";
if (!attribs.mapType)
attribs.mapType = "skirmish";
@ -228,7 +234,7 @@ function displayReplayList()
return {
"directories": replay.directory,
"months": compatibilityColor(getReplayDateTime(replay), works),
"popCaps": compatibilityColor(translatePopulationCapacity(replay.attribs.settings.PopulationCap, !!replay.attribs.settings.WorldPopulation), works),
"popCaps": compatibilityColor(translatePopulationCapacity(replay.attribs.settings.PopulationCap, replay.attribs.settings.PopulationCapType), works),
"mapNames": compatibilityColor(getReplayMapName(replay), works),
"mapSizes": compatibilityColor(translateMapSize(replay.attribs.settings.Size), works),
"durations": compatibilityColor(getReplayDuration(replay), works),

View file

@ -5,7 +5,6 @@ const g_CivData = loadCivData(false, true);
const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes);
const g_MapTypes = prepareForDropdown(g_Settings && g_Settings.MapTypes);
const g_PopulationCapacities = prepareForDropdown(g_Settings && g_Settings.PopulationCapacities);
const g_WorldPopulationCapacities = prepareForDropdown(g_Settings && g_Settings.WorldPopulationCapacities);
const g_StartingResources = prepareForDropdown(g_Settings && g_Settings.StartingResources);
const g_VictoryConditions = g_Settings && g_Settings.VictoryConditions;

View file

@ -81,6 +81,7 @@ Diplomacy.prototype.ChangeTeam = function(team)
if (this.team !== -1)
warn("A change in teams is requested while the player already had a team, previous alliances are maintained.");
const oldTeam = this.team;
this.team = team;
if (this.team !== -1)
@ -97,9 +98,10 @@ Diplomacy.prototype.ChangeTeam = function(team)
}
}
Engine.BroadcastMessage(MT_DiplomacyChanged, {
Engine.BroadcastMessage(MT_TeamChanged, {
"player": playerID,
"otherPlayer": null
"oldTeam": oldTeam,
"newTeam": team
});
};

View file

@ -162,7 +162,9 @@ GuiInterface.prototype.GetSimulationState = function()
ret.victoryConditions = cmpEndGameManager.GetVictoryConditions();
ret.alliedVictory = cmpEndGameManager.GetAlliedVictory();
ret.maxWorldPopulation = cmpPlayerManager.GetMaxWorldPopulation();
let cmpPopulationCapManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PopulationCapManager);
ret.populationCapType = cmpPopulationCapManager.GetPopulationCapType();
ret.populationCap = cmpPopulationCapManager.GetPopulationCap();
for (let i = 0; i < numPlayers; ++i)
{

View file

@ -152,35 +152,4 @@ PlayerManager.prototype.RemoveLastPlayer = function()
Engine.DestroyEntity(lastId);
};
PlayerManager.prototype.SetMaxWorldPopulation = function(max)
{
this.maxWorldPopulation = max;
this.RedistributeWorldPopulation();
};
PlayerManager.prototype.GetMaxWorldPopulation = function()
{
return this.maxWorldPopulation;
};
PlayerManager.prototype.RedistributeWorldPopulation = function()
{
const worldPopulation = this.GetMaxWorldPopulation();
if (!worldPopulation)
return;
const activePlayers = this.GetActivePlayers();
if (!activePlayers.length)
return;
const newMaxPopulation = worldPopulation / activePlayers.length;
for (const playerID of activePlayers)
Engine.QueryInterface(this.GetPlayerByID(playerID), IID_Player).SetMaxPopulation(newMaxPopulation);
};
PlayerManager.prototype.OnGlobalPlayerDefeated = function(msg)
{
this.RedistributeWorldPopulation();
};
Engine.RegisterSystemComponentType(IID_PlayerManager, "PlayerManager", PlayerManager);

View file

@ -0,0 +1,184 @@
const CAPTYPE_PLAYER_POPULATION = "player";
const CAPTYPE_TEAM_POPULATION = "team";
const CAPTYPE_WORLD_POPULATION = "world";
function PopulationCapManager() {}
PopulationCapManager.prototype.Schema =
"<a:component type='system'/><empty/>";
PopulationCapManager.prototype.Init = function()
{
};
/**
* Set the pop cap type and, if possible, initialize the first distribution.
* @param {string} type
*/
PopulationCapManager.prototype.SetPopulationCapType = function(type)
{
if ([CAPTYPE_PLAYER_POPULATION, CAPTYPE_TEAM_POPULATION, CAPTYPE_WORLD_POPULATION].includes(type))
this.popCapType = type;
else
{
warn(`Attempted to set an unknown population capacity type: '${type}'. Continuing with type 'Player Population'...`);
this.popCapType = CAPTYPE_PLAYER_POPULATION;
}
if (this.popCap)
this.InitializePopCaps();
};
/**
* Get the current pop cap type.
* @returns {string}
*/
PopulationCapManager.prototype.GetPopulationCapType = function()
{
return this.popCapType;
};
/**
* Set the pop cap and, if possible, initialize the first distribution.
* @param {number} cap
*/
PopulationCapManager.prototype.SetPopulationCap = function(cap)
{
this.popCap = cap;
if (this.popCapType)
this.InitializePopCaps();
};
/**
* Get the current pop cap.
* @returns {number}
*/
PopulationCapManager.prototype.GetPopulationCap = function()
{
return this.popCap;
};
/**
* Calculate and distribute the pop caps for the first time. Called as soon as cap and cap type are set.
*/
PopulationCapManager.prototype.InitializePopCaps = function()
{
switch(this.popCapType)
{
case CAPTYPE_PLAYER_POPULATION:
this.InitializePlayerPopCaps();
break;
case CAPTYPE_TEAM_POPULATION:
this.InitializeTeamPopCaps();
break;
case CAPTYPE_WORLD_POPULATION:
this.RedistributeWorldPopCap();
break;
default:
break;
}
};
/**
* Assign all players the same, fixed pop cap.
*/
PopulationCapManager.prototype.InitializePlayerPopCaps = function()
{
const players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetActivePlayers();
for (const player of players)
QueryPlayerIDInterface(player, IID_Player)
.SetMaxPopulation(this.popCap);
};
/**
* Loop through all teams and distribute the fixed pop cap among their living members.
*/
PopulationCapManager.prototype.InitializeTeamPopCaps = function()
{
const players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetActivePlayers();
let processedTeams = [];
for (const player of players)
{
const team = QueryPlayerIDInterface(player, IID_Diplomacy).GetTeam();
if (processedTeams.includes(team))
continue;
processedTeams.push(team);
this.RedistributeTeamPopCap(team);
}
};
/**
* Recalculate and update a single team's members' pop caps.
* @param {number} team - ID specifying the team.
*/
PopulationCapManager.prototype.RedistributeTeamPopCap = function(team)
{
const activePlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetActivePlayers();
const teamMembers = activePlayers.reduce((list, player) => {
if (QueryPlayerIDInterface(player, IID_Diplomacy).GetTeam() === team)
list.push(player);
return list;
}, []);
// Players of team -1 aren't part of any team and need to be assigned the full team pop cap.
const newPopulationCap = team === -1 ? this.popCap : Math.round(this.popCap / teamMembers.length);
for (const teamMember of teamMembers)
QueryPlayerIDInterface(teamMember, IID_Player)
.SetMaxPopulation(newPopulationCap);
};
/**
* Recalculate the players' new pop cap and assign it to all of them.
*/
PopulationCapManager.prototype.RedistributeWorldPopCap = function()
{
const activePlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetActivePlayers();
if (!activePlayers.length)
return;
const newPopulationCap = Math.round(this.popCap / activePlayers.length);
for (const player of activePlayers)
QueryPlayerIDInterface(player, IID_Player).SetMaxPopulation(newPopulationCap);
};
/**
* Redistribute the pop caps depending on the pop cap type.
* @param {number} msg.playerId - the defeated player's ID.
*/
PopulationCapManager.prototype.OnGlobalPlayerDefeated = function(msg)
{
switch(this.popCapType)
{
case CAPTYPE_TEAM_POPULATION:
const team = QueryPlayerIDInterface(msg.playerId, IID_Diplomacy).GetTeam();
if (team != -1)
this.RedistributeTeamPopCap(team);
break;
case CAPTYPE_WORLD_POPULATION:
this.RedistributeWorldPopCap();
break;
case CAPTYPE_PLAYER_POPULATION:
default: break;
}
};
/**
* Redistribute pop caps when a player is moved from one team to another.
* @param {number} msg.player - the ID of the player.
* @param {number} msg.oldTeam - the ID of the team the player was previously part of.
* @param {number} msg.newTeam - the ID of the team the player is moved to.
*/
PopulationCapManager.prototype.OnTeamChanged = function(msg)
{
if (this.popCapType != CAPTYPE_TEAM_POPULATION)
return;
this.RedistributeTeamPopCap(msg.oldTeam);
this.RedistributeTeamPopCap(msg.newTeam);
};
Engine.RegisterSystemComponentType(IID_PopulationCapManager, "PopulationCapManager", PopulationCapManager);

View file

@ -5,3 +5,9 @@ Engine.RegisterInterface("Diplomacy");
* sent from Diplomacy component when diplomacy changed for one player or between two players.
*/
Engine.RegisterMessageType("DiplomacyChanged");
/**
* Message of the form { "player": number, "oldTeam": number, "newTeam", number }
* sent from the Diplomacy component when a player switches teams.
*/
Engine.RegisterMessageType("TeamChanged");

View file

@ -0,0 +1 @@
Engine.RegisterInterface("PopulationCapManager");

View file

@ -22,6 +22,7 @@ Engine.LoadComponentScript("interfaces/Loot.js");
Engine.LoadComponentScript("interfaces/Market.js");
Engine.LoadComponentScript("interfaces/Pack.js");
Engine.LoadComponentScript("interfaces/Population.js");
Engine.LoadComponentScript("interfaces/PopulationCapManager.js");
Engine.LoadComponentScript("interfaces/ProductionQueue.js");
Engine.LoadComponentScript("interfaces/Promotion.js");
Engine.LoadComponentScript("interfaces/Repairable.js");
@ -95,6 +96,11 @@ AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"GetTemplate": function(name) { return ""; }
});
AddMock(SYSTEM_ENTITY, IID_PopulationCapManager, {
"GetPopulationCapType": function() { return "player"; },
"GetPopulationCap": function() { return 200; }
});
AddMock(SYSTEM_ENTITY, IID_Timer, {
"GetTime": function() { return 0; },
"SetTimeout": function(ent, iid, funcname, time, data) { return 0; }
@ -404,7 +410,8 @@ TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), {
"timeElapsed": 0,
"victoryConditions": ["conquest", "wonder"],
"alliedVictory": false,
"maxWorldPopulation": undefined
"populationCapType": "player",
"populationCap": 200
});
TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedSimulationState(), {
@ -562,7 +569,8 @@ TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedSimulationState(), {
"timeElapsed": 0,
"victoryConditions": ["conquest", "wonder"],
"alliedVictory": false,
"maxWorldPopulation": undefined
"populationCapType": "player",
"populationCap": 200
});

View file

@ -0,0 +1,91 @@
Engine.LoadHelperScript("Player.js");
Engine.LoadComponentScript("interfaces/PlayerManager.js");
Engine.LoadComponentScript("interfaces/Diplomacy.js");
Engine.LoadComponentScript("interfaces/PopulationCapManager.js");
Engine.LoadComponentScript("PopulationCapManager.js");
const CAPTYPE_PLAYER_POPULATION = "player";
const CAPTYPE_TEAM_POPULATION = "team";
const CAPTYPE_WORLD_POPULATION = "world";
const cmpPopulationCapManager = ConstructComponent(SYSTEM_ENTITY, "PopulationCapManager");
const playerData = [
{
"team": -1,
"state": "active"
},
{
"team": -1,
"state": "active"
},
{
"team": -1,
"state": "active"
},
{
"team": 0,
"state": "active"
},
{
"team": 1,
"state": "active"
},
{
"team": 1,
"state": "active"
},
{
"team": 1,
"state": "active"
},
{
"team": 2,
"state": "active"
},
{
"team": 2,
"state": "active"
}
];
let currentPopCaps = [];
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetNonGaiaPlayers": () => { return Object.keys(playerData).slice(1); },
"GetActivePlayers": () => { return Object.keys(playerData.filter(player => player.state == "active")).slice(1); },
"GetPlayerByID": (id) => id
});
for (const playerID in Object.keys(playerData))
{
AddMock(playerID, IID_Player, {
"SetMaxPopulation": (val) => { currentPopCaps[playerID] = Math.round(val); }
});
AddMock(playerID, IID_Diplomacy, {
"GetTeam": () => { return playerData[playerID].team; }
});
}
cmpPopulationCapManager.SetPopulationCap(400);
cmpPopulationCapManager.SetPopulationCapType(CAPTYPE_PLAYER_POPULATION);
TS_ASSERT_UNEVAL_EQUALS(currentPopCaps, [, 400, 400, 400, 400, 400, 400, 400, 400]);
cmpPopulationCapManager.SetPopulationCapType(CAPTYPE_TEAM_POPULATION);
TS_ASSERT_UNEVAL_EQUALS(currentPopCaps, [, 400, 400, 400, 133, 133, 133, 200, 200]);
playerData[6].team = 2;
cmpPopulationCapManager.OnTeamChanged({ "player": 6, "oldTeam": 1, "newTeam": 2 });
TS_ASSERT_UNEVAL_EQUALS(currentPopCaps, [, 400, 400, 400, 200, 200, 133, 133, 133]);
playerData[8].state = "defeated";
currentPopCaps.pop();
cmpPopulationCapManager.OnGlobalPlayerDefeated({ "playerId": 8 });
TS_ASSERT_UNEVAL_EQUALS(currentPopCaps, [, 400, 400, 400, 200, 200, 200, 200]);
cmpPopulationCapManager.SetPopulationCapType(CAPTYPE_WORLD_POPULATION);
TS_ASSERT_UNEVAL_EQUALS(currentPopCaps, [, 57, 57, 57, 57, 57, 57, 57]);
playerData[7].state = "defeated";
currentPopCaps.pop();
cmpPopulationCapManager.OnGlobalPlayerDefeated({ "playerId": 7 });
TS_ASSERT_UNEVAL_EQUALS(currentPopCaps, [, 67, 67, 67, 67, 67, 67]);

View file

@ -1,4 +1,43 @@
{
"PopulationCapacities": [50, 100, 150, 200, 250, 300, 10000],
"Default": 300
"TranslatedKeys": ["Title", "Tooltip", "CapTitle", "CapTooltip", "Appendage"],
"Data":
[
{
"Name": "player",
"Title": "Player Population",
"CapTitle": "Player Population Cap",
"Tooltip": "Locked population cap for all players.",
"CapTooltip": "Choose the player population cap.",
"Appendage": "per player",
"Default": true,
"Options": {
"List": [50, 100, 150, 200, 250, 300, 10000],
"Default": 300
}
},
{
"Name": "team",
"Title": "Team Population",
"CapTitle": "Team Population Cap",
"Tooltip": "Distributes a team's total population cap evenly over all its living members. Enables the setting 'Locked teams'.",
"CapTooltip": "Choose the team population cap.",
"Appendage": "per team",
"Options": {
"List": [100, 150, 200, 250, 300, 350, 400, 450, 500, 550, 600, 700, 800, 1000, 10000],
"Default": 400
}
},
{
"Name": "world",
"Title": "World Population",
"CapTitle": "World Population Cap",
"Tooltip": "Distributes the total population cap evenly over all living players.",
"CapTooltip": "Choose the world population cap.",
"Appendage": "globally",
"Options": {
"List": [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1600, 2000, 2400, 10000],
"Default": 600
}
}
]
}

View file

@ -1,4 +0,0 @@
{
"WorldPopulationCapacities": [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1600, 2000, 2400, 63000],
"Default": 600
}

View file

@ -12,6 +12,7 @@ function Cheat(input)
return;
const cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
const cmpPopulationManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PopulationCapManager);
switch(input.action)
{
@ -24,7 +25,7 @@ function Cheat(input)
Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).SetLosRevealAll(-1, true);
return;
case "maxpopulation":
cmpPlayer.SetPopulationBonuses((cmpPlayerManager.GetMaxWorldPopulation() || cmpPlayer.GetMaxPopulation()) + 500);
cmpPlayer.SetPopulationBonuses(cmpPopulationManager.GetPopulationCap() + 500);
return;
case "changemaxpopulation":
{

View file

@ -53,14 +53,13 @@ function InitGame(settings)
cmpPlayer.SetAI(true);
}
if (settings.PopulationCap)
cmpPlayer.SetMaxPopulation(settings.PopulationCap);
if (settings.AllyView)
Engine.QueryInterface(cmpPlayer.entity, IID_TechnologyManager)?.ResearchTechnology(Engine.QueryInterface(cmpPlayer.entity, IID_Diplomacy).template.SharedLosTech);
}
if (settings.WorldPopulationCap)
Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).SetMaxWorldPopulation(settings.WorldPopulationCap);
const cmpPopulationCapManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PopulationCapManager);
cmpPopulationCapManager.SetPopulationCapType(settings.PopulationCapType);
cmpPopulationCapManager.SetPopulationCap(settings.PopulationCap);
// Update the grid with all entities created for the map init.
Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).UpdateGrid();

View file

@ -69,21 +69,6 @@ function LoadPlayerSettings(settings, newPlayers)
if (i == 0)
continue;
// PopulationLimit
{
const maxPopulation =
settings.PlayerData[i].PopulationLimit !== undefined ?
settings.PlayerData[i].PopulationLimit :
settings.PopulationCap !== undefined ?
settings.PopulationCap :
playerDefaults[i].PopulationLimit !== undefined ?
playerDefaults[i].PopulationLimit :
undefined;
if (maxPopulation !== undefined)
cmpPlayer.SetMaxPopulation(maxPopulation);
}
// StartingResources
if (settings.PlayerData[i].Resources !== undefined)
cmpPlayer.SetResourceCounts(settings.PlayerData[i].Resources);