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 @@