1
0
forked from 0ad/0ad

Gamesetup class rewrite, fixes #5322, refs #5387.

* Decouples settings logically which in turn allows fixing many problems
arising from previous coupling.
* Fixes the persist-match-settings feature, refs #2963, refs #3049.
* Improves performance of the matchsetup by rebuilding GUI objects only
when necessary.

Provides groundwork for:
* UI to control per-player handicap, such as StartingResources,
PopulationCap, StartingTechnologies, DisabledTechnologies,
DisabledTemplates, ..., refs #812.
* Map specific settings (onMapChange event), refs #4838.
* Chat notifications announcing which settings changed, refs D1195,
* Multiple controllers setting up the game (since setting types can
check for permissions in onUpdateGameAttributes without the need for a
new data model or a second gamesetup data network message type), refs
#3806, subsequently dedicated server, refs #3556.
* MapBrowser (MapCache, MapTypes, onUpdateGameAttributes interface),
refs D1703 and D1777,
* Multiplayer saved games (decoupling and setting dependent unique
logic), refs #1088.
Refs
https://wildfiregames.com/forum/index.php?/topic/20787-paid-development-2016/
https://wildfiregames.com/forum/index.php?/topic/20789-paid-development-2016/

Enable maps to restrict setting values:
* If a map specifies an AI or Civs for a playerslot, the controller
can't assign a player/other AI or Civ to that slot, refs #3049, #3013.

Fix per player StartingResources, PopulationCap, StartingTechnologies,
DisabledTechnologies, DisabledTemplates following 9177683653, refs #812,
fixes #4504. Use this for DisabledTechnologies on Polar Sea.

Persist user settings for Skirmish maps:
* All user chosen settings are persisted when changing the selected map
or maptype,
  except where the selected map overwrites the setting value and
  except for Scenario maps which still use the default value where the
map doesn't specify the setting value.

* Tickets relating to that Skirmish mapchange user setting persistance:
 - Selecting a map doesn't change the selected civilizations, fixes
#3120 (together with 7cf83f19fd removing map specified Civs).
 - Selecting a map type doesn't reset the selected settings, fixes
#5372.
 - Selecting a map doesn't change the selected victory conditions,
unless the map specifies those, refs #4661, #3209. (Atlas still writes
VictoryConditions to every map.)
 - Consume the player color palette from Skirmish maps, refs 4996d28110
/ #1580. Preserve the selected playercolors when switching the
Skirmish/Random map by chosing the most similar colors if the map comes
with a different palette.

Rated games:
* Hide and disable Rated game setting unless there are exactly two
players, fixes #3950, supersedes D2117.
* Display conspicuous warning if the game is rated, so players are
perfectly aware.

Autostarted games:
* Allow using the gamesetup page to autostart matches with arbitrary
maps, not only this one tutorial, as reported in D194 and 15e2b42525,
refs D11.

Networking:
* Keep gamesetup page open after disconnect, allowing players to read
chat messages indicating why the host stopped the server, fixes #4114.
* The message subscription system allows new and mod settings to run
custom logic on arbitrary setting changes (most importantly on map
change).
  This removes hardcoded logic restrictions from the gamesetup option
unification rewrite in b4e5858f6d/D322, refs #3994,
  such as the hardcoding of setting references in selectMap to biomes
from f2550705d3/D852 and the difficulty from 9daa7520ef/D1189,
RelicDuration, WonderDuration, LastManStanding, RegicideGarrison,
TriggerScripts, CircularMap, Garrison, DisabledTemplates.

Checkboxes:
* Display values of disabled checkboxes with Yes/No labels, fixes D2349,
reviewed by nani.

Clean g_GameAttributes of invalid values and gamesetup GUI temporaries,
refs #3049, #3883:
* Delete useless values:
 - VictoryScripts, because they are redundant with TriggerScripts,
introduced in 8915037631.
 - mapType which was written twice to g_GameAttributes following
9177683653
 - Description, Keywords, Preview since that doesn't impact simulation
and can be loaded from the MapCache
 - mapFilter, mapPath, SupportedBiomes, SupportedTriggerDifficulties
since they are only used in the gamesetup
* Delete conditional values if the condition is not met:
 - AIDiff, AIBehavior if there is no AI in that slot
 - Nomad and Size if the maptype is not Random
 - Biome, TriggerDifficulty if the map doesn't support that
 - WonderDuration, RegicideGarrison, RelicCount, RelicDuration if the
according VictoryConditions are not enabled
 - LastManStanding if TeamsLocked
 - Rating if there are more than 2 players

MapCache:
* Refactor to MapCache class, store maps of all types and use it in the
replaymenu, lobby and session as well.

SettingTabsPanel:
* Remove hardcodings and coupling of the SettingTabsPanel with
biomes/difficulties/chat UI from D1027/ac7b5ce861.

GamesetupPage.xml:
* Restructure the page to use hierarchical object organization
(topPanel, centerPanel, centerLeftPanel, bottomPanel, centerCenterPanel,
centerRightPanel, bottomLeftPanel, bottomRightPanel), allowing to
deduplicate object position margins and size math and ease navigation.

New defaults:
* Check LockedTeams default in multiplayer (not only rated games).
* Persist the rated game setting instead of defaulting to true when
restarting a match, which often lead to unintentional rated games when
rehosting.
* 60 FPS in menus since they are animated

Autocomplete sorting fixed (playernames should be completed first).
Refactoring encompasses the one proposed in Polakrity and bb D1651.

Differential Revision: https://code.wildfiregames.com/D2483
Tested by: nani
Discussed with:
* nani for blackbox testing, code architecture, performance and
MapBrowser in PMs on 2019-12-19, 2019-12-31, 2020-01-06
* Angen for the simulation diff on
http://irclogs.wildfiregames.com/2020-01/2020-01-03-QuakeNet-%230ad-dev.log
* bb on SettingsTabPanel on
http://irclogs.wildfiregames.com/2020-01/2020-01-05-QuakeNet-%230ad-dev.log
* Imarok on data model and revised multi-controller plans for #3806 on
http://irclogs.wildfiregames.com/2020-01/2020-01-07-QuakeNet-%230ad-dev.log
Emojis by: asterix, Imarok, fpre, nani, Krinkle, Stan, Angen, Freagarach
This was SVN commit r23374.
This commit is contained in:
elexis 2020-01-11 20:14:17 +00:00
parent 6159797710
commit 34138a7764
120 changed files with 6434 additions and 3113 deletions

View File

@ -133,7 +133,7 @@ skycolor = "0 0 0"
[adaptivefps]
session = 60 ; Throttle FPS in running games (prevents 100% CPU workload).
menu = 30 ; Throttle FPS in menus only.
menu = 60 ; Throttle FPS in menus only.
[hotkey]
; Each one of the specified keys will trigger the action on the left

View File

@ -522,7 +522,7 @@
- Misc. -
==========================================
-->
<sprite name = "ModernWindowCornerBottomRight">
<sprite name = "ModernDarkBoxOpaque">
<!-- background -->
<image backcolor = "12 12 12"/>
<image texture = "global/modern/background.png"
@ -532,14 +532,8 @@
<!-- shading -->
<image texture = "global/modern/shadow-high.png"
texture_size = "0 0 1024 256"
size = "0 100%-268 100%-12 100%-12"
/>
<!-- bottom edge -->
<image texture = "global/modern/border.png"
real_texture_placement = "0 0 2048 8"
size = "0 100%-16 100%-10 100%-8"
texture_size = "0 0 1024 128"
size = "0 100%-140 100%-12 100%-12"
/>
</sprite>
<sprite name = "ModernDropDownArrow">

View File

@ -33,16 +33,18 @@ function init(settings)
// Remember the player ID that we change the AI settings for
g_PlayerSlot = settings.playerSlot;
let enabled = g_IsController && !settings.fixed;
for (let name in g_AIControls)
{
let control = Engine.GetGUIObjectByName(name);
control.list = g_AIControls[name].labels;
control.selected = g_AIControls[name].selected(settings);
control.hidden = !g_IsController;
control.hidden = !enabled;
let label = Engine.GetGUIObjectByName(name + "Text");
label.caption = control.list[control.selected];
label.hidden = g_IsController;
label.hidden = enabled;
}
checkBehavior();

View File

@ -0,0 +1,100 @@
/**
* This class obtains, caches and provides the gamesettings from map XML and JSON files.
*/
class MapCache
{
constructor()
{
this.cache = {};
}
getMapData(mapType, mapPath)
{
if (!mapPath || mapPath == "random")
return undefined;
if (!this.cache[mapPath])
{
let mapData = g_Settings.MapTypes.find(type => type.Name == mapType).GetData(mapPath);
// Remove gaia, TODO: Maps should be consistent
if (mapData &&
mapData.settings &&
mapData.settings.PlayerData &&
mapData.settings.PlayerData.length &&
!mapData.settings.PlayerData[0])
{
mapData.settings.PlayerData.shift();
}
this.cache[mapPath] = mapData;
}
return this.cache[mapPath];
}
/**
* Doesn't translate, so that lobby page viewers can do that locally.
* The result is to be used with translateMapName.
*/
getTranslatableMapName(mapType, mapPath)
{
if (mapPath == "random")
return "random";
let mapData = this.getMapData(mapType, mapPath);
return mapData && mapData.settings && mapData.settings.Name || undefined;
}
translateMapName(mapName)
{
return mapName == "random" ?
translateWithContext("map selection", "Random") :
mapName ? translate(mapName) : "";
}
getTranslatedMapDescription(mapType, mapPath)
{
if (mapPath == "random")
return translate("A randomly selected map.");
let mapData = this.getMapData(mapType, mapPath);
return mapData && mapData.settings && translate(mapData.settings.Description) || "";
}
getMapPreview(mapType, mapPath, gameAttributes = undefined)
{
let mapData = this.getMapData(mapType, mapPath);
let biomePreviewFile =
basename(mapPath) + "_" +
basename(gameAttributes && gameAttributes.settings.Biome || "") + ".png";
let biomePreview = Engine.TextureExists(
this.TexturesPath + this.PreviewsPath + biomePreviewFile) && biomePreviewFile;
let filename =
biomePreview ?
biomePreview :
mapData && mapData.settings && mapData.settings.Preview ?
mapData.settings.Preview :
this.DefaultPreview;
return "cropped:" + this.PreviewWidth + "," + this.PreviewHeight + ":" + this.PreviewsPath + filename;
}
}
MapCache.prototype.TexturesPath =
"art/textures/ui/";
MapCache.prototype.PreviewsPath =
"session/icons/mappreview/";
MapCache.prototype.DefaultPreview =
"nopreview.png";
MapCache.prototype.PreviewWidth =
400 / 512;
MapCache.prototype.PreviewHeight =
300 / 512;

View File

@ -114,11 +114,6 @@ function stringifiedTeamListToPlayerData(stringifiedTeamList)
return playerData;
}
function translateMapTitle(mapTitle)
{
return mapTitle == "random" ? translateWithContext("map selection", "Random") : translate(mapTitle);
}
function removeDupes(array)
{
// loop backwards to make splice operations cheaper

View File

@ -34,55 +34,6 @@ var g_Buddies = Engine.ConfigDB_GetValue("user", "lobby.buddies").split(g_BuddyL
*/
var g_BuddySymbol = '•';
var g_MapPreviewPath = "session/icons/mappreview/";
/**
* Returns the biome specific mappreview image if it exists, or empty string otherwise.
*/
function getBiomePreview(mapName, biomeName)
{
let biomePreview = basename(mapName) + "_" + basename(biomeName) + ".png";
if (Engine.TextureExists("art/textures/ui/" + g_MapPreviewPath + biomePreview))
return biomePreview;
return "";
}
/**
* Returns map description and preview image or placeholder.
*/
function getMapDescriptionAndPreview(mapType, mapName, gameAttributes = undefined)
{
let mapData;
if (mapType == "random" && mapName == "random")
mapData = { "settings": { "Description": translate("A randomly selected map.") } };
else if (mapType == "random" && Engine.FileExists(mapName + ".json"))
mapData = Engine.ReadJSONFile(mapName + ".json");
else if (Engine.FileExists(mapName + ".xml"))
mapData = Engine.LoadMapSettings(mapName + ".xml");
let biomePreview = getBiomePreview(mapName, gameAttributes && gameAttributes.settings.Biome || "");
return deepfreeze({
"description": mapData && mapData.settings && mapData.settings.Description ? translate(mapData.settings.Description) : translate("Sorry, no description available."),
"preview": biomePreview ? biomePreview :
mapData && mapData.settings && mapData.settings.Preview ? mapData.settings.Preview : "nopreview.png"
});
}
/**
* Sets the mappreview image correctly.
* It needs to be cropped as the engine only allows loading square textures.
*
* @param {string} filename
*/
function getMapPreviewImage(filename)
{
return "cropped:" + 400 / 512 + "," + 300 / 512 + ":" +
g_MapPreviewPath + filename;
}
/**
* Returns a formatted string describing the player assignments.
* Needs g_CivData to translate!
@ -229,7 +180,7 @@ function formatPlayerInfo(playerDataArray, playerStates)
*
* Requires g_GameAttributes and g_VictoryConditions.
*/
function getGameDescription()
function getGameDescription(mapCache)
{
let titles = [];
if (!g_GameAttributes.settings.VictoryConditions.length)
@ -341,13 +292,13 @@ function getGameDescription()
{
titles.push({
"label": translate("Map Name"),
"value": translate(g_GameAttributes.settings.Name)
"value": mapCache.translateMapName(
mapCache.getTranslatableMapName(g_GameAttributes.mapType, g_GameAttributes.map, g_GameAttributes))
});
titles.push({
"label": translate("Map Description"),
"value": g_GameAttributes.settings.Description ?
translate(g_GameAttributes.settings.Description) :
translate("Sorry, no description available.")
"value": mapCache.getTranslatedMapDescription(g_GameAttributes.mapType, g_GameAttributes.map)
});
}
@ -356,12 +307,6 @@ function getGameDescription()
"value": g_MapTypes.Title[g_MapTypes.Name.indexOf(g_GameAttributes.mapType)]
});
if (typeof g_MapFilterList !== "undefined")
titles.push({
"label": translate("Map Filter"),
"value": g_MapFilterList.name[g_MapFilterList.id.findIndex(id => id == g_GameAttributes.mapFilter)]
});
if (g_GameAttributes.mapType == "random")
{
let mapSize = g_MapSizes.Name[g_MapSizes.Tiles.indexOf(g_GameAttributes.settings.Size)];
@ -390,32 +335,42 @@ function getGameDescription()
});
}
titles.push({
"label": g_GameAttributes.settings.Nomad ? translate("Nomad Mode") : translate("Civic Centers"),
"value":
g_GameAttributes.settings.Nomad ?
translate("Players start with only few units and have to find a suitable place to build their city.") :
translate("Players start with a Civic Center.")
});
if (g_GameAttributes.settings.Nomad !== undefined)
titles.push({
"label": g_GameAttributes.settings.Nomad ? translate("Nomad Mode") : translate("Civic Centers"),
"value":
g_GameAttributes.settings.Nomad ?
translate("Players start with only few units and have to find a suitable place to build their city.") :
translate("Players start with a Civic Center.")
});
titles.push({
"label": translate("Starting Resources"),
"value": sprintf(translate("%(startingResourcesTitle)s (%(amount)s)"), {
"startingResourcesTitle":
g_StartingResources.Title[
g_StartingResources.Resources.indexOf(
g_GameAttributes.settings.StartingResources)],
"amount": g_GameAttributes.settings.StartingResources
})
});
if (g_GameAttributes.settings.StartingResources !== undefined)
titles.push({
"label": translate("Starting Resources"),
"value":
g_GameAttributes.settings.PlayerData &&
g_GameAttributes.settings.PlayerData.some(pData => pData && pData.Resources !== undefined) ?
translateWithContext("starting resources", "Per Player") :
sprintf(translate("%(startingResourcesTitle)s (%(amount)s)"), {
"startingResourcesTitle":
g_StartingResources.Title[
g_StartingResources.Resources.indexOf(
g_GameAttributes.settings.StartingResources)],
"amount": g_GameAttributes.settings.StartingResources
})
});
titles.push({
"label": translate("Population Limit"),
"value":
g_PopulationCapacities.Title[
g_PopulationCapacities.Population.indexOf(
g_GameAttributes.settings.PopulationCap)]
});
if (g_GameAttributes.settings.PopulationCap !== undefined)
titles.push({
"label": translate("Population Limit"),
"value":
g_GameAttributes.settings.PlayerData &&
g_GameAttributes.settings.PlayerData.some(pData => pData && pData.PopulationLimit !== undefined) ?
translateWithContext("population limit", "Per Player") :
g_PopulationCapacities.Title[
g_PopulationCapacities.Population.indexOf(
g_GameAttributes.settings.PopulationCap)]
});
titles.push({
"label": translate("Treasures"),

View File

@ -209,8 +209,6 @@ function loadCeasefire()
/**
* Hardcoded, as modding is not supported without major changes.
*
* @returns {Array}
*/
function loadMapTypes()
{
@ -219,17 +217,26 @@ function loadMapTypes()
"Name": "skirmish",
"Title": translateWithContext("map", "Skirmish"),
"Description": translate("A map with a predefined landscape and number of players. Freely select the other gamesettings."),
"Default": true
"Default": true,
"Path": "maps/skirmishes/",
"Suffix": ".xml",
"GetData": Engine.LoadMapSettings
},
{
"Name": "random",
"Title": translateWithContext("map", "Random"),
"Description": translate("Create a unique map with a different resource distribution each time. Freely select the number of players and teams.")
"Description": translate("Create a unique map with a different resource distribution each time. Freely select the number of players and teams."),
"Path": "maps/random/",
"Suffix": ".json",
"GetData": mapPath => Engine.ReadJSONFile(mapPath + ".json")
},
{
"Name": "scenario",
"Title": translateWithContext("map", "Scenario"),
"Description": translate("A map with a predefined landscape and matchsettings.")
"Description": translate("A map with a predefined landscape and matchsettings."),
"Path": "maps/scenarios/",
"Suffix": ".xml",
"GetData": Engine.LoadMapSettings
}
];
}

View File

@ -0,0 +1,269 @@
/**
* This class provides a property independent interface to g_GameAttributes events.
* Classes may use this interface in order to react to changing g_GameAttributes.
*/
class GameSettingsControl
{
constructor(gamesetupPage, netMessages, startGameControl, mapCache)
{
this.startGameControl = startGameControl;
this.mapCache = mapCache;
this.gameSettingsFile = new GameSettingsFile(gamesetupPage);
this.previousMap = undefined;
this.depth = 0;
// This property may be read from publicly
this.autostart = false;
this.gameAttributesChangeHandlers = new Set();
this.gameAttributesBatchChangeHandlers = new Set();
this.gameAttributesFinalizeHandlers = new Set();
this.pickRandomItemsHandlers = new Set();
this.assignPlayerHandlers = new Set();
this.mapChangeHandlers = new Set();
gamesetupPage.registerLoadHandler(this.onLoad.bind(this));
gamesetupPage.registerGetHotloadDataHandler(this.onGetHotloadData.bind(this));
startGameControl.registerLaunchGameHandler(this.onLaunchGame.bind(this));
if (g_IsNetworked)
netMessages.registerNetMessageHandler("gamesetup", this.onGamesetupMessage.bind(this));
}
registerMapChangeHandler(handler)
{
this.mapChangeHandlers.add(handler);
}
unregisterMapChangeHandler(handler)
{
this.mapChangeHandlers.delete(handler);
}
/**
* This message is triggered everytime g_GameAttributes change.
* Handlers may subsequently change g_GameAttributes and trigger this message again.
*/
registerGameAttributesChangeHandler(handler)
{
this.gameAttributesChangeHandlers.add(handler);
}
unregisterGameAttributesChangeHandler(handler)
{
this.gameAttributesChangeHandlers.delete(handler);
}
/**
* This message is triggered after g_GameAttributes changed and recursed gameAttributesChangeHandlers finished.
* The use case for this is to update GUI objects which do not change g_GameAttributes but only display the attributes.
*/
registerGameAttributesBatchChangeHandler(handler)
{
this.gameAttributesBatchChangeHandlers.add(handler);
}
unregisterGameAttributesBatchChangeHandler(handler)
{
this.gameAttributesBatchChangeHandlers.delete(handler);
}
registerGameAttributesFinalizeHandler(handler)
{
this.gameAttributesFinalizeHandlers.add(handler);
}
unregisterGameAttributesFinalizeHandler(handler)
{
this.gameAttributesFinalizeHandlers.delete(handler);
}
registerAssignPlayerHandler(handler)
{
this.assignPlayerHandlers.add(handler);
}
unregisterAssignPlayerHandler(handler)
{
this.assignPlayerHandlers.delete(handler);
}
registerPickRandomItemsHandler(handler)
{
this.pickRandomItemsHandlers.add(handler);
}
unregisterPickRandomItemsHandler(handler)
{
this.pickRandomItemsHandlers.delete(handler);
}
onLoad(initData, hotloadData)
{
if (initData && initData.map && initData.mapType)
{
Object.defineProperty(this, "autostart", {
"value": true,
"writable": false,
"configurable": false
});
// TODO: Fix g_GameAttributes, g_GameAttributes.settings,
// g_GameAttributes.settings.PlayerData object references and
// copy over each attribute individually when receiving
// settings from the server or the local file.
g_GameAttributes = {
"mapType": initData.mapType,
"map": initData.map
};
this.updateGameAttributes();
// Don't launchGame before all Load handlers finished
}
else
{
if (hotloadData)
g_GameAttributes = hotloadData.gameAttributes;
else if (g_IsController && this.gameSettingsFile.enabled)
g_GameAttributes = this.gameSettingsFile.loadFile();
this.updateGameAttributes();
this.setNetworkGameAttributes();
}
}
onGetHotloadData(object)
{
object.gameAttributes = g_GameAttributes;
}
onGamesetupMessage(message)
{
if (!message.data)
return;
g_GameAttributes = message.data;
this.updateGameAttributes();
}
/**
* This is to be called whenever g_GameAttributes has been changed except on gameAttributes finalization.
*/
updateGameAttributes()
{
if (this.depth == 0)
Engine.ProfileStart("updateGameAttributes");
if (this.depth >= this.MaxDepth)
{
error("Infinite loop: " + new Error().stack);
Engine.ProfileStop();
return;
}
++this.depth;
// Basic sanitization
{
if (!g_GameAttributes.settings)
g_GameAttributes.settings = {};
if (!g_GameAttributes.settings.PlayerData)
g_GameAttributes.settings.PlayerData = new Array(this.DefaultPlayerCount);
for (let i = 0; i < g_GameAttributes.settings.PlayerData.length; ++i)
if (!g_GameAttributes.settings.PlayerData[i])
g_GameAttributes.settings.PlayerData[i] = {};
}
// Map change handlers are triggered first, so that GameSettingControls can update their
// gameAttributes model prior to applying that model in their gameAttributesChangeHandler.
if (g_GameAttributes.map && this.previousMap != g_GameAttributes.map && g_GameAttributes.mapType)
{
this.previousMap = g_GameAttributes.map;
let mapData = this.mapCache.getMapData(g_GameAttributes.mapType, g_GameAttributes.map);
for (let handler of this.mapChangeHandlers)
handler(mapData);
}
for (let handler of this.gameAttributesChangeHandlers)
handler();
--this.depth;
if (this.depth == 0)
{
for (let handler of this.gameAttributesBatchChangeHandlers)
handler();
Engine.ProfileStop();
}
}
/**
* This function is to be called when a GUI control has initiated a value change.
*
* To avoid an infinite loop, do not call this function when a gamesetup message was
* received and the data had only been modified deterministically.
*/
setNetworkGameAttributes()
{
if (g_IsNetworked)
Engine.SetNetworkGameAttributes(g_GameAttributes);
}
getPlayerData(gameAttributes, playerIndex)
{
return gameAttributes &&
gameAttributes.settings &&
gameAttributes.settings.PlayerData &&
gameAttributes.settings.PlayerData[playerIndex] || undefined;
}
assignPlayer(sourcePlayerIndex, playerIndex)
{
if (playerIndex == -1)
return;
let target = this.getPlayerData(g_GameAttributes, playerIndex);
let source = this.getPlayerData(g_GameAttributes, sourcePlayerIndex);
for (let handler of this.assignPlayerHandlers)
handler(source, target);
this.updateGameAttributes();
this.setNetworkGameAttributes();
}
/**
* This function is called everytime a random setting selection was resolved,
* so that subsequent random settings are triggered too,
* for example picking a random biome after picking a random map.
*/
pickRandomItems()
{
for (let handler of this.pickRandomItemsHandlers)
handler();
}
onLaunchGame()
{
if (!this.autostart)
this.gameSettingsFile.saveFile();
this.pickRandomItems();
for (let handler of this.gameAttributesFinalizeHandlers)
handler();
this.setNetworkGameAttributes();
}
}
GameSettingsControl.prototype.MaxDepth = 512;
/**
* This number is used when selecting the random map type, which doesn't provide PlayerData.
*/
GameSettingsControl.prototype.DefaultPlayerCount = 4;

View File

@ -0,0 +1,64 @@
/**
* This class provides a way to save g_GameAttributes to a file and load them.
*/
class GameSettingsFile
{
constructor(gamesetupPage)
{
this.filename = g_IsNetworked ?
this.GameAttributesFileMultiplayer :
this.GameAttributesFileSingleplayer;
this.engineInfo = Engine.GetEngineInfo();
this.enabled = Engine.ConfigDB_GetValue("user", this.ConfigName) == "true";
gamesetupPage.registerClosePageHandler(this.saveFile.bind(this));
}
loadFile()
{
Engine.ProfileStart("loadPersistMatchSettingsFile");
let data =
this.enabled &&
g_IsController &&
Engine.FileExists(this.filename) &&
Engine.ReadJSONFile(this.filename);
let gameAttributes =
data &&
data.attributes &&
data.engine_info &&
data.engine_info.engine_version == this.engineInfo.engine_version &&
hasSameMods(data.engine_info.mods, this.engineInfo.mods) &&
data.attributes || {};
Engine.ProfileStop();
return gameAttributes;
}
/**
* Delete settings if disabled, so that players are not confronted with old settings after enabling the setting again.
*/
saveFile()
{
if (!g_IsController)
return;
Engine.ProfileStart("savePersistMatchSettingsFile");
Engine.WriteJSONFile(this.filename, {
"attributes": this.enabled ? g_GameAttributes : {},
"engine_info": this.engineInfo
});
Engine.ProfileStop();
}
}
GameSettingsFile.prototype.ConfigName =
"persistmatchsettings";
GameSettingsFile.prototype.GameAttributesFileSingleplayer =
"config/matchsettings.json";
GameSettingsFile.prototype.GameAttributesFileMultiplayer =
"config/matchsettings.mp.json";

View File

@ -0,0 +1,119 @@
class MapFilters
{
constructor(mapCache)
{
this.mapCache = mapCache;
}
/**
* Some map filters may reject every map of a particular mapType.
* This function allows identifying which map filters have any matches for that maptype.
*/
getAvailableMapFilters(mapTypeName)
{
return this.Filters.filter(filter =>
this.getFilteredMaps(mapTypeName, filter.Name, true));
}
/**
* This function identifies all maps matching the given mapType and mapFilter.
* If existence is true, it will only test if there is at least one file for that mapType and mapFilter.
* Otherwise it returns an array with filename, translated map title and map description.
*/
getFilteredMaps(mapTypeName, filterName, existence)
{
let index = g_MapTypes.Name.findIndex(name => name == mapTypeName);
if (index == -1)
{
error("Can't get filtered maps for invalid maptype: " + mapTypeName);
return undefined;
}
let mapFilter = this.Filters.find(filter => filter.Name == filterName);
if (!mapFilter)
{
error("Invalid mapfilter name: " + filterName);
return undefined;
}
Engine.ProfileStart("getFilteredMaps");
let maps = [];
let mapTypePath = g_MapTypes.Path[index];
for (let filename of listFiles(mapTypePath, g_MapTypes.Suffix[index], false))
{
if (filename.startsWith(this.HiddenFilesPrefix))
continue;
let mapPath = mapTypePath + filename;
let mapData = this.mapCache.getMapData(mapTypeName, mapPath);
// Map files may come with custom json files
if (!mapData || !mapData.settings)
continue;
if (MatchesClassList(mapData.settings.Keywords || [], mapFilter.Match))
{
if (existence)
{
Engine.ProfileStop();
return true;
}
maps.push({
"file": mapPath,
"name": translate(mapData.settings.Name),
"description": translate(mapData.settings.Description)
});
}
}
Engine.ProfileStop();
return existence ? false : maps;
}
}
/**
* When maps start with this prefix, they will not appear in the maplist.
* Used for the Atlas _default.xml for instance.
*/
MapFilters.prototype.HiddenFilesPrefix = "_";
MapFilters.prototype.Filters = [
{
"Name": "default",
"Title": translate("Default"),
"Description": translate("All maps except naval and demo maps."),
"Match": ["!naval !demo !hidden"]
},
{
"Name": "naval",
"Title": translate("Naval Maps"),
"Description": translate("Maps where ships are needed to reach the enemy."),
"Match": ["naval"]
},
{
"Name": "demo",
"Title": translate("Demo Maps"),
"Description": translate("These maps are not playable but for demonstration purposes only."),
"Match": ["demo"]
},
{
"Name": "new",
"Title": translate("New Maps"),
"Description": translate("Maps that are brand new in this release of the game."),
"Match": ["new"]
},
{
"Name": "trigger",
"Title": translate("Trigger Maps"),
"Description": translate("Maps that come with scripted events and potentially spawn enemy units."),
"Match": ["trigger"]
},
{
"Name": "all",
"Title": translate("All Maps"),
"Description": translate("Every map of the chosen maptype."),
"Match": "!"
}
];

View File

@ -0,0 +1,164 @@
/**
* This class provides a property independent interface to g_PlayerAssignment events and actions.
*/
class PlayerAssignmentsControl
{
constructor(gamesetupPage, netMessages)
{
this.clientJoinHandlers = new Set();
this.clientLeaveHandlers = new Set();
this.playerAssignmentsChangeHandlers = new Set();
if (!g_IsNetworked)
{
let name = singleplayerName();
// Replace empty player name when entering a single-player match for the first time.
Engine.ConfigDB_CreateAndWriteValueToFile("user", this.ConfigNameSingleplayer, name, "config/user.cfg");
g_PlayerAssignments = {
"local": {
"name": name,
"player": -1
}
};
}
gamesetupPage.registerLoadHandler(this.onLoad.bind(this));
gamesetupPage.registerGetHotloadDataHandler(this.onGetHotloadData.bind(this));
netMessages.registerNetMessageHandler("players", this.onPlayerAssignmentMessage.bind(this));
}
registerPlayerAssignmentsChangeHandler(handler)
{
this.playerAssignmentsChangeHandlers.add(handler);
}
unregisterPlayerAssignmentsChangeHandler(handler)
{
this.playerAssignmentsChangeHandlers.delete(handler);
}
registerClientJoinHandler(handler)
{
this.clientJoinHandlers.add(handler);
}
unregisterClientJoinHandler(handler)
{
this.clientJoinHandlers.delete(handler);
}
registerClientLeaveHandler(handler)
{
this.clientLeaveHandlers.add(handler);
}
unregisterClientLeaveHandler(handler)
{
this.clientLeaveHandlers.delete(handler);
}
onLoad(initData, hotloadData)
{
if (hotloadData)
{
g_PlayerAssignments = hotloadData.playerAssignments;
this.updatePlayerAssignments();
}
}
onGetHotloadData(object)
{
object.playerAssignments = g_PlayerAssignments;
}
/**
* To be called when g_PlayerAssignments is modified.
*/
updatePlayerAssignments()
{
Engine.ProfileStart("updatePlayerAssignments");
for (let handler of this.playerAssignmentsChangeHandlers)
handler();
Engine.ProfileStop();
}
/**
* Called whenever a client joins/leaves or any gamesetting is changed.
*/
onPlayerAssignmentMessage(message)
{
let newAssignments = message.newAssignments;
for (let guid in newAssignments)
if (!g_PlayerAssignments[guid])
for (let handler of this.clientJoinHandlers)
handler(guid, message.newAssignments);
for (let guid in g_PlayerAssignments)
if (!newAssignments[guid])
for (let handler of this.clientLeaveHandlers)
handler(guid);
g_PlayerAssignments = newAssignments;
this.updatePlayerAssignments();
}
assignClient(guid, playerIndex)
{
if (g_IsNetworked)
Engine.AssignNetworkPlayer(playerIndex, guid);
else
{
g_PlayerAssignments[guid].player = playerIndex;
this.updatePlayerAssignments();
}
}
/**
* If both clients are assigned players, this will swap their assignments.
*/
assignPlayer(guidToAssign, playerIndex)
{
if (g_PlayerAssignments[guidToAssign].player != -1)
for (let guid in g_PlayerAssignments)
if (g_PlayerAssignments[guid].player == playerIndex + 1)
{
this.assignClient(guid, g_PlayerAssignments[guidToAssign].player);
break;
}
this.assignClient(guidToAssign, playerIndex + 1);
if (!g_IsNetworked)
this.updatePlayerAssignments();
}
unassignClient(playerID)
{
if (g_IsNetworked)
Engine.AssignNetworkPlayer(playerID, "");
else if (g_PlayerAssignments.local.player == playerID)
{
g_PlayerAssignments.local.player = -1;
this.updatePlayerAssignments();
}
}
unassignInvalidPlayers()
{
if (g_IsNetworked)
for (let playerID = g_GameAttributes.settings.PlayerData.length + 1; playerID <= g_MaxPlayers; ++playerID)
// Remove obsolete playerIDs from the servers playerassignments copy
Engine.AssignNetworkPlayer(playerID, "");
else if (g_PlayerAssignments.local.player > g_GameAttributes.settings.PlayerData.length)
{
g_PlayerAssignments.local.player = -1;
this.updatePlayerAssignments();
}
}
}
PlayerAssignmentsControl.prototype.ConfigNameSingleplayer =
"playername.singleplayer";

View File

@ -0,0 +1,126 @@
/**
* Ready system:
*
* The ready mechanism protects the players from being assigned to a match with settings they didn't explicitly agree with.
* It shall be technically possible to start a networked game until all participating players formally agree with the chosen settings.
*
* Therefore assume the readystate from the user interface rather than trusting the server whether the current player is ready.
* The server may set readiness to false but not to true.
*
* The ReadyControl class stores the ready state of the current player and fires an event if the agreed settings changed.
*/
class ReadyControl
{
constructor(netMessages, gameSettingsControl, startGameControl, playerAssignmentsControl)
{
this.startGameControl = startGameControl;
this.playerAssignmentsControl = playerAssignmentsControl;
this.resetReadyHandlers = new Set();
this.previousAssignments = {};
// This variable keeps track whether the local player is ready
// As part of cheat prevention, the server may set this to NotReady, but
// only the UI may set it to Ready or StayReady.
this.readyState = this.NotReady;
netMessages.registerNetMessageHandler("ready", this.onReadyMessage.bind(this));
gameSettingsControl.registerGameAttributesBatchChangeHandler(this.onGameAttributesBatchChange.bind(this));
playerAssignmentsControl.registerClientJoinHandler(this.onClientJoin.bind(this));
playerAssignmentsControl.registerClientLeaveHandler(this.onClientLeave.bind(this));
}
registerResetReadyHandler(handler)
{
this.resetReadyHandlers.add(handler);
}
onClientJoin(newGUID, newAssignments)
{
if (newAssignments[newGUID].player != -1)
this.resetReady();
}
onClientLeave(guid)
{
if (g_PlayerAssignments[guid].player != -1)
this.resetReady();
}
onReadyMessage(message)
{
let playerAssignment = g_PlayerAssignments[message.guid];
if (playerAssignment)
{
playerAssignment.status = message.status;
this.playerAssignmentsControl.updatePlayerAssignments();
}
}
onPlayerAssignmentsChange()
{
// Don't let the host tell you that you're ready when you're not.
let playerAssignment = g_PlayerAssignments[Engine.GetPlayerGUID()];
if (playerAssignment && playerAssignment.status > this.readyState)
playerAssignment.status = this.readyState;
for (let guid in g_PlayerAssignments)
if (this.previousAssignments[guid] &&
this.previousAssignments[guid].player != g_PlayerAssignments[guid].player)
{
this.resetReady();
return;
}
}
onGameAttributesBatchChange()
{
this.resetReady();
}
setReady(ready, sendMessage)
{
this.readyState = ready;
if (sendMessage)
Engine.SendNetworkReady(ready);
// Update GUI objects instantly if relevant settingchange was detected
let playerAssignment = g_PlayerAssignments[Engine.GetPlayerGUID()];
if (playerAssignment)
{
playerAssignment.status = ready;
this.playerAssignmentsControl.updatePlayerAssignments();
}
}
resetReady()
{
// The gameStarted check is only necessary to allow the host to
// determine the Seed and random items after clicking start
if (!g_IsNetworked || this.startGameControl.gameStarted)
return;
for (let handler of this.resetReadyHandlers)
handler();
if (g_IsController)
{
Engine.ClearAllPlayerReady();
this.playerAssignmentsControl.updatePlayerAssignments();
}
else if (this.readyState != this.StayReady)
this.setReady(this.NotReady, false);
}
getLocalReadyState()
{
return this.readyState;
}
}
ReadyControl.prototype.NotReady = 0;
ReadyControl.prototype.Ready = 1;
ReadyControl.prototype.StayReady = 2;

View File

@ -0,0 +1,53 @@
/**
* Cheat prevention:
*
* 1. Ensure that the host cannot start the game unless all clients agreed on the gamesettings using the ready system.
*
* TODO:
* 2. Ensure that the host cannot start the game with GameAttributes different from the agreed ones.
* This may be achieved by:
* - Determining the seed collectively.
* - passing the agreed gamesettings to the engine when starting the game instance
* - rejecting new gamesettings from the server after the game launch event
*/
class StartGameControl
{
constructor(netMessages)
{
this.gameLaunchHandlers = new Set();
// This may be read from publicly
this.gameStarted = false;
netMessages.registerNetMessageHandler("start", this.switchToLoadingPage.bind(this));
}
registerLaunchGameHandler(handler)
{
this.gameLaunchHandlers.add(handler);
}
launchGame()
{
this.gameStarted = true;
for (let handler of this.gameLaunchHandlers)
handler();
if (g_IsNetworked)
Engine.StartNetworkGame();
else
{
Engine.StartGame(g_GameAttributes, g_PlayerAssignments.local.player);
this.switchToLoadingPage();
}
}
switchToLoadingPage()
{
Engine.SwitchGuiPage("page_loading.xml", {
"attribs": g_GameAttributes,
"playerAssignments": g_PlayerAssignments
});
}
}

View File

@ -0,0 +1,178 @@
/**
* The GameSettingControl is an abstract class that is inherited by gamesetting control classes specific to a GUI object type,
* such as the GameSettingControlCheckbox or GameSettingControlDropdown.
*
* These classes are abstract classes too and are implemented by each handler class specific to one logical setting of g_GameAttributes.
* The purpose of these classes is to control precisely one logical setting of g_GameAttributes.
* Having one class per logical setting allows to handle each setting without making a restriction as to how the property should be written to g_GameAttributes or g_PlayerAssignments.
* The base classes allow implementing that while avoiding duplication.
*
* A GameSettingControl may depend on and read from other g_GameAttribute values,
* but the class instance is to be the sole instance writing to its setting value in g_GameAttributes and
* shall not write to setting values of other logical settings.
*
* The derived classes shall not make assumptions on the validity of g_GameAttributes,
* sanitize or delete their value if it is incompatible.
*
* A class should only write values to g_GameAttributes that it itself has confirmed to be accurate.
* This means that handlers may not copy an entire object or array of values, for example on mapchange.
* This avoids writing a setting value to g_GameAttributes that is not tracked and deleted when it becomes invalid.
*
* Since GameSettingControls shall be able to subscribe to g_GameAttributes changes,
* it is an obligation of the derived GameSettingControl class to broadcast the GameAttributesChange event each time it changes g_GameAttributes.
*/
class GameSettingControl
{
// The constructor and inherited constructors shall not modify game attributes,
// since all GameSettingControl shall be able to subscribe to any gamesetting change.
constructor(gameSettingControlManager, category, playerIndex, gamesetupPage, gameSettingsControl, mapCache, mapFilters, netMessages, playerAssignmentsControl)
{
// Store arguments
{
this.category = category;
if (playerIndex !== undefined)
this.playerIndex = playerIndex;
this.gamesetupPage = gamesetupPage;
this.gameSettingsControl = gameSettingsControl;
this.mapCache = mapCache;
this.mapFilters = mapFilters;
this.netMessages = netMessages;
this.playerAssignmentsControl = playerAssignmentsControl;
}
// enabled and hidden should only be modified through their setters or
// by calling updateVisibility after modification.
this.enabled = true;
this.hidden = false;
if (this.setControl)
this.setControl(gameSettingControlManager);
// This variable also used for autocompleting chat.
this.autocompleteTitle = undefined;
if (this.title && this.TitleCaption)
this.setTitle(this.TitleCaption);
if (this.Tooltip)
this.setTooltip(this.Tooltip);
this.setHidden(false);
if (this.onMapChange)
gameSettingsControl.registerMapChangeHandler(this.onMapChange.bind(this));
if (this.onLoad)
gamesetupPage.registerLoadHandler(this.onLoad.bind(this));
if (this.onGameAttributesChange)
gameSettingsControl.registerGameAttributesChangeHandler(this.onGameAttributesChange.bind(this));
if (this.onGameAttributesBatchChange)
gameSettingsControl.registerGameAttributesBatchChangeHandler(this.onGameAttributesBatchChange.bind(this));
if (this.onAssignPlayer && this.playerIndex === 0)
this.gameSettingsControl.registerAssignPlayerHandler(this.onAssignPlayer.bind(this));
if (this.onPickRandomItems)
gameSettingsControl.registerPickRandomItemsHandler(this.onPickRandomItems.bind(this));
if (this.onGameAttributesFinalize)
gameSettingsControl.registerGameAttributesFinalizeHandler(this.onGameAttributesFinalize.bind(this));
if (this.onPlayerAssignmentsChange)
playerAssignmentsControl.registerPlayerAssignmentsChangeHandler(this.onPlayerAssignmentsChange.bind(this));
}
setTitle(titleCaption)
{
this.autocompleteTitle = titleCaption;
this.title.caption = sprintf(this.TitleCaptionFormat, {
"setting": titleCaption
});
}
setTooltip(tooltip)
{
if (this.title)
this.title.tooltip = tooltip;
if (this.label)
this.label.tooltip = tooltip;
if (this.setControlTooltip)
this.setControlTooltip(tooltip);
}
/**
* Do not call functions calling updateVisibility onMapChange but onGameAttributesChange,
* so that changes take effect when increasing the playercount as well.
*/
setEnabled(enabled)
{
this.enabled = enabled;
this.updateVisibility();
}
setHidden(hidden)
{
this.hidden = hidden;
this.updateVisibility();
}
updateVisibility()
{
let hidden =
this.hidden ||
this.playerIndex === undefined &&
this.category != g_TabCategorySelected ||
this.playerIndex !== undefined &&
g_GameAttributes.settings && this.playerIndex >= g_GameAttributes.settings.PlayerData.length;
if (this.frame)
this.frame.hidden = hidden;
if (hidden)
return;
let enabled = g_IsController && this.enabled;
if (this.setControlHidden)
this.setControlHidden(!enabled);
if (this.label)
this.label.hidden = !!enabled;
}
/**
* Returns whether the control specifies an order but didn't implement the function.
*/
addAutocompleteEntries(name, autocomplete)
{
if (this.autocompleteTitle)
autocomplete[0].push(this.autocompleteTitle);
if (!Number.isInteger(this.AutocompleteOrder))
return;
if (!this.getAutocompleteEntries)
{
error(name + " specifies AutocompleteOrder but didn't implement getAutocompleteEntries");
return;
}
let newEntries = this.getAutocompleteEntries();
if (newEntries)
autocomplete[this.AutocompleteOrder] =
(autocomplete[this.AutocompleteOrder] || []).concat(newEntries);
}
}
GameSettingControl.prototype.TitleCaptionFormat =
translateWithContext("Title for specific setting", "%(setting)s:");
/**
* Derived classes can set this to a number to enable chat autocompleting of setting values.
* Higher numbers are autocompleted first.
*/
GameSettingControl.prototype.AutocompleteOrder = undefined;

View File

@ -0,0 +1,60 @@
/**
* This class is implemented by gamesettings that are controlled by a checkbox.
*/
class GameSettingControlCheckbox extends GameSettingControl
{
constructor(...args)
{
super(...args);
this.isInGuiUpdate = false;
this.previousSelectedValue = undefined;
}
setControl(gameSettingControlManager)
{
let row = gameSettingControlManager.getNextRow("checkboxSettingFrame");
this.frame = Engine.GetGUIObjectByName("checkboxSettingFrame[" + row + "]");
this.checkbox = Engine.GetGUIObjectByName("checkboxSettingControl[" + row + "]");
this.checkbox.onPress = this.onPressSuper.bind(this);
let labels = this.frame.children[0].children;
this.title = labels[0];
this.label = labels[1];
}
setControlTooltip(tooltip)
{
this.checkbox.tooltip = tooltip;
}
setControlHidden(hidden)
{
this.checkbox.hidden = hidden;
}
setChecked(checked)
{
if (this.previousSelectedValue == checked)
return;
this.isInGuiUpdate = true;
this.checkbox.checked = checked;
this.isInGuiUpdate = false;
if (this.label)
this.label.caption = checked ? this.Checked : this.Unchecked;
}
onPressSuper()
{
if (!this.isInGuiUpdate)
this.onPress(this.checkbox.checked);
}
}
GameSettingControlCheckbox.prototype.Checked =
translate("Yes");
GameSettingControlCheckbox.prototype.Unchecked =
translate("No");

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<object name="checkboxSettingFrame[n]" size="0 2 100% 32" hidden="true">
<include file="gui/gamesetup/GameSettings/GameSettingControlLabels.xml"/>
<object
name="checkboxSettingControl[n]"
type="checkbox"
size="175 5 193 30"
style="ModernTickBox"
tooltip_style="onscreenToolTip"
hidden="true"
z="1"
/>
</object>

View File

@ -0,0 +1,64 @@
/**
* This class is implemented by gamesettings that are controlled by a dropdown.
*/
class GameSettingControlDropdown extends GameSettingControl
{
constructor(...args)
{
super(...args);
this.isInGuiUpdate = false;
this.dropdown.onSelectionChange = this.onSelectionChangeSuper.bind(this);
if (this.onHoverChange)
this.dropdown.onHoverChange = this.onHoverChange.bind(this);
}
setControl(gameSettingControlManager)
{
let row = gameSettingControlManager.getNextRow("dropdownSettingFrame");
this.frame = Engine.GetGUIObjectByName("dropdownSettingFrame[" + row + "]");
this.dropdown = Engine.GetGUIObjectByName("dropdownSettingControl[" + row + "]");
let labels = this.frame.children[0].children;
this.title = labels[0];
this.label = labels[1];
}
setControlTooltip(tooltip)
{
this.dropdown.tooltip = tooltip;
}
setControlHidden(hidden)
{
this.dropdown.hidden = hidden;
}
setSelectedValue(value)
{
let index = this.dropdown.list_data.indexOf(String(value));
this.isInGuiUpdate = true;
this.dropdown.selected = index;
this.isInGuiUpdate = false;
if (this.label)
this.label.caption = index == -1 ? this.UnknownValue : this.dropdown.list[index];
}
onSelectionChangeSuper()
{
if (!this.isInGuiUpdate)
this.onSelectionChange(this.dropdown.selected);
}
}
/**
* Highlight the "random" dropdownlist item.
*/
GameSettingControlDropdown.prototype.RandomItemTags = {
"color": "orange"
};
GameSettingControlDropdown.prototype.UnknownValue =
translateWithContext("settings value", "Unknown");

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<object name="dropdownSettingFrame[n]" size="0 2 100% 32" hidden="true">
<include file="gui/gamesetup/GameSettings/GameSettingControlLabels.xml"/>
<object
name="dropdownSettingControl[n]"
type="dropdown"
size="175 0 100% 30"
style="ModernDropDown"
tooltip_style="onscreenToolTip"
hidden="true"
z="1"
/>
</object>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<object>
<!-- This GUI object displays the name of the setting. -->
<object
type="text"
size="0 0 170 30"
style="ModernRightLabelText"
tooltip_style="onscreenToolTip"
z="1"
/>
<!-- This GUI object displays the settings value if the control is disabled. -->
<object
type="text"
size="175 0 100% 30"
style="ModernLeftLabelText"
tooltip_style="onscreenToolTip"
z="1"
/>
</object>

View File

@ -0,0 +1,66 @@
/**
* Each property of this class is a class that inherits GameSettingControl and is
* instantiated by the GameSettingControlManager.
*/
class GameSettingControls
{
}
/**
* The GameSettingControlManager owns all classes that handle a logical property of g_GameAttributes.
*/
class GameSettingControlManager
{
constructor(gamesetupPage, gameSettingsControl, mapCache, mapFilters, netMessages, playerAssignmentsControl)
{
this.gameSettingsControl = gameSettingsControl;
this.rows = {};
this.gameSettingControls = {};
let args = Array.from(arguments);
let getCategory = name =>
g_GameSettingsLayout.findIndex(category => category.settings.indexOf(name) != -1);
for (let name in GameSettingControls)
this.gameSettingControls[name] =
new GameSettingControls[name](
this, getCategory(name), undefined, ...args);
for (let victoryCondition of g_VictoryConditions)
this.gameSettingControls[victoryCondition.Name] =
new VictoryConditionCheckbox(
victoryCondition, this, getCategory(victoryCondition.Name), undefined, ...args);
this.playerSettingControlManagers = Array.from(
new Array(g_MaxPlayers),
(value, playerIndex) =>
new PlayerSettingControlManager(playerIndex, ...args));
}
getNextRow(name)
{
if (this.rows[name] === undefined)
this.rows[name] = 0;
else
++this.rows[name];
return this.rows[name];
}
updateSettingVisibility()
{
for (let name in this.gameSettingControls)
this.gameSettingControls[name].updateVisibility();
}
addAutocompleteEntries(entries)
{
for (let name in this.gameSettingControls)
this.gameSettingControls[name].addAutocompleteEntries(name, entries);
for (let playerSettingControlManager of this.playerSettingControlManagers)
playerSettingControlManager.addAutocompleteEntries(entries);
}
}

View File

@ -0,0 +1,46 @@
/**
* This array determines the order in which the GUI controls are shown in the GameSettingTabs panel.
* The names correspond to property names of the GameSettingControls prototype.
*/
var g_GameSettingsLayout = [
{
"label": translateWithContext("Match settings tab name", "Map"),
"settings": [
"MapType",
"MapFilter",
"MapSelection",
"MapSize",
"Biome",
"TriggerDifficulty",
"Nomad",
"Treasures",
"ExploredMap",
"RevealedMap"
]
},
{
"label": translateWithContext("Match settings tab name", "Player"),
"settings": [
"PlayerCount",
"PopulationCap",
"StartingResources",
"Spies",
"Cheats"
]
},
{
"label": translateWithContext("Match settings tab name", "Game Type"),
"settings": [
...g_VictoryConditions.map(victoryCondition => victoryCondition.Name),
"RelicCount",
"RelicDuration",
"WonderDuration",
"RegicideGarrison",
"GameSpeed",
"Ceasefire",
"LockedTeams",
"LastManStanding",
"Rating"
]
}
];

View File

@ -0,0 +1,188 @@
PlayerSettingControls.AIConfigButton = class extends GameSettingControl
{
constructor(...args)
{
super(...args);
this.playerConfig = Engine.GetGUIObjectByName("playerConfig[" + this.playerIndex + "]");
this.isPageOpen = false;
this.guid = undefined;
this.fixedAI = undefined;
this.defaultAIDiff = +Engine.ConfigDB_GetValue("user", this.ConfigDifficulty);
this.defaultBehavior = Engine.ConfigDB_GetValue("user", this.ConfigBehavior);
// Save little performance by not reallocating every call
this.sprintfArgs = {};
this.playerConfig.onPress = this.openConfigPage.bind(this, this.playerIndex);
}
onMapChange(mapData)
{
let pData = this.gameSettingsControl.getPlayerData(g_GameAttributes, this.playerIndex);
if (!pData)
return;
let isScenario = g_GameAttributes.mapType == "scenario";
let mapPData = this.gameSettingsControl.getPlayerData(mapData, this.playerIndex);
if (mapPData && mapPData.AI)
{
let defaultPData = g_Settings.PlayerDefaults[this.playerIndex + 1];
this.fixedAI = {
"AI": mapPData.AI,
"AIDiff":
mapPData.AIDiff !== undefined ?
mapPData.AIDiff :
defaultPData.AIDiff,
"AIBehavior":
mapPData.AIBehavior !== undefined ?
mapPData.AIBehavior :
defaultPData.AIBehavior
};
}
else
this.fixedAI = undefined;
}
onAssignPlayer(source, target)
{
if (source && target.AI)
{
source.AI = target.AI;
source.AIDiff = target.AIDiff;
source.AIBehavior = target.AIBehavior;
}
target.AI = false;
delete target.AIDiff;
delete target.AIBehavior;
}
onPlayerAssignmentsChange()
{
this.guid = undefined;
for (let guid in g_PlayerAssignments)
if (g_PlayerAssignments[guid].player == this.playerIndex + 1)
this.guid = guid;
}
onGameAttributesChange()
{
let pData = this.gameSettingsControl.getPlayerData(g_GameAttributes, this.playerIndex);
if (!pData)
return;
// Enforce map specified AI
if (this.fixedAI &&
(pData.AI !== this.fixedAI.AI ||
pData.AIDiff !== this.fixedAI.AIDiff ||
pData.AIBehavior !== this.fixedAI.AIBehavior))
{
pData.AI = this.fixedAI.AI;
pData.AIDiff = this.fixedAI.AIDiff;
pData.AIBehavior = this.fixedAI.AIBehavior;
this.gameSettingsControl.updateGameAttributes();
}
// Sanitize, make AI state self-consistent
if (pData.AI)
{
if (!pData.AIDiff || !pData.AIBehavior)
{
if (!pData.AIDiff)
pData.AIDiff = this.defaultAIDiff;
if (!pData.AIBehavior)
pData.AIBehavior = this.defaultBehavior;
this.gameSettingsControl.updateGameAttributes();
}
}
else if (pData.AI === undefined)
{
if (this.guid)
pData.AI = false;
else
{
pData.AI = g_Settings.PlayerDefaults[this.playerIndex + 1].AI;
pData.AIDiff = this.defaultAIDiff;
pData.AIBehavior = this.defaultBehavior;
}
this.gameSettingsControl.updateGameAttributes();
}
else if (pData.AIBehavior || pData.AIDiff)
{
pData.AI = false;
delete pData.AIBehavior;
delete pData.AIDiff;
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesBatchChange()
{
let isPageOpen = this.isPageOpen;
if (isPageOpen)
Engine.PopGuiPage();
let pData = this.gameSettingsControl.getPlayerData(g_GameAttributes, this.playerIndex);
if (!pData)
return;
this.sprintfArgs.description = translateAISettings(pData);
this.playerConfig.tooltip = sprintf(this.Tooltip, this.sprintfArgs);
this.playerConfig.hidden = !pData.AI;
if (isPageOpen)
this.openConfigPage();
}
openConfigPage()
{
let pData = this.gameSettingsControl.getPlayerData(g_GameAttributes, this.playerIndex);
if (!pData || !pData.AI)
return;
this.isPageOpen = true;
Engine.PushGuiPage(
"page_aiconfig.xml",
{
"playerSlot": this.playerIndex,
"id": pData.AI,
"difficulty": pData.AIDiff,
"behavior": pData.AIBehavior,
"fixed": !!this.fixedAI
},
this.onConfigPageClosed.bind(this));
}
onConfigPageClosed(data)
{
this.isPageOpen = false;
let pData = this.gameSettingsControl.getPlayerData(g_GameAttributes, this.playerIndex);
if (!data || !data.save || !g_IsController || !pData)
return;
pData.AI = data.id;
pData.AIDiff = data.difficulty;
pData.AIBehavior = data.behavior;
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
}
};
PlayerSettingControls.AIConfigButton.prototype.Tooltip =
translate("Configure AI: %(description)s.");
PlayerSettingControls.AIConfigButton.prototype.ConfigDifficulty =
"gui.gamesetup.aidifficulty";
PlayerSettingControls.AIConfigButton.prototype.ConfigBehavior =
"gui.gamesetup.aibehavior";

View File

@ -0,0 +1,269 @@
PlayerSettingControls.PlayerAssignment = class extends GameSettingControlDropdown
{
constructor(...args)
{
super(...args);
this.clientItemFactory = new PlayerAssignmentItem.Client();
this.aiItemFactory = new PlayerAssignmentItem.AI();
this.unassignedItem = new PlayerAssignmentItem.Unassigned().createItem();
this.aiItems =
g_Settings.AIDescriptions.filter(ai => !ai.data.hidden).map(
this.aiItemFactory.createItem.bind(this.aiItemFactory));
this.values = undefined;
this.assignedGUID = undefined;
this.fixedAI = undefined;
this.playerAssignmentsControl.registerClientJoinHandler(this.onClientJoin.bind(this));
}
setControl()
{
this.dropdown = Engine.GetGUIObjectByName("playerAssignment[" + this.playerIndex + "]");
this.label = Engine.GetGUIObjectByName("playerAssignmentText[" + this.playerIndex + "]");
}
onLoad(initData, hotloadData)
{
if (!hotloadData && !g_IsNetworked)
this.onClientJoin("local", g_PlayerAssignments);
}
onClientJoin(newGUID, newAssignments)
{
if (!g_IsController || this.fixedAI || newAssignments[newGUID].player != -1)
return;
let pData = this.gameSettingsControl.getPlayerData(g_GameAttributes, this.playerIndex);
if (!pData)
return;
// Assign the client (or only buddies if prefered) to a free slot
if (newGUID != Engine.GetPlayerGUID())
{
let assignOption = Engine.ConfigDB_GetValue("user", this.ConfigAssignPlayers);
if (assignOption == "disabled" ||
assignOption == "buddies" && g_Buddies.indexOf(splitRatingFromNick(newAssignments[newGUID].name).nick) == -1)
return;
}
for (let guid in newAssignments)
if (newAssignments[guid].player == this.playerIndex + 1)
return;
if (pData.AI)
{
pData.AI = false;
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
}
newAssignments[newGUID].player = this.playerIndex + 1;
this.playerAssignmentsControl.assignClient(newGUID, this.playerIndex + 1);
}
onPlayerAssignmentsChange()
{
this.assignedGUID = undefined;
for (let guid in g_PlayerAssignments)
if (g_PlayerAssignments[guid].player == this.playerIndex + 1)
{
this.assignedGUID = guid;
break;
}
this.playerItems = sortGUIDsByPlayerID().map(
this.clientItemFactory.createItem.bind(this.clientItemFactory));
this.rebuildList();
this.updateSelection();
}
onMapChange(mapData)
{
let mapPData = this.gameSettingsControl.getPlayerData(mapData, this.playerIndex);
this.fixedAI = mapPData && mapPData.AI || undefined;
}
onGameAttributesChange()
{
let pData = this.gameSettingsControl.getPlayerData(g_GameAttributes, this.playerIndex);
if (!pData)
return;
if (this.fixedAI && pData.AI != this.fixedAI)
{
pData.AI = this.fixedAI;
this.gameSettingsControl.updateGameAttributes();
this.playerAssignmentsControl.unassignClient(this.playerIndex + 1);
}
}
onGameAttributesBatchChange()
{
let pData = this.gameSettingsControl.getPlayerData(g_GameAttributes, this.playerIndex);
if (!pData)
return;
this.setEnabled(!this.fixedAI);
this.updateSelection();
}
updateSelection()
{
let pData = this.gameSettingsControl.getPlayerData(g_GameAttributes, this.playerIndex);
if (pData && this.values)
this.setSelectedValue(
this.values.Value.findIndex((value, i) =>
this.values.Handler[i].isSelected(pData, this.assignedGUID, value)));
}
rebuildList()
{
Engine.ProfileStart("updatePlayerAssignmentsList");
this.values = prepareForDropdown([
...this.playerItems,
...this.aiItems,
this.unassignedItem
]);
this.dropdown.list = this.values.Caption;
this.dropdown.list_data = this.values.Value.map((value, i) => i);
Engine.ProfileStop();
}
onSelectionChange(itemIdx)
{
this.values.Handler[itemIdx].onSelectionChange(
this.gameSettingsControl,
this.playerAssignmentsControl,
this.playerIndex,
this.values.Value[itemIdx]);
}
getAutocompleteEntries()
{
return this.values.Autocomplete;
}
};
PlayerSettingControls.PlayerAssignment.prototype.Tooltip =
translate("Select player.");
PlayerSettingControls.PlayerAssignment.prototype.AutocompleteOrder = 100;
PlayerSettingControls.PlayerAssignment.prototype.ConfigAssignPlayers =
"gui.gamesetup.assignplayers";
class PlayerAssignmentItem
{
}
{
PlayerAssignmentItem.Client = class
{
createItem(guid)
{
return {
"Handler": this,
"Value": guid,
"Autocomplete": g_PlayerAssignments[guid].name,
"Caption": setStringTags(
g_PlayerAssignments[guid].name,
g_PlayerAssignments[guid].player == -1 ? this.ObserverTags : this.PlayerTags)
};
}
onSelectionChange(gameSettingsControl, playerAssignmentsControl, playerIndex, guidToAssign)
{
let sourcePlayer = g_PlayerAssignments[guidToAssign].player - 1;
playerAssignmentsControl.assignPlayer(guidToAssign, playerIndex);
gameSettingsControl.assignPlayer(sourcePlayer, playerIndex);
}
isSelected(pData, guid, value)
{
return guid !== undefined && guid == value;
}
};
PlayerAssignmentItem.Client.prototype.PlayerTags =
{ "color": "white" };
PlayerAssignmentItem.Client.prototype.ObserverTags =
{ "color": "170 170 250" };
}
{
PlayerAssignmentItem.AI = class
{
createItem(ai)
{
let aiName = translate(ai.data.name);
return {
"Handler": this,
"Value": ai.id,
"Autocomplete": aiName,
"Caption": setStringTags(sprintf(this.Label, { "ai": aiName }), this.Tags)
};
}
onSelectionChange(gameSettingsControl, playerAssignmentsControl, playerIndex, value)
{
playerAssignmentsControl.unassignClient(playerIndex + 1);
g_GameAttributes.settings.PlayerData[playerIndex].AI = value;
gameSettingsControl.updateGameAttributes();
gameSettingsControl.setNetworkGameAttributes();
}
isSelected(pData, guid, value)
{
return !guid && pData.AI && pData.AI == value;
}
};
PlayerAssignmentItem.AI.prototype.Label =
translate("AI: %(ai)s");
PlayerAssignmentItem.AI.prototype.Tags =
{ "color": "70 150 70" };
}
{
PlayerAssignmentItem.Unassigned = class
{
createItem()
{
return {
"Handler": this,
"Value": undefined,
"Autocomplete": this.Label,
"Caption": setStringTags(this.Label, this.Tags)
};
}
onSelectionChange(gameSettingsControl, playerAssignmentsControl, playerIndex)
{
playerAssignmentsControl.unassignClient(playerIndex + 1);
g_GameAttributes.settings.PlayerData[playerIndex].AI = false;
gameSettingsControl.updateGameAttributes();
gameSettingsControl.setNetworkGameAttributes();
}
isSelected(pData, guid, value)
{
return !guid && !pData.AI;
}
};
PlayerAssignmentItem.Unassigned.prototype.Label =
translate("Unassigned");
PlayerAssignmentItem.Unassigned.prototype.Tags =
{ "color": "140 140 140" };
}

View File

@ -0,0 +1,142 @@
PlayerSettingControls.PlayerCiv = class extends GameSettingControlDropdown
{
constructor(...args)
{
super(...args);
this.fixedCiv = undefined;
this.values = prepareForDropdown(this.getItems());
this.dropdown.list = this.values.name;
this.dropdown.list_data = this.values.civ;
}
setControl()
{
this.label = Engine.GetGUIObjectByName("playerCivText[" + this.playerIndex + "]");
this.dropdown = Engine.GetGUIObjectByName("playerCiv[" + this.playerIndex + "]");
}
onHoverChange()
{
this.dropdown.tooltip = this.values && this.values.tooltip[this.dropdown.hovered] || this.Tooltip;
}
onMapChange(mapData)
{
let mapPData = this.gameSettingsControl.getPlayerData(mapData, this.playerIndex);
this.fixedCiv = mapPData && mapPData.Civ || undefined;
}
onAssignPlayer(source, target)
{
if (g_GameAttributes.mapType != "scenario" && source && target)
[source.Civ, target.Civ] = [target.Civ, source.Civ];
}
onGameAttributesChange()
{
let pData = this.gameSettingsControl.getPlayerData(g_GameAttributes, this.playerIndex);
if (!pData || !g_GameAttributes.mapType)
return;
if (this.fixedCiv)
{
if (!pData.Civ || this.fixedCiv != pData.Civ)
{
pData.Civ = this.fixedCiv;
this.gameSettingsControl.updateGameAttributes();
}
}
else if (this.values.civ.indexOf(pData.Civ || undefined) == -1)
{
pData.Civ =
g_GameAttributes.mapType == "scenario" ?
g_Settings.PlayerDefaults[this.playerIndex + 1].Civ :
this.RandomCivId;
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesBatchChange()
{
let pData = this.gameSettingsControl.getPlayerData(g_GameAttributes, this.playerIndex);
if (!pData || !g_GameAttributes.mapType)
return;
this.setEnabled(!this.fixedCiv);
this.setSelectedValue(pData.Civ);
}
getItems()
{
let values = [];
for (let civ in g_CivData)
if (g_CivData[civ].SelectableInGameSetup)
values.push({
"name": g_CivData[civ].Name,
"autocomplete": g_CivData[civ].Name,
"tooltip": g_CivData[civ].History,
"civ": civ
});
values.sort(sortNameIgnoreCase);
values.unshift({
"name": setStringTags(this.RandomCivCaption, this.RandomItemTags),
"autocomplete": this.RandomCivCaption,
"tooltip": this.RandomCivTooltip,
"civ": this.RandomCivId
});
return values;
}
getAutocompleteEntries()
{
return this.values.autocomplete;
}
onSelectionChange(itemIdx)
{
let pData = this.gameSettingsControl.getPlayerData(g_GameAttributes, this.playerIndex);
pData.Civ = this.values.civ[itemIdx];
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
}
onPickRandomItems()
{
let pData = this.gameSettingsControl.getPlayerData(g_GameAttributes, this.playerIndex);
if (!pData || pData.Civ != this.RandomCivId)
return;
// Get a unique array of selectable cultures
let cultures = Object.keys(g_CivData).filter(civ => g_CivData[civ].SelectableInGameSetup).map(civ => g_CivData[civ].Culture);
cultures = cultures.filter((culture, index) => cultures.indexOf(culture) === index);
// Pick a random civ of a random culture
let culture = pickRandom(cultures);
pData.Civ = pickRandom(Object.keys(g_CivData).filter(civ =>
g_CivData[civ].Culture == culture && g_CivData[civ].SelectableInGameSetup));
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.pickRandomItems();
}
};
PlayerSettingControls.PlayerCiv.prototype.Tooltip =
translate("Choose the civilization for this player.");
PlayerSettingControls.PlayerCiv.prototype.RandomCivCaption =
translateWithContext("civilization", "Random");
PlayerSettingControls.PlayerCiv.prototype.RandomCivId =
"random";
PlayerSettingControls.PlayerCiv.prototype.RandomCivTooltip =
translate("Picks one civilization at random when the game starts.");
PlayerSettingControls.PlayerCiv.prototype.AutocompleteOrder = 90;

View File

@ -0,0 +1,175 @@
PlayerSettingControls.PlayerColor = class extends GameSettingControlDropdown
{
constructor(...args)
{
super(...args);
this.defaultColors = g_Settings.PlayerDefaults.slice(1).map(pData => pData.Color);
}
setControl()
{
this.dropdown = Engine.GetGUIObjectByName("playerColor[" + this.playerIndex + "]");
this.playerBackgroundColor = Engine.GetGUIObjectByName("playerBackgroundColor[" + this.playerIndex + "]");
this.playerColorHeading = Engine.GetGUIObjectByName("playerColorHeading");
}
onMapChange(mapData)
{
Engine.ProfileStart("updatePlayerColorList");
let hidden = !g_IsController || g_GameAttributes.mapType == "scenario";
this.dropdown.hidden = hidden;
this.playerColorHeading.hidden = hidden;
// Step 1: Pick colors that the map specifies, add most unsimilar default colors
// Provide the access to g_MaxPlayers different colors, regardless of current playercount.
let values = [];
for (let i = 0; i < g_MaxPlayers; ++i)
{
let pData = this.gameSettingsControl.getPlayerData(mapData, i);
values.push(pData && pData.Color || this.findFarthestUnusedColor(values));
}
// Step 2: Sort these colors so that the order is most reminiscent of the default colors
this.values = [];
for (let i = 0; i < g_MaxPlayers; ++i)
{
let closestColor;
let smallestDistance = Infinity;
for (let color of values)
{
if (this.values.some(col => sameColor(col, color)))
continue;
let distance = colorDistance(color, this.defaultColors[i]);
if (distance <= smallestDistance)
{
closestColor = color;
smallestDistance = distance;
}
}
this.values.push(closestColor);
}
this.dropdown.list = this.values.map(color => coloredText(this.ColorIcon, rgbToGuiColor(color)));
this.dropdown.list_data = this.values.map((color, i) => i);
// If the map specified a color for this slot, use that
let mapPlayerData = this.gameSettingsControl.getPlayerData(mapData, this.playerIndex);
let playerData = this.gameSettingsControl.getPlayerData(g_GameAttributes, this.playerIndex);
if (playerData && mapPlayerData && mapPlayerData.Color &&
(!playerData.Color || !sameColor(playerData.Color, mapPlayerData.Color)))
{
playerData.Color = mapPlayerData.Color;
this.gameSettingsControl.updateGameAttributes();
}
Engine.ProfileStop();
}
onGameAttributesChange()
{
let pData = this.gameSettingsControl.getPlayerData(g_GameAttributes, this.playerIndex);
if (!pData || !this.values)
return;
let inUse =
pData.Color &&
g_GameAttributes.settings.PlayerData.some((otherPData, i) =>
i < this.playerIndex &&
otherPData.Color &&
sameColor(pData.Color, otherPData.Color));
if (!pData.Color || this.values.indexOf(pData.Color) == -1 || inUse)
{
pData.Color =
(pData.Color && !inUse) ?
this.findClosestColor(pData.Color, this.values) :
this.getUnusedColor();
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesBatchChange()
{
let pData = this.gameSettingsControl.getPlayerData(g_GameAttributes, this.playerIndex);
if (!pData || !this.values)
return;
this.setSelectedValue(this.values.indexOf(pData.Color));
this.playerBackgroundColor.sprite = "color:" + rgbToGuiColor(pData.Color, 100);
}
onAssignPlayer(source, target)
{
if (g_GameAttributes.mapType != "scenario" && source && target)
[source.Color, target.Color] = [target.Color, source.Color];
}
onSelectionChange(itemIdx)
{
let pData = this.gameSettingsControl.getPlayerData(g_GameAttributes, this.playerIndex);
// If someone else has that color, give that player the old color
let otherPData = g_GameAttributes.settings.PlayerData.find(data =>
sameColor(this.values[itemIdx], data.Color));
if (otherPData)
otherPData.Color = pData.Color;
pData.Color = this.values[itemIdx];
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
}
findClosestColor(targetColor, colors)
{
let colorDistances = colors.map(color => colorDistance(color, targetColor));
let smallestDistance = colorDistances.find(
distance => colorDistances.every(distance2 => distance2 >= distance));
return colors.find(color => colorDistance(color, targetColor) == smallestDistance);
}
findFarthestUnusedColor(values)
{
let farthestColor;
let farthestDistance = 0;
for (let defaultColor of this.defaultColors)
{
let smallestDistance = Infinity;
for (let usedColor of values)
{
let distance = colorDistance(usedColor, defaultColor);
if (distance < smallestDistance)
smallestDistance = distance;
}
if (smallestDistance >= farthestDistance)
{
farthestColor = defaultColor;
farthestDistance = smallestDistance;
}
}
return farthestColor;
}
getUnusedColor()
{
return this.values.find(color =>
g_GameAttributes &&
g_GameAttributes.settings &&
g_GameAttributes.settings.PlayerData &&
g_GameAttributes.settings.PlayerData.every(pData => !pData.Color || !sameColor(color, pData.Color)));
}
};
PlayerSettingControls.PlayerColor.prototype.Tooltip =
translate("Pick a color.");
PlayerSettingControls.PlayerColor.prototype.ColorIcon =
"■";

View File

@ -0,0 +1,78 @@
PlayerSettingControls.PlayerTeam = class extends GameSettingControlDropdown
{
constructor(...args)
{
super(...args);
this.values = prepareForDropdown([
{
"label": this.NoTeam,
"id": this.NoTeamId
},
...Array.from(
new Array(g_MaxTeams),
(v, i) => ({
"label": i + 1,
"id": i
}))
]);
this.dropdown.list = this.values.label;
this.dropdown.list_data = this.values.id;
}
setControl()
{
this.label = Engine.GetGUIObjectByName("playerTeamText[" + this.playerIndex + "]");
this.dropdown = Engine.GetGUIObjectByName("playerTeam[" + this.playerIndex + "]");
}
onMapChange(mapData)
{
let mapPData = this.gameSettingsControl.getPlayerData(mapData, this.playerIndex);
let pData = this.gameSettingsControl.getPlayerData(g_GameAttributes, this.playerIndex);
if (pData && mapPData && mapPData.Team !== undefined)
{
pData.Team = mapPData.Team;
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesChange()
{
let pData = this.gameSettingsControl.getPlayerData(g_GameAttributes, this.playerIndex);
if (!pData)
return;
if (pData.Team === undefined)
{
pData.Team = this.NoTeamId;
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesBatchChange()
{
let pData = this.gameSettingsControl.getPlayerData(g_GameAttributes, this.playerIndex);
if (!pData)
return;
this.setEnabled(g_GameAttributes.mapType != "scenario");
this.setSelectedValue(pData.Team);
}
onSelectionChange(itemIdx)
{
g_GameAttributes.settings.PlayerData[this.playerIndex].Team = itemIdx - 1;
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
}
};
PlayerSettingControls.PlayerTeam.prototype.Tooltip =
translate("Select player's team.");
PlayerSettingControls.PlayerTeam.prototype.NoTeam =
translateWithContext("team", "None");
PlayerSettingControls.PlayerTeam.prototype.NoTeamId = -1;

View File

@ -0,0 +1,23 @@
PlayerSettingControls.PlayerFrame = class extends GameSettingControl
{
constructor(...args)
{
super(...args);
this.playerFrame = Engine.GetGUIObjectByName("playerFrame[" + this.playerIndex + "]");
{
let size = this.playerFrame.size;
size.top = this.Height * this.playerIndex;
size.bottom = this.Height * (this.playerIndex + 1);
this.playerFrame.size = size;
}
}
onGameAttributesBatchChange()
{
this.playerFrame.hidden = !this.gameSettingsControl.getPlayerData(g_GameAttributes, this.playerIndex);
}
}
PlayerSettingControls.PlayerFrame.prototype.Height = 32;

View File

@ -0,0 +1,129 @@
// TODO: There should be an indication which player is not ready yet
// The color does not indicate it's meaning and is insufficient to inform many players.
PlayerSettingControls.PlayerName = class extends GameSettingControl
{
constructor(...args)
{
super(...args);
this.playerName = Engine.GetGUIObjectByName("playerName[" + this.playerIndex + "]");
this.displayedName = undefined;
this.guid = undefined;
}
onMapChange(mapData)
{
let pData = this.gameSettingsControl.getPlayerData(g_GameAttributes, this.playerIndex);
if (!pData)
return;
let mapPData = this.gameSettingsControl.getPlayerData(mapData, this.playerIndex);
pData.Name = mapPData && mapPData.Name || g_Settings.PlayerDefaults[this.playerIndex + 1].Name;
this.gameSettingsControl.updateGameAttributes();
}
onGameAttributesChange()
{
let pData = this.gameSettingsControl.getPlayerData(g_GameAttributes, this.playerIndex);
if (!pData)
return;
if (!pData.Name)
{
pData.Name = g_Settings.PlayerDefaults[this.playerIndex + 1].Name;
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesBatchChange()
{
let pData = this.gameSettingsControl.getPlayerData(g_GameAttributes, this.playerIndex);
if (!pData)
return;
this.displayedName = g_IsNetworked ? pData.Name : translate(pData.Name);
this.rebuild();
}
onPlayerAssignmentsChange()
{
this.guid = undefined;
for (let guid in g_PlayerAssignments)
if (g_PlayerAssignments[guid].player == this.playerIndex + 1)
{
this.guid = guid;
break;
}
this.rebuild();
}
rebuild()
{
let name = this.displayedName;
if (!name)
return;
if (g_IsNetworked)
{
let status = this.guid ? g_PlayerAssignments[this.guid].status : this.ReadyTags.length - 1;
name = setStringTags(this.displayedName, this.ReadyTags[status]);
}
this.playerName.caption = name;
}
onGameAttributesFinalize()
{
let pData = this.gameSettingsControl.getPlayerData(g_GameAttributes, this.playerIndex);
if (!pData)
return;
if (g_GameAttributes.mapType != "scenario" && pData.AI)
{
// Pick one of the available botnames for the chosen civ
// Determine botnames
let chosenName = pickRandom(g_CivData[pData.Civ].AINames);
if (!g_IsNetworked)
chosenName = translate(chosenName);
// Count how many players use the chosenName
let usedName = g_GameAttributes.settings.PlayerData.filter(otherPData =>
otherPData.Name && otherPData.Name.indexOf(chosenName) !== -1).length;
pData.Name =
usedName ?
sprintf(this.RomanLabel, {
"playerName": chosenName,
"romanNumber": this.RomanNumbers[usedName + 1]
}) :
chosenName;
}
else
// Copy client playernames so they appear in replays
for (let guid in g_PlayerAssignments)
if (g_PlayerAssignments[guid].player == this.playerIndex + 1)
pData.Name = g_PlayerAssignments[guid].name;
}
};
PlayerSettingControls.PlayerName.prototype.RomanLabel =
translate("%(playerName)s %(romanNumber)s");
PlayerSettingControls.PlayerName.prototype.RomanNumbers =
[undefined, "I", "II", "III", "IV", "V", "VI", "VII", "VIII"];
PlayerSettingControls.PlayerName.prototype.ReadyTags = [
{
"color": "white",
},
{
"color": "green",
},
{
"color": "150 150 250",
}
];

View File

@ -0,0 +1,45 @@
// TODO: There should be a dialog allowing to specify starting resources and population capacity per player
PlayerSettingControls.PlayerSettings = class extends GameSettingControl
{
onMapChange(mapData)
{
let pData = this.gameSettingsControl.getPlayerData(g_GameAttributes, this.playerIndex);
if (!pData)
return;
let mapPData = this.gameSettingsControl.getPlayerData(mapData, this.playerIndex);
let isScenario = mapPData && g_GameAttributes.mapType == "scenario";
if (isScenario && mapPData.Resources)
pData.Resources = mapPData.Resources;
else
delete pData.Resources;
if (isScenario && mapPData.PopulationLimit)
pData.PopulationLimit = mapPData.PopulationLimit;
else
delete pData.PopulationLimit;
}
onGameAttributesFinalize()
{
// Copy map well known properties (and only well known properties)
let mapData = this.mapCache.getMapData(g_GameAttributes.mapType, g_GameAttributes.map);
let pData = this.gameSettingsControl.getPlayerData(g_GameAttributes, this.playerIndex);
let mapPData = this.gameSettingsControl.getPlayerData(mapData, this.playerIndex);
if (!pData || !mapPData)
return;
for (let property of this.MapSettings)
if (mapPData[property] !== undefined)
pData[property] = mapPData[property];
}
};
PlayerSettingControls.PlayerSettings.prototype.MapSettings = [
"StartingTechnologies",
"DisabledTechnologies",
"DisabledTemplates",
"StartingCamera"
];

View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<object name="playerAssignmentsPanel" type="image" sprite="ModernDarkBoxGold">
<object size="0 6 100% 30">
<object name="playerNameHeading" type="text" style="ModernLabelText" size="0 0 20%+5 100%">
<translatableAttribute id="caption">Player Name</translatableAttribute>
</object>
<object name="playerColorHeading" type="text" style="ModernLabelText" size="20%+5 0 22%+45 100%">
<translatableAttribute id="caption">Color</translatableAttribute>
</object>
<object name="playerPlacementHeading" type="text" style="ModernLabelText" size="22%+45 0 50%+35 100%">
<translatableAttribute id="caption">Player Placement</translatableAttribute>
</object>
<object name="playerCivHeading" type="text" style="ModernLabelText" size="50%+69 0 85%-37 100%">
<translatableAttribute id="caption">Civilization</translatableAttribute>
</object>
<object name="civInfoButton" type="button" style="IconButton" sprite="iconInfoGold" sprite_over="iconInfoWhite" size="85%-37 0 85%-21 16"/>
<object name="civResetButton" type="button" style="IconButton" sprite="iconResetGold" sprite_over="iconResetWhite" size="85%-16 0 85% 16"/>
<object name="playerTeamHeading" type="text" style="ModernLabelText" size="85%+5 0 100%-21 100%">
<translatableAttribute id="caption">Team</translatableAttribute>
</object>
<object name="teamResetButton" type="button" style="IconButton" sprite="iconResetGold" sprite_over="iconResetWhite" size="100%-21 0 100%-5 16" />
</object>
<object size="1 36 100%-1 100%">
<repeat count="8">
<object name="playerFrame[n]" size="0 0 100% 32" hidden="true">
<object name="playerBackgroundColor[n]" type="image"/>
<object name="playerName[n]" type="text" style="ModernLabelText" size="0 2 22% 30"/>
<object name="playerColor[n]" type="dropdown" style="ModernDropDown" size="22%+5 2 22%+33 30" sprite="" scrollbar="false" button_width="22" font="sans-stroke-14" tooltip_style="onscreenToolTip"/>
<object name="playerAssignment[n]" type="dropdown" style="ModernDropDown" size="22%+37 2 50%+35 30" tooltip_style="onscreenToolTip"/>
<object name="playerAssignmentText[n]" type="text" style="ModernLabelText" size="22%+37 0 50%+35 30"/>
<object name="playerConfig[n]" type="button" style="StoneButton" size="50%+40 4 50%+64 28" tooltip_style="onscreenToolTip" font="sans-bold-stroke-12" sprite="ModernGear" sprite_over="ModernGearHover" sprite_pressed="ModernGearPressed"/>
<object name="playerCiv[n]" type="dropdown" style="ModernDropDown" size="50%+69 2 85% 30" tooltip_style="onscreenToolTip" dropdown_size="424"/>
<object name="playerCivText[n]" type="text" style="ModernLabelText" size="50%+65 0 85% 30"/>
<object name="playerTeam[n]" type="dropdown" style="ModernDropDown" size="85%+5 2 100%-5 30" tooltip_style="onscreenToolTip"/>
<object name="playerTeamText[n]" type="text" style="ModernLabelText" size="85%+5 0 100%-5 100%"/>
</object>
</repeat>
</object>
</object>

View File

@ -0,0 +1,29 @@
/**
* Each property of this class is a class that inherits GameSettingControl and is
* instantiated by the PlayerSettingControlManager.
*/
class PlayerSettingControls
{
}
/**
* The purpose of the PlayerSettingControlManager is to own all controls that handle a property of g_GameAttributes.settings.PlayerData.
*/
class PlayerSettingControlManager
{
constructor(playerIndex, gamesetupPage, gameSettingsControl, mapCache, mapFilters, netMessages, playerAssignmentsControl)
{
this.playerSettingControls = {};
for (let name in PlayerSettingControls)
this.playerSettingControls[name] =
new PlayerSettingControls[name](
undefined, undefined, playerIndex, gamesetupPage, gameSettingsControl, mapCache, mapFilters, netMessages, playerAssignmentsControl);
}
addAutocompleteEntries(autocomplete)
{
for (let name in this.playerSettingControls)
this.playerSettingControls[name].addAutocompleteEntries(name, autocomplete);
}
}

View File

@ -0,0 +1,43 @@
/**
* Cheats are always enabled in singleplayer mode, since they are the choice of that one player.
*/
GameSettingControls.Cheats = class extends GameSettingControlCheckbox
{
constructor(...args)
{
super(...args);
this.setHidden(!g_IsNetworked);
}
onGameAttributesChange()
{
if (g_GameAttributes.settings.CheatsEnabled === undefined ||
g_GameAttributes.settings.CheatsEnabled && g_GameAttributes.settings.RatingEnabled ||
!g_GameAttributes.settings.CheatsEnabled && !g_IsNetworked)
{
g_GameAttributes.settings.CheatsEnabled = !g_IsNetworked;
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesBatchChange()
{
this.setChecked(g_GameAttributes.settings.CheatsEnabled);
this.setEnabled(!g_GameAttributes.settings.RatingEnabled);
}
onPress(checked)
{
g_GameAttributes.settings.CheatsEnabled =
!g_IsNetworked ||
checked && !g_GameAttributes.settings.RatingEnabled;
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
}
};
GameSettingControls.Cheats.prototype.TitleCaption =
translate("Cheats");
GameSettingControls.Cheats.prototype.Tooltip =
translate("Toggle the usability of cheats.");

View File

@ -0,0 +1,58 @@
GameSettingControls.ExploredMap = class extends GameSettingControlCheckbox
{
onMapChange(mapData)
{
let mapValue =
mapData &&
mapData.settings &&
mapData.settings.ExploreMap || undefined;
if (mapValue !== undefined && mapValue != g_GameAttributes.settings.ExploreMap)
{
g_GameAttributes.settings.ExploreMap = mapValue;
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesChange()
{
if (!g_GameAttributes.mapType)
return;
if (g_GameAttributes.settings.ExploreMap === undefined)
{
g_GameAttributes.settings.ExploreMap = !!g_GameAttributes.settings.RevealMap;
this.gameSettingsControl.updateGameAttributes();
}
else if (g_GameAttributes.settings.RevealMap &&
!g_GameAttributes.settings.ExploreMap)
{
g_GameAttributes.settings.ExploreMap = true;
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesBatchChange()
{
if (!g_GameAttributes.mapType)
return;
this.setChecked(g_GameAttributes.settings.ExploreMap);
this.setEnabled(g_GameAttributes.mapType != "scenario" && !g_GameAttributes.settings.RevealMap);
}
onPress(checked)
{
g_GameAttributes.settings.ExploreMap = checked;
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
}
};
GameSettingControls.ExploredMap.prototype.TitleCaption =
// Translation: Make sure to differentiate between the revealed map and explored map settings!
translate("Explored Map");
GameSettingControls.ExploredMap.prototype.Tooltip =
// Translation: Make sure to differentiate between the revealed map and explored map settings!
translate("Toggle explored map (see initial map).");

View File

@ -0,0 +1,61 @@
GameSettingControls.LastManStanding = class extends GameSettingControlCheckbox
{
onMapChange(mapData)
{
let mapValue =
mapData &&
mapData.settings &&
!mapData.settings.LockTeams &&
mapData.settings.LastManStanding;
if (mapValue !== undefined && mapValue != g_GameAttributes.settings.LastManStanding)
{
g_GameAttributes.settings.LastManStanding = mapValue;
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesChange()
{
if (!g_GameAttributes.mapType)
return;
this.available = !g_GameAttributes.settings.LockTeams;
if (this.available)
{
if (g_GameAttributes.settings.LastManStanding === undefined)
{
g_GameAttributes.settings.LastManStanding = false;
this.gameSettingsControl.updateGameAttributes();
}
}
else if (g_GameAttributes.settings.LastManStanding !== undefined)
{
delete g_GameAttributes.settings.LastManStanding;
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesBatchChange()
{
if (!g_GameAttributes.mapType)
return;
// Always display this, so that players are aware that there is this gamemode
this.setChecked(!!g_GameAttributes.settings.LastManStanding);
this.setEnabled(g_GameAttributes.mapType != "scenario" && !g_GameAttributes.settings.LockTeams);
}
onPress(checked)
{
g_GameAttributes.settings.LastManStanding = checked;
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
}
};
GameSettingControls.LastManStanding.prototype.TitleCaption =
translate("Last Man Standing");
GameSettingControls.LastManStanding.prototype.Tooltip =
translate("Toggle whether the last remaining player or the last remaining set of allies wins.");

View File

@ -0,0 +1,62 @@
GameSettingControls.LockedTeams = class extends GameSettingControlCheckbox
{
onMapChange(mapData)
{
let mapValue =
mapData &&
mapData.settings &&
!mapData.settings.LockTeams &&
mapData.settings.LastManStanding;
if (mapValue !== undefined && mapValue != g_GameAttributes.settings.LastManStanding)
{
g_GameAttributes.settings.LastManStanding = mapValue;
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesChange()
{
if (!g_GameAttributes.mapType)
return;
if (g_GameAttributes.settings.LockTeams === undefined ||
g_GameAttributes.settings.RatingEnabled && !g_GameAttributes.settings.LockTeams)
{
g_GameAttributes.settings.LockTeams = g_IsNetworked;
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesBatchChange()
{
if (!g_GameAttributes.mapType)
return;
this.setChecked(g_GameAttributes.settings.LockTeams);
this.setEnabled(
g_GameAttributes.mapType != "scenario" &&
!g_GameAttributes.settings.RatingEnabled);
}
onPress(checked)
{
g_GameAttributes.settings.LockTeams = checked;
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
}
};
GameSettingControls.LockedTeams.prototype.TitleCaption =
translate("Teams Locked");
GameSettingControls.LockedTeams.prototype.Tooltip =
translate("Toggle locked teams.");
/**
* In multiplayer mode, players negotiate teams before starting the match and
* expect to play the match with these teams unless explicitly stated otherwise during the match settings.
* For singleplayermode, preserve the historic default of open diplomacies.
*/
GameSettingControls.LockedTeams.prototype.DefaultValue = Engine.HasNetClient();

View File

@ -0,0 +1,63 @@
GameSettingControls.Nomad = class extends GameSettingControlCheckbox
{
onMapChange(mapData)
{
let available = g_GameAttributes.mapType == "random";
this.setHidden(!available);
if (!available)
return;
let mapValue =
mapData &&
mapData.settings &&
mapData.settings.Nomad;
if (mapValue !== undefined && mapValue != g_GameAttributes.settings.Nomad)
{
g_GameAttributes.settings.Nomad = mapValue;
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesChange()
{
if (!g_GameAttributes.mapType)
return;
if (g_GameAttributes.mapType == "random")
{
if (g_GameAttributes.settings.Nomad === undefined)
{
g_GameAttributes.settings.Nomad = false;
this.gameSettingsControl.updateGameAttributes();
}
}
else if (g_GameAttributes.settings.Nomad !== undefined)
{
delete g_GameAttributes.settings.Nomad;
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesBatchChange()
{
if (!g_GameAttributes.mapType)
return;
if (g_GameAttributes.mapType == "random")
this.setChecked(g_GameAttributes.settings.Nomad);
}
onPress(checked)
{
g_GameAttributes.settings.Nomad = checked;
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
}
};
GameSettingControls.Nomad.prototype.TitleCaption =
translate("Nomad");
GameSettingControls.Nomad.prototype.Tooltip =
translate("In Nomad mode, players start with only few units and have to find a suitable place to build their city. Ceasefire is recommended.");

View File

@ -0,0 +1,54 @@
GameSettingControls.Rating = class extends GameSettingControlCheckbox
{
constructor(...args)
{
super(...args);
this.hasXmppClient = Engine.HasXmppClient();
this.available = false;
}
onGameAttributesChange()
{
this.available = this.hasXmppClient && g_GameAttributes.settings.PlayerData.length == 2;
if (this.available)
{
if (g_GameAttributes.settings.RatingEnabled === undefined)
{
g_GameAttributes.settings.RatingEnabled = true;
this.gameSettingsControl.updateGameAttributes();
}
}
else if (g_GameAttributes.settings.RatingEnabled !== undefined)
{
delete g_GameAttributes.settings.RatingEnabled;
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesBatchChange()
{
this.setHidden(!this.available);
if (this.available)
this.setChecked(g_GameAttributes.settings.RatingEnabled);
}
onPress(checked)
{
g_GameAttributes.settings.RatingEnabled = checked;
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
}
onGameAttributesFinalize()
{
if (this.hasXmppClient)
Engine.SetRankedGame(!!g_GameAttributes.settings.RatingEnabled);
}
};
GameSettingControls.Rating.prototype.TitleCaption =
translate("Rated Game");
GameSettingControls.Rating.prototype.Tooltip =
translate("Toggle if this game will be rated for the leaderboard.");

View File

@ -0,0 +1,67 @@
GameSettingControls.RegicideGarrison = class extends GameSettingControlCheckbox
{
onMapChange(mapData)
{
this.setEnabled(g_GameAttributes.mapType != "scenario");
let mapValue =
mapData &&
mapData.settings &&
mapData.settings.VictoryConditions &&
mapData.settings.VictoryConditions.indexOf(this.RegicideName) != -1 &&
mapData.settings.RegicideGarrison;
if (mapValue !== undefined || !g_GameAttributes.settings || mapValue == g_GameAttributes.settings.RegicideGarrison)
return;
if (!g_GameAttributes.settings.VictoryConditions)
g_GameAttributes.settings.VictoryConditions = [];
if (g_GameAttributes.settings.VictoryConditions.indexOf(this.RegicideName) == -1)
g_GameAttributes.settings.VictoryConditions.push(this.RegicideName);
g_GameAttributes.settings.RegicideGarrison = mapValue;
this.gameSettingsControl.updateGameAttributes();
}
onGameAttributesChange()
{
if (!g_GameAttributes.settings.VictoryConditions)
return;
let available = g_GameAttributes.settings.VictoryConditions.indexOf(this.RegicideName) != -1;
this.setHidden(!available);
if (available)
{
if (g_GameAttributes.settings.RegicideGarrison === undefined)
{
g_GameAttributes.settings.RegicideGarrison = false;
this.gameSettingsControl.updateGameAttributes();
}
this.setChecked(g_GameAttributes.settings.RegicideGarrison);
}
else if (g_GameAttributes.settings.RegicideGarrison !== undefined)
{
delete g_GameAttributes.settings.RegicideGarrison;
this.gameSettingsControl.updateGameAttributes();
}
}
onPress(checked)
{
g_GameAttributes.settings.RegicideGarrison = checked;
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
}
};
GameSettingControls.RegicideGarrison.prototype.TitleCaption =
translate("Hero Garrison");
GameSettingControls.RegicideGarrison.prototype.Tooltip =
translate("Toggle whether heroes can be garrisoned.");
GameSettingControls.RegicideGarrison.prototype.RegicideName =
"regicide";

View File

@ -0,0 +1,53 @@
GameSettingControls.RevealedMap = class extends GameSettingControlCheckbox
{
onMapChange(mapData)
{
let mapValue =
mapData &&
mapData.settings &&
mapData.settings.RevealMap || undefined;
if (mapValue !== undefined && mapValue != g_GameAttributes.settings.RevealMap)
{
g_GameAttributes.settings.RevealMap = mapValue;
this.gameSettingsControl.updateGameAttributes();
}
this.setEnabled(g_GameAttributes.mapType != "scenario");
}
onGameAttributesChange()
{
if (!g_GameAttributes.mapType)
return;
if (g_GameAttributes.settings.RevealMap === undefined)
{
g_GameAttributes.settings.RevealMap = false;
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesBatchChange()
{
if (!g_GameAttributes.mapType)
return;
this.setChecked(g_GameAttributes.settings.RevealMap);
}
onPress(checked)
{
g_GameAttributes.settings.RevealMap = checked;
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
}
};
GameSettingControls.RevealedMap.prototype.TitleCaption =
// Translation: Make sure to differentiate between the revealed map and explored map settings!
translate("Revealed Map");
GameSettingControls.RevealedMap.prototype.Tooltip =
// Translation: Make sure to differentiate between the revealed map and explored map settings!
translate("Toggle revealed map (see everything).");

View File

@ -0,0 +1,45 @@
GameSettingControls.Spies = class extends GameSettingControlCheckbox
{
onMapChange(mapData)
{
let mapValue =
mapData &&
mapData.settings &&
mapData.settings.DisableSpies;
if (mapValue !== undefined && mapValue != g_GameAttributes.settings.DisableSpies)
{
g_GameAttributes.settings.DisableSpies = mapValue;
this.gameSettingsControl.updateGameAttributes();
}
this.setEnabled(g_GameAttributes.mapType != "scenario");
}
onGameAttributesChange()
{
if (g_GameAttributes.settings.DisableSpies === undefined)
{
g_GameAttributes.settings.DisableSpies = false;
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesBatchChange()
{
this.setChecked(g_GameAttributes.settings.DisableSpies);
}
onPress(checked)
{
g_GameAttributes.settings.DisableSpies = checked;
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
}
};
GameSettingControls.Spies.prototype.TitleCaption =
translate("Disable Spies");
GameSettingControls.Spies.prototype.Tooltip =
translate("Disable spies during the game.");

View File

@ -0,0 +1,45 @@
GameSettingControls.Treasures = class extends GameSettingControlCheckbox
{
onMapChange(mapData)
{
let mapValue =
mapData &&
mapData.settings &&
mapData.settings.DisableTreasures;
if (mapValue !== undefined && mapValue != g_GameAttributes.settings.DisableTreasures)
{
g_GameAttributes.settings.DisableTreasures = mapValue;
this.gameSettingsControl.updateGameAttributes();
}
this.setEnabled(g_GameAttributes.mapType != "scenario");
}
onGameAttributesChange()
{
if (g_GameAttributes.settings.DisableTreasures === undefined)
{
g_GameAttributes.settings.DisableTreasures = false;
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesBatchChange()
{
this.setChecked(g_GameAttributes.settings.DisableTreasures);
}
onPress(checked)
{
g_GameAttributes.settings.DisableTreasures = checked;
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
}
};
GameSettingControls.Treasures.prototype.TitleCaption =
translate("Disable Treasures");
GameSettingControls.Treasures.prototype.Tooltip =
translate("Do not add treasures to the map.");

View File

@ -0,0 +1,133 @@
GameSettingControls.Biome = class extends GameSettingControlDropdown
{
constructor(...args)
{
super(...args);
this.values = undefined;
this.biomeValues = undefined;
this.randomItem = {
"Id": this.RandomBiomeId,
"Title": setStringTags(this.RandomBiome, this.RandomItemTags),
"Autocomplete": this.RandomBiome,
"Description": this.RandomDescription
};
}
onHoverChange()
{
this.dropdown.tooltip =
this.values && this.values.Description && this.values.Description[this.dropdown.hovered] ||
this.Tooltip;
}
onMapChange(mapData)
{
let available = g_GameAttributes.mapType == "random" &&
mapData && mapData.settings && mapData.settings.SupportedBiomes || undefined;
this.setHidden(!available);
if (available)
{
Engine.ProfileStart("updateBiomeList");
this.biomeValues =
g_Settings.Biomes.filter(this.filterBiome(available)).map(this.createBiomeItem);
this.values = prepareForDropdown([
this.randomItem,
...this.biomeValues
]);
this.dropdown.list = this.values.Title;
this.dropdown.list_data = this.values.Id;
Engine.ProfileStop();
}
else
this.values = undefined;
}
onGameAttributesChange()
{
if (!g_GameAttributes.mapType)
return;
if (this.values)
{
if (this.values.Id.indexOf(g_GameAttributes.settings.Biome || undefined) == -1)
{
g_GameAttributes.settings.Biome = this.RandomBiomeId;
this.gameSettingsControl.updateGameAttributes();
}
}
else if (g_GameAttributes.settings.Biome)
{
delete g_GameAttributes.settings.Biome;
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesBatchChange()
{
if (this.values)
this.setSelectedValue(g_GameAttributes.settings.Biome);
}
filterBiome(available)
{
if (typeof available == "string")
return biome => biome.Id.startsWith(available);
return biome => available.indexOf(biome.Id) != -1;
}
createBiomeItem(biome)
{
return {
"Id": biome.Id,
"Title": biome.Title,
"Autocomplete": biome.Title,
"Description": biome.Description
};
}
getAutocompleteEntries()
{
return this.values && this.values.Autocomplete;
}
onSelectionChange(itemIdx)
{
g_GameAttributes.settings.Biome = this.values.Id[itemIdx];
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
}
onPickRandomItems()
{
if (this.values && g_GameAttributes.settings.Biome == this.RandomBiomeId)
{
g_GameAttributes.settings.Biome = pickRandom(this.biomeValues).Id;
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.pickRandomItems();
}
}
};
GameSettingControls.Biome.prototype.TitleCaption =
translate("Biome");
GameSettingControls.Biome.prototype.RandomBiomeId =
"random";
GameSettingControls.Biome.prototype.Tooltip =
translate("Select the flora and fauna.");
GameSettingControls.Biome.prototype.RandomBiome =
translateWithContext("biome", "Random");
GameSettingControls.Biome.prototype.RandomDescription =
translate("Pick a biome at random.");
GameSettingControls.Biome.prototype.AutocompleteOrder = 0;

View File

@ -0,0 +1,55 @@
GameSettingControls.Ceasefire = class extends GameSettingControlDropdown
{
constructor(...args)
{
super(...args);
this.values = prepareForDropdown(g_Settings.Ceasefire);
this.dropdown.list = this.values.Title;
this.dropdown.list_data = this.values.Duration;
}
onMapChange(mapData)
{
let mapValue =
mapData &&
mapData.settings &&
mapData.settings.Ceasefire || undefined;
if (mapValue !== undefined && mapValue != g_GameAttributes.settings.Ceasefire)
{
g_GameAttributes.settings.Ceasefire = mapValue;
this.gameSettingsControl.updateGameAttributes();
}
this.setEnabled(g_GameAttributes.mapType != "scenario");
}
onGameAttributesChange()
{
if (g_GameAttributes.settings.Ceasefire == undefined)
{
g_GameAttributes.settings.Ceasefire = this.values.Default;
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesBatchChange()
{
this.setSelectedValue(g_GameAttributes.settings.Ceasefire);
}
onSelectionChange(itemIdx)
{
g_GameAttributes.settings.Ceasefire = this.values.Duration[itemIdx];
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
}
};
GameSettingControls.Ceasefire.prototype.TitleCaption =
translate("Ceasefire");
GameSettingControls.Ceasefire.prototype.Tooltip =
translate("Set time where no attacks are possible.");

View File

@ -0,0 +1,76 @@
GameSettingControls.GameSpeed = class extends GameSettingControlDropdown
{
constructor(...args)
{
super(...args);
this.previousAllowFastForward = undefined;
}
onMapChange(mapData)
{
let mapValue = mapData && mapData.gameSpeed || undefined;
if (mapValue !== undefined && mapValue != g_GameAttributes.gameSpeed)
{
g_GameAttributes.gameSpeed = mapValue;
this.gameSettingsControl.updateGameAttributes();
}
}
onPlayerAssignmentsChange()
{
this.update();
}
onGameAttributesChange()
{
this.update();
}
update()
{
let allowFastForward = true;
for (let guid in g_PlayerAssignments)
if (g_PlayerAssignments[guid].player != -1)
{
allowFastForward = false;
break;
}
if (this.previousAllowFastForward !== allowFastForward)
{
Engine.ProfileStart("updateGameSpeedList");
this.previousAllowFastForward = allowFastForward;
this.values = prepareForDropdown(
g_Settings.GameSpeeds.filter(speed => !speed.FastForward || allowFastForward));
this.dropdown.list = this.values.Title;
this.dropdown.list_data = this.values.Speed;
Engine.ProfileStop();
}
if (this.values.Speed.indexOf(g_GameAttributes.gameSpeed || undefined) == -1)
{
g_GameAttributes.gameSpeed = this.values.Speed[this.values.Default];
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesBatchChange()
{
this.setSelectedValue(g_GameAttributes.gameSpeed);
}
onSelectionChange(itemIdx)
{
g_GameAttributes.gameSpeed = this.values.Speed[itemIdx];
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
}
};
GameSettingControls.GameSpeed.prototype.TitleCaption =
translate("Game Speed");
GameSettingControls.GameSpeed.prototype.Tooltip =
translate("Select game speed.");

View File

@ -0,0 +1,94 @@
GameSettingControls.MapFilter = class extends GameSettingControlDropdown
{
constructor(...args)
{
super(...args);
this.values = undefined;
this.previousMapType = undefined;
}
onHoverChange()
{
this.dropdown.tooltip = this.values.Description[this.dropdown.hovered] || this.Tooltip;
}
onGameAttributesChange()
{
this.checkMapTypeChange();
if (this.values)
{
if (this.values.Name.indexOf(g_GameAttributes.mapFilter || undefined) == -1)
{
g_GameAttributes.mapFilter = this.values.Name[this.values.Default];
this.gameSettingsControl.updateGameAttributes();
}
}
else if (g_GameAttributes.mapFilter !== undefined)
{
delete g_GameAttributes.mapFilter;
this.gameSettingsControl.updateGameAttributes();
}
}
checkMapTypeChange()
{
if (!g_GameAttributes.mapType || this.previousMapType == g_GameAttributes.mapType)
return;
Engine.ProfileStart("updateMapFilterList");
this.previousMapType = g_GameAttributes.mapType;
let values = prepareForDropdown(
this.mapFilters.getAvailableMapFilters(
g_GameAttributes.mapType));
if (values.Name.length)
{
this.dropdown.list = values.Title;
this.dropdown.list_data = values.Name;
this.values = values;
}
else
this.values = undefined;
this.setHidden(!this.values);
Engine.ProfileStop();
}
onGameAttributesBatchChange()
{
if (this.values && g_GameAttributes.mapFilter)
this.setSelectedValue(g_GameAttributes.mapFilter);
}
getAutocompleteEntries()
{
return this.values && this.values.Title;
}
onSelectionChange(itemIdx)
{
if (this.values)
{
g_GameAttributes.mapFilter = this.values.Name[itemIdx];
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
}
}
onGameAttributesFinalize()
{
// The setting is only relevant to the gamesetup stage!
delete g_GameAttributes.mapFilter;
}
};
GameSettingControls.MapFilter.prototype.TitleCaption =
translate("Map Filter");
GameSettingControls.MapFilter.prototype.Tooltip =
translate("Select a map filter.");
GameSettingControls.MapFilter.prototype.AutocompleteOrder = 0;

View File

@ -0,0 +1,171 @@
GameSettingControls.MapSelection = class extends GameSettingControlDropdown
{
constructor(...args)
{
super(...args);
this.values = undefined;
this.previousMapType = undefined;
this.previousMapFilter = undefined;
this.randomItem = {
"file": this.RandomMapId,
"name": setStringTags(this.RandomMapCaption, this.RandomItemTags),
"description": this.RandomMapDescription
};
}
onHoverChange()
{
this.dropdown.tooltip = this.values.description[this.dropdown.hovered] || this.Tooltip;
}
onGameAttributesChange()
{
if (!g_GameAttributes.mapType || !g_GameAttributes.mapFilter)
return;
this.updateMapList();
if (!this.gameSettingsControl.autostart &&
this.values &&
this.values.file.indexOf(g_GameAttributes.map || undefined) == -1)
{
g_GameAttributes.map = this.values.file[0];
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesBatchChange()
{
if (g_GameAttributes.map)
this.setSelectedValue(g_GameAttributes.map);
}
updateMapList()
{
if (this.previousMapType &&
this.previousMapType == g_GameAttributes.mapType &&
this.previousMapFilter &&
this.previousMapFilter == g_GameAttributes.mapFilter)
return;
Engine.ProfileStart("updateMapSelectionList");
this.previousMapType = g_GameAttributes.mapType;
this.previousMapFilter = g_GameAttributes.mapFilter;
{
let values =
this.mapFilters.getFilteredMaps(
g_GameAttributes.mapType,
g_GameAttributes.mapFilter,
false);
values.sort(sortNameIgnoreCase);
if (g_GameAttributes.mapType == "random")
values.unshift(this.randomItem);
this.values = prepareForDropdown(values);
}
this.dropdown.list = this.values.name;
this.dropdown.list_data = this.values.file;
Engine.ProfileStop();
}
onSelectionChange(itemIdx)
{
this.selectMap(this.values.file[itemIdx]);
this.gameSettingsControl.setNetworkGameAttributes();
}
/**
* @param mapPath - for example "maps/skirmishes/Acropolis Bay (2)"
*/
selectMap(mapPath)
{
if (g_GameAttributes.map == mapPath)
return;
Engine.ProfileStart("selectMap");
// For scenario map, reset every setting per map selection
// For skirmish and random maps, persist player choices
if (g_GameAttributes.mapType == "scenario")
g_GameAttributes = {
"mapType": g_GameAttributes.mapType,
"mapFilter": g_GameAttributes.mapFilter
};
g_GameAttributes.map = mapPath;
this.gameSettingsControl.updateGameAttributes();
Engine.ProfileStop();
}
getAutocompleteEntries()
{
return this.values.name;
}
onPickRandomItems()
{
if (g_GameAttributes.map == this.RandomMapId)
{
this.selectMap(pickRandom(this.values.file.slice(1)));
this.gameSettingsControl.pickRandomItems();
}
}
onGameAttributesFinalize()
{
// Copy map well known properties (and only well known properties)
let mapData = this.mapCache.getMapData(g_GameAttributes.mapType, g_GameAttributes.map);
g_GameAttributes.settings.CircularMap =
mapData.settings.CircularMap || true;
if (g_GameAttributes.mapType == "random")
g_GameAttributes.script = mapData.settings.Script;
g_GameAttributes.settings.TriggerScripts = Array.from(new Set([
...(g_GameAttributes.settings.TriggerScripts || []),
...(mapData.settings.TriggerScripts || [])
]));
for (let property of this.MapSettings)
if (mapData.settings[property] !== undefined)
g_GameAttributes.settings[property] = mapData.settings[property];
}
};
GameSettingControls.MapSelection.prototype.TitleCaption =
translate("Select Map");
GameSettingControls.MapSelection.prototype.Tooltip =
translate("Select a map to play on.");
GameSettingControls.MapSelection.prototype.RandomMapId =
"random";
GameSettingControls.MapSelection.prototype.RandomMapCaption =
translateWithContext("map selection", "Random");
GameSettingControls.MapSelection.prototype.RandomMapDescription =
translate("Pick any of the given maps at random.");
GameSettingControls.MapSelection.prototype.AutocompleteOrder = 0;
GameSettingControls.MapSelection.prototype.MapSettings = [
"StartingTechnologies",
"DisabledTechnologies",
"DisabledTemplates",
"StartingCamera",
"Garrison",
// Copy the map name so that the replay menu doesn't have to load hundreds of map descriptions on init
// Also it allows determining the english mapname from the replay file if the map is not available.
"Name"
];

View File

@ -0,0 +1,79 @@
GameSettingControls.MapSize = class extends GameSettingControlDropdown
{
constructor(...args)
{
super(...args);
this.previousMapType = undefined;
this.dropdown.list = g_MapSizes.Name;
this.dropdown.list_data = g_MapSizes.Tiles;
}
onHoverChange()
{
this.dropdown.tooltip = g_MapSizes.Tooltip[this.dropdown.hovered] || this.Tooltip;
}
onMapChange(mapData)
{
let mapValue =
mapData &&
mapData.settings &&
mapData.settings.Size || undefined;
if (g_GameAttributes.mapType == "random" && mapValue !== undefined && mapValue != g_GameAttributes.settings.Size)
{
g_GameAttributes.settings.Size = mapValue;
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesChange()
{
if (!g_GameAttributes.mapType ||
!g_GameAttributes.settings ||
this.previousMapType == g_GameAttributes.mapType)
return;
this.previousMapType = g_GameAttributes.mapType;
let available = g_GameAttributes.mapType == "random";
this.setHidden(!available);
if (available)
{
if (g_GameAttributes.settings.Size === undefined)
{
g_GameAttributes.settings.Size = g_MapSizes.Tiles[g_MapSizes.Default];
this.gameSettingsControl.updateGameAttributes();
}
this.setSelectedValue(g_GameAttributes.settings.Size);
}
else if (g_GameAttributes.settings.Size !== undefined)
{
delete g_GameAttributes.settings.Size;
this.gameSettingsControl.updateGameAttributes();
}
}
getAutocompleteEntries()
{
return g_MapSizes.Name;
}
onSelectionChange(itemIdx)
{
g_GameAttributes.settings.Size = g_MapSizes.Tiles[itemIdx];
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
}
};
GameSettingControls.MapSize.prototype.TitleCaption =
translate("Map Size");
GameSettingControls.MapSize.prototype.Tooltip =
translate("Select map size. (Larger sizes may reduce performance.)");
GameSettingControls.MapSize.prototype.AutocompleteOrder = 0;

View File

@ -0,0 +1,66 @@
/**
* Maptype design:
* Scenario maps have fixed terrain and all settings predetermined.
* Skirmish maps have fixed terrain, playercount but settings are free.
* For random maps, settings are fully determined by the player and the terrain is generated based on them.
*/
GameSettingControls.MapType = class extends GameSettingControlDropdown
{
constructor(...args)
{
super(...args);
this.dropdown.list = g_MapTypes.Title;
this.dropdown.list_data = g_MapTypes.Name;
}
onHoverChange()
{
this.dropdown.tooltip = g_MapTypes.Description[this.dropdown.hovered] || this.Tooltip;
}
onGameAttributesChange()
{
if (g_MapTypes.Name.indexOf(g_GameAttributes.mapType || undefined) == -1)
{
g_GameAttributes.mapType = g_MapTypes.Name[g_MapTypes.Default];
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesBatchChange()
{
this.setSelectedValue(g_GameAttributes.mapType);
}
getAutocompleteEntries()
{
return g_MapTypes.Title;
}
onSelectionChange(itemIdx)
{
let mapType = g_MapTypes.Name[itemIdx];
if (g_GameAttributes.mapType == mapType)
return;
if (mapType == "scenario")
g_GameAttributes = {
"mapFilter": g_GameAttributes.mapFilter
};
g_GameAttributes.mapType = mapType;
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
}
};
GameSettingControls.MapType.prototype.TitleCaption =
translate("Map Type");
GameSettingControls.MapType.prototype.Tooltip =
translate("Select a map type.");
GameSettingControls.MapType.prototype.AutocompleteOrder = 0;

View File

@ -0,0 +1,52 @@
GameSettingControls.PlayerCount = class extends GameSettingControlDropdown
{
constructor(...args)
{
super(...args);
this.values = Array.from(
new Array(g_MaxPlayers),
(v, i) => i + 1);
this.dropdown.list = this.values;
this.dropdown.list_data = this.values;
}
onMapChange(mapData)
{
if (mapData &&
mapData.settings &&
mapData.settings.PlayerData &&
mapData.settings.PlayerData.length != g_GameAttributes.settings.PlayerData.length)
{
this.onSelectionChange(this.values.indexOf(mapData.settings.PlayerData.length));
}
this.setEnabled(g_GameAttributes.mapType == "random");
}
onGameAttributesBatchChange()
{
this.setSelectedValue(g_GameAttributes.settings.PlayerData.length);
}
onSelectionChange(itemIdx)
{
let length = this.values[itemIdx];
if (g_GameAttributes.settings.PlayerData.length == length)
return;
g_GameAttributes.settings.PlayerData.length = length;
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
this.playerAssignmentsControl.unassignInvalidPlayers();
}
};
GameSettingControls.PlayerCount.prototype.TitleCaption =
translate("Number of Players");
GameSettingControls.PlayerCount.prototype.Tooltip =
translate("Select number of players.");

View File

@ -0,0 +1,101 @@
GameSettingControls.PopulationCap = class extends GameSettingControlDropdown
{
constructor(...args)
{
super(...args);
this.perPlayer = false;
this.dropdown.list = g_PopulationCapacities.Title;
this.dropdown.list_data = g_PopulationCapacities.Population;
this.sprintfArgs = {};
}
onMapChange(mapData)
{
let mapValue =
mapData &&
mapData.settings &&
mapData.settings.PopulationCap || undefined;
if (mapValue !== undefined && mapValue != g_GameAttributes.settings.PopulationCap)
{
g_GameAttributes.settings.PopulationCap = mapValue;
this.gameSettingsControl.updateGameAttributes();
}
let isScenario = g_GameAttributes.mapType == "scenario";
this.perPlayer =
isScenario &&
mapData.settings.PlayerData &&
mapData.settings.PlayerData.some(pData => pData && pData.PopulationLimit !== undefined);
this.setEnabled(!isScenario && !this.perPlayer);
if (this.perPlayer)
this.label.caption = this.PerPlayerCaption;
}
onGameAttributesChange()
{
if (g_GameAttributes.settings.PopulationCap === undefined)
{
g_GameAttributes.settings.PopulationCap = g_PopulationCapacities.Population[g_PopulationCapacities.Default];
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesBatchChange()
{
if (!this.perPlayer)
this.setSelectedValue(g_GameAttributes.settings.PopulationCap);
}
onHoverChange()
{
let tooltip = this.Tooltip;
if (this.dropdown.hovered != -1)
{
let popCap = g_PopulationCapacities.Population[this.dropdown.hovered];
let players = g_GameAttributes.settings.PlayerData.length;
if (popCap * players >= this.PopulationCapacityRecommendation)
{
this.sprintfArgs.players = players;
this.sprintfArgs.popCap = popCap;
tooltip = setStringTags(sprintf(this.HoverTooltip, this.sprintfArgs), this.HoverTags);
}
}
this.dropdown.tooltip = tooltip;
}
onSelectionChange(itemIdx)
{
g_GameAttributes.settings.PopulationCap = g_PopulationCapacities.Population[itemIdx];
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
}
};
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.HoverTags = {
"color": "orange"
};
/**
* Total number of units that the engine can run with smoothly.
* It means a 4v4 with 150 population can still run nicely, but more than that might "lag".
*/
GameSettingControls.PopulationCap.prototype.PopulationCapacityRecommendation = 1200;

View File

@ -0,0 +1,85 @@
GameSettingControls.RelicCount = class extends GameSettingControlDropdown
{
constructor(...args)
{
super(...args);
this.values = Array.from(new Array(g_CivData.length), (v, i) => i + 1);
this.dropdown.list = this.values;
this.dropdown.list_data = this.values;
this.available = false;
}
onMapChange(mapData)
{
this.setEnabled(g_GameAttributes.mapType != "scenario");
let mapValue =
mapData &&
mapData.settings &&
mapData.settings.VictoryConditions &&
mapData.settings.VictoryConditions.indexOf(this.NameCaptureTheRelic) != -1 &&
mapData.settings.RelicCount || undefined;
if (mapValue === undefined || mapValue == g_GameAttributes.settings.RelicCount)
return;
if (!g_GameAttributes.settings.VictoryConditions)
g_GameAttributes.settings.VictoryConditions = [];
if (g_GameAttributes.settings.VictoryConditions.indexOf(this.NameCaptureTheRelic) == -1)
g_GameAttributes.settings.VictoryConditions.push(this.NameCaptureTheRelic);
g_GameAttributes.settings.RelicCount = mapValue;
this.gameSettingsControl.updateGameAttributes();
}
onGameAttributesChange()
{
if (!g_GameAttributes.settings.VictoryConditions)
return;
this.available = g_GameAttributes.settings.VictoryConditions.indexOf(this.NameCaptureTheRelic) != -1;
if (this.available)
{
if (g_GameAttributes.settings.RelicCount === undefined)
{
g_GameAttributes.settings.RelicCount = this.DefaultRelicCount;
this.gameSettingsControl.updateGameAttributes();
}
}
else if (g_GameAttributes.settings.RelicCount !== undefined)
{
delete g_GameAttributes.settings.RelicCount;
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesBatchChange()
{
this.setHidden(!this.available);
if (this.available)
this.setSelectedValue(g_GameAttributes.settings.RelicCount);
}
onSelectionChange(itemIdx)
{
g_GameAttributes.settings.RelicCount = this.values[itemIdx];
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
}
};
GameSettingControls.RelicCount.prototype.TitleCaption =
translate("Relic Count");
GameSettingControls.RelicCount.prototype.Tooltip =
translate("Total number of relics spawned on the map. Relic victory is most realistic with only one or two relics. With greater numbers, the relics are important to capture to receive aura bonuses.");
GameSettingControls.RelicCount.prototype.NameCaptureTheRelic =
this.NameCaptureTheRelic;
GameSettingControls.RelicCount.prototype.DefaultRelicCount = 2;

View File

@ -0,0 +1,85 @@
GameSettingControls.RelicDuration = class extends GameSettingControlDropdown
{
constructor(...args)
{
super(...args);
this.values = prepareForDropdown(g_Settings.VictoryDurations);
this.dropdown.list = this.values.Title;
this.dropdown.list_data = this.values.Duration;
this.available = false;
}
onMapChange(mapData)
{
this.setEnabled(g_GameAttributes.mapType != "scenario");
let mapValue =
mapData &&
mapData.settings &&
mapData.settings.VictoryConditions &&
mapData.settings.VictoryConditions.indexOf(this.NameCaptureTheRelic) != -1 &&
mapData.settings.RelicDuration || undefined;
if (mapValue === undefined || mapValue == g_GameAttributes.settings.RelicDuration)
return;
if (!g_GameAttributes.settings.VictoryConditions)
g_GameAttributes.settings.VictoryConditions = [];
if (g_GameAttributes.settings.VictoryConditions.indexOf(this.NameCaptureTheRelic) == -1)
g_GameAttributes.settings.VictoryConditions.push(this.NameCaptureTheRelic);
g_GameAttributes.settings.RelicDuration = mapValue;
this.gameSettingsControl.updateGameAttributes();
}
onGameAttributesChange()
{
if (!g_GameAttributes.settings.VictoryConditions)
return;
this.available = g_GameAttributes.settings.VictoryConditions.indexOf(this.NameCaptureTheRelic) != -1;
if (this.available)
{
if (g_GameAttributes.settings.RelicDuration === undefined)
{
g_GameAttributes.settings.RelicDuration = this.values.Duration[this.values.Default];
this.gameSettingsControl.updateGameAttributes();
}
}
else if (g_GameAttributes.settings.RelicDuration !== undefined)
{
delete g_GameAttributes.settings.RelicDuration;
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesBatchChange()
{
this.setHidden(!this.available);
if (this.available)
this.setSelectedValue(g_GameAttributes.settings.RelicDuration);
}
onSelectionChange(itemIdx)
{
g_GameAttributes.settings.RelicDuration = this.values.Duration[itemIdx];
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
}
};
GameSettingControls.RelicDuration.prototype.TitleCaption =
translate("Relic Duration");
GameSettingControls.RelicDuration.prototype.Tooltip =
translate("Minutes until the player has achieved Relic Victory.");
GameSettingControls.RelicDuration.prototype.NameCaptureTheRelic =
"capture_the_relic";

View File

@ -0,0 +1,92 @@
GameSettingControls.StartingResources = class extends GameSettingControlDropdown
{
constructor(...args)
{
super(...args);
this.dropdown.list = g_StartingResources.Title;
this.dropdown.list_data = g_StartingResources.Resources;
this.perPlayer = false;
this.sprintfArgs = {};
}
onHoverChange()
{
let tooltip = this.Tooltip;
if (this.dropdown.hovered != -1)
{
this.sprintfArgs.resources = g_StartingResources.Resources[this.dropdown.hovered];
tooltip = sprintf(this.HoverTooltip, this.sprintfArgs);
}
this.dropdown.tooltip = tooltip;
}
onMapChange(mapData)
{
let mapValue =
mapData &&
mapData.settings &&
mapData.settings.StartingResources || undefined;
if (mapValue !== undefined && mapValue != g_GameAttributes.settings.StartingResources)
{
g_GameAttributes.settings.StartingResources = mapValue;
this.gameSettingsControl.updateGameAttributes();
}
let isScenario = g_GameAttributes.mapType == "scenario";
this.perPlayer =
isScenario &&
mapData.settings.PlayerData &&
mapData.settings.PlayerData.some(pData => pData && pData.Resources !== undefined);
this.setEnabled(!isScenario && !this.perPlayer);
if (this.perPlayer)
this.label.caption = this.PerPlayerCaption;
}
onGameAttributesChange()
{
if (g_GameAttributes.settings.StartingResources === undefined)
{
g_GameAttributes.settings.StartingResources =
g_StartingResources.Resources[g_StartingResources.Default];
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesBatchChange()
{
if (!this.perPlayer)
this.setSelectedValue(g_GameAttributes.settings.StartingResources);
}
getAutocompleteEntries()
{
return g_StartingResources.Title;
}
onSelectionChange(itemIdx)
{
g_GameAttributes.settings.StartingResources = g_StartingResources.Resources[itemIdx];
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
}
};
GameSettingControls.StartingResources.prototype.TitleCaption =
translate("Starting Resources");
GameSettingControls.StartingResources.prototype.Tooltip =
translate("Select the game's starting resources.");
GameSettingControls.StartingResources.prototype.HoverTooltip =
translate("Initial amount of each resource: %(resources)s.");
GameSettingControls.StartingResources.prototype.PerPlayerCaption =
translateWithContext("starting resources", "Per Player");
GameSettingControls.StartingResources.prototype.AutocompleteOrder = 0;

View File

@ -0,0 +1,84 @@
GameSettingControls.TriggerDifficulty = class extends GameSettingControlDropdown
{
constructor(...args)
{
super(...args);
this.values = undefined;
}
onHoverChange()
{
this.dropdown.tooltip =
this.values && this.values.Tooltip[this.dropdown.hovered] ||
this.Tooltip;
}
onMapChange(mapData)
{
let available = mapData && mapData.settings && mapData.settings.SupportedTriggerDifficulties || undefined;
this.setHidden(!available);
if (available)
{
Engine.ProfileStart("updateTriggerDifficultyList");
let values = g_Settings.TriggerDifficulties;
if (Array.isArray(available.Values))
values = values.filter(diff => available.Values.indexOf(diff.Name) != -1);
this.values = prepareForDropdown(values);
this.dropdown.list = this.values.Title;
this.dropdown.list_data = this.values.Difficulty;
if (mapData.settings.TriggerDifficulty &&
this.values.Difficulty.indexOf(mapData.settings.TriggerDifficulty) != -1)
g_GameAttributes.settings.TriggerDifficulty = mapData.settings.TriggerDifficulty;
Engine.ProfileStop();
}
else
{
this.values = undefined;
this.defaultValue = undefined;
}
}
onGameAttributesChange()
{
if (!g_GameAttributes.mapType)
return;
if (this.values)
{
if (this.values.Difficulty.indexOf(g_GameAttributes.settings.TriggerDifficulty || undefined) == -1)
{
g_GameAttributes.settings.TriggerDifficulty = this.values.Difficulty[this.values.Default];
this.gameSettingsControl.updateGameAttributes();
}
}
else if (g_GameAttributes.settings.TriggerDifficulty !== undefined)
{
delete g_GameAttributes.settings.TriggerDifficulty;
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesBatchChange()
{
if (this.values)
this.setSelectedValue(g_GameAttributes.settings.TriggerDifficulty);
}
onSelectionChange(itemIdx)
{
g_GameAttributes.settings.TriggerDifficulty = this.values.Difficulty[itemIdx];
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
}
};
GameSettingControls.TriggerDifficulty.prototype.TitleCaption =
translate("Difficulty");
GameSettingControls.TriggerDifficulty.prototype.Tooltip =
translate("Select the difficulty of this scenario.");

View File

@ -0,0 +1,85 @@
GameSettingControls.WonderDuration = class extends GameSettingControlDropdown
{
constructor(...args)
{
super(...args);
this.values = prepareForDropdown(g_Settings.VictoryDurations);
this.dropdown.list = this.values.Title;
this.dropdown.list_data = this.values.Duration;
this.available = false;
}
onMapChange(mapData)
{
this.setEnabled(g_GameAttributes.mapType != "scenario");
let mapValue =
mapData &&
mapData.settings &&
mapData.settings.VictoryConditions &&
mapData.settings.VictoryConditions.indexOf(this.NameWonderVictory) != -1 &&
mapData.settings.WonderDuration || undefined;
if (mapValue === undefined || mapValue == g_GameAttributes.settings.WonderDuration)
return;
if (!g_GameAttributes.settings.VictoryConditions)
g_GameAttributes.settings.VictoryConditions = [];
if (g_GameAttributes.settings.VictoryConditions.indexOf(this.NameWonderVictory) == -1)
g_GameAttributes.settings.VictoryConditions.push(this.NameWonderVictory);
g_GameAttributes.settings.WonderDuration = mapValue;
this.gameSettingsControl.updateGameAttributes();
}
onGameAttributesChange()
{
if (!g_GameAttributes.settings.VictoryConditions)
return;
this.available = g_GameAttributes.settings.VictoryConditions.indexOf(this.NameWonderVictory) != -1;
if (this.available)
{
if (g_GameAttributes.settings.WonderDuration === undefined)
{
g_GameAttributes.settings.WonderDuration = this.values.Duration[this.values.Default];
this.gameSettingsControl.updateGameAttributes();
}
}
else if (g_GameAttributes.settings.WonderDuration !== undefined)
{
delete g_GameAttributes.settings.WonderDuration;
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesBatchChange()
{
this.setHidden(!this.available);
if (this.available)
this.setSelectedValue(g_GameAttributes.settings.WonderDuration);
}
onSelectionChange(itemIdx)
{
g_GameAttributes.settings.WonderDuration = this.values.Duration[itemIdx];
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
}
};
GameSettingControls.WonderDuration.prototype.TitleCaption =
translate("Wonder Duration");
GameSettingControls.WonderDuration.prototype.Tooltip =
translate("Minutes until the player has achieved Wonder Victory");
GameSettingControls.WonderDuration.prototype.NameWonderVictory =
"wonder";

View File

@ -0,0 +1,12 @@
GameSettingControls.Seed = class extends GameSettingControl
{
onGameAttributesFinalize()
{
// The matchID is used for identifying rated game reports for the lobby and possibly when sharing replays.
g_GameAttributes.matchID = Engine.GetMatchID();
// Seed is used for map generation and simulation.
g_GameAttributes.settings.Seed = randIntExclusive(0, Math.pow(2, 32));
g_GameAttributes.settings.AISeed = randIntExclusive(0, Math.pow(2, 32));
}
};

View File

@ -0,0 +1,105 @@
/**
* This is an abstract class instantiated per defined VictoryCondition.
*/
class VictoryConditionCheckbox extends GameSettingControlCheckbox
{
constructor(victoryCondition, ...args)
{
super(...args);
this.victoryCondition = victoryCondition;
this.setTitle(this.victoryCondition.Title);
this.setTooltip(this.victoryCondition.Description);
}
onMapChange(mapData)
{
let mapIndex =
mapData &&
mapData.settings &&
mapData.settings.VictoryConditions &&
mapData.settings.VictoryConditions.indexOf(this.victoryCondition.Name);
if (mapIndex === undefined)
return;
let index =
g_GameAttributes.settings &&
g_GameAttributes.settings.VictoryConditions &&
g_GameAttributes.settings.VictoryConditions.indexOf(this.victoryCondition.Name);
if (index !== undefined && (mapIndex == -1) == (index == -1))
return;
if (!g_GameAttributes.settings.VictoryConditions)
g_GameAttributes.settings.VictoryConditions = [];
if (mapIndex == -1)
{
if (index !== undefined)
g_GameAttributes.settings.VictoryConditions.splice(index, 1);
}
else
g_GameAttributes.settings.VictoryConditions.push(this.victoryCondition.Name);
this.gameSettingsControl.updateGameAttributes();
}
onGameAttributesChange()
{
if (!g_GameAttributes.settings.VictoryConditions)
{
g_GameAttributes.settings.VictoryConditions = [];
for (let victoryCondition of g_VictoryConditions)
if (victoryCondition.Default)
g_GameAttributes.settings.VictoryConditions.push(victoryCondition.Name);
this.gameSettingsControl.updateGameAttributes();
}
}
onGameAttributesBatchChange()
{
this.setEnabled(
g_GameAttributes.mapType != "scenario" &&
(!this.victoryCondition.DisabledWhenChecked ||
this.victoryCondition.DisabledWhenChecked.every(name =>
g_GameAttributes.settings.VictoryConditions.indexOf(name) == -1)));
this.setChecked(g_GameAttributes.settings.VictoryConditions.indexOf(this.victoryCondition.Name) != -1);
}
onPress(checked)
{
let victoryCondition = new Set(g_GameAttributes.settings.VictoryConditions);
if (checked)
{
victoryCondition.add(this.victoryCondition.Name);
if (this.victoryCondition.ChangeOnChecked)
for (let name in this.victoryCondition.ChangeOnChecked)
if (this.victoryCondition.ChangeOnChecked[name])
victoryCondition.add(name);
else
victoryCondition.delete(name);
}
else
victoryCondition.delete(this.victoryCondition.Name);
g_GameAttributes.settings.VictoryConditions = Array.from(victoryCondition);
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
}
onGameAttributesFinalize()
{
if (!g_GameAttributes.settings.TriggerScripts)
g_GameAttributes.settings.TriggerScripts = [];
if (g_GameAttributes.settings.VictoryConditions.indexOf(this.victoryCondition.Name) != -1)
for (let script of this.victoryCondition.Scripts)
if (g_GameAttributes.settings.TriggerScripts.indexOf(script) == -1)
g_GameAttributes.settings.TriggerScripts.push(script);
}
}

View File

@ -0,0 +1,147 @@
/**
* The GamesetupPage is the root class owning all other class instances.
* The class shall be ineligible to perform any GUI object logic and shall defer that task to owned classes.
*/
class GamesetupPage
{
constructor(initData, hotloadData)
{
if (!g_Settings)
return;
Engine.ProfileStart("GamesetupPage");
this.loadHandlers = new Set();
this.closePageHandlers = new Set();
this.getHotloadDataHandlers = new Set();
let netMessages = new NetMessages(this);
let startGameControl = new StartGameControl(netMessages);
let mapCache = new MapCache();
let mapFilters = new MapFilters(mapCache);
let gameSettingsControl = new GameSettingsControl(this, netMessages, startGameControl, mapCache);
let playerAssignmentsControl = new PlayerAssignmentsControl(this, netMessages);
let readyControl = new ReadyControl(netMessages, gameSettingsControl, startGameControl, playerAssignmentsControl);
// These class instances control central data and do not manage any GUI Object.
this.controls = {
"gameSettingsControl": gameSettingsControl,
"playerAssignmentsControl": playerAssignmentsControl,
"mapCache": mapCache,
"mapFilters": mapFilters,
"readyControl": readyControl,
"startGameControl": startGameControl
};
// These class instances are interfaces to networked messages and do not manage any GUI Object.
this.netMessages = {
"netMessages": netMessages,
"gameRegisterStanza":
Engine.HasXmppClient() &&
new GameRegisterStanza(
initData, this, netMessages, gameSettingsControl, playerAssignmentsControl, mapCache)
};
// This class instance owns all gamesetting GUI controls such as dropdowns and checkboxes.
// The controls also deterministically sanitize g_GameAttributes and g_PlayerAssignments
// without broadcasting the change.
this.gameSettingControlManager =
new GameSettingControlManager(this, gameSettingsControl, mapCache, mapFilters, netMessages, playerAssignmentsControl);
// These classes manage GUI buttons.
{
let startGameButton = new StartGameButton(this, startGameControl, netMessages, readyControl, playerAssignmentsControl);
let readyButton = new ReadyButton(readyControl, netMessages, playerAssignmentsControl);
this.panelButtons = {
"cancelButton": new CancelButton(this, startGameButton, readyButton, this.netMessages.gameRegisterStanza),
"civInfoButton": new CivInfoButton(),
"lobbyButton": new LobbyButton(),
"readyButton": readyButton,
"startGameButton": startGameButton
};
}
// These classes manage GUI Objects.
{
let gameSettingTabs = new GameSettingTabs(this, this.panelButtons.lobbyButton);
let gameSettingsPanel = new GameSettingsPanel(
this, gameSettingTabs, gameSettingsControl, this.gameSettingControlManager);
this.panels = {
"chatPanel": new ChatPanel(this.gameSettingControlManager, gameSettingsControl, netMessages, playerAssignmentsControl, readyControl, gameSettingsPanel),
"gameSettingWarning": new GameSettingWarning(gameSettingsControl, this.panelButtons.cancelButton),
"gameDescription": new GameDescription(mapCache, gameSettingTabs, gameSettingsControl),
"gameSettingsPanel": gameSettingsPanel,
"gameSettingsTabs": gameSettingTabs,
"loadingWindow": new LoadingWindow(netMessages),
"mapPreview": new MapPreview(gameSettingsControl, mapCache),
"resetCivsButton": new ResetCivsButton(gameSettingsControl),
"resetTeamsButton": new ResetTeamsButton(gameSettingsControl),
"soundNotification": new SoundNotification(netMessages, playerAssignmentsControl),
"tipsPanel": new TipsPanel(gameSettingsPanel),
"tooltip": new Tooltip(this.panelButtons.cancelButton)
};
}
setTimeout(displayGamestateNotifications, 1000);
Engine.GetGUIObjectByName("setupWindow").onTick = updateTimers;
// This event is triggered after all classes have been instantiated and subscribed to each others events
for (let handler of this.loadHandlers)
handler(initData, hotloadData);
Engine.ProfileStop();
if (gameSettingsControl.autostart)
startGameControl.launchGame();
}
registerLoadHandler(handler)
{
this.loadHandlers.add(handler);
}
unregisterLoadHandler(handler)
{
this.loadHandlers.delete(handler);
}
registerClosePageHandler(handler)
{
this.closePageHandlers.add(handler);
}
unregisterClosePageHandler(handler)
{
this.closePageHandlers.delete(handler);
}
registerGetHotloadDataHandler(handler)
{
this.getHotloadDataHandlers.add(handler);
}
unregisterGetHotloadDataHandler(handler)
{
this.getHotloadDataHandlers.delete(handler);
}
getHotloadData()
{
let object = {};
for (let handler of this.getHotloadDataHandlers)
handler(object);
return object;
}
closePage()
{
for (let handler of this.closePageHandlers)
handler();
if (Engine.HasXmppClient())
Engine.SwitchGuiPage("page_lobby.xml", { "dialog": false });
else
Engine.SwitchGuiPage("page_pregame.xml");
}
}

View File

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<object hidden="true" name="setupWindow">
<!-- TODO: Implement Engine.SetNetMessageHandler(msg => handlerFunction); instead of pulling onTick -->
<object name="netMessages"/>
<object name="topPanel" size="24 40 100%-24 336">
<object size="0 0 100%-416 100%">
<include file="gui/gamesetup/GameSettings/PerPlayer/PlayersPanel.xml"/>
</object>
<object size="100%-402 0 100% 100%">
<include file="gui/gamesetup/Panels/MapPreview.xml"/>
</object>
</object>
<object name="centerPanel" size="0 0 100% 100%-60">
<object name="centerLeftPanel" size="24 0 100% 100%">
<object size="0 346 100%-795-24 100%">
<include file="gui/gamesetup/Panels/Chat/ChatPanel.xml"/>
</object>
<object size="0 370 500-24 510">
<include file="gui/gamesetup/Panels/TipsPanel.xml"/>
</object>
</object>
<object name="centerCenterPanel" size="100%-315 346 100%-315 100%">
<include file="gui/gamesetup/Panels/GameSettingsPanel.xml"/>
</object>
<object name="centerRightPanel" size="100%-315 346 100% 100%">
<!-- Opaque Background to hide GameSettingsPanel parts during animation -->
<object type="image" sprite="ModernDarkBoxOpaque" z="45"/>
<object size="100%-41 0 100%-25 16">
<include file="gui/gamesetup/Panels/Buttons/LobbyButton.xml"/>
</object>
<object size="0 0 100%-25 0" z="45">
<include file="gui/gamesetup/Panels/GameSettingsTabs.xml"/>
</object>
<object size="0 0 100%-25 100%" z="45">
<include file="gui/gamesetup/Panels/GameDescription.xml"/>
</object>
</object>
</object>
<object name="bottomPanel" size="0 0 100% 100%-24">
<object name="bottomLeftPanel">
<object size="20 100%-32 100%-312 100%">
<include file="gui/gamesetup/Panels/Tooltip.xml"/>
</object>
<object size="0 100%-28 100%-320 100%">
<include file="gui/gamesetup/Panels/GameSettingWarning.xml"/>
</object>
</object>
<object name="bottomRightPanel" size="100%-314 100%-28 100% 100%">
<object size="0 0 140 100%">
<include file="gui/gamesetup/Panels/Buttons/CancelButton.xml"/>
</object>
<object size="150 0 290 100%">
<include file="gui/gamesetup/Panels/Buttons/ReadyButton.xml"/>
<include file="gui/gamesetup/Panels/Buttons/StartGameButton.xml"/>
</object>
</object>
</object>
</object>

View File

@ -0,0 +1,146 @@
/**
* If there is an XmppClient, this class informs the XPartaMuPP lobby bot that
* this match is being setup so that others can join.
* It informs of the lobby of some setting values and the participating clients.
*/
class GameRegisterStanza
{
constructor(initData, gamesetupPage, netMessages, gameSettingsControl, playerAssignmentsControl, mapCache)
{
this.mapCache = mapCache;
this.serverName = initData.serverName;
this.serverPort = initData.serverPort;
this.stunEndpoint = initData.stunEndpoint;
this.mods = JSON.stringify(Engine.GetEngineInfo().mods);
this.timer = undefined;
// Only send a lobby update when its data changed
this.lastStanza = undefined;
// Events
let sendImmediately = this.sendImmediately.bind(this);
playerAssignmentsControl.registerClientJoinHandler(sendImmediately);
playerAssignmentsControl.registerClientLeaveHandler(sendImmediately);
gamesetupPage.registerClosePageHandler(this.onClosePage.bind(this));
gameSettingsControl.registerGameAttributesBatchChangeHandler(this.onGameAttributesBatchChange.bind(this));
netMessages.registerNetMessageHandler("start", this.onGameStart.bind(this));
}
onGameAttributesBatchChange()
{
if (this.lastStanza)
this.sendDelayed();
else
this.sendImmediately();
}
onGameStart()
{
if (!g_IsController || !Engine.HasXmppClient())
return;
this.sendImmediately();
let clients = this.formatClientsForStanza();
Engine.SendChangeStateGame(clients.connectedPlayers, clients.list);
}
onClosePage()
{
if (g_IsController && Engine.HasXmppClient())
Engine.SendUnregisterGame();
}
/**
* Send the relevant gamesettings to the lobbybot in a deferred manner.
*/
sendDelayed()
{
if (!g_IsController || !Engine.HasXmppClient())
return;
if (this.timer !== undefined)
clearTimeout(this.timer);
this.timer = setTimeout(this.sendImmediately.bind(this), this.Timeout);
}
/**
* Send the relevant gamesettings to the lobbybot immediately.
*/
sendImmediately()
{
if (!g_IsController || !Engine.HasXmppClient())
return;
Engine.ProfileStart("sendRegisterGameStanza");
if (this.timer !== undefined)
{
clearTimeout(this.timer);
this.timer = undefined;
}
let clients = this.formatClientsForStanza();
let stanza = {
"name": this.serverName,
"port": this.serverPort,
"hostUsername": Engine.LobbyGetNick(),
"mapName": g_GameAttributes.map,
"niceMapName": this.mapCache.getTranslatableMapName(g_GameAttributes.mapType, g_GameAttributes.map),
"mapSize": g_GameAttributes.mapType == "random" ? g_GameAttributes.settings.Size : "Default",
"mapType": g_GameAttributes.mapType,
"victoryConditions": g_GameAttributes.settings.VictoryConditions.join(","),
"nbp": clients.connectedPlayers,
"maxnbp": g_GameAttributes.settings.PlayerData.length,
"players": clients.list,
"stunIP": this.stunEndpoint ? this.stunEndpoint.ip : "",
"stunPort": this.stunEndpoint ? this.stunEndpoint.port : "",
"mods": this.mods
};
// Only send the stanza if one of these properties changed
if (this.lastStanza && Object.keys(stanza).every(prop => this.lastStanza[prop] == stanza[prop]))
return;
this.lastStanza = stanza;
Engine.SendRegisterGame(stanza);
Engine.ProfileStop();
}
/**
* Send a list of playernames and distinct between players and observers.
* Don't send teams, AIs or anything else until the game was started.
* The playerData format from g_GameAttributes is kept to reuse the GUI function presenting the data.
*/
formatClientsForStanza()
{
let connectedPlayers = 0;
let playerData = [];
for (let guid in g_PlayerAssignments)
{
let pData = { "Name": g_PlayerAssignments[guid].name };
if (g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player - 1])
++connectedPlayers;
else
pData.Team = "observer";
playerData.push(pData);
}
return {
"list": playerDataToStringifiedTeamList(playerData),
"connectedPlayers": connectedPlayers
};
}
}
/**
* Send the current gamesettings to the lobby bot if the settings didn't change for this number of milliseconds.
*/
GameRegisterStanza.prototype.Timeout = 2000;

View File

@ -0,0 +1,71 @@
/**
* This class enables other classes to subscribe to specific CNetMessage types (see NetMessage.h, NetMessages.h) sent by the CNetServer.
*/
class NetMessages
{
constructor(gamesetupPage)
{
this.netMessageHandlers = {};
for (let messageType of this.MessageTypes)
this.netMessageHandlers[messageType] = new Set();
this.registerNetMessageHandler("netwarn", addNetworkWarning);
Engine.GetGUIObjectByName("netMessages").onTick = this.onTick.bind(this);
gamesetupPage.registerClosePageHandler(this.onClosePage.bind(this));
}
registerNetMessageHandler(messageType, handler)
{
if (this.netMessageHandlers[messageType])
this.netMessageHandlers[messageType].add(handler);
else
error("Unknown net message type: " + uneval(messageType));
}
unregisterNetMessageHandler(messageType, handler)
{
if (this.netMessageHandlers[messageType])
this.netMessageHandlers[messageType].delete(handler);
else
error("Unknown net message type: " + uneval(messageType));
}
onTick()
{
while (true)
{
let message = Engine.PollNetworkClient();
if (!message)
break;
log("Net message: " + uneval(message));
if (this.netMessageHandlers[message.type])
for (let handler of this.netMessageHandlers[message.type])
handler(message);
else
error("Unrecognized net message type " + message.type);
}
}
onClosePage()
{
Engine.DisconnectNetworkGame();
}
}
/**
* Messages types are present here if and only if they are sent by NetClient.cpp.
*/
NetMessages.prototype.MessageTypes = [
"chat",
"ready",
"gamesetup",
"kicked",
"netstatus",
"netwarn",
"players",
"start"
];

View File

@ -0,0 +1,47 @@
class CancelButton
{
constructor(gamesetupPage, startGameButton, readyButton, gameSettingsControl)
{
this.gamesetupPage = gamesetupPage;
this.startGameButton = startGameButton;
this.readyButton = readyButton;
this.gameSettingsControl = gameSettingsControl;
this.cancelButtonResizeHandlers = new Set();
this.buttonPositions = Engine.GetGUIObjectByName("bottomRightPanel").children;
this.cancelButton = Engine.GetGUIObjectByName("cancelButton");
this.cancelButton.caption = this.Caption;
this.cancelButton.tooltip = Engine.HasXmppClient() ? this.TooltipLobby : this.TooltipMenu;
this.cancelButton.onPress = gamesetupPage.closePage.bind(gamesetupPage);
readyButton.registerButtonHiddenChangeHandler(this.onNeighborButtonHiddenChange.bind(this));
startGameButton.registerButtonHiddenChangeHandler(this.onNeighborButtonHiddenChange.bind(this));
}
registerCancelButtonResizeHandler(handler)
{
this.cancelButtonResizeHandlers.add(handler);
}
onNeighborButtonHiddenChange()
{
this.cancelButton.size = this.buttonPositions[
this.buttonPositions[1].children.every(button => button.hidden) ? 1 : 0].size;
for (let handler of this.cancelButtonResizeHandlers)
handler(this.cancelButton);
}
}
CancelButton.prototype.Caption =
translate("Back");
CancelButton.prototype.TooltipLobby =
translate("Return to the lobby.");
CancelButton.prototype.TooltipMenu =
translate("Return to the main menu.");
CancelButton.prototype.Margin = 0;

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<object
name="cancelButton"
type="button"
style="StoneButton"
tooltip_style="onscreenToolTip"
z="21"
/>

View File

@ -0,0 +1,48 @@
class CivInfoButton
{
constructor()
{
this.civInfo = {
"civ": "",
"page": "page_civinfo.xml"
};
let civInfoButton = Engine.GetGUIObjectByName("civInfoButton");
civInfoButton.onPress = this.onPress.bind(this);
civInfoButton.tooltip =
sprintf(translate(this.Tooltip), {
"hotkey_civinfo": colorizeHotkey("%(hotkey)s", "civinfo"),
"hotkey_structree": colorizeHotkey("%(hotkey)s", "structree")
});
Engine.SetGlobalHotkey("structree", this.openPage.bind(this, "page_structree.xml"));
Engine.SetGlobalHotkey("civinfo", this.openPage.bind(this, "page_civinfo.xml"));
}
onPress()
{
this.openPage(this.civInfo.page);
}
openPage(page)
{
Engine.PushGuiPage(
page,
{ "civ": this.civInfo.civ },
this.storeCivInfoPage.bind(this));
}
storeCivInfoPage(data)
{
if (data.nextPage)
Engine.PushGuiPage(
data.nextPage,
{ "civ": data.civ },
this.storeCivInfoPage.bind(this));
else
this.civInfo = data;
}
}
CivInfoButton.prototype.Tooltip =
translate("%(hotkey_civinfo)s / %(hotkey_structree)s: View History / Structure Tree\nLast opened will be reopened on click.");

View File

@ -0,0 +1,19 @@
class LobbyButton
{
constructor()
{
this.lobbyButton = Engine.GetGUIObjectByName("lobbyButton");
this.lobbyButton.tooltip = this.Tooltip;
this.lobbyButton.onPress = this.onPress.bind(this);
this.lobbyButton.hidden = !Engine.HasXmppClient();
}
onPress()
{
if (Engine.HasXmppClient())
Engine.PushGuiPage("page_lobby.xml", { "dialog": true });
}
}
LobbyButton.prototype.Tooltip =
translate("Show the multiplayer lobby in a dialog window.");

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<object
name="lobbyButton"
type="button"
hotkey="lobby"
style="IconButton"
tooltip_style="onscreenToolTip"
z="100"
sprite="iconBubbleGold"
sprite_over="iconBubbleWhite"
/>

View File

@ -0,0 +1,88 @@
class ReadyButton
{
constructor(readyControl, netMessages, playerAssignmentsControl)
{
this.readyControl = readyControl;
this.hidden = undefined;
this.buttonHiddenChangeHandlers = new Set();
this.readyButtonPressHandlers = new Set();
this.readyButton = Engine.GetGUIObjectByName("readyButton");
this.readyButton.onPress = this.onPress.bind(this);
this.readyButton.onPressRight = this.onPressRight.bind(this);
playerAssignmentsControl.registerPlayerAssignmentsChangeHandler(this.onPlayerAssignmentsChange.bind(this));
netMessages.registerNetMessageHandler("netstatus", this.onNetStatusMessage.bind(this));
if (g_IsController && g_IsNetworked)
this.readyControl.setReady(this.readyControl.StayReady, true);
}
registerButtonHiddenChangeHandler(handler)
{
this.buttonHiddenChangeHandlers.add(handler);
}
onNetStatusMessage(message)
{
if (message.status == "disconnected")
this.readyButton.enabled = false;
}
onPlayerAssignmentsChange()
{
let playerAssignment = g_PlayerAssignments[Engine.GetPlayerGUID()];
let hidden = g_IsController || !playerAssignment || playerAssignment.player == -1;
if (!hidden)
{
this.readyButton.caption = this.Caption[playerAssignment.status];
this.readyButton.tooltip = this.Tooltip[playerAssignment.status];
}
if (hidden == this.hidden)
return;
this.hidden = hidden;
this.readyButton.hidden = hidden;
for (let handler of this.buttonHiddenChangeHandlers)
handler(this.readyButton);
}
registerReadyButtonPressHandler(handler)
{
this.readyButtonPressHandlers.add(handler);
}
onPress()
{
let newState =
(g_PlayerAssignments[Engine.GetPlayerGUID()].status + 1) % (this.readyControl.StayReady + 1);
for (let handler of this.readyButtonPressHandlers)
handler(newState);
this.readyControl.setReady(newState, true);
}
onPressRight()
{
if (g_PlayerAssignments[Engine.GetPlayerGUID()].status != this.readyControl.NotReady)
this.readyControl.setReady(this.readyControl.NotReady, true);
}
}
ReadyButton.prototype.Caption = [
translate("I'm ready"),
translate("Stay ready"),
translate("I'm not ready!")
];
ReadyButton.prototype.Tooltip = [
translate("State that you are ready to play."),
translate("Stay ready even when the game settings change."),
translate("State that you are not ready to play.")
];

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<object
name="readyButton"
type="button"
style="StoneButton"
tooltip_style="onscreenToolTip"
z="21"
/>

View File

@ -0,0 +1,36 @@
class ResetCivsButton
{
constructor(gameSettingsControl)
{
this.gameSettingsControl = gameSettingsControl;
this.gameSettingsControl.registerGameAttributesBatchChangeHandler(this.onGameAttributesBatchChange.bind(this));
this.civResetButton = Engine.GetGUIObjectByName("civResetButton");
this.civResetButton.tooltip = this.Tooltip;
this.civResetButton.onPress = this.onPress.bind(this);
}
onGameAttributesBatchChange()
{
if (g_GameAttributes.mapType)
this.civResetButton.hidden = g_GameAttributes.mapType == "scenario" || !g_IsController;
}
onPress()
{
if (!g_GameAttributes.settings || !g_GameAttributes.settings.PlayerData)
return;
for (let pData of g_GameAttributes.settings.PlayerData)
pData.Civ = this.RandomCivId;
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
}
}
ResetCivsButton.prototype.Tooltip =
translate("Reset any civilizations that have been selected to the default (random).");
ResetCivsButton.prototype.RandomCivId =
"random";

View File

@ -0,0 +1,35 @@
class ResetTeamsButton
{
constructor(gameSettingsControl)
{
this.gameSettingsControl = gameSettingsControl;
this.gameSettingsControl.registerGameAttributesBatchChangeHandler(this.onGameAttributesBatchChange.bind(this));
this.teamResetButton = Engine.GetGUIObjectByName("teamResetButton");
this.teamResetButton.tooltip = this.Tooltip;
this.teamResetButton.onPress = this.onPress.bind(this);
}
onGameAttributesBatchChange()
{
if (!g_GameAttributes.mapType)
return;
this.teamResetButton.hidden = g_GameAttributes.mapType == "scenario" || !g_IsController;
}
onPress()
{
if (!g_GameAttributes.settings || !g_GameAttributes.settings.PlayerData)
return;
for (let pData of g_GameAttributes.settings.PlayerData)
pData.Team = -1;
this.gameSettingsControl.updateGameAttributes();
this.gameSettingsControl.setNetworkGameAttributes();
}
}
ResetTeamsButton.prototype.Tooltip =
translate("Reset all teams to the default.");

View File

@ -0,0 +1,72 @@
class StartGameButton
{
constructor(gamesetupPage, startGameControl, netMessages, readyControl, playerAssignmentsControl)
{
this.startGameControl = startGameControl;
this.readyControl = readyControl;
this.gameStarted = false;
this.buttonHiddenChangeHandlers = new Set();
this.startGameButton = Engine.GetGUIObjectByName("startGameButton");
this.startGameButton.caption = this.Caption;
this.startGameButton.onPress = this.onPress.bind(this);
gamesetupPage.registerLoadHandler(this.onLoad.bind(this));
playerAssignmentsControl.registerPlayerAssignmentsChangeHandler(this.update.bind(this));
}
registerButtonHiddenChangeHandler(handler)
{
this.buttonHiddenChangeHandlers.add(handler);
}
onLoad()
{
this.startGameButton.hidden = !g_IsController;
for (let handler of this.buttonHiddenChangeHandlers)
handler();
}
update()
{
let isEveryoneReady = this.isEveryoneReady();
this.startGameButton.enabled = !this.gameStarted && isEveryoneReady;
this.startGameButton.tooltip =
!g_IsNetworked || isEveryoneReady ?
this.ReadyTooltip :
this.ReadyTooltipWaiting;
}
isEveryoneReady()
{
if (!g_IsNetworked)
return true;
for (let guid in g_PlayerAssignments)
if (g_PlayerAssignments[guid].player != -1 &&
g_PlayerAssignments[guid].status == this.readyControl.NotReady)
return false;
return true;
}
onPress()
{
if (this.gameStarted)
return;
this.gameStarted = true;
this.update();
this.startGameControl.launchGame();
}
}
StartGameButton.prototype.Caption =
translate("Start Game!");
StartGameButton.prototype.ReadyTooltip =
translate("Start a new game with the current settings.");
StartGameButton.prototype.ReadyTooltipWaiting =
translate("Start a new game with the current settings (disabled until all players are ready).");

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<object
name="startGameButton"
type="button"
style="StoneButton"
tooltip_style="onscreenToolTip"
z="21"
/>

View File

@ -0,0 +1,37 @@
class ChatInputAutocomplete
{
constructor(gameSettingControlManager, gameSettingsControl, playerAssignmentsControl)
{
this.gameSettingControlManager = gameSettingControlManager;
this.entries = undefined;
playerAssignmentsControl.registerPlayerAssignmentsChangeHandler(this.onAutocompleteChange.bind(this));
gameSettingsControl.registerGameAttributesBatchChangeHandler(this.onAutocompleteChange.bind(this));
}
onAutocompleteChange()
{
this.entries = undefined;
}
// Collects all strings that can be autocompleted and
// sorts them by priority (so that playernames are always autocompleted first).
getAutocompleteEntries()
{
if (this.entries)
return this.entries;
// Maps from priority to autocompletable strings
let entries = { "0": [] };
this.gameSettingControlManager.addAutocompleteEntries(entries);
let allEntries = Object.keys(entries).sort((a, b) => +b - +a).reduce(
(all, priority) => all.concat(entries[priority]),
[]);
this.entries = Array.from(new Set(allEntries));
return this.entries;
}
}

View File

@ -0,0 +1,55 @@
class ChatInputPanel
{
constructor(netMessages, chatInputAutocomplete)
{
this.chatInputAutocomplete = chatInputAutocomplete;
this.chatInput = Engine.GetGUIObjectByName("chatInput");
this.chatInput.tooltip = colorizeAutocompleteHotkey(this.Tooltip);
this.chatInput.onPress = this.onPress.bind(this);
this.chatInput.onTab = this.onTab.bind(this);
this.chatInput.focus();
this.chatSubmitButton = Engine.GetGUIObjectByName("chatSubmitButton");
this.chatSubmitButton.onPress = this.onPress.bind(this);
netMessages.registerNetMessageHandler("netstatus", this.onNetStatusMessage.bind(this));
}
onNetStatusMessage(message)
{
if (message.status == "disconnected")
{
reportDisconnect(message.reason, true);
this.chatInput.hidden = true;
this.chatSubmitButton.hidden = true;
}
}
onTab()
{
autoCompleteText(
this.chatInput,
this.chatInputAutocomplete.getAutocompleteEntries());
}
onPress()
{
if (!g_IsNetworked)
return;
let text = this.chatInput.caption;
if (!text.length)
return;
this.chatInput.caption = "";
if (!executeNetworkCommand(text))
Engine.SendNetworkChat(text);
this.chatInput.focus();
}
}
ChatInputPanel.prototype.Tooltip =
translate("Press %(hotkey)s to autocomplete player names or settings.");

View File

@ -0,0 +1,53 @@
ChatMessageEvents.ClientChat = class
{
constructor(chatMessagesPanel, netMessages)
{
this.chatMessagesPanel = chatMessagesPanel;
netMessages.registerNetMessageHandler("chat", this.onClientChat.bind(this));
this.usernameArgs = {};
this.messageArgs = {};
// TODO: Remove this global required by gui/common/
global.colorizePlayernameByGUID = this.colorizePlayernameByGUID.bind(this);
}
onClientChat(message)
{
this.usernameArgs.username = this.colorizePlayernameByGUID(message.guid);
this.messageArgs.username = setStringTags(sprintf(this.SenderFormat, this.usernameArgs), this.SenderTags);
this.messageArgs.message = escapeText(message.text);
this.chatMessagesPanel.addText(sprintf(this.MessageFormat, this.messageArgs));
}
colorizePlayernameByGUID(guid)
{
// TODO: Controllers should have the moderator-prefix
let username = g_PlayerAssignments[guid] ? escapeText(g_PlayerAssignments[guid].name) : translate("Unknown Player");
let playerID = g_PlayerAssignments[guid] ? g_PlayerAssignments[guid].player : -1;
let color = "white";
if (playerID > 0)
{
color = g_GameAttributes.settings.PlayerData[playerID - 1].Color;
// Enlighten playercolor to improve readability
let [h, s, l] = rgbToHsl(color.r, color.g, color.b);
let [r, g, b] = hslToRgb(h, s, Math.max(0.6, l));
color = rgbToGuiColor({ "r": r, "g": g, "b": b });
}
return coloredText(username, color);
}
};
ChatMessageEvents.ClientChat.prototype.SenderFormat =
translate("<%(username)s>");
ChatMessageEvents.ClientChat.prototype.MessageFormat =
translate("%(username)s %(message)s");
ChatMessageEvents.ClientChat.prototype.SenderTags = {
"font": "sans-bold-13"
};

View File

@ -0,0 +1,30 @@
ChatMessageEvents.ClientConnection = class
{
constructor(chatMessagesPanel, netMessages, gameSettingsControl, playerAssignmentsControl)
{
this.chatMessagesPanel = chatMessagesPanel;
playerAssignmentsControl.registerClientJoinHandler(this.onClientJoin.bind(this));
playerAssignmentsControl.registerClientLeaveHandler(this.onClientLeave.bind(this));
this.args = {};
}
onClientJoin(newGUID, newAssignments)
{
this.args.username = newAssignments[newGUID].name;
this.chatMessagesPanel.addStatusMessage(sprintf(this.JoinText, this.args));
}
onClientLeave(guid)
{
this.args.username = g_PlayerAssignments[guid].name;
this.chatMessagesPanel.addStatusMessage(sprintf(this.LeaveText, this.args));
}
};
ChatMessageEvents.ClientConnection.prototype.JoinText =
translate("%(username)s has joined");
ChatMessageEvents.ClientConnection.prototype.LeaveText =
translate("%(username)s has left");

View File

@ -0,0 +1,25 @@
ChatMessageEvents.ClientKicked = class
{
constructor(chatMessagesPanel, netMessages)
{
this.chatMessagesPanel = chatMessagesPanel;
this.messageArgs = {};
netMessages.registerNetMessageHandler("kicked", this.onClientKicked.bind(this));
}
onClientKicked(message)
{
this.messageArgs.username = message.username;
this.chatMessagesPanel.addStatusMessage(sprintf(
message.banned ? this.BannedMessage : this.KickedMessage,
this.messageArgs));
}
};
ChatMessageEvents.ClientKicked.prototype.KickedMessage =
translate("%(username)s has been kicked");
ChatMessageEvents.ClientKicked.prototype.BannedMessage =
translate("%(username)s has been banned");

View File

@ -0,0 +1,30 @@
ChatMessageEvents.ClientReady = class
{
constructor(chatMessagesPanel, netMessages, gameSettingsControl, playerAssignmentsControl, readyControl)
{
this.chatMessagesPanel = chatMessagesPanel;
this.args = {};
netMessages.registerNetMessageHandler("ready", this.onReadyMessage.bind(this));
}
onReadyMessage(message)
{
let playerAssignment = g_PlayerAssignments[message.guid];
if (!playerAssignment || playerAssignment.player == -1)
return;
let text = this.ReadyMessage[message.status] || undefined;
if (!text)
return;
this.args.username = playerAssignment.name;
this.chatMessagesPanel.addText(sprintf(text, this.args));
}
};
ChatMessageEvents.ClientReady.prototype.ReadyMessage = [
translate("* %(username)s is not ready."),
translate("* %(username)s is ready!")
];

View File

@ -0,0 +1,22 @@
/**
* The purpose of this message is to indicate to the local player when settings they had agreed on changed.
*/
ChatMessageEvents.GameSettingsChanged = class
{
constructor(chatMessagesPanel, netMessages, gameSettingsControl, playerAssignmentsControl, readyControl)
{
this.readyControl = readyControl;
this.chatMessagesPanel = chatMessagesPanel;
readyControl.registerResetReadyHandler(this.onResetReady.bind(this));
}
onResetReady()
{
if (this.readyControl.getLocalReadyState() == this.readyControl.Ready)
this.chatMessagesPanel.addStatusMessage(this.MessageText);
}
};
ChatMessageEvents.GameSettingsChanged.prototype.MessageText =
translate("Game settings have been changed");

View File

@ -0,0 +1,81 @@
/**
* This class stores and displays the chat history since the login and
* displays timestamps if enabled.
*/
class ChatMessagesPanel
{
constructor(gameSettingsPanel)
{
this.gameSettingsPanel = gameSettingsPanel;
this.chatHistory = "";
this.statusMessageFormat = new StatusMessageFormat();
if (Engine.ConfigDB_GetValue("user", this.ConfigTimestamp) == "true")
this.timestampWrapper = new TimestampWrapper();
this.chatText = Engine.GetGUIObjectByName("chatText");
this.chatPanel = Engine.GetGUIObjectByName("chatPanel");
this.chatPanel.onWindowResized = this.onWindowResized.bind(this);
gameSettingsPanel.registerGameSettingsPanelResizeHandler(this.onGameSettingsPanelResize.bind(this));
// TODO: Remove global requirements by gui/common/network.js
g_NetworkCommands["/list"] = () => { this.addText(getUsernameList()); };
g_NetworkCommands["/clear"] = this.clearChatMessages.bind(this);
global.kickError = () => {};
}
addText(text)
{
if (this.timestampWrapper)
text = this.timestampWrapper.format(text);
this.chatHistory += this.chatHistory ? "\n" + text : text;
this.chatText.caption = this.chatHistory;
}
addStatusMessage(text)
{
this.addText(this.statusMessageFormat.format(text));
}
clearChatMessages()
{
this.chatHistory = "";
this.chatText.caption = "";
}
updateHidden()
{
let size = this.chatPanel.getComputedSize();
this.chatPanel.hidden = !g_IsNetworked || size.right - size.left < this.MinimumWidth;
}
onWindowResized()
{
this.updateHidden();
}
onGameSettingsPanelResize(settingsPanel)
{
let size = this.chatPanel.size;
size.right = settingsPanel.size.left + this.gameSettingsPanel.MaxColumnWidth + this.Margin;
this.chatPanel.size = size;
this.updateHidden();
}
}
/**
* Minimum amount of pixels required for the chat panel to be visible.
*/
ChatMessagesPanel.prototype.MinimumWidth = 96;
/**
* Horizontal space between the chat window and the settings panel.
*/
ChatMessagesPanel.prototype.Margin = 10;
ChatMessagesPanel.prototype.ConfigTimestamp =
"chat.timestamp";

View File

@ -0,0 +1,26 @@
/**
* Properties of this prototype are classes that subscribe to one or more events and
* construct a formatted chat message to be displayed on that event.
*
* Important: Apply escapeText on player provided input to avoid players breaking the game for everybody.
*/
class ChatMessageEvents
{
}
class ChatPanel
{
constructor(gameSettingControlManager, gameSettingsControl, netMessages, playerAssignmentsControl, readyControl, gameSettingsPanel)
{
this.statusMessageFormat = new StatusMessageFormat();
this.chatMessagesPanel = new ChatMessagesPanel(gameSettingsPanel);
this.chatInputAutocomplete = new ChatInputAutocomplete(gameSettingControlManager, gameSettingsControl, playerAssignmentsControl);
this.chatInputPanel = new ChatInputPanel(netMessages, this.chatInputAutocomplete);
this.chatMessageEvents = [];
for (let name in ChatMessageEvents)
this.chatMessageEvents.push(new ChatMessageEvents[name](
this.chatMessagesPanel, netMessages, gameSettingsControl, playerAssignmentsControl, readyControl));
}
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<object name="chatPanel" type="image" sprite="ModernDarkBoxGold">
<object name="chatText" type="text" size="2 2 100%-2 100%-28" style="ChatPanel"/>
<object name="chatInput" type="input" size="4 100%-26 100%-96 100%-4" style="ModernInput"/>
<object name="chatSubmitButton" size="100%-92 100%-26 100%-4 100%-4" type="button" style="StoneButton">
<translatableAttribute id="caption">Send</translatableAttribute>
</object>
</object>

View File

@ -0,0 +1,28 @@
/**
* Status messages are textual event notifications triggered by multi-user chat room actions.
*/
class StatusMessageFormat
{
constructor()
{
this.args = {};
}
/**
* escapeText is the responsibility of the caller.
*/
format(text)
{
this.args.message = text;
return setStringTags(
sprintf(this.MessageFormat, this.args),
this.MessageTags);
}
}
StatusMessageFormat.prototype.MessageFormat =
translate("== %(message)s");
StatusMessageFormat.prototype.MessageTags = {
"font": "sans-bold-13"
};

View File

@ -0,0 +1,31 @@
/**
* This class wraps a string with a timestamp dating to when the message was sent.
*/
class TimestampWrapper
{
constructor()
{
this.timeArgs = {};
this.timestampArgs = {};
}
format(text)
{
this.timeArgs.time =
Engine.FormatMillisecondsIntoDateStringLocal(Date.now(), this.TimeFormat);
this.timestampArgs.time = sprintf(this.TimestampFormat, this.timeArgs);
this.timestampArgs.message = text;
return sprintf(this.TimestampedMessageFormat, this.timestampArgs);
}
}
TimestampWrapper.prototype.TimestampedMessageFormat =
translate("%(time)s %(message)s");
TimestampWrapper.prototype.TimestampFormat =
translate("\\[%(time)s]");
TimestampWrapper.prototype.TimeFormat =
translate("HH:mm");

View File

@ -0,0 +1,27 @@
class GameDescription
{
constructor(mapCache, gameSettingTabs, gameSettingsControl)
{
this.mapCache = mapCache;
this.gameDescriptionFrame = Engine.GetGUIObjectByName("gameDescriptionFrame");
this.gameDescription = Engine.GetGUIObjectByName("gameDescription");
gameSettingsControl.registerGameAttributesBatchChangeHandler(this.onGameAttributesBatchChange.bind(this));
gameSettingTabs.registerTabsResizeHandler(this.onTabsResize.bind(this));
}
onTabsResize(settingsTabButtonsFrame)
{
let size = this.gameDescriptionFrame.size;
size.top = settingsTabButtonsFrame.size.bottom + this.Margin;
this.gameDescriptionFrame.size = size;
}
onGameAttributesBatchChange()
{
this.gameDescription.caption = getGameDescription(this.mapCache);
}
}
GameDescription.prototype.Margin = 3;

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<object name="gameDescriptionFrame" type="image" sprite="ModernDarkBoxGold" z="21">
<object name="gameDescription" type="text" style="ModernText"/>
</object>

View File

@ -0,0 +1,41 @@
class GameSettingWarning
{
constructor(gameSettingsControl, cancelButton)
{
if (!g_IsNetworked)
return;
this.gameSettingWarning = Engine.GetGUIObjectByName("gameSettingWarning");
gameSettingsControl.registerGameAttributesBatchChangeHandler(this.onGameAttributesBatchChange.bind(this));
cancelButton.registerCancelButtonResizeHandler(this.onCancelButtonResize.bind(this));
}
onGameAttributesBatchChange()
{
let caption =
g_GameAttributes.settings.CheatsEnabled ?
this.CheatsEnabled :
g_GameAttributes.settings.RatingEnabled ?
this.RatingEnabled :
"";
this.gameSettingWarning.caption = caption;
this.gameSettingWarning.hidden = !caption;
}
onCancelButtonResize(cancelButton)
{
let size = this.gameSettingWarning.size;
size.right = cancelButton.size.left - this.Margin;
this.gameSettingWarning.size = size;
}
}
GameSettingWarning.prototype.Margin = 10;
GameSettingWarning.prototype.CheatsEnabled =
translate("Cheats enabled.");
GameSettingWarning.prototype.RatingEnabled =
translate("Rated game.");

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<object name="gameSettingWarning" type="text" textcolor="red" style="ModernRightLabelText" size="0 0 100% 30"/>

View File

@ -0,0 +1,187 @@
class GameSettingsPanel
{
constructor(gamesetupPage, gameSettingTabs, gameSettingsControl, gameSettingControlManager)
{
this.settingTabButtonsFrame = Engine.GetGUIObjectByName("settingTabButtonsFrame");
this.settingsPanelFrame = Engine.GetGUIObjectByName("settingsPanelFrame");
this.gameSettingControlManager = gameSettingControlManager;
this.gameSettingsPanelResizeHandlers = new Set();
this.setupWindow = Engine.GetGUIObjectByName("setupWindow");
this.setupWindow.onWindowResized = this.onTabSelect.bind(this);
this.settingsPanel = Engine.GetGUIObjectByName("settingsPanel");
this.enabled = Engine.ConfigDB_GetValue("user", this.ConfigNameSlide) == "true";
this.slideSpeed = this.enabled ? this.SlideSpeed : Infinity;
this.lastTickTime = undefined;
gameSettingTabs.registerTabSelectHandler(this.onTabSelect.bind(this));
gameSettingsControl.registerGameAttributesBatchChangeHandler(this.onGameAttributesBatchChange.bind(this));
gamesetupPage.registerLoadHandler(this.onLoad.bind(this));
}
registerGameSettingsPanelResizeHandler(handler)
{
this.gameSettingsPanelResizeHandlers.add(handler);
}
onLoad()
{
for (let handler of this.gameSettingsPanelResizeHandlers)
handler(this.settingsPanelFrame);
}
onGameAttributesBatchChange()
{
this.gameSettingControlManager.updateSettingVisibility();
this.positionSettings();
}
onTabSelect()
{
this.gameSettingControlManager.updateSettingVisibility();
this.positionSettings();
this.lastTickTime = undefined;
this.settingsPanelFrame.onTick = this.onTick.bind(this);
}
onTick()
{
let now = Date.now();
let tickLength = now - this.lastTickTime;
let previousTime = this.lastTickTime;
this.lastTickTime = now;
if (previousTime === undefined)
return;
let distance = this.slideSpeed * tickLength;
let rightBorder = this.settingTabButtonsFrame.size.left;
let offset = 0;
if (g_TabCategorySelected === undefined)
{
let maxOffset = rightBorder - this.settingsPanelFrame.size.left;
if (maxOffset > 0)
offset = Math.min(distance, maxOffset);
}
else if (rightBorder > this.settingsPanelFrame.size.right)
{
offset = Math.min(distance, rightBorder - this.settingsPanelFrame.size.right);
}
else
{
let maxOffset = this.settingsPanelFrame.size.left - rightBorder + (this.settingsPanelFrame.size.right - this.settingsPanelFrame.size.left);
if (maxOffset > 0)
offset = -Math.min(distance, maxOffset);
}
if (offset)
this.changePanelWidth(offset);
else
{
delete this.settingsPanelFrame.onTick;
this.lastTickTime = undefined;
}
}
changePanelWidth(offset)
{
if (!offset)
return;
let size = this.settingsPanelFrame.size;
size.left += offset;
size.right += offset;
this.settingsPanelFrame.size = size;
for (let handler of this.gameSettingsPanelResizeHandlers)
handler(this.settingsPanelFrame);
}
/**
* Distribute the currently visible settings over the settings panel.
* First calculate the number of columns required, then place the setting frames.
*/
positionSettings()
{
let setupWindowSize = this.setupWindow.getComputedSize();
let columnWidth = Math.min(
this.MaxColumnWidth,
(setupWindowSize.right - setupWindowSize.left + this.settingTabButtonsFrame.size.left) / 2);
let settingsPerColumn;
{
let settingPanelSize = this.settingsPanel.getComputedSize();
let maxSettingsPerColumn = Math.floor((settingPanelSize.bottom - settingPanelSize.top) / this.SettingHeight);
let settingCount = this.settingsPanel.children.filter(child => !child.children[0].hidden).length;
settingsPerColumn = settingCount / Math.ceil(settingCount / maxSettingsPerColumn);
}
let yPos = this.SettingMarginBottom;
let column = 0;
let settingsThisColumn = 0;
let selectedTab = g_GameSettingsLayout[g_TabCategorySelected];
if (!selectedTab)
return;
for (let name of selectedTab.settings)
{
let settingFrame = this.gameSettingControlManager.gameSettingControls[name].frame;
if (settingFrame.hidden)
continue;
if (settingsThisColumn >= settingsPerColumn)
{
yPos = this.SettingMarginBottom;
++column;
settingsThisColumn = 0;
}
settingFrame.size = new GUISize(
columnWidth * column,
yPos,
columnWidth * (column + 1) - this.SettingMarginRight,
yPos + this.SettingHeight - this.SettingMarginBottom);
yPos += this.SettingHeight;
++settingsThisColumn;
}
{
let size = this.settingsPanelFrame.size;
size.right = size.left + (column + 1) * columnWidth;
this.settingsPanelFrame.size = size;
}
}
}
GameSettingsPanel.prototype.ConfigNameSlide =
"gui.gamesetup.settingsslide";
/**
* Maximum width of a column in the settings panel.
*/
GameSettingsPanel.prototype.MaxColumnWidth = 470;
/**
* Pixels per millisecond the settings panel slides when opening/closing.
*/
GameSettingsPanel.prototype.SlideSpeed = 1.2;
/**
* Vertical size of a setting frame.
*/
GameSettingsPanel.prototype.SettingHeight = 32;
/**
* Horizontal space between two setting frames.
*/
GameSettingsPanel.prototype.SettingMarginRight = 10;
/**
* Vertical space between two setting frames.
*/
GameSettingsPanel.prototype.SettingMarginBottom = 2;

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<object name="settingsPanelFrame">
<object name="settingsBackground" type="image" sprite="ModernDarkBoxGold"/>
<object name="settingsPanel" size="0 5 100% 100%-5" z="1">
<repeat count="40" var="n">
<object>
<include file="gui/gamesetup/GameSettings/GameSettingControlDropdown.xml"/>
</object>
</repeat>
<repeat count="30" var="n">
<object>
<include file="gui/gamesetup/GameSettings/GameSettingControlCheckbox.xml"/>
</object>
</repeat>
</object>
</object>

View File

@ -0,0 +1,97 @@
class GameSettingTabs
{
constructor(gamesetupPage, lobbyButton)
{
this.lobbyButton = lobbyButton;
this.tabSelectHandlers = new Set();
this.tabsResizeHandlers = new Set();
this.settingsTabButtonsFrame = Engine.GetGUIObjectByName("settingTabButtonsFrame");
for (let tab in g_GameSettingsLayout)
g_GameSettingsLayout[tab].tooltip =
sprintf(this.ToggleTooltip, { "name": g_GameSettingsLayout[tab].label }) +
colorizeHotkey("\n" + this.HotkeyDownTooltip, this.ConfigNameHotkeyDown) +
colorizeHotkey("\n" + this.HotkeyUpTooltip, this.ConfigNameHotkeyUp);
gamesetupPage.registerLoadHandler(this.onLoad.bind(this));
Engine.SetGlobalHotkey("cancel", selectPanel);
}
registerTabsResizeHandler(handler)
{
this.tabsResizeHandlers.add(handler);
}
registerTabSelectHandler(handler)
{
this.tabSelectHandlers.add(handler);
}
onLoad()
{
placeTabButtons(
g_GameSettingsLayout,
this.TabButtonHeight,
this.TabButtonMargin,
this.onTabPress.bind(this),
this.onTabSelect.bind(this));
this.resize();
if (!g_IsController)
selectPanel();
}
resize()
{
let size = this.settingsTabButtonsFrame.size;
size.bottom = size.top + g_GameSettingsLayout.length * (this.TabButtonHeight + this.TabButtonMargin);
if (!this.lobbyButton.lobbyButton.hidden)
{
let lobbyButtonSize = this.lobbyButton.lobbyButton.parent.size;
size.right -= lobbyButtonSize.right - lobbyButtonSize.left + this.LobbyButtonMargin;
}
this.settingsTabButtonsFrame.size = size;
for (let handler of this.tabsResizeHandlers)
handler(this.settingsTabButtonsFrame);
}
onTabPress(category)
{
selectPanel(category == g_TabCategorySelected ? undefined : category);
}
onTabSelect()
{
for (let handler of this.tabSelectHandlers)
handler();
}
}
GameSettingTabs.prototype.ToggleTooltip =
translate("Toggle the %(name)s settings tab.");
GameSettingTabs.prototype.HotkeyUpTooltip =
translate("Use %(hotkey)s to move a settings tab up.");
GameSettingTabs.prototype.HotkeyDownTooltip =
translate("Use %(hotkey)s to move a settings tab down.");
GameSettingTabs.prototype.ConfigNameHotkeyUp =
"tab.next";
GameSettingTabs.prototype.ConfigNameHotkeyDown =
"tab.prev";
GameSettingTabs.prototype.TabButtonHeight = 30;
GameSettingTabs.prototype.TabButtonMargin = 4;
/**
* Horizontal space between tab buttons and lobby button.
*/
GameSettingTabs.prototype.LobbyButtonMargin = 8;

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<object name="settingTabButtonsFrame" z="21">
<object type="image" sprite="ModernDarkBoxGold"/>
<include file="gui/common/tab_buttons.xml"/>
</object>

View File

@ -0,0 +1,25 @@
/**
* The purpose of this page is to display a placeholder in multiplayer until the settings from the server have been received.
* This is not technically necessary, but only performed to avoid confusion or irritation when showing the clients first the
* default settings and then switching to the server settings quickly thereafter.
*/
class LoadingWindow
{
constructor(netMessages)
{
if (g_IsNetworked)
netMessages.registerNetMessageHandler("gamesetup", this.hideLoadingWindow.bind(this));
else
this.hideLoadingWindow();
}
hideLoadingWindow()
{
let loadingWindow = Engine.GetGUIObjectByName("loadingWindow");
if (loadingWindow.hidden)
return;
loadingWindow.hidden = true;
Engine.GetGUIObjectByName("setupWindow").hidden = false;
}
}

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<object name="loadingWindow" type="image" style="ModernDialog" size="50%-190 50%-80 50%+190 50%+80">
<object type="text" style="TitleText" size="50%-128 0%-16 50%+128 16">
<translatableAttribute id="caption">Loading</translatableAttribute>
</object>
<object type="text" style="ModernLabelText">
<translatableAttribute id="caption">Loading map data. Please wait…</translatableAttribute>
</object>
</object>

View File

@ -0,0 +1,24 @@
class MapPreview
{
constructor(gameSettingsControl, mapCache)
{
this.mapCache = mapCache;
this.mapInfoName = Engine.GetGUIObjectByName("mapInfoName");
this.mapPreview = Engine.GetGUIObjectByName("mapPreview");
gameSettingsControl.registerGameAttributesBatchChangeHandler(this.onGameAttributesBatchChange.bind(this));
}
onGameAttributesBatchChange()
{
if (!g_GameAttributes.map || !g_GameAttributes.mapType)
return;
this.mapInfoName.caption = this.mapCache.translateMapName(
this.mapCache.getTranslatableMapName(g_GameAttributes.mapType, g_GameAttributes.map));
this.mapPreview.sprite =
this.mapCache.getMapPreview(g_GameAttributes.mapType, g_GameAttributes.map, g_GameAttributes);
}
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<object type="image" sprite="ModernDarkBoxGold" name="gamePreviewBox">
<object name="mapPreview" type="image" size="1 1 401 294"/>
<object name="mapInfoName" type="text" size="5 100%-20 100% 100%-1" style="ModernLeftLabelText"/>
</object>

View File

@ -0,0 +1,28 @@
class SoundNotification
{
constructor(netMessages, playerAssignmentsControl)
{
netMessages.registerNetMessageHandler("chat", this.onClientChat.bind(this));
playerAssignmentsControl.registerClientJoinHandler(this.onClientJoin.bind(this));
}
onClientJoin(guid)
{
if (guid != Engine.GetPlayerGUID())
soundNotification(this.ConfigJoinNotification);
}
onClientChat(message)
{
if (message.guid != Engine.GetPlayerGUID() &&
message.text.toLowerCase().indexOf(
splitRatingFromNick(g_PlayerAssignments[Engine.GetPlayerGUID()].name).nick.toLowerCase()) != -1)
soundNotification(this.ConfigNickNotification);
}
}
SoundNotification.prototype.ConfigJoinNotification =
"gamesetup.join";
SoundNotification.prototype.ConfigNickNotification =
"nick";

View File

@ -0,0 +1,45 @@
/**
* The TipsPanel shows some hints to newcomers.
* It is only shown in Singleplayer mode since the chat is shown instead in multiplayer mode.
*/
class TipsPanel
{
constructor(gameSettingsPanel)
{
let available = !g_IsNetworked && Engine.ConfigDB_GetValue("user", this.Config) == "true";
this.spTips = Engine.GetGUIObjectByName("spTips");
this.spTips.hidden = !available;
if (!available)
return;
this.displaySPTips = Engine.GetGUIObjectByName("displaySPTips");
this.displaySPTips.onPress = this.onPress.bind(this);
Engine.GetGUIObjectByName("aiTips").caption =
Engine.TranslateLines(Engine.ReadFile(this.File));
gameSettingsPanel.registerGameSettingsPanelResizeHandler(this.onGameSettingsPanelResize.bind(this));
}
onPress()
{
Engine.ConfigDB_CreateAndWriteValueToFile(
"user",
this.Config,
String(this.displaySPTips.checked),
"config/user.cfg");
}
onGameSettingsPanelResize(settingsPanel)
{
this.spTips.hidden =
this.spTips.getComputedSize().right > settingsPanel.getComputedSize().left;
}
}
TipsPanel.prototype.File =
"gui/gamesetup/Panels/Tips.txt";
TipsPanel.prototype.Config =
"gui.gamesetup.enabletips";

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<object name="spTips" type="image" hidden="true">
<object type="image" size="4 10 28 34" sprite="ModernGear"/>
<object name="aiTips" type="text" size="32 0 100%-20 100%-32" style="ModernLeftLabelText" />
<object type="text" size="30 100%-30 100% 100%-8" style="ModernLeftLabelText">
<translatableAttribute id="caption">Show this message in the future.</translatableAttribute>
</object>
<object name="displaySPTips" type="checkbox" style="ModernTickBox" size="8 100%-30 28 100%-8" checked="true"/>
</object>

Some files were not shown because too many files have changed in this diff Show More