Index: ps/trunk/binaries/data/mods/mod/autostart/entrypoint.js
===================================================================
--- ps/trunk/binaries/data/mods/mod/autostart/entrypoint.js
+++ ps/trunk/binaries/data/mods/mod/autostart/entrypoint.js
@@ -0,0 +1,23 @@
+/**
+ * This file is called from the visual & non-visual paths when autostarting.
+ * To avoid relying on the GUI, this script has access to a special 'LoadScript' function.
+ * See implementation in the public mod for more details.
+ */
+
+function autostartClient(initData)
+{
+ throw new Error("Autostart is not implemented in the 'mod' mod");
+}
+
+function autostartHost(initData, networked = false)
+{
+ throw new Error("Autostart is not implemented in the 'mod' mod");
+}
+
+/**
+ * @returns false if the loop should carry on.
+ */
+function onTick()
+{
+ return true;
+}
Index: ps/trunk/binaries/data/mods/public/autostart/autostart.js
===================================================================
--- ps/trunk/binaries/data/mods/public/autostart/autostart.js
+++ ps/trunk/binaries/data/mods/public/autostart/autostart.js
@@ -0,0 +1,29 @@
+class AutoStart
+{
+ constructor(initData)
+ {
+ this.settings = new GameSettings().init();
+ this.settings.fromInitAttributes(initData.attribs);
+
+ this.playerAssignments = initData.playerAssignments;
+
+ this.settings.launchGame(this.playerAssignments, initData.storeReplay);
+
+ this.onLaunch();
+ }
+
+ onTick()
+ {
+ }
+
+ /**
+ * In the visual autostart path, we need to show the loading screen.
+ */
+ onLaunch()
+ {
+ Engine.SwitchGuiPage("page_loading.xml", {
+ "attribs": this.settings.finalizedAttributes,
+ "playerAssignments": this.playerAssignments
+ });
+ }
+}
Index: ps/trunk/binaries/data/mods/public/autostart/autostart_client.js
===================================================================
--- ps/trunk/binaries/data/mods/public/autostart/autostart_client.js
+++ ps/trunk/binaries/data/mods/public/autostart/autostart_client.js
@@ -0,0 +1,54 @@
+class AutoStartClient
+{
+ constructor(initData)
+ {
+ this.playerAssignments = {};
+
+ try
+ {
+ Engine.StartNetworkJoin(initData.playerName, initData.ip, initData.port, initData.storeReplay);
+ }
+ catch (e)
+ {
+ const message = sprintf(translate("Cannot join game: %(message)s."), { "message": e.message });
+ messageBox(400, 200, message, translate("Error"));
+ }
+ }
+
+ onTick()
+ {
+ while (true)
+ {
+ const message = Engine.PollNetworkClient();
+ if (!message)
+ break;
+
+ switch (message.type)
+ {
+ case "players":
+ this.playerAssignments = message.newAssignments;
+ Engine.SendNetworkReady(2);
+ break;
+ case "start":
+ this.onLaunch(message);
+ // Process further pending netmessages in the session page.
+ return true;
+ default:
+ }
+ }
+ return false;
+ }
+
+ /**
+ * In the visual autostart path, we need to show the loading screen.
+ * Overload this as appropriate, the default implementation works for the public mod.
+ */
+ onLaunch(message)
+ {
+ Engine.SwitchGuiPage("page_loading.xml", {
+ "attribs": message.initAttributes,
+ "isRejoining": true,
+ "playerAssignments": this.playerAssignments
+ });
+ }
+}
Index: ps/trunk/binaries/data/mods/public/autostart/autostart_host.js
===================================================================
--- ps/trunk/binaries/data/mods/public/autostart/autostart_host.js
+++ ps/trunk/binaries/data/mods/public/autostart/autostart_host.js
@@ -0,0 +1,85 @@
+class AutoStartHost
+{
+ constructor(initData)
+ {
+ this.launched = false;
+ this.maxPlayers = initData.maxPlayers;
+ this.storeReplay = initData.storeReplay;
+ this.playerAssignments = {};
+
+ this.initAttribs = initData.attribs;
+
+ try
+ {
+ // Stun and password not implemented for autostart.
+ Engine.StartNetworkHost(initData.playerName, initData.port, false, "", initData.storeReplay);
+ }
+ catch (e)
+ {
+ const message = sprintf(translate("Cannot host game: %(message)s."), { "message": e.message });
+ messageBox(400, 200, message, translate("Error"));
+ }
+ }
+
+ /**
+ * Handles a simple implementation of player assignments.
+ * Should not need be overloaded in mods unless you want to change that logic.
+ */
+ onTick()
+ {
+ while (true)
+ {
+ const message = Engine.PollNetworkClient();
+ if (!message)
+ break;
+
+ switch (message.type)
+ {
+ case "players":
+ this.playerAssignments = message.newAssignments;
+ Engine.SendNetworkReady(2);
+ let max = 0;
+ for (const uid in this.playerAssignments)
+ {
+ max = Math.max(this.playerAssignments[uid].player, max);
+ if (this.playerAssignments[uid].player == -1)
+ Engine.AssignNetworkPlayer(++max, uid);
+ }
+ break;
+ case "ready":
+ this.playerAssignments[message.guid].status = message.status;
+ break;
+ case "start":
+ return true;
+ default:
+ }
+ }
+
+ if (!this.launched && Object.keys(this.playerAssignments).length == this.maxPlayers)
+ {
+ for (const uid in this.playerAssignments)
+ if (this.playerAssignments[uid].player == -1 || this.playerAssignments[uid].status == 0)
+ return false;
+ this.onLaunch();
+ }
+ return false;
+ }
+
+ /**
+ * In the visual autostart path, we need to show the loading screen.
+ * Overload this as appropriate.
+ */
+ onLaunch()
+ {
+ this.launched = true;
+
+ this.settings = new GameSettings().init();
+ this.settings.fromInitAttributes(this.initAttribs);
+ this.settings.playerCount.setNb(Object.keys(this.playerAssignments).length);
+ this.settings.launchGame(this.playerAssignments, this.storeReplay);
+ Engine.SwitchGuiPage("page_loading.xml", {
+ "attribs": this.initAttribs,
+ "playerAssignments": this.playerAssignments
+ });
+ }
+}
Index: ps/trunk/binaries/data/mods/public/autostart/entrypoint.js
===================================================================
--- ps/trunk/binaries/data/mods/public/autostart/entrypoint.js
+++ ps/trunk/binaries/data/mods/public/autostart/entrypoint.js
@@ -0,0 +1,58 @@
+/**
+ * Implement autostart for the public mod.
+ * We want to avoid relying on too many specific files, so we'll mock a few engine functions.
+ * Depending on the path, these may get overwritten with the real function.
+ */
+
+Engine.HasXmppClient = () => false;
+Engine.SetRankedGame = () => {};
+Engine.TextureExists = () => false;
+Engine.PushGuiPage = () => {};
+Engine.SwitchGuiPage = () => {};
+
+var translateObjectKeys = () => {}
+var translate = x => x;
+var translateWithContext = x => x;
+
+// Required for functions such as sprintf.
+Engine.LoadScript("globalscripts/");
+// MsgBox is used in the failure path.
+// TODO: clean this up and show errors better in the non-visual path.
+Engine.LoadScript("gui/common/functions_msgbox.js");
+
+var autostartInstance;
+
+function autostartClient(initData)
+{
+ autostartInstance = new AutoStartClient(initData);
+}
+
+/**
+ * This path depends on files currently stored under gui/, which should be moved.
+ * The best place would probably be a new 'engine' mod, independent from the 'mod' mod and the public mod.
+ */
+function autostartHost(initData, networked = false)
+{
+ Engine.LoadScript("gui/common/color.js");
+ Engine.LoadScript("gui/common/functions_utility.js");
+ Engine.LoadScript("gui/common/Observable.js");
+ Engine.LoadScript("gui/common/settings.js");
+
+ Engine.LoadScript("gui/maps/MapCache.js")
+
+ Engine.LoadScript("gamesettings/");
+ Engine.LoadScript("gamesettings/attributes/");
+
+ if (networked)
+ autostartInstance = new AutoStartHost(initData);
+ else
+ autostartInstance = new AutoStart(initData);
+}
+
+/**
+ * @returns false if the loop should carry on.
+ */
+function onTick()
+{
+ return autostartInstance.onTick();
+}
Index: ps/trunk/binaries/data/mods/public/gamesettings/GameSetting.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/GameSetting.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/GameSetting.js
@@ -0,0 +1,36 @@
+/**
+ * The game settings are split in subclasses that are only responsible
+ * for a logical subset of settings.
+ * These are observables so updates are automated.
+ */
+class GameSetting extends Observable /* ProfilableMixin(Observable) /* Replace to profile automatically. */
+{
+ constructor(settings)
+ {
+ super();
+ this.settings = settings;
+ }
+
+ getDefaultValue(settingsProp, dataProp)
+ {
+ for (let index in g_Settings[settingsProp])
+ if (g_Settings[settingsProp][index].Default)
+ return g_Settings[settingsProp][index][dataProp];
+ return undefined;
+ }
+
+ getLegacySetting(attrib, name)
+ {
+ if (!attrib || !attrib.settings || attrib.settings[name] === undefined)
+ return undefined;
+ return attrib.settings[name];
+ }
+
+ getMapSetting(name)
+ {
+ if (!this.settings.map.data || !this.settings.map.data.settings ||
+ this.settings.map.data.settings[name] === undefined)
+ return undefined;
+ return this.settings.map.data.settings[name];
+ }
+}
Index: ps/trunk/binaries/data/mods/public/gamesettings/GameSettings.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/GameSettings.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/GameSettings.js
@@ -0,0 +1,155 @@
+/**
+ * Data store for game settings.
+ *
+ * This is intended as a helper to create the settings object for a game.
+ * This object is referred to as:
+ * - g_InitAttributes in the GUI session context
+ * - InitAttributes in the JS simulation context
+ * - Either InitAttributes or MapSettings in the C++ simulation.
+ * Settings can depend on each other, and the map provides many.
+ * This class's job is thus to provide a simpler interface around that.
+ */
+class GameSettings
+{
+ init(mapCache)
+ {
+ if (!mapCache)
+ mapCache = new MapCache();
+ Object.defineProperty(this, "mapCache", {
+ "value": mapCache,
+ });
+
+ // Load all possible civ data - don't presume that some will be available.
+ Object.defineProperty(this, "civData", {
+ "value": loadCivData(false, false),
+ });
+
+ Object.defineProperty(this, "isNetworked", {
+ "value": Engine.HasNetClient(),
+ });
+
+ // Load attributes as regular enumerable (i.e. iterable) properties.
+ for (let comp in GameSettings.prototype.Attributes)
+ {
+ let name = comp[0].toLowerCase() + comp.substr(1);
+ if (name in this)
+ error("Game Settings attribute '" + name + "' is already used.");
+ this[name] = new GameSettings.prototype.Attributes[comp](this);
+ }
+ for (let comp in this)
+ if (this[comp].init)
+ this[comp].init();
+
+ return this;
+ }
+
+ /**
+ * 'Serialize' the settings into the InitAttributes format,
+ * which can then be saved as JSON.
+ * Used to set the InitAttributes, for network synching, for hotloading & for persistence.
+ * TODO: it would probably be better to have different paths for at least a few of these.
+ */
+ toInitAttributes()
+ {
+ let attribs = {
+ "settings": {}
+ };
+ for (let comp in this)
+ if (this[comp].toInitAttributes)
+ this[comp].toInitAttributes(attribs);
+
+ return attribs;
+ }
+
+ /**
+ * Deserialize from a the InitAttributes format (i.e. parsed JSON).
+ * TODO: this could/should maybe support partial deserialization,
+ * which means MP might actually send only the bits that change.
+ */
+ fromInitAttributes(attribs)
+ {
+ // Settings may depend on eachother. Some selections of settings
+ // might be illegal. So keep looping through all settings until
+ // we find something stable.
+ const components = Object.keys(this);
+
+ // To check in the loop below if something change we just compare
+ // the entire component. However, we must ignore the "settings"
+ // keyword to avoid cyclic objects.
+ const getComponentData = comp => Object.keys(this[comp]).map(key => key == "settings" ? undefined : this[comp][key]);
+
+ // When we have looped components.length + 1 times, we are considered stuck.
+ for (let i = 0; i <= components.length; ++i)
+ {
+ // Re-init if any setting was changed, to make sure dependencies are cleared.
+ let reInit = false;
+ for (const comp in this)
+ {
+ const oldSettings = clone(getComponentData(comp));
+ if (this[comp].fromInitAttributes)
+ this[comp].fromInitAttributes(attribs);
+ reInit = reInit || !deepCompare(oldSettings, getComponentData(comp));
+ }
+ if (!reInit)
+ return;
+ }
+
+ throw new Error("Infinite loop initializing attributes detected, components: " + uneval(components));
+ }
+
+ /**
+ * Change "random" settings into their proper settings.
+ */
+ pickRandomItems()
+ {
+ const components = Object.keys(this);
+
+ // When we have looped components.length + 1 times, we are considered stuck.
+ for (let i = 0; i <= components.length; ++i)
+ {
+ // Re-pick if any random setting was unrandomised, to make sure dependencies are cleared.
+ let rePick = false;
+ for (const comp in this)
+ if (this[comp].pickRandomItems)
+ rePick = this[comp].pickRandomItems() || rePick;
+ if (!rePick)
+ return;
+ }
+
+ throw new Error("Infinite loop picking random items detected, components: " + uneval(components));
+ }
+
+ /**
+ * Start the game & switch to the loading page.
+ * This is here because there's limited value in having a separate folder/file for it,
+ * since you'll need a GameSettings object anyways.
+ * @param playerAssignments - A dict of 'local'/GUID per player and their name/slot.
+ */
+ launchGame(playerAssignments, storeReplay)
+ {
+ this.pickRandomItems();
+
+ // Let the settings finalize themselves. Let them do anything they need to do before the
+ // game starts and set any value in the attributes which mustn't be persisted.
+ const attribs = this.toInitAttributes();
+ for (const comp in this)
+ if (this[comp].onFinalizeAttributes)
+ this[comp].onFinalizeAttributes(attribs, playerAssignments);
+
+ Object.defineProperty(this, "finalizedAttributes", {
+ "value": deepfreeze(attribs)
+ });
+
+ // NB: for multiplayer support, the clients must be listening to "start" net messages.
+ if (this.isNetworked)
+ Engine.StartNetworkGame(this.finalizedAttributes, storeReplay);
+ else
+ Engine.StartGame(this.finalizedAttributes, playerAssignments.local.player, storeReplay);
+ }
+}
+
+Object.defineProperty(GameSettings.prototype, "Attributes", {
+ "value": {},
+ "enumerable": false,
+ "writable": true,
+});
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/Biome.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/Biome.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/Biome.js
@@ -0,0 +1,91 @@
+GameSettings.prototype.Attributes.Biome = class Biome extends GameSetting
+{
+ init()
+ {
+ this.biomes = loadBiomes();
+ this.biomeData = {};
+ for (const biome of this.biomes)
+ this.biomeData[biome.Id] = biome;
+ this.cachedMapData = undefined;
+
+ this.biome = undefined;
+ // NB: random is always available.
+ this.available = new Set();
+
+ this.settings.map.watch(() => this.onMapChange(), ["map"]);
+ }
+
+ toInitAttributes(attribs)
+ {
+ if (!this.biome)
+ return;
+ attribs.settings.Biome = this.biome;
+ }
+
+ fromInitAttributes(attribs)
+ {
+ if (!this.getLegacySetting(attribs, "Biome"))
+ this.setBiome(undefined);
+ else
+ this.setBiome(this.getLegacySetting(attribs, "Biome"));
+ }
+
+ filterBiome(available)
+ {
+ if (typeof available === "string")
+ return biome => biome.Id.startsWith(available);
+
+ return biome => available.indexOf(biome.Id) !== -1;
+ }
+
+ onMapChange()
+ {
+ const mapData = this.settings.map.data;
+ if (mapData && mapData.settings && mapData.settings.SupportedBiomes !== undefined)
+ {
+ if (mapData.settings.SupportedBiomes === this.cachedMapData)
+ return;
+ this.cachedMapData = mapData.settings.SupportedBiomes;
+ this.available = new Set(this.biomes.filter(this.filterBiome(mapData.settings.SupportedBiomes))
+ .map(biome => biome.Id));
+ this.biome = "random";
+ }
+ else if (this.cachedMapData !== undefined)
+ {
+ this.cachedMapData = undefined;
+ this.available = new Set();
+ this.biome = undefined;
+ }
+ }
+
+ setBiome(biome)
+ {
+ // TODO: more validation.
+ if (this.available.size)
+ this.biome = biome || "random";
+ else
+ this.biome = undefined;
+ }
+
+ getAvailableBiomeData()
+ {
+ return Array.from(this.available).map(biome => this.biomeData[biome]);
+ }
+
+ getData()
+ {
+ if (!this.biome)
+ return undefined;
+ return this.biomeData[this.biome];
+ }
+
+ pickRandomItems()
+ {
+ // If the map is random, we need to wait until it selects to know if we need to pick a biome.
+ if (this.settings.map.map === "random" || this.biome !== "random")
+ return false;
+
+ this.biome = pickRandom(Array.from(this.available));
+ return true;
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/CampaignData.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/CampaignData.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/CampaignData.js
@@ -0,0 +1,17 @@
+/**
+ * Store campaign-related data.
+ * This is just a passthrough and makes no assumption about the data.
+ */
+GameSettings.prototype.Attributes.CampaignData = class CampaignData extends GameSetting
+{
+ toInitAttributes(attribs)
+ {
+ if (this.value)
+ attribs.campaignData = this.value;
+ }
+
+ fromInitAttributes(attribs)
+ {
+ this.value = attribs.campaignData;
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/Ceasefire.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/Ceasefire.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/Ceasefire.js
@@ -0,0 +1,36 @@
+GameSettings.prototype.Attributes.Ceasefire = class Ceasefire extends GameSetting
+{
+ init()
+ {
+ this.value = 0;
+ this.settings.map.watch(() => this.onMapChange(), ["map"]);
+ }
+
+ toInitAttributes(attribs)
+ {
+ attribs.settings.Ceasefire = this.value;
+ }
+
+ fromInitAttributes(attribs)
+ {
+ if (!this.getLegacySetting(attribs, "Ceasefire"))
+ this.value = 0;
+ else
+ this.value = +this.getLegacySetting(attribs, "Ceasefire");
+ }
+
+ onMapChange()
+ {
+ if (this.settings.map.type != "scenario")
+ return;
+ if (!this.getMapSetting("Ceasefire"))
+ this.value = 0;
+ else
+ this.value = +this.getMapSetting("Ceasefire");
+ }
+
+ setValue(val)
+ {
+ this.value = Math.round(val);
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/Cheats.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/Cheats.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/Cheats.js
@@ -0,0 +1,33 @@
+GameSettings.prototype.Attributes.Cheats = class Cheats extends GameSetting
+{
+ init()
+ {
+ this.enabled = false;
+ this.settings.rating.watch(() => this.maybeUpdate(), ["enabled"]);
+ }
+
+ toInitAttributes(attribs)
+ {
+ attribs.settings.CheatsEnabled = this.enabled;
+ }
+
+ fromInitAttributes(attribs)
+ {
+ this.enabled = !!this.getLegacySetting(attribs, "CheatsEnabled");
+ }
+
+ _set(enabled)
+ {
+ this.enabled = (enabled && !this.settings.rating.enabled);
+ }
+
+ setEnabled(enabled)
+ {
+ this._set(enabled);
+ }
+
+ maybeUpdate()
+ {
+ this._set(this.enabled);
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/CircularMap.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/CircularMap.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/CircularMap.js
@@ -0,0 +1,32 @@
+/**
+ * This doesn't have a GUI setting.
+ */
+GameSettings.prototype.Attributes.CircularMap = class CircularMap extends GameSetting
+{
+ init()
+ {
+ this.value = false;
+ this.settings.map.watch(() => this.onMapChange(), ["map"]);
+ }
+
+ toInitAttributes(attribs)
+ {
+ attribs.settings.CircularMap = this.value;
+ }
+
+ fromInitAttributes(attribs)
+ {
+ if (this.getLegacySetting(attribs, "CircularMap") !== undefined)
+ this.value = !!this.getLegacySetting(attribs, "CircularMap");
+ }
+
+ onMapChange()
+ {
+ this.value = this.getMapSetting("CircularMap") || false;
+ }
+
+ setValue(val)
+ {
+ this.value = val;
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/Daytime.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/Daytime.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/Daytime.js
@@ -0,0 +1,66 @@
+GameSettings.prototype.Attributes.Daytime = class Daytime extends GameSetting
+{
+ init()
+ {
+ this.setDataValueHelper(undefined, undefined);
+ this.settings.map.watch(() => this.onMapChange(), ["map"]);
+ }
+
+ toInitAttributes(attribs)
+ {
+ if (this.value)
+ attribs.settings.Daytime = this.value;
+ }
+
+ fromInitAttributes(attribs)
+ {
+ if (!this.getLegacySetting(attribs, "Daytime"))
+ this.setValue(undefined);
+ else
+ this.setValue(this.getLegacySetting(attribs, "Daytime"));
+ }
+
+ onMapChange()
+ {
+ let mapData = this.settings.map.data;
+ if (!mapData || !mapData.settings || !mapData.settings.Daytime)
+ {
+ this.setDataValueHelper(undefined, undefined);
+ return;
+ }
+ // TODO: validation
+ this.setDataValueHelper(mapData.settings.Daytime, "random");
+ }
+
+ setValue(val)
+ {
+ // TODO: more validation.
+ if (this.data)
+ this.value = val || "random";
+ else
+ this.value = undefined;
+ }
+
+ pickRandomItems()
+ {
+ // If the map is random, we need to wait until it is selected.
+ if (this.settings.map.map === "random" || this.value !== "random")
+ return false;
+
+ this.value = pickRandom(this.data).Id;
+ return true;
+ }
+
+ /**
+ * Helper function to ensure this.data and this.value
+ * are assigned in the correct order to prevent
+ * crashes in the renderer.
+ * @param {object} data - The day time option data.
+ * @param {string} value - The option's key.
+ */
+ setDataValueHelper(data, value)
+ {
+ this.data = data;
+ this.value = value;
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/DisableSpies.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/DisableSpies.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/DisableSpies.js
@@ -0,0 +1,30 @@
+GameSettings.prototype.Attributes.DisableSpies = class DisableSpies extends GameSetting
+{
+ init()
+ {
+ this.enabled = false;
+ this.settings.map.watch(() => this.onMapChange(), ["map"]);
+ }
+
+ toInitAttributes(attribs)
+ {
+ attribs.settings.DisableSpies = this.enabled;
+ }
+
+ fromInitAttributes(attribs)
+ {
+ this.enabled = !!this.getLegacySetting(attribs, "DisableSpies");
+ }
+
+ onMapChange()
+ {
+ if (this.settings.map.type != "scenario")
+ return;
+ this.setEnabled(!!this.getMapSetting("DisableSpies"));
+ }
+
+ setEnabled(enabled)
+ {
+ this.enabled = enabled;
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/DisableTreasures.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/DisableTreasures.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/DisableTreasures.js
@@ -0,0 +1,30 @@
+GameSettings.prototype.Attributes.DisableTreasures = class DisableTreasures extends GameSetting
+{
+ init()
+ {
+ this.enabled = false;
+ this.settings.map.watch(() => this.onMapChange(), ["map"]);
+ }
+
+ toInitAttributes(attribs)
+ {
+ attribs.settings.DisableTreasures = this.enabled;
+ }
+
+ fromInitAttributes(attribs)
+ {
+ this.enabled = !!this.getLegacySetting(attribs, "DisableTreasures");
+ }
+
+ onMapChange()
+ {
+ if (this.settings.map.type != "scenario")
+ return;
+ this.setEnabled(!!this.getMapSetting("DisableTreasures"));
+ }
+
+ setEnabled(enabled)
+ {
+ this.enabled = enabled;
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/GameSpeed.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/GameSpeed.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/GameSpeed.js
@@ -0,0 +1,31 @@
+GameSettings.prototype.Attributes.GameSpeed = class GameSpeed extends GameSetting
+{
+ init()
+ {
+ this.gameSpeed = 1;
+ this.settings.map.watch(() => this.onMapChange(), ["map"]);
+ }
+
+ toInitAttributes(attribs)
+ {
+ attribs.gameSpeed = +this.gameSpeed;
+ }
+
+ fromInitAttributes(attribs)
+ {
+ if (attribs.gameSpeed)
+ this.gameSpeed = +attribs.gameSpeed;
+ }
+
+ onMapChange()
+ {
+ if (!this.getMapSetting("gameSpeed"))
+ return;
+ this.setSpeed(+this.getMapSetting("gameSpeed"));
+ }
+
+ setSpeed(speed)
+ {
+ this.gameSpeed = +speed;
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/Landscape.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/Landscape.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/Landscape.js
@@ -0,0 +1,75 @@
+GameSettings.prototype.Attributes.Landscape = class Landscape extends GameSetting
+{
+ init()
+ {
+ this.data = undefined;
+ this.value = undefined;
+ this.settings.map.watch(() => this.onMapChange(), ["map"]);
+ }
+
+ toInitAttributes(attribs)
+ {
+ if (this.value)
+ attribs.settings.Landscape = this.value;
+ }
+
+ fromInitAttributes(attribs)
+ {
+ if (!this.getLegacySetting(attribs, "Landscape"))
+ this.setValue(undefined);
+ else
+ this.setValue(this.getLegacySetting(attribs, "Landscape"));
+ }
+
+ onMapChange()
+ {
+ if (!this.getMapSetting("Landscapes"))
+ {
+ this.value = undefined;
+ this.data = undefined;
+ return;
+ }
+ // TODO: validation
+ this.data = this.getMapSetting("Landscapes");
+ this.value = "random";
+ }
+
+ setValue(val)
+ {
+ // TODO: more validation.
+ if (this.data)
+ this.value = val || "random";
+ else
+ this.value = undefined;
+ }
+
+ getPreviewFilename()
+ {
+ if (!this.value)
+ return undefined;
+ for (let group of this.data)
+ for (let item of group.Items)
+ if (item.Id == this.value)
+ return item.Preview;
+ return undefined;
+ }
+
+ pickRandomItems()
+ {
+ // If the map is random, we need to wait until it is selected.
+ if (this.settings.map.map === "random" || !this.value || !this.value.startsWith("random"))
+ return false;
+
+ let items = [];
+ if (this.value.indexOf("_") !== -1)
+ {
+ let subgroup = this.data.find(x => x.Id == this.value);
+ items = subgroup.Items.map(x => x.Id);
+ }
+ else
+ items = this.data.map(x => x.Items.map(item => item.Id)).flat();
+
+ this.value = pickRandom(items);
+ return true;
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/LastManStanding.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/LastManStanding.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/LastManStanding.js
@@ -0,0 +1,38 @@
+GameSettings.prototype.Attributes.LastManStanding = class LastManStanding extends GameSetting
+{
+ init()
+ {
+ this.available = !this.settings.lockedTeams.enabled;
+ this.enabled = false;
+ this.settings.lockedTeams.watch(() => this.maybeUpdate(), ["enabled"]);
+ this.settings.map.watch(() => this.onMapChange(), ["map"]);
+ }
+
+ toInitAttributes(attribs)
+ {
+ attribs.settings.LastManStanding = this.enabled;
+ }
+
+ fromInitAttributes(attribs)
+ {
+ this.enabled = !!this.getLegacySetting(attribs, "LastManStanding");
+ }
+
+ onMapChange()
+ {
+ if (this.settings.map.type != "scenario")
+ return;
+ this.setEnabled(!!this.getMapSetting("LastManStanding"));
+ }
+
+ setEnabled(enabled)
+ {
+ this.available = !this.settings.lockedTeams.enabled;
+ this.enabled = (enabled && this.available);
+ }
+
+ maybeUpdate()
+ {
+ this.setEnabled(this.enabled);
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/LockedTeams.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/LockedTeams.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/LockedTeams.js
@@ -0,0 +1,43 @@
+GameSettings.prototype.Attributes.LockedTeams = class LockedTeams extends GameSetting
+{
+ init()
+ {
+ this.enabled = false;
+ this.settings.map.watch(() => this.onMapChange(), ["map"]);
+ this.settings.rating.watch(() => this.onRatingChange(), ["enabled"]);
+ this.onRatingChange();
+ }
+
+ toInitAttributes(attribs)
+ {
+ attribs.settings.LockTeams = this.enabled;
+ }
+
+ fromInitAttributes(attribs)
+ {
+ this.enabled = !!this.getLegacySetting(attribs, "LockTeams");
+ }
+
+ onMapChange()
+ {
+ if (this.settings.map.type != "scenario")
+ return;
+ this.setEnabled(!!this.getMapSetting("LockTeams"));
+ }
+
+ onRatingChange()
+ {
+ if (this.settings.rating.enabled)
+ {
+ this.available = false;
+ this.setEnabled(true);
+ }
+ else
+ this.available = true;
+ }
+
+ setEnabled(enabled)
+ {
+ this.enabled = enabled;
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/Map.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/Map.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/Map.js
@@ -0,0 +1,69 @@
+/**
+ * Map choice. This handles:
+ * - the map itself
+ * - map type (which is mostly a GUI thing and should probably be refactored out)
+ * - map script (for random maps).
+ * When a non-"random" map is selected, the map 'script settings' are available at this.data.
+ * TODO: the map description is currently tied to the map itself.
+ */
+GameSettings.prototype.Attributes.Map = class Map extends GameSetting
+{
+ init()
+ {
+ this.watch(() => this.updateMapMetadata(), ["map"]);
+ this.randomOptions = [];
+ }
+
+ toInitAttributes(attribs)
+ {
+ attribs.map = this.map;
+ attribs.mapType = this.type;
+ if (this.script)
+ attribs.script = this.script;
+ }
+
+ fromInitAttributes(attribs)
+ {
+ if (attribs.mapType)
+ this.setType(attribs.mapType);
+
+ if (!attribs.map)
+ return;
+
+ this.selectMap(attribs.map);
+ }
+
+ setType(mapType)
+ {
+ this.type = mapType;
+ }
+
+ selectMap(map)
+ {
+ this.data = this.settings.mapCache.getMapData(this.type, map);
+ this.map = map;
+ }
+
+ updateMapMetadata()
+ {
+ if (this.type == "random" && this.data)
+ this.script = this.data.settings.Script;
+ else
+ this.script = undefined;
+ }
+
+ pickRandomItems()
+ {
+ if (this.map !== "random")
+ return false;
+ this.selectMap(pickRandom(this.randomOptions));
+ return true;
+ }
+
+ setRandomOptions(options)
+ {
+ this.randomOptions = clone(options);
+ if (this.randomOptions.indexOf("random") !== -1)
+ this.randomOptions.splice(this.randomOptions.indexOf("random"), 1);
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/MapExploration.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/MapExploration.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/MapExploration.js
@@ -0,0 +1,62 @@
+GameSettings.prototype.Attributes.MapExploration = class MapExploration extends GameSetting
+{
+ init()
+ {
+ this.resetAll();
+ this.settings.map.watch(() => this.onMapChange(), ["map"]);
+ }
+
+ resetAll()
+ {
+ this.explored = false;
+ this.revealed = false;
+ this.allied = false;
+ }
+
+ toInitAttributes(attribs)
+ {
+ attribs.settings.RevealMap = this.revealed;
+ attribs.settings.ExploreMap = this.explored;
+ attribs.settings.AllyView = this.allied;
+ }
+
+ fromInitAttributes(attribs)
+ {
+ this.explored = !!this.getLegacySetting(attribs, "ExploreMap");
+ this.revealed = !!this.getLegacySetting(attribs, "RevealMap");
+ this.allied = !!this.getLegacySetting(attribs, "AllyView");
+ }
+
+ onMapChange(mapData)
+ {
+ if (this.settings.map.type != "scenario")
+ return;
+ this.resetAll();
+ this.setExplored(this.getMapSetting("ExploreMap"));
+ this.setRevealed(this.getMapSetting("RevealMap"));
+ this.setAllied(this.getMapSetting("AllyView"));
+ }
+
+ setExplored(enabled)
+ {
+ if (enabled === undefined)
+ return;
+ this.explored = enabled;
+ this.revealed = this.revealed && this.explored;
+ }
+
+ setRevealed(enabled)
+ {
+ if (enabled === undefined)
+ return;
+ this.revealed = enabled;
+ this.explored = this.explored || this.revealed;
+ }
+
+ setAllied(enabled)
+ {
+ if (enabled === undefined)
+ return;
+ this.allied = enabled;
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/MapName.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/MapName.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/MapName.js
@@ -0,0 +1,28 @@
+/**
+ * Map name.
+ * This is usually just the regular map name, but can be overwritten.
+ */
+GameSettings.prototype.Attributes.MapName = class MapName extends GameSetting
+{
+ init()
+ {
+ this.value = undefined;
+ this.settings.map.watch(() => this.updateName(), ["map"]);
+ }
+
+ toInitAttributes(attribs)
+ {
+ attribs.settings.mapName = this.value;
+ }
+
+ fromInitAttributes(attribs)
+ {
+ if (attribs?.settings?.mapName)
+ this.value = attribs.settings.mapName;
+ }
+
+ updateName()
+ {
+ this.value = this.settings.map?.data?.settings?.Name || this.settings.map.map;
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/MapPreview.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/MapPreview.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/MapPreview.js
@@ -0,0 +1,64 @@
+/**
+ * Map Preview.
+ * Can optionally overwrite the default map preview.
+ */
+GameSettings.prototype.Attributes.MapPreview = class MapPreview extends GameSetting
+{
+ init()
+ {
+ this.value = undefined;
+ this.settings.map.watch(() => this.updatePreview(), ["map"]);
+ this.settings.biome.watch(() => this.updatePreview(), ["biome"]);
+ this.settings.landscape.watch(() => this.updatePreview(), ["value"]);
+ this.settings.daytime.watch(() => this.updatePreview(), ["value"]);
+ }
+
+ toInitAttributes(attribs)
+ {
+ if (this.value !== undefined)
+ attribs.settings.mapPreview = this.value;
+ }
+
+ fromInitAttributes(attribs)
+ {
+ if (!!this.getLegacySetting(attribs, "mapPreview"))
+ this.value = this.getLegacySetting(attribs, "mapPreview");
+ }
+
+ getPreviewForSubtype(basepath, subtype)
+ {
+ if (!subtype)
+ return undefined;
+ let substr = subtype.substr(subtype.lastIndexOf("/") + 1);
+ let path = basepath + "_" + substr + ".png";
+ if (this.settings.mapCache.previewExists(path))
+ return this.settings.mapCache.getMapPreview(this.settings.map.type,
+ this.settings.map.map, path);
+ return undefined;
+ }
+
+ getLandscapePreview()
+ {
+ let filename = this.settings.landscape.getPreviewFilename();
+ if (!filename)
+ return undefined;
+ return this.settings.mapCache.getMapPreview(this.settings.map.type,
+ this.settings.map.map, filename);
+ }
+
+ updatePreview()
+ {
+ if (!this.settings.map.map)
+ {
+ this.value = undefined;
+ return;
+ }
+
+ // This handles "random" map type (mostly for convenience).
+ let mapPath = basename(this.settings.map.map);
+ this.value = this.getPreviewForSubtype(mapPath, this.settings.biome.biome) ||
+ this.getLandscapePreview() ||
+ this.getPreviewForSubtype(mapPath, this.settings.daytime.value) ||
+ this.settings.mapCache.getMapPreview(this.settings.map.type, this.settings.map.map);
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/MapSize.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/MapSize.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/MapSize.js
@@ -0,0 +1,35 @@
+GameSettings.prototype.Attributes.MapSize = class MapSize extends GameSetting
+{
+ init()
+ {
+ this.defaultValue = this.getDefaultValue("MapSizes", "Tiles") || 256;
+ this.setSize(this.defaultValue);
+ this.settings.map.watch(() => this.onTypeChange(), ["type"]);
+ }
+
+ toInitAttributes(attribs)
+ {
+ if (this.settings.map.type === "random")
+ attribs.settings.Size = this.size;
+ }
+
+ fromInitAttributes(attribs)
+ {
+ if (!!this.getLegacySetting(attribs, "Size"))
+ this.setSize(this.getLegacySetting(attribs, "Size"));
+ }
+
+ setSize(size)
+ {
+ this.available = this.settings.map.type === "random";
+ this.size = size;
+ }
+
+ onTypeChange(old)
+ {
+ if (this.settings.map.type === "random" && old !== "random")
+ this.setSize(this.defaultValue);
+ else if (this.settings.map.type !== "random")
+ this.available = false;
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/MatchID.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/MatchID.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/MatchID.js
@@ -0,0 +1,7 @@
+GameSettings.prototype.Attributes.MatchID = class MatchID extends GameSetting
+{
+ onFinalizeAttributes(attribs)
+ {
+ attribs.matchID = Engine.GetMatchID();
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/Nomad.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/Nomad.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/Nomad.js
@@ -0,0 +1,23 @@
+GameSettings.prototype.Attributes.Nomad = class Nomad extends GameSetting
+{
+ init()
+ {
+ this.enabled = false;
+ }
+
+ toInitAttributes(attribs)
+ {
+ if (this.settings.map.type == "random")
+ attribs.settings.Nomad = this.enabled;
+ }
+
+ fromInitAttributes(attribs)
+ {
+ this.setEnabled(!!this.getLegacySetting(attribs, "Nomad"));
+ }
+
+ setEnabled(enabled)
+ {
+ this.enabled = enabled;
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/PlayerAI.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/PlayerAI.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/PlayerAI.js
@@ -0,0 +1,127 @@
+/**
+ * Stores AI settings for all players.
+ * Note that tby default, this does not assign AI
+ * unless an AI bot is explicitly specified.
+ * This is because:
+ * - the regular GameSetup does that on its own
+ * - this makes campaign/autostart scenarios easier to handle.
+ * - cleans the code here.
+ */
+GameSettings.prototype.Attributes.PlayerAI = class PlayerAI extends GameSetting
+{
+ init()
+ {
+ // NB: watchers aren't auto-triggered when modifying array elements.
+ this.values = [];
+ this.settings.playerCount.watch(() => this.maybeUpdate(), ["nbPlayers"]);
+ }
+
+ toInitAttributes(attribs)
+ {
+ if (!attribs.settings.PlayerData)
+ attribs.settings.PlayerData = [];
+ while (attribs.settings.PlayerData.length < this.values.length)
+ attribs.settings.PlayerData.push({});
+ for (let i = 0; i < this.values.length; ++i)
+ if (this.values[i])
+ {
+ attribs.settings.PlayerData[i].AI = this.values[i].bot;
+ attribs.settings.PlayerData[i].AIDiff = this.values[i].difficulty;
+ attribs.settings.PlayerData[i].AIBehavior = this.values[i].behavior;
+ }
+ else
+ attribs.settings.PlayerData[i].AI = false;
+ }
+
+ fromInitAttributes(attribs)
+ {
+ if (!this.getLegacySetting(attribs, "PlayerData"))
+ return;
+ const pData = this.getLegacySetting(attribs, "PlayerData");
+ for (let i = 0; i < this.values.length; ++i)
+ {
+ // Also covers the "" case.
+ if (!pData[i] || !pData[i].AI)
+ {
+ this.set(+i, undefined);
+ continue;
+ }
+ this.set(+i, {
+ "bot": pData[i].AI,
+ "difficulty": pData[i].AIDiff || +Engine.ConfigDB_GetValue("user", "gui.gamesetup.aidifficulty"),
+ "behavior": pData[i].AIBehavior || Engine.ConfigDB_GetValue("user", "gui.gamesetup.aibehavior"),
+ });
+ }
+ }
+
+ _resize(nb)
+ {
+ while (this.values.length > nb)
+ this.values.pop();
+ while (this.values.length < nb)
+ this.values.push(undefined);
+ }
+
+ maybeUpdate()
+ {
+ if (this.values.length === this.settings.playerCount.nbPlayers)
+ return;
+ this._resize(this.settings.playerCount.nbPlayers);
+ this.trigger("values");
+ }
+
+ swap(sourceIndex, targetIndex)
+ {
+ [this.values[sourceIndex], this.values[targetIndex]] = [this.values[targetIndex], this.values[sourceIndex]];
+ this.trigger("values");
+ }
+
+ set(playerIndex, botSettings)
+ {
+ this.values[playerIndex] = botSettings;
+ this.trigger("values");
+ }
+
+ setAI(playerIndex, ai)
+ {
+ let old = this.values[playerIndex] ? this.values[playerIndex].bot : undefined;
+ if (!ai)
+ this.values[playerIndex] = undefined;
+ else
+ this.values[playerIndex].bot = ai;
+ if (old !== (this.values[playerIndex] ? this.values[playerIndex].bot : undefined))
+ this.trigger("values");
+ }
+
+ setBehavior(playerIndex, value)
+ {
+ if (!this.values[playerIndex])
+ return;
+ this.values[playerIndex].behavior = value;
+ this.trigger("values");
+ }
+
+ setDifficulty(playerIndex, value)
+ {
+ if (!this.values[playerIndex])
+ return;
+ this.values[playerIndex].difficulty = value;
+ this.trigger("values");
+ }
+
+ get(playerIndex)
+ {
+ return this.values[playerIndex];
+ }
+
+ describe(playerIndex)
+ {
+ if (!this.values[playerIndex])
+ return "";
+ return translateAISettings({
+ "AI": this.values[playerIndex].bot,
+ "AIDiff": this.values[playerIndex].difficulty,
+ "AIBehavior": this.values[playerIndex].behavior,
+ });
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/PlayerCiv.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/PlayerCiv.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/PlayerCiv.js
@@ -0,0 +1,136 @@
+/**
+ * Stores civ settings for all players.
+ */
+GameSettings.prototype.Attributes.PlayerCiv = class PlayerCiv extends GameSetting
+{
+ init()
+ {
+ // NB: watchers aren't auto-triggered when modifying array elements.
+ this.values = [];
+ this.locked = [];
+ this.settings.playerCount.watch(() => this.maybeUpdate(), ["nbPlayers"]);
+ this.settings.map.watch(() => this.onMapChange(), ["map"]);
+ }
+
+ toInitAttributes(attribs)
+ {
+ if (!attribs.settings.PlayerData)
+ attribs.settings.PlayerData = [];
+ while (attribs.settings.PlayerData.length < this.values.length)
+ attribs.settings.PlayerData.push({});
+ for (let i in this.values)
+ if (this.values[i])
+ attribs.settings.PlayerData[i].Civ = this.values[i];
+ }
+
+ fromInitAttributes(attribs)
+ {
+ if (!this.getLegacySetting(attribs, "PlayerData"))
+ return;
+ const pData = this.getLegacySetting(attribs, "PlayerData");
+ for (let i = 0; i < this.values.length; ++i)
+ if (pData[i] && pData[i].Civ)
+ this.setValue(i, pData[i].Civ);
+ }
+
+ _resize(nb)
+ {
+ while (this.values.length > nb)
+ {
+ this.values.pop();
+ this.locked.pop();
+ }
+ while (this.values.length < nb)
+ {
+ this.values.push("random");
+ this.locked.push(false);
+ }
+ }
+
+ onMapChange()
+ {
+ // Reset.
+ if (this.settings.map.type == "scenario" ||
+ this.getMapSetting("PlayerData") &&
+ this.getMapSetting("PlayerData").some(data => data && data.Civ))
+ {
+ this._resize(0);
+ this.maybeUpdate();
+ }
+ else
+ {
+ this.locked = this.locked.map(x => false);
+ this.trigger("locked");
+ }
+ }
+
+ maybeUpdate()
+ {
+ this._resize(this.settings.playerCount.nbPlayers);
+ this.values.forEach((c, i) => this._set(i, c));
+ this.trigger("values");
+ }
+
+ pickRandomItems()
+ {
+ // Get a unique array of selectable cultures
+ let cultures = Object.keys(this.settings.civData).filter(civ => this.settings.civData[civ].SelectableInGameSetup).map(civ => this.settings.civData[civ].Culture);
+ cultures = cultures.filter((culture, index) => cultures.indexOf(culture) === index);
+
+ let picked = false;
+ for (let i in this.values)
+ {
+ if (this.values[i] != "random")
+ continue;
+ picked = true;
+
+ // Pick a random civ of a random culture
+ let culture = pickRandom(cultures);
+ this.values[i] = pickRandom(Object.keys(this.settings.civData).filter(civ =>
+ this.settings.civData[civ].Culture == culture && this.settings.civData[civ].SelectableInGameSetup));
+
+ }
+ if (picked)
+ this.trigger("values");
+
+ return picked;
+ }
+
+ _getMapData(i)
+ {
+ let data = this.settings.map.data;
+ if (!data || !data.settings || !data.settings.PlayerData)
+ return undefined;
+ if (data.settings.PlayerData.length <= i)
+ return undefined;
+ return data.settings.PlayerData[i].Civ;
+ }
+
+ _set(playerIndex, value)
+ {
+ let map = this._getMapData(playerIndex);
+ if (!!map)
+ {
+ this.values[playerIndex] = map;
+ this.locked[playerIndex] = true;
+ }
+ else
+ {
+ this.values[playerIndex] = value;
+ this.locked[playerIndex] = this.settings.map.type == "scenario";
+ }
+ }
+
+ setValue(playerIndex, val)
+ {
+ this._set(playerIndex, val);
+ this.trigger("values");
+ }
+
+ swap(sourceIndex, targetIndex)
+ {
+ [this.values[sourceIndex], this.values[targetIndex]] = [this.values[targetIndex], this.values[sourceIndex]];
+ [this.locked[sourceIndex], this.locked[targetIndex]] = [this.locked[targetIndex], this.locked[sourceIndex]];
+ this.trigger("values");
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/PlayerColor.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/PlayerColor.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/PlayerColor.js
@@ -0,0 +1,191 @@
+/**
+ * Stores player color for all players.
+ */
+GameSettings.prototype.Attributes.PlayerColor = class PlayerColor extends GameSetting
+{
+ init()
+ {
+ this.defaultColors = g_Settings.PlayerDefaults.slice(1).map(pData => pData.Color);
+
+ this.watch(() => this.maybeUpdate(), ["available"]);
+ this.settings.playerCount.watch(() => this.maybeUpdate(), ["nbPlayers"]);
+ this.settings.map.watch(() => this.onMapChange(), ["map"]);
+
+ // NB: watchers aren't auto-triggered when modifying array elements.
+ this.values = [];
+ this.locked = [];
+ this._updateAvailable();
+ }
+
+ toInitAttributes(attribs)
+ {
+ if (!attribs.settings.PlayerData)
+ attribs.settings.PlayerData = [];
+ while (attribs.settings.PlayerData.length < this.values.length)
+ attribs.settings.PlayerData.push({});
+ for (let i in this.values)
+ if (this.values[i])
+ attribs.settings.PlayerData[i].Color = this.values[i];
+ }
+
+ fromInitAttributes(attribs)
+ {
+ if (!this.getLegacySetting(attribs, "PlayerData"))
+ return;
+ const pData = this.getLegacySetting(attribs, "PlayerData");
+ for (let i = 0; i < this.values.length; ++i)
+ if (pData[i] && pData[i].Color)
+ this.setColor(i, pData[i].Color);
+ }
+
+ _resize(nb)
+ {
+ while (this.values.length > nb)
+ {
+ this.values.pop();
+ this.locked.pop();
+ }
+ while (this.values.length < nb)
+ {
+ this.values.push(undefined);
+ this.locked.push(false);
+ }
+ }
+
+ onMapChange()
+ {
+ // Reset.
+ this.locked = this.locked.map(x => this.settings.map.type == "scenario");
+ this.trigger("locked");
+ if (this.settings.map.type === "scenario")
+ this._resize(0);
+ this._updateAvailable();
+ this.maybeUpdate();
+ }
+
+ maybeUpdate()
+ {
+ this._resize(this.settings.playerCount.nbPlayers);
+
+ this.values.forEach((c, i) => this._set(i, c));
+ this.trigger("values");
+ }
+
+ _set(playerIndex, color)
+ {
+ let inUse = this.values.findIndex((otherColor, i) =>
+ color && otherColor &&
+ sameColor(color, otherColor));
+ if (inUse != -1 && inUse != playerIndex)
+ {
+ // Swap colors.
+ let col = this.values[playerIndex];
+ this.values[playerIndex] = undefined;
+ this._set(inUse, col);
+ }
+ if (!color || this.available.indexOf(color) == -1)
+ {
+ this.values[playerIndex] = color ?
+ this._findClosestColor(color, this.available) :
+ this._getUnusedColor();
+ }
+ else
+ this.values[playerIndex] = color;
+ }
+
+ get(playerIndex)
+ {
+ if (playerIndex >= this.values.length)
+ return undefined;
+ return this.values[playerIndex];
+ }
+
+ setColor(playerIndex, color)
+ {
+ this._set(playerIndex, color);
+ this.trigger("values");
+ }
+
+ swap(sourceIndex, targetIndex)
+ {
+ [this.values[sourceIndex], this.values[targetIndex]] = [this.values[targetIndex], this.values[sourceIndex]];
+ [this.locked[sourceIndex], this.locked[targetIndex]] = [this.locked[targetIndex], this.locked[sourceIndex]];
+ this.trigger("values");
+ }
+
+ _getMapData(i)
+ {
+ let data = this.settings.map.data;
+ if (!data || !data.settings || !data.settings.PlayerData)
+ return undefined;
+ if (data.settings.PlayerData.length <= i)
+ return undefined;
+ return data.settings.PlayerData[i].Color;
+ }
+
+ _updateAvailable()
+ {
+ // 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 = [];
+ let mapColors = false;
+ for (let i = 0; i < g_MaxPlayers; ++i)
+ {
+ let col = this._getMapData(i);
+ if (col)
+ mapColors = true;
+ if (mapColors)
+ values.push(col || this._findFarthestUnusedColor(values));
+ else
+ values.push(this.defaultColors[i]);
+ }
+ this.available = values;
+ }
+
+ _findClosestColor(targetColor, colors)
+ {
+ let closestColor;
+ let closestColorDistance = 0;
+ for (let color of colors)
+ {
+ let dist = colorDistance(targetColor, color);
+ if (!closestColor || dist < closestColorDistance)
+ {
+ closestColor = color;
+ closestColorDistance = dist;
+ }
+ }
+ return closestColor;
+ }
+
+ _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.available.find(color => {
+ return this.values.every(otherColor => !otherColor || !sameColor(color, otherColor));
+ });
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/PlayerCount.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/PlayerCount.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/PlayerCount.js
@@ -0,0 +1,65 @@
+GameSettings.prototype.Attributes.PlayerCount = class PlayerCount extends GameSetting
+{
+ init()
+ {
+ this.nbPlayers = 1;
+ this.settings.map.watch(() => this.onMapTypeChange(), ["type"]);
+ this.settings.map.watch(() => this.onMapChange(), ["map"]);
+ }
+
+ toInitAttributes(attribs)
+ {
+ if (!attribs.settings.PlayerData)
+ attribs.settings.PlayerData = [];
+ while (attribs.settings.PlayerData.length < this.nbPlayers)
+ attribs.settings.PlayerData.push({});
+ }
+
+ fromInitAttributes(attribs)
+ {
+ if (this.settings.map.map !== undefined)
+ this.reloadFromLegacy(attribs);
+ }
+
+ onMapTypeChange(old)
+ {
+ if (this.settings.map.type == "random" && old != "random")
+ this.nbPlayers = 2;
+ }
+
+ onMapChange()
+ {
+ if (this.settings.map.type == "random")
+ return;
+ if (!this.settings.map.data || !this.settings.map.data.settings ||
+ !this.settings.map.data.settings.PlayerData)
+ return;
+ this.nbPlayers = this.settings.map.data.settings.PlayerData.length;
+ }
+
+ reloadFromLegacy(data)
+ {
+ if (this.settings.map.type != "random")
+ {
+ if (this.settings.map.data && this.settings.map.data.settings && this.settings.map.data.settings.PlayerData)
+ this.nbPlayers = this.settings.map.data.settings.PlayerData.length;
+ return;
+ }
+ if (!data || !data.settings || data.settings.PlayerData === undefined)
+ return;
+ this.nbPlayers = data.settings.PlayerData.length;
+ }
+
+ /**
+ * @param index - Player Index, 0 is 'player 1' since GAIA isn't there.
+ */
+ get(index)
+ {
+ return this.data[index];
+ }
+
+ setNb(nb)
+ {
+ this.nbPlayers = Math.max(1, Math.min(g_MaxPlayers, nb));
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/PlayerName.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/PlayerName.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/PlayerName.js
@@ -0,0 +1,136 @@
+/**
+ * Stores in-game names for all players.
+ *
+ * NB: the regular gamesetup has a particular handling of this setting.
+ * The names are loaded from the map, but the GUI also show playernames.
+ * Force these at the start of the match.
+ */
+GameSettings.prototype.Attributes.PlayerName = class PlayerName extends GameSetting
+{
+ init()
+ {
+ // NB: watchers aren't auto-triggered when modifying array elements.
+ this.values = [];
+ this.settings.playerCount.watch(() => this.maybeUpdate(), ["nbPlayers"]);
+ this.settings.map.watch(() => this.onMapChange(), ["map"]);
+ }
+
+ toInitAttributes(attribs)
+ {
+ if (!attribs.settings.PlayerData)
+ attribs.settings.PlayerData = [];
+ while (attribs.settings.PlayerData.length < this.values.length)
+ attribs.settings.PlayerData.push({});
+ for (let i in this.values)
+ if (this.values[i])
+ attribs.settings.PlayerData[i].Name = this.values[i];
+ }
+
+ fromInitAttributes(attribs)
+ {
+ if (!this.getLegacySetting(attribs, "PlayerData"))
+ return;
+ const pData = this.getLegacySetting(attribs, "PlayerData");
+ for (let i = 0; i < this.values.length; ++i)
+ if (pData[i] && pData[i].Name !== undefined)
+ {
+ this.values[i] = pData[i].Name;
+ this.trigger("values");
+ }
+ }
+
+ _resize(nb)
+ {
+ while (this.values.length > nb)
+ this.values.pop();
+ while (this.values.length < nb)
+ this.values.push(undefined);
+ }
+
+ onMapChange()
+ {
+ // Reset.
+ this._resize(0);
+ this.maybeUpdate();
+ }
+
+ maybeUpdate()
+ {
+ this._resize(this.settings.playerCount.nbPlayers);
+ this.values.forEach((_, i) => this._set(i));
+ this.trigger("values");
+ }
+
+ /**
+ * Pick bot names.
+ */
+ pickRandomItems()
+ {
+ let picked = false;
+ for (let i in this.values)
+ {
+ if (!!this.values[i] &&
+ this.values[i] !== g_Settings.PlayerDefaults[+i + 1].Name)
+ continue;
+
+ let ai = this.settings.playerAI.values[i];
+ if (!ai)
+ continue;
+
+ let civ = this.settings.playerCiv.values[i];
+ if (!civ || civ == "random")
+ continue;
+
+ picked = true;
+ // Pick one of the available botnames for the chosen civ
+ // Determine botnames
+ let chosenName = pickRandom(this.settings.civData[civ].AINames);
+ if (!this.settings.isNetworked)
+ chosenName = translate(chosenName);
+
+ // Count how many players use the chosenName
+ let usedName = this.values.filter(oName => oName && oName.indexOf(chosenName) !== -1).length;
+
+ this.values[i] =
+ usedName ?
+ sprintf(this.RomanLabel, {
+ "playerName": chosenName,
+ "romanNumber": this.RomanNumbers[usedName + 1]
+ }) :
+ chosenName;
+ }
+ if (picked)
+ this.trigger("values");
+ return picked;
+ }
+
+ onFinalizeAttributes(attribs, playerAssignments)
+ {
+ // Replace client player names with the real players.
+ for (const guid in playerAssignments)
+ if (playerAssignments[guid].player !== -1)
+ attribs.settings.PlayerData[playerAssignments[guid].player -1].Name = playerAssignments[guid].name;
+ }
+
+ _getMapData(i)
+ {
+ let data = this.settings.map.data;
+ if (!data || !data.settings || !data.settings.PlayerData)
+ return undefined;
+ if (data.settings.PlayerData.length <= i)
+ return undefined;
+ return data.settings.PlayerData[i].Name;
+ }
+
+ _set(playerIndex)
+ {
+ this.values[playerIndex] = this._getMapData(playerIndex) || g_Settings && g_Settings.PlayerDefaults[playerIndex + 1].Name || "";
+ }
+};
+
+
+GameSettings.prototype.Attributes.PlayerName.prototype.RomanLabel =
+ translate("%(playerName)s %(romanNumber)s");
+
+GameSettings.prototype.Attributes.PlayerName.prototype.RomanNumbers =
+ [undefined, "I", "II", "III", "IV", "V", "VI", "VII", "VIII"];
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/PlayerTeam.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/PlayerTeam.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/PlayerTeam.js
@@ -0,0 +1,80 @@
+/**
+ * Stores team settings for all players.
+ */
+GameSettings.prototype.Attributes.PlayerTeam = class PlayerTeam extends GameSetting
+{
+ init()
+ {
+ // NB: watchers aren't auto-triggered when modifying array elements.
+ this.values = [];
+ this.locked = [];
+ this.settings.playerCount.watch(() => this.maybeUpdate(), ["nbPlayers"]);
+ this.settings.map.watch(() => this.onMapChange(), ["map"]);
+ }
+
+ toInitAttributes(attribs)
+ {
+ if (!attribs.settings.PlayerData)
+ attribs.settings.PlayerData = [];
+ while (attribs.settings.PlayerData.length < this.values.length)
+ attribs.settings.PlayerData.push({});
+ for (let i in this.values)
+ if (this.values[i] !== undefined)
+ attribs.settings.PlayerData[i].Team = this.values[i];
+ }
+
+ fromInitAttributes(attribs)
+ {
+ if (!this.getLegacySetting(attribs, "PlayerData"))
+ return;
+ const pData = this.getLegacySetting(attribs, "PlayerData");
+ for (let i = 0; i < this.values.length; ++i)
+ if (pData[i] && pData[i].Team !== undefined)
+ this.setValue(i, pData[i].Team);
+ }
+
+ _resize(nb)
+ {
+ while (this.values.length > nb)
+ {
+ this.values.pop();
+ this.locked.pop();
+ }
+ while (this.values.length < nb)
+ {
+ // -1 is None
+ this.values.push(-1);
+ this.locked.push(false);
+ }
+ }
+
+ onMapChange()
+ {
+ this.locked = this.locked.map(x => this.settings.map.type === "scenario");
+ this.trigger("locked");
+ if (this.settings.map.type !== "scenario")
+ return;
+ const pData = this.getMapSetting("PlayerData");
+ for (const p in pData)
+ this._set(+p, pData[p].Team === undefined ? -1 : pData[p].Team);
+ this.trigger("values");
+ }
+
+ maybeUpdate()
+ {
+ this._resize(this.settings.playerCount.nbPlayers);
+ this.values.forEach((c, i) => this._set(i, c));
+ this.trigger("values");
+ }
+
+ _set(playerIndex, value)
+ {
+ this.values[playerIndex] = value;
+ }
+
+ setValue(playerIndex, val)
+ {
+ this._set(playerIndex, val);
+ this.trigger("values");
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/Population.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/Population.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/Population.js
@@ -0,0 +1,75 @@
+/**
+ * Combines the worldPopulation and regular population cap.
+ * At the moment those are incompatible so this makes sense.
+ * TODO: Should there be a dialog allowing per-player pop limits?
+ */
+GameSettings.prototype.Attributes.Population = class Population extends GameSetting
+{
+ init()
+ {
+ this.popDefault = this.getDefaultValue("PopulationCapacities", "Population") || 200;
+ this.worldPopDefault = this.getDefaultValue("WorldPopulationCapacities", "Population") || 800;
+
+ this.perPlayer = false;
+ this.useWorldPop = false;
+ this.cap = this.popDefault;
+ this.settings.map.watch(() => this.onMapChange(), ["map"]);
+ }
+
+ toInitAttributes(attribs)
+ {
+ if (this.perPlayer)
+ {
+ if (!attribs.settings.PlayerData)
+ attribs.settings.PlayerData = [];
+ while (attribs.settings.PlayerData.length < this.perPlayer.length)
+ attribs.settings.PlayerData.push({});
+ for (let i in this.perPlayer)
+ if (this.perPlayer[i])
+ attribs.settings.PlayerData[i].PopulationLimit = this.perPlayer[i];
+ }
+ if (this.useWorldPop)
+ {
+ attribs.settings.WorldPopulation = true;
+ attribs.settings.WorldPopulationCap = this.cap;
+ }
+ else
+ attribs.settings.PopulationCap = this.cap;
+ }
+
+ fromInitAttributes(attribs)
+ {
+ if (!!this.getLegacySetting(attribs, "WorldPopulation"))
+ this.setPopCap(true, this.getLegacySetting(attribs, "WorldPopulationCap"));
+ else if (!!this.getLegacySetting(attribs, "PopulationCap"))
+ this.setPopCap(false, this.getLegacySetting(attribs, "PopulationCap"));
+ }
+
+ onMapChange()
+ {
+ this.perPlayer = undefined;
+ if (this.settings.map.type != "scenario")
+ return;
+ if (this.getMapSetting("PlayerData")?.some(data => data.PopulationLimit))
+ this.perPlayer = this.getMapSetting("PlayerData").map(data => data.PopulationLimit || undefined);
+ else if (this.getMapSetting("WorldPopulation"))
+ this.setPopCap(true, +this.getMapSetting("WorldPopulationCap"));
+ else
+ this.setPopCap(false, +this.getMapSetting("PopulationCap"));
+ }
+
+ setPopCap(worldPop, cap = undefined)
+ {
+ if (worldPop != this.useWorldPop)
+ this.cap = undefined;
+
+ this.useWorldPop = worldPop;
+
+ if (!!cap)
+ this.cap = cap;
+ else if (!this.cap && !this.useWorldPop)
+ this.cap = this.popDefault;
+ else if (!this.cap && this.useWorldPop)
+ this.cap = this.worldPopDefault;
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/Rating.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/Rating.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/Rating.js
@@ -0,0 +1,44 @@
+GameSettings.prototype.Attributes.Rating = class Rating extends GameSetting
+{
+ init()
+ {
+ this.hasXmppClient = Engine.HasXmppClient();
+ this.settings.playerCount.watch(() => this.maybeUpdate(), ["nbPlayers"]);
+ this.settings.cheats.watch(() => this.maybeUpdate(), ["enabled"]);
+ this.maybeUpdate();
+ }
+
+ toInitAttributes(attribs)
+ {
+ if (this.available)
+ attribs.settings.RatingEnabled = this.enabled;
+ }
+
+ fromInitAttributes(attribs)
+ {
+ if (this.getLegacySetting(attribs, "RatingEnabled") !== undefined)
+ {
+ this.available = this.hasXmppClient && this.settings.playerCount.nbPlayers === 2;
+ this.enabled = this.available && !!this.getLegacySetting(attribs, "RatingEnabled");
+ }
+ }
+
+ setEnabled(enabled)
+ {
+ this.enabled = this.available && enabled;
+ }
+
+ maybeUpdate()
+ {
+ // This setting is activated by default if it's possible.
+ this.available = this.hasXmppClient &&
+ this.settings.playerCount.nbPlayers === 2 &&
+ !this.settings.cheats.enabled;
+ this.enabled = this.available;
+ }
+
+ onFinalizeAttributes()
+ {
+ Engine.SetRankedGame(this.enabled);
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/RegicideGarrison.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/RegicideGarrison.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/RegicideGarrison.js
@@ -0,0 +1,38 @@
+GameSettings.prototype.Attributes.RegicideGarrison = class RegicideGarrison extends GameSetting
+{
+ init()
+ {
+ this.setEnabled(false);
+ this.settings.victoryConditions.watch(() => this.maybeUpdate(), ["active"]);
+ this.settings.map.watch(() => this.onMapChange(), ["map"]);
+ }
+
+ toInitAttributes(attribs)
+ {
+ if (this.available)
+ attribs.settings.RegicideGarrison = this.enabled;
+ }
+
+ fromInitAttributes(attribs)
+ {
+ this.enabled = !!this.getLegacySetting(attribs, "RegicideGarrison");
+ }
+
+ onMapChange()
+ {
+ if (this.settings.map.type != "scenario")
+ return;
+ this.setEnabled(!!this.getMapSetting("RegicideGarrison"));
+ }
+
+ setEnabled(enabled)
+ {
+ this.available = this.settings.victoryConditions.active.has("regicide");
+ this.enabled = (enabled && this.available);
+ }
+
+ maybeUpdate()
+ {
+ this.setEnabled(this.enabled);
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/Relic.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/Relic.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/Relic.js
@@ -0,0 +1,62 @@
+GameSettings.prototype.Attributes.Relic = class Relic extends GameSetting
+{
+ init()
+ {
+ this.available = false;
+ this.count = 0;
+ this.duration = 0;
+ this.settings.victoryConditions.watch(() => this.maybeUpdate(), ["active"]);
+ this.settings.map.watch(() => this.onMapChange(), ["map"]);
+ }
+
+ toInitAttributes(attribs)
+ {
+ // For consistency, only save this if the victory condition is active.
+ if (this.available)
+ {
+ attribs.settings.RelicCount = this.count;
+ attribs.settings.RelicDuration = this.duration;
+ }
+ }
+
+ fromInitAttributes(attribs)
+ {
+ if (!!this.getLegacySetting(attribs, "RelicCount"))
+ this.setCount(this.getLegacySetting(attribs, "RelicCount"));
+ if (!!this.getLegacySetting(attribs, "RelicDuration"))
+ this.setDuration(this.getLegacySetting(attribs, "RelicDuration"));
+ }
+
+ onMapChange()
+ {
+ if (this.settings.map.type != "scenario")
+ return;
+ // TODO: probably should sync the victory condition.
+ if (!this.getMapSetting("RelicCount"))
+ this.available = false;
+ else
+ this._set(+this.getMapSetting("RelicCount"), +this.getMapSetting("RelicDuration"));
+ }
+
+ _set(count, duration)
+ {
+ this.available = this.settings.victoryConditions.active.has("capture_the_relic");
+ this.count = Math.max(1, count);
+ this.duration = duration;
+ }
+
+ setCount(val)
+ {
+ this._set(Math.round(val), this.duration);
+ }
+
+ setDuration(val)
+ {
+ this._set(this.count, Math.round(val));
+ }
+
+ maybeUpdate()
+ {
+ this._set(this.count, this.duration);
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/SeaLevelRise.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/SeaLevelRise.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/SeaLevelRise.js
@@ -0,0 +1,43 @@
+GameSettings.prototype.Attributes.SeaLevelRise = class SeaLevelRise extends GameSetting
+{
+ init()
+ {
+ this.min = undefined;
+ this.max = undefined;
+ this.value = undefined;
+ this.settings.map.watch(() => this.onMapChange(), ["map"]);
+ }
+
+ toInitAttributes(attribs)
+ {
+ if (this.value !== undefined)
+ attribs.settings.SeaLevelRiseTime = this.value;
+ }
+
+ fromInitAttributes(attribs)
+ {
+ if (!!this.getLegacySetting(attribs, "SeaLevelRiseTime"))
+ this.setValue(this.getLegacySetting(attribs, "SeaLevelRiseTime"));
+ }
+
+ onMapChange()
+ {
+ if (!this.getMapSetting("SeaLevelRise"))
+ {
+ this.value = undefined;
+ return;
+ }
+ let mapData = this.settings.map.data;
+ this.min = mapData.settings.SeaLevelRise.Min;
+ this.max = mapData.settings.SeaLevelRise.Max;
+ this.value = mapData.settings.SeaLevelRise.Default;
+ }
+
+ setValue(val)
+ {
+ if (!this.getMapSetting("SeaLevelRise"))
+ this.value = undefined;
+ else
+ this.value = Math.max(this.min, Math.min(this.max, Math.round(val)));
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/Seeds.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/Seeds.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/Seeds.js
@@ -0,0 +1,39 @@
+GameSettings.prototype.Attributes.Seeds = class Seeds extends GameSetting
+{
+ init()
+ {
+ this.seed = "random";
+ this.AIseed = "random";
+ }
+
+ toInitAttributes(attribs)
+ {
+ attribs.settings.Seed = this.seed == "random" ? this.seed : +this.seed;
+ attribs.settings.AISeed = this.AIseed == "random" ? this.AIseed : +this.AIseed;
+ }
+
+ fromInitAttributes(attribs)
+ {
+ if (this.getLegacySetting(attribs, "Seed") !== undefined)
+ this.seed = this.getLegacySetting(attribs, "Seed");
+ if (this.getLegacySetting(attribs, "AISeed") !== undefined)
+ this.AIseed = this.getLegacySetting(attribs, "AISeed");
+ }
+
+ pickRandomItems()
+ {
+ let picked = false;
+ if (this.seed === "random")
+ {
+ this.seed = randIntExclusive(0, Math.pow(2, 32));
+ picked = true;
+ }
+
+ if (this.AIseed === "random")
+ {
+ this.AIseed = randIntExclusive(0, Math.pow(2, 32));
+ picked = true;
+ }
+ return picked;
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/StartingCamera.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/StartingCamera.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/StartingCamera.js
@@ -0,0 +1,62 @@
+/**
+ * For compatibility reasons, this loads the per-player StartingCamera from the map data
+ * In general, this is probably better handled by map triggers or the default camera placement.
+ * This doesn't have a GUI setting.
+ */
+GameSettings.prototype.Attributes.StartingCamera = class StartingCamera extends GameSetting
+{
+ init()
+ {
+ this.values = [];
+ this.settings.map.watch(() => this.onMapChange(), ["map"]);
+ this.settings.playerCount.watch(() => this.maybeUpdate(), ["nbPlayers"]);
+ }
+
+ toInitAttributes(attribs)
+ {
+ if (!attribs.settings.PlayerData)
+ attribs.settings.PlayerData = [];
+ while (attribs.settings.PlayerData.length < this.values.length)
+ attribs.settings.PlayerData.push({});
+ for (const i in this.values)
+ if (this.values[i])
+ attribs.settings.PlayerData[i].StartingCamera = this.values[i];
+ }
+
+ fromInitAttributes(attribs)
+ {
+ if (!this.getLegacySetting(attribs, "PlayerData"))
+ return;
+ const pData = this.getLegacySetting(attribs, "PlayerData");
+ for (let i = 0; i < this.values.length; ++i)
+ if (pData[i] && pData[i].StartingCamera !== undefined)
+ {
+ this.values[i] = pData[i].StartingCamera;
+ this.trigger("values");
+ }
+ }
+
+ _resize(nb)
+ {
+ while (this.values.length > nb)
+ this.values.pop();
+ while (this.values.length < nb)
+ this.values.push(undefined);
+ }
+
+ onMapChange()
+ {
+ let pData = this.getMapSetting("PlayerData");
+ this._resize(pData?.length || 0);
+ for (let i in pData)
+ this.values[i] = pData?.[i]?.StartingCamera;
+ }
+
+ maybeUpdate()
+ {
+ if (this.values.length === this.settings.playerCount.nbPlayers)
+ return;
+ this._resize(this.settings.playerCount.nbPlayers);
+ this.trigger("values");
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/StartingResources.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/StartingResources.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/StartingResources.js
@@ -0,0 +1,53 @@
+/**
+ * TODO: There should be a dialog allowing to specify starting resources per player
+ */
+GameSettings.prototype.Attributes.StartingResources = class StartingResources extends GameSetting
+{
+ init()
+ {
+ this.defaultValue = this.getDefaultValue("StartingResources", "Resources") || 300;
+ this.perPlayer = undefined;
+ this.setResources(this.defaultValue);
+ this.settings.map.watch(() => this.onMapChange(), ["map"]);
+ }
+
+ toInitAttributes(attribs)
+ {
+ if (this.perPlayer)
+ {
+ if (!attribs.settings.PlayerData)
+ attribs.settings.PlayerData = [];
+ while (attribs.settings.PlayerData.length < this.perPlayer.length)
+ attribs.settings.PlayerData.push({});
+ for (let i in this.perPlayer)
+ if (this.perPlayer[i])
+ attribs.settings.PlayerData[i].Resources = this.perPlayer[i];
+ }
+ attribs.settings.StartingResources = this.resources;
+ }
+
+ fromInitAttributes(attribs)
+ {
+ if (this.getLegacySetting(attribs, "StartingResources") !== undefined)
+ this.setResources(this.getLegacySetting(attribs, "StartingResources"));
+ }
+
+ onMapChange()
+ {
+ this.perPlayer = undefined;
+ if (this.settings.map.type != "scenario")
+ return;
+ if (!!this.getMapSetting("PlayerData") &&
+ this.getMapSetting("PlayerData").some(data => data.Resources))
+ this.perPlayer = this.getMapSetting("PlayerData").map(data => data.Resources || undefined);
+ else if (!this.getMapSetting("StartingResources"))
+ this.setResources(this.defaultValue);
+ else
+ this.setResources(this.getMapSetting("StartingResources"));
+ }
+
+ setResources(res)
+ {
+ this.resources = res;
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/TeamPlacement.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/TeamPlacement.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/TeamPlacement.js
@@ -0,0 +1,73 @@
+GameSettings.prototype.Attributes.TeamPlacement = class TeamPlacement extends GameSetting
+{
+ init()
+ {
+ this.available = undefined;
+ this.value = undefined;
+ this.settings.map.watch(() => this.onMapChange(), ["map"]);
+ }
+
+ toInitAttributes(attribs)
+ {
+ if (this.value !== undefined)
+ attribs.settings.TeamPlacement = this.value;
+ }
+
+ fromInitAttributes(attribs)
+ {
+ if (!!this.getLegacySetting(attribs, "TeamPlacement"))
+ this.value = this.getLegacySetting(attribs, "TeamPlacement");
+ }
+
+ onMapChange()
+ {
+ if (!this.getMapSetting("TeamPlacements"))
+ {
+ this.value = undefined;
+ this.available = undefined;
+ return;
+ }
+ // TODO: should probably validate that they fit one of the known schemes.
+ this.available = this.getMapSetting("TeamPlacements");
+ this.value = "random";
+ }
+
+ setValue(val)
+ {
+ this.value = val;
+ }
+
+ pickRandomItems()
+ {
+ // If the map is random, we need to wait until it is selected.
+ if (this.settings.map.map === "random" || this.value !== "random")
+ return false;
+
+ this.value = pickRandom(this.available).Id;
+ return true;
+ }
+};
+
+
+GameSettings.prototype.Attributes.TeamPlacement.prototype.StartingPositions = [
+ {
+ "Id": "radial",
+ "Name": translateWithContext("team placement", "Circle"),
+ "Description": translate("Allied players are grouped and placed with opposing players on one circle spanning the map.")
+ },
+ {
+ "Id": "line",
+ "Name": translateWithContext("team placement", "Line"),
+ "Description": translate("Allied players are placed in a linear pattern."),
+ },
+ {
+ "Id": "randomGroup",
+ "Name": translateWithContext("team placement", "Random Group"),
+ "Description": translate("Allied players are grouped, but otherwise placed randomly on the map."),
+ },
+ {
+ "Id": "stronghold",
+ "Name": translateWithContext("team placement", "Stronghold"),
+ "Description": translate("Allied players are grouped in one random place of the map."),
+ }
+];
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/TriggerDifficulty.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/TriggerDifficulty.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/TriggerDifficulty.js
@@ -0,0 +1,52 @@
+GameSettings.prototype.Attributes.TriggerDifficulty = class TriggerDifficulty extends GameSetting
+{
+ init()
+ {
+ this.difficulties = loadSettingValuesFile("trigger_difficulties.json");
+ this.available = undefined;
+ this.value = undefined;
+ this.settings.map.watch(() => this.onMapChange(), ["map"]);
+ }
+
+ toInitAttributes(attribs)
+ {
+ if (this.available)
+ attribs.settings.TriggerDifficulty = this.value;
+ }
+
+ fromInitAttributes(attribs)
+ {
+ if (this.getLegacySetting(attribs, "TriggerDifficulty") !== undefined)
+ this.setValue(this.getLegacySetting(attribs, "TriggerDifficulty"));
+ }
+
+ getAvailableSettings()
+ {
+ return this.difficulties.filter(x => this.available.indexOf(x.Name) !== -1);
+ }
+
+ onMapChange()
+ {
+ if (!this.getMapSetting("SupportedTriggerDifficulties"))
+ {
+ this.value = undefined;
+ this.available = undefined;
+ return;
+ }
+ // TODO: should probably validate that they fit one of the known schemes.
+ this.available = this.getMapSetting("SupportedTriggerDifficulties").Values;
+ this.value = this.difficulties.find(x => x.Default && this.available.indexOf(x.Name) !== -1).Difficulty;
+ }
+
+ setValue(val)
+ {
+ this.value = val;
+ }
+
+ getData()
+ {
+ if (!this.value)
+ return undefined;
+ return this.difficulties[this.value - 1];
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/TriggerScripts.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/TriggerScripts.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/TriggerScripts.js
@@ -0,0 +1,52 @@
+GameSettings.prototype.Attributes.TriggerScripts = class TriggerScripts extends GameSetting
+{
+ init()
+ {
+ this.customScripts = new Set();
+ this.victoryScripts = new Set();
+ this.mapScripts = new Set();
+ this.settings.map.watch(() => this.updateMapScripts(), ["map"]);
+ this.settings.victoryConditions.watch(() => this.updateVictoryScripts(), ["active"]);
+ }
+
+ toInitAttributes(attribs)
+ {
+ attribs.settings.TriggerScripts = Array.from(this.customScripts);
+ }
+
+ fromInitAttributes(attribs)
+ {
+ if (!!this.getLegacySetting(attribs, "TriggerScripts"))
+ this.customScripts = new Set(this.getLegacySetting(attribs, "TriggerScripts"));
+ }
+
+ updateVictoryScripts()
+ {
+ let setting = this.settings.victoryConditions;
+ let scripts = new Set();
+ for (let cond of setting.active)
+ setting.conditions[cond].Scripts.forEach(script => scripts.add(script));
+ this.victoryScripts = scripts;
+ }
+
+ updateMapScripts()
+ {
+ if (!this.settings.map.data || !this.settings.map.data.settings ||
+ !this.settings.map.data.settings.TriggerScripts)
+ {
+ this.mapScripts = new Set();
+ return;
+ }
+ this.mapScripts = new Set(this.settings.map.data.settings.TriggerScripts);
+ }
+
+ onFinalizeAttributes(attribs)
+ {
+ const scripts = this.customScripts;
+ for (const elem of this.victoryScripts)
+ scripts.add(elem);
+ for (const elem of this.mapScripts)
+ scripts.add(elem);
+ attribs.settings.TriggerScripts = Array.from(scripts);
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/VictoryConditions.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/VictoryConditions.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/VictoryConditions.js
@@ -0,0 +1,96 @@
+GameSettings.prototype.Attributes.VictoryConditions = class VictoryConditions extends GameSetting
+{
+ constructor(settings)
+ {
+ super(settings);
+ // Set of victory condition names.
+ this.active = new Set();
+ this.disabled = new Set();
+ this.conditions = {};
+ }
+
+ init()
+ {
+ this.settings.map.watch(() => this.onMapChange(), ["map"]);
+
+ let conditions = loadVictoryConditions();
+ for (let cond of conditions)
+ this.conditions[cond.Name] = cond;
+
+ for (let cond in this.conditions)
+ if (this.conditions[cond].Default)
+ this._add(this.conditions[cond].Name);
+ }
+
+ toInitAttributes(attribs)
+ {
+ attribs.settings.VictoryConditions = Array.from(this.active);
+ }
+
+ fromInitAttributes(attribs)
+ {
+ let legacy = this.getLegacySetting(attribs, "VictoryConditions");
+ if (legacy)
+ {
+ this.disabled = new Set();
+ this.active = new Set();
+ for (let cond of legacy)
+ this._add(cond);
+ }
+ }
+
+ onMapChange()
+ {
+ if (this.settings.map.type != "scenario")
+ return;
+ // If a map specifies victory conditions, replace them all.
+ if (!this.getMapSetting("VictoryConditions"))
+ return;
+ this.disabled = new Set();
+ this.active = new Set();
+ // TODO: could be optimised.
+ for (let cond of this.getMapSetting("VictoryConditions"))
+ this._add(cond);
+ }
+
+ _reconstructDisabled(active)
+ {
+ let disabled = new Set();
+ for (let cond of active)
+ if (this.conditions[cond].DisabledWhenChecked)
+ this.conditions[cond].DisabledWhenChecked.forEach(x => disabled.add(x));
+
+ return disabled;
+ }
+
+ _add(name)
+ {
+ if (this.disabled.has(name))
+ return;
+ let active = clone(this.active);
+ active.add(name);
+ // Assume we want to remove incompatible ones.
+ if (this.conditions[name].DisabledWhenChecked)
+ this.conditions[name].DisabledWhenChecked.forEach(x => active.delete(x));
+ // TODO: sanity check
+ this.disabled = this._reconstructDisabled(active);
+ this.active = active;
+ }
+
+ _delete(name)
+ {
+ let active = clone(this.active);
+ active.delete(name);
+ // TODO: sanity check
+ this.disabled = this._reconstructDisabled(active);
+ this.active = active;
+ }
+
+ setEnabled(name, enabled)
+ {
+ if (enabled)
+ this._add(name);
+ else
+ this._delete(name);
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gamesettings/attributes/Wonder.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gamesettings/attributes/Wonder.js
+++ ps/trunk/binaries/data/mods/public/gamesettings/attributes/Wonder.js
@@ -0,0 +1,40 @@
+GameSettings.prototype.Attributes.Wonder = class Wonder extends GameSetting
+{
+ init()
+ {
+ this.available = false;
+ this.duration = 0;
+ this.settings.victoryConditions.watch(() => this.maybeUpdate(), ["active"]);
+ this.settings.map.watch(() => this.onMapChange(), ["map"]);
+ }
+
+ toInitAttributes(attribs)
+ {
+ if (this.available)
+ attribs.settings.WonderDuration = this.duration;
+ }
+
+ fromInitAttributes(attribs)
+ {
+ if (this.getLegacySetting(attribs, "WonderDuration") !== undefined)
+ this.setDuration(+this.getLegacySetting(attribs, "WonderDuration"));
+ }
+
+ onMapChange()
+ {
+ if (this.settings.map.type != "scenario")
+ return;
+ this.setDuration(+this.getMapSetting("WonderDuration") || 0);
+ }
+
+ setDuration(duration)
+ {
+ this.available = this.settings.victoryConditions.active.has("wonder");
+ this.duration = Math.round(duration);
+ }
+
+ maybeUpdate()
+ {
+ this.setDuration(this.duration);
+ }
+};
Index: ps/trunk/binaries/data/mods/public/gui/autostart/autostart.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/autostart/autostart.js
+++ ps/trunk/binaries/data/mods/public/gui/autostart/autostart.js
@@ -1,12 +0,0 @@
-function init(initData)
-{
- let settings = new GameSettings().init();
- settings.fromInitAttributes(initData.attribs);
-
- settings.launchGame(initData.playerAssignments, initData.storeReplay);
-
- Engine.SwitchGuiPage("page_loading.xml", {
- "attribs": settings.finalizedAttributes,
- "playerAssignments": initData.playerAssignments
- });
-}
Index: ps/trunk/binaries/data/mods/public/gui/autostart/autostart.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/autostart/autostart.xml
+++ ps/trunk/binaries/data/mods/public/gui/autostart/autostart.xml
@@ -2,7 +2,8 @@
-
-
+
+
+
Index: ps/trunk/binaries/data/mods/public/gui/autostart/autostart_client.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/autostart/autostart_client.js
+++ ps/trunk/binaries/data/mods/public/gui/autostart/autostart_client.js
@@ -1,54 +0,0 @@
-class AutoStartClient
-{
- constructor(initData)
- {
- Engine.GetGUIObjectByName("ticker").onTick = this.onTick.bind(this);
-
- this.playerAssignments = {};
-
- try
- {
- Engine.StartNetworkJoin(initData.playerName, initData.ip, initData.port, initData.storeReplay);
- }
- catch (e)
- {
- messageBox(
- 400, 200,
- sprintf(translate("Cannot join game: %(message)s."), { "message": e.message }),
- translate("Error")
- );
- }
- }
-
- onTick()
- {
- while (true)
- {
- const message = Engine.PollNetworkClient();
- if (!message)
- break;
-
- switch (message.type)
- {
- case "players":
- this.playerAssignments = message.newAssignments;
- break;
- case "start":
- Engine.SwitchGuiPage("page_loading.xml", {
- "attribs": message.initAttributes,
- "isRejoining": true,
- "playerAssignments": this.playerAssignments
- });
-
- // Process further pending netmessages in the session page.
- return;
- default:
- }
- }
- }
-}
-
-function init(initData)
-{
- new AutoStartClient(initData);
-}
Index: ps/trunk/binaries/data/mods/public/gui/autostart/autostart_client.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/autostart/autostart_client.xml
+++ ps/trunk/binaries/data/mods/public/gui/autostart/autostart_client.xml
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
Index: ps/trunk/binaries/data/mods/public/gui/autostart/autostart_host.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/autostart/autostart_host.js
+++ ps/trunk/binaries/data/mods/public/gui/autostart/autostart_host.js
@@ -1,61 +0,0 @@
-class AutoStartHost
-{
- constructor(initData)
- {
- this.maxPlayers = initData.maxPlayers;
- this.storeReplay = initData.storeReplay;
- this.playerAssignments = {};
-
- Engine.GetGUIObjectByName("ticker").onTick = this.onTick.bind(this);
-
- try
- {
- // Stun and password not implemented for autostart.
- Engine.StartNetworkHost(initData.playerName, initData.port, false, "", initData.storeReplay);
- }
- catch (e)
- {
- messageBox(
- 400, 200,
- sprintf(translate("Cannot host game: %(message)s."), { "message": e.message }),
- translate("Error")
- );
- }
-
- this.settings = new GameSettings().init();
- this.settings.fromInitAttributes(initData.attribs);
- }
-
- onTick()
- {
- while (true)
- {
- const message = Engine.PollNetworkClient();
- if (!message)
- break;
-
- switch (message.type)
- {
- case "players":
- this.playerAssignments = message.newAssignments;
- break;
- default:
- }
- }
-
- if (Object.keys(this.playerAssignments).length == this.maxPlayers)
- {
- this.settings.launchGame(this.playerAssignments, this.storeReplay);
-
- Engine.SwitchGuiPage("page_loading.xml", {
- "attribs": this.settings.finalizedAttributes,
- "playerAssignments": this.playerAssignments
- });
- }
- }
-}
-
-function init(initData)
-{
- new AutoStartHost(initData);
-}
Index: ps/trunk/binaries/data/mods/public/gui/autostart/autostart_host.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/autostart/autostart_host.xml
+++ ps/trunk/binaries/data/mods/public/gui/autostart/autostart_host.xml
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
Index: ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/CampaignMenu.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/CampaignMenu.xml
+++ ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/CampaignMenu.xml
@@ -3,8 +3,8 @@
-
-
+
+