Index: binaries/data/mods/public/gamesettings/GameSetting.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/GameSetting.js @@ -0,0 +1,28 @@ +/** + * 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 +{ + constructor(settings) + { + super(); + this.settings = settings; + } + + getLegacySetting(attrib, name) + { + if (!attrib || !attrib.settings || !attrib.settings[name]) + return undefined; + return attrib.settings[name]; + } + + getMapSetting(name) + { + if (!this.settings.map.data || !this.settings.map.data.settings || + !this.settings.map.data.settings[name]) + return undefined; + return this.settings.map.data.settings[name]; + } +} Index: binaries/data/mods/public/gamesettings/GameSettings.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/GameSettings.js @@ -0,0 +1,88 @@ +/** + * Data store for game settings. + * Serializes to/from JSON. + */ +class GameSettings +{ + Init() + { + for (let comp in GameSettings.prototype.Attributes) + { + let name = comp[0].toLowerCase() + comp.substr(1); + this[name] = new GameSettings.prototype.Attributes[comp](this); + } + for (let comp in this) + this[comp].Init(); + return this; + } + + /** + * Serialize the settings into a g_GameAttributes-compatible format. + * This is currently also used for hotloading and MP sync, + * so it should be equivalent to the non-serialized state. + * TODO: does it make sense to serialize differently? + */ + LegacySerialize() + { + let attribs = { + "settings": {} + }; + for (let comp in this) + if (this[comp].LegacySerialize) + this[comp].LegacySerialize(attribs); + return attribs; + } + + /** + * Deserialize from a g_GameAttributes-compatible format. + * TODO: this could/should maybe support partial deserialization, + * which means MP might actually send only the bits that change. + */ + LegacyDeserialize(attribs) + { + // Settings depend on the map, but some settings + // may also be illegal for a given map. + // It would be good to validate, but just bulk-accept at the moment. + // Deserialize the map first as that leads to requirements in other areas. + this.map.LegacyDeserialize(attribs); + for (let comp in this) + if (this[comp].LegacyDeserialize) + this[comp].LegacyDeserialize(attribs); + } + + /** + * Call this before starting the game. + */ + prepareForLaunch() + { + this.pickRandomItems(); + } + + /** + * Change "random" settings into their proper settings. + */ + pickRandomItems() + { + let components = Object.keys(this); + let i = 0; + while (components.length && i < 100) + { + // Re-pick if any random setting was unrandomised, + // to make sure dependencies are cleared. + // TODO: there's probably a better way to handle this. + components = components.filter(comp => this[comp].pickRandomItems ? + !!this[comp].pickRandomItems() : false); + ++i; + } + if (i === 100) + { + throw new Error("Infinite loop picking random items, remains : " + uneval(components)); + } + } +} + +Object.defineProperty(GameSettings.prototype, "Attributes", { + "value": {}, + "enumerable": false, + "writable": true, +}); Index: binaries/data/mods/public/gamesettings/attributes/Biome.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/Biome.js @@ -0,0 +1,76 @@ +GameSettings.prototype.Attributes.Biome = class extends GameSetting +{ + Init() + { + this.biomes = loadBiomes(); + this.biomeData = {}; + for (let biome of this.biomes) + this.biomeData[biome.Id] = biome; + this.biome = undefined; + // NB: random is always available. + this.available = new Set(this.biomes.map(x => x.Id)); + this.settings.map.watch(() => this.onMapChange(), ["map"]); + } + + LegacySerialize(attribs) + { + if (!this.biome) + return; + attribs.settings.Biome = this.biome; + } + + LegacyDeserialize(attribs) + { + if (!this.getLegacySetting(attribs, "Biome")) + this.biome = undefined; + else + this.biome = this.getLegacySetting(attribs, "Biome"); + } + + onMapChange() + { + let mapData = this.settings.map.data; + if (mapData && mapData.settings && mapData.settings.SupportedBiomes !== undefined) + { + this.available = new Set(this.biomes.filter(biome => biome.Id.indexOf(mapData.settings.SupportedBiomes) !== -1) + .map(biome => biome.Id)); + this.biome = "random"; + } + else if (this.settings.map.type == "random") + { + this.available = new Set(this.biomes.map(x => x.Id)); + this.biome = "random"; + } + else + { + this.available = new Set(); + this.biome = undefined; + } + } + + setBiome(biome) + { + // TODO: validation. + this.biome = biome; + } + + getAvailableBiomeData() + { + return Array.from(this.available).map(biome => this.biomeData[biome]); + } + + getData() + { + if (!this.biome) + return undefined; + return this.biomeData[this.biome]; + } + + pickRandomItems() + { + if (this.biome !== "random") + return false; + this.biome = pickRandom(this.available); + return true; + } +}; Index: binaries/data/mods/public/gamesettings/attributes/Ceasefire.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/Ceasefire.js @@ -0,0 +1,38 @@ +GameSettings.prototype.Attributes.Ceasefire = class extends GameSetting +{ + Init() + { + this.value = 0; + this.settings.map.watch(() => this.onMapChange(), ["map"]); + } + + LegacySerialize(attribs) + { + if (!this.value) + return; + attribs.settings.Ceasefire = this.value; + } + + LegacyDeserialize(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: binaries/data/mods/public/gamesettings/attributes/Cheats.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/Cheats.js @@ -0,0 +1,28 @@ +GameSettings.prototype.Attributes.Cheats = class extends GameSetting +{ + Init() + { + this.enabled = false; + this.settings.rating.watch(() => this.maybeUpdate(), ["enabled"]); + } + + LegacySerialize(attribs) + { + attribs.settings.CheatEnabled = this.enabled; + } + + _set(enabled) + { + this.enabled = (enabled && !this.settings.rating.enabled); + } + + setEnabled(enabled) + { + this._set(enabled); + } + + maybeUpdate() + { + this._set(this.enabled); + } +}; Index: binaries/data/mods/public/gamesettings/attributes/Daytime.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/Daytime.js @@ -0,0 +1,51 @@ +GameSettings.prototype.Attributes.Daytime = class extends GameSetting +{ + Init() + { + this.data = undefined; + this.value = undefined; + this.settings.map.watch(() => this.onMapChange(), ["map"]); + } + + LegacySerialize(attribs) + { + if (this.value) + attribs.settings.Daytime = this.value; + } + + LegacyDeserialize(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.value = undefined; + this.data = undefined; + return; + } + // TODO: validation + this.data = mapData.settings.Daytime; + this.value = "random"; + } + + setValue(val) + { + // TODO: validation. + this.value = val; + } + + pickRandomItems() + { + if (this.value !== "random") + return false; + this.value = pickRandom(this.data).Id; + return true; + } +}; Index: binaries/data/mods/public/gamesettings/attributes/DisableSpies.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/DisableSpies.js @@ -0,0 +1,30 @@ +GameSettings.prototype.Attributes.DisableSpies = class extends GameSetting +{ + Init() + { + this.enabled = false; + this.settings.map.watch(() => this.onMapChange(), ["map"]); + } + + LegacySerialize(attribs) + { + attribs.settings.DisableSpies = this.enabled; + } + + LegacyDeserialize(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: binaries/data/mods/public/gamesettings/attributes/DisableTreasures.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/DisableTreasures.js @@ -0,0 +1,30 @@ +GameSettings.prototype.Attributes.DisableTreasures = class extends GameSetting +{ + Init() + { + this.enabled = false; + this.settings.map.watch(() => this.onMapChange(), ["map"]); + } + + LegacySerialize(attribs) + { + attribs.settings.DisableTreasures = this.enabled; + } + + LegacyDeserialize(attribs) + { + this.value = !!this.getLegacySetting(attribs, "DisableTreasures"); + } + + onMapChange() + { + if (this.settings.map.type != "scenario") + return; + this.setEnabled(!!this.getMapSetting("DisableTreasures")); + } + + setEnabled(enabled) + { + this.enabled = enabled; + } +}; Index: binaries/data/mods/public/gamesettings/attributes/DisabledTechnologies.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/DisabledTechnologies.js @@ -0,0 +1,31 @@ +/** + * TODO: does this need a GUI setting? + * Also TODO: would probably be better to let maps implement custom modifiers. + */ +GameSettings.prototype.Attributes.DisabledTechnologies = class extends GameSetting +{ + Init() + { + this.value = undefined; + this.settings.map.watch(() => this.onMapChange(), ["map"]); + } + + LegacySerialize(attribs) + { + if (this.value) + attribs.settings.DisabledTechnologies = this.value; + } + + onMapChange() + { + if (!this.getMapSetting("DisabledTechnologies")) + this.setValue(undefined); + else + this.setValue(this.getMapSetting("DisabledTechnologies")); + } + + setValue(val) + { + this.value = val; + } +}; Index: binaries/data/mods/public/gamesettings/attributes/DisabledTemplates.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/DisabledTemplates.js @@ -0,0 +1,31 @@ +/** + * TODO: does this need a GUI setting? + * Also TODO: would probably be better to let maps implement custom modifiers. + */ +GameSettings.prototype.Attributes.DisabledTemplates = class extends GameSetting +{ + Init() + { + this.value = undefined; + this.settings.map.watch(() => this.onMapChange(), ["map"]); + } + + LegacySerialize(attribs) + { + if (this.value) + attribs.settings.DisabledTemplates = this.value; + } + + onMapChange() + { + if (!this.getMapSetting("DisabledTemplates")) + this.setValue(undefined); + else + this.setValue(this.getMapSetting("DisabledTemplates")); + } + + setValue(val) + { + this.value = val; + } +}; Index: binaries/data/mods/public/gamesettings/attributes/GameSpeed.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/GameSpeed.js @@ -0,0 +1,32 @@ +GameSettings.prototype.Attributes.GameSpeed = class extends GameSetting +{ + Init() + { + this.gameSpeed = 1; + this.settings.map.watch(() => this.onMapChange(), ["map"]); + } + + LegacySerialize(attribs) + { + attribs.gameSpeed = this.gameSpeed; + } + + LegacyDeserialize(attribs) + { + if (!attribs.gameSpeed) + return; + this.gameSpeed = +attribs.gameSpeed; + } + + onMapChange() + { + if (!this.getMapSetting("gameSpeed")) + return; + this.setSpeed(+this.getMapSetting("gameSpeed")); + } + + setSpeed(speed) + { + this.speed = speed; + } +}; Index: binaries/data/mods/public/gamesettings/attributes/Landscape.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/Landscape.js @@ -0,0 +1,65 @@ +GameSettings.prototype.Attributes.Landscape = class extends GameSetting +{ + Init() + { + this.data = undefined; + this.value = undefined; + this.settings.map.watch(() => this.onMapChange(), ["map"]); + } + + LegacySerialize(attribs) + { + if (this.value) + attribs.settings.Landscape = this.value; + } + + 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: validation. + this.value = val; + } + + 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 (!this.value || this.value.startsWith("random")) + return false; + + let items = []; + + if (this.value.indexOf("_") !== -1) + { + let subgroup = this.value.substr(this.value.indexOf("_")+1); + subgroup = this.data.find(x => x.Id == subgroup); + items = subgroup.map(x => x.Id); + } + else + items = this.data.map(x => x.Items.map(item => item.Id)); + + this.value = pickRandom(items); + return true; + } +}; Index: binaries/data/mods/public/gamesettings/attributes/LastManStanding.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/LastManStanding.js @@ -0,0 +1,38 @@ +GameSettings.prototype.Attributes.LastManStanding = class 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"]); + } + + LegacySerialize(attribs) + { + attribs.settings.LastManStanding = this.enabled; + } + + LegacyDeserialize(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: binaries/data/mods/public/gamesettings/attributes/LockedTeams.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/LockedTeams.js @@ -0,0 +1,30 @@ +GameSettings.prototype.Attributes.LockedTeams = class extends GameSetting +{ + Init() + { + this.enabled = false; + this.settings.map.watch(() => this.onMapChange(), ["map"]); + } + + LegacySerialize(attribs) + { + attribs.settings.LockTeams = this.enabled; + } + + LegacyDeserialize(attribs) + { + this.enabled = !!this.getLegacySetting(attribs, "LockTeams"); + } + + onMapChange() + { + if (this.settings.map.type != "scenario") + return; + this.setEnabled(!!this.getMapSetting("LockTeams")); + } + + setEnabled(enabled) + { + this.enabled = enabled; + } +}; Index: binaries/data/mods/public/gamesettings/attributes/Map.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/Map.js @@ -0,0 +1,81 @@ +GameSettings.prototype.Attributes.Map = class extends GameSetting +{ + Init() + { + this.watch(() => this.updateMapMetadata(), ["map"]); + this.map = undefined; + this.type = g_MapTypes.Name[g_MapTypes.Default]; + this.script = undefined; + this.randomOptions = []; + } + + LegacySerialize(attribs) + { + attribs.map = this.map; + attribs.mapType = this.type; + if (this.script) + attribs.script = this.script; + + if (!this.data) + return; + + // Some map attributes cannot be changed but are required + // copy them explicitly. + let copy = ["CircularMap", + "StartingCamera", + "Garrison", + // Copy the map name so that the replay menu doesn't have to load hundreds of map descriptions on init + // Also it allows determining the english mapname from the replay file if the map is not available. + // TODO: this should probably be moved out of settings and into the core engine bit. + "Name"]; + + for (let property of copy) + if (this.data.settings[property] !== undefined) + attribs.settings[property] = this.data.settings[property]; + } + + LegacyDeserialize(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.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: binaries/data/mods/public/gamesettings/attributes/MapExploration.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/MapExploration.js @@ -0,0 +1,42 @@ +GameSettings.prototype.Attributes.MapExploration = class extends GameSetting +{ + Init() + { + this.explored = false; + this.revealed = false; + + this.settings.map.watch(() => this.onMapChange(), ["map"]); + } + + LegacySerialize(attribs) + { + attribs.settings.RevealMap = this.revealed; + attribs.settings.ExploreMap = this.explored; + } + + LegacyDeserialize(attribs) + { + this.explored = !!this.getLegacySetting(attribs, "ExploreMap"); + this.revealed = !!this.getLegacySetting(attribs, "RevealMap"); + } + + onMapChange(mapData) + { + if (this.settings.map.type != "scenario") + return; + this.setExplored(this.getMapSetting("ExploreMap")); + this.setRevealed(this.getMapSetting("RevealMap")); + } + + setExplored(enabled) + { + this.explored = enabled; + this.revealed = this.revealed && this.explored; + } + + setRevealed(enabled) + { + this.explored = this.explored || enabled; + this.revealed = enabled; + } +}; Index: binaries/data/mods/public/gamesettings/attributes/MapSize.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/MapSize.js @@ -0,0 +1,32 @@ +GameSettings.prototype.Attributes.MapSize = class extends GameSetting +{ + Init() + { + this.setSize(g_MapSizes.Tiles[g_MapSizes.Default]); + this.settings.map.watch(() => this.onTypeChange(), ["type"]); + } + + LegacySerialize(attribs) + { + if (this.settings.map.type == "random") + attribs.settings.Size = this.size; + } + + LegacyDeserialize(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(g_MapSizes.Tiles[g_MapSizes.Default]); + } +}; Index: binaries/data/mods/public/gamesettings/attributes/MatchID.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/MatchID.js @@ -0,0 +1,17 @@ +GameSettings.prototype.Attributes.MatchID = class extends GameSetting +{ + Init() + { + this.matchID = Engine.GetMatchID(); + } + + LegacySerialize(attribs) + { + attribs.matchID = this.matchID; + } + + LegacyDeserialize(attribs) + { + this.matchID = attribs.matchID; + } +}; Index: binaries/data/mods/public/gamesettings/attributes/Nomad.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/Nomad.js @@ -0,0 +1,24 @@ +GameSettings.prototype.Attributes.Nomad = class extends GameSetting +{ + Init() + { + this.enabled = false; + } + + LegacySerialize(attribs) + { + if (this.settings.map.type == "random") + attribs.settings.Nomad = this.enabled; + } + + LegacyDeserialize(attribs) + { + if (!!this.getLegacySetting(attribs, "Nomad")) + this.setEnabled(!!this.getLegacySetting(attribs, "Nomad")); + } + + setEnabled(enabled) + { + this.enabled = enabled; + } +}; Index: binaries/data/mods/public/gamesettings/attributes/PlayerAI.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/PlayerAI.js @@ -0,0 +1,139 @@ +GameSettings.prototype.Attributes.PlayerAI = class 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"]); + } + + LegacySerialize(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].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; + } + + LegacyDeserialize(attribs) + { + if (!this.getLegacySetting(attribs, "PlayerData")) + return; + let pData = this.getLegacySetting(attribs, "PlayerData"); + if (this.values.length < pData.length) + this._resize(pData.length); + for (let i in pData) + { + if (!pData[i]) + continue; + if (pData[i].AI) + this.setAI(+i, pData[i].AI); + if (pData[i].AIBehavior) + this.setBehavior(+i, pData[i].AIBehavior); + if (pData[i].AIDiff) + this.setDifficulty(+i, pData[i].AIDiff); + } + } + + _defaultAI(playerIndex) + { + return { + "bot": g_Settings.PlayerDefaults[playerIndex + 1].AI, + "difficulty": +Engine.ConfigDB_GetValue("user", "gui.gamesetup.aidifficulty"), + "behavior": 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(this._defaultAI(this.values.length)); + } + + onMapChange() + { + // Reset. + if (this.settings.map.type == "scenario") + this._resize(0); + this.maybeUpdate(); + } + + maybeUpdate() + { + this._resize(this.settings.playerCount.nbPlayers); + this.values.forEach((ai, i) => this._set(i, ai)); + 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].Name; + } + + _set(playerIndex, ai) + { + if (!ai) + { + this.values[playerIndex] = undefined; + return; + } + this.values[playerIndex] = this._defaultAI(playerIndex); + } + + setAI(playerIndex, ai) + { + if (ai === "undefined") + ai = undefined; + this._set(playerIndex, ai); + 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: binaries/data/mods/public/gamesettings/attributes/PlayerCiv.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/PlayerCiv.js @@ -0,0 +1,119 @@ +GameSettings.prototype.Attributes.PlayerCiv = class 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"]); + } + + LegacySerialize(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]; + } + + LegacyDeserialize(attribs) + { + if (!this.getLegacySetting(attribs, "PlayerData")) + return; + let pData = this.getLegacySetting(attribs, "PlayerData"); + if (this.values.length < pData.length) + this._resize(pData.length); + for (let i in pData) + 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._resize(0); + this.maybeUpdate(); + } + + 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(g_CivData).filter(civ => g_CivData[civ].SelectableInGameSetup).map(civ => g_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(g_CivData).filter(civ => + g_CivData[civ].Culture == culture && g_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"); + } +}; Index: binaries/data/mods/public/gamesettings/attributes/PlayerColor.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/PlayerColor.js @@ -0,0 +1,170 @@ +GameSettings.prototype.Attributes.PlayerColor = class extends GameSetting +{ + Init() + { + // TODO: update this + 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(); + } + + LegacySerialize(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]; + } + + LegacyDeserialize(attribs) + { + if (!this.getLegacySetting(attribs, "PlayerData")) + return; + let pData = this.getLegacySetting(attribs, "PlayerData"); + if (this.values.length < pData.length) + this._resize(pData.length); + for (let i in pData) + 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. + if (this.settings.map.type == "scenario" || + this.getMapSetting("PlayerData") && + this.getMapSetting("PlayerData").some(data => data && data.Color)) + 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"); + } + + _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 = []; + for (let i = 0; i < g_MaxPlayers; ++i) + values.push(this._getMapData(i) || + this.defaultColors[i] || this._findFarthestUnusedColor(values)); + this.available = values; + } + + _findClosestColor(targetColor, colors) + { + let colorDistances = colors.map(color => colorDistance(color, targetColor)); + + let smallestDistance = colorDistances.find( + distance => colorDistances.every(distance2 => distance2 >= distance)); + + return colors.find(color => colorDistance(color, targetColor) == smallestDistance); + } + + _findFarthestUnusedColor(values) + { + let farthestColor; + let farthestDistance = 0; + + for (let defaultColor of this.defaultColors) + { + let smallestDistance = Infinity; + for (let usedColor of values) + { + let distance = colorDistance(usedColor, defaultColor); + if (distance < smallestDistance) + smallestDistance = distance; + } + + if (smallestDistance >= farthestDistance) + { + farthestColor = defaultColor; + farthestDistance = smallestDistance; + } + } + return farthestColor; + } + + _getUnusedColor() + { + return this.available.find(color => { + return this.values.every(otherColor => !otherColor || !sameColor(color, otherColor)); + }); + } +}; Index: binaries/data/mods/public/gamesettings/attributes/PlayerCount.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/PlayerCount.js @@ -0,0 +1,67 @@ +GameSettings.prototype.Attributes.PlayerCount = class extends GameSetting +{ + Init() + { + this.nbPlayers = 1; + this.settings.map.watch(() => this.onMapTypeChange(), ["type"]); + this.settings.map.watch(() => this.onMapChange(), ["map"]); + } + + LegacySerialize(attribs) + { + if (!attribs.settings.PlayerData) + attribs.settings.PlayerData = []; + while (attribs.settings.PlayerData.length < this.nbPlayers) + attribs.settings.PlayerData.push({}); + } + + LegacyDeserialize(attribs) + { + if (!this.getLegacySetting(attribs, "PlayerData")) + return; + let pData = this.getLegacySetting(attribs, "PlayerData"); + if (this.nbPlayers < pData.length) + this.nbPlayers = pData.length; + } + + 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") + { + 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: binaries/data/mods/public/gamesettings/attributes/PlayerName.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/PlayerName.js @@ -0,0 +1,114 @@ +/** + * NB: the regular gamesetup has a particular handling of this setting. + * The names are loaded from the map, but the GUI also show playernames + * and forces them when starting the game. + */ +GameSettings.prototype.Attributes.PlayerName = class 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"]); + } + + LegacySerialize(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]; + } + + _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(g_CivData[civ].AINames); + + if (!g_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; + } + + _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.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: binaries/data/mods/public/gamesettings/attributes/PlayerTeam.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/PlayerTeam.js @@ -0,0 +1,76 @@ +GameSettings.prototype.Attributes.PlayerTeam = class 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"]); + } + + LegacySerialize(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]; + } + + LegacyDeserialize(attribs) + { + if (!this.getLegacySetting(attribs, "PlayerData")) + return; + let pData = this.getLegacySetting(attribs, "PlayerData"); + if (this.values.length < pData.length) + this._resize(pData.length); + for (let i in pData) + 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() + { + // Reset. + if (this.settings.map.type == "scenario") + this._resize(0); + this.maybeUpdate(); + } + + 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; + this.locked[playerIndex] = this.settings.map.type == "scenario"; + } + + setValue(playerIndex, val) + { + this._set(playerIndex, val); + this.trigger("values"); + } +}; Index: binaries/data/mods/public/gamesettings/attributes/Population.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/Population.js @@ -0,0 +1,73 @@ +/** + * 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 extends GameSetting +{ + Init() + { + this.perPlayer = false; + this.useWorldPop = false; + this.cap = g_PopulationCapacities.Population[g_PopulationCapacities.Default]; + this.settings.map.watch(() => this.onMapChange(), ["map"]); + } + + LegacySerialize(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 = this.enabled; + attribs.settings.WorldPopulationCap = this.cap; + } + else + attribs.settings.PopulationCap = this.cap; + } + + LegacyDeserialize(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") && + 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 = g_PopulationCapacities.Population[g_PopulationCapacities.Default]; + else if (!this.cap && this.useWorldPop) + this.cap = g_WorldPopulationCapacities.Population[g_WorldPopulationCapacities.Default]; + } +}; Index: binaries/data/mods/public/gamesettings/attributes/Rating.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/Rating.js @@ -0,0 +1,30 @@ +GameSettings.prototype.Attributes.Rating = class extends GameSetting +{ + Init() + { + this.hasXmppClient = Engine.HasXmppClient(); + this.available = false; + this.enabled = false; + } + + LegacySerialize(attribs) + { + attribs.settings.RatingEnabled = this.enabled; + } + + LegacyDeserialize(attribs) + { + this.enabled = !!this.getLegacySetting(attribs, "RatingEnabled"); + } + + setEnabled(enabled) + { + this.enabled = this.available && enabled; + } + + maybeUpdate() + { + this.available = this.hasXmppClient && g_GameAttributes.settings.PlayerData.length == 2; + this.enabled = this.available && this.enabled; + } +}; Index: binaries/data/mods/public/gamesettings/attributes/RegicideGarrison.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/RegicideGarrison.js @@ -0,0 +1,37 @@ +GameSettings.prototype.Attributes.RegicideGarrison = class extends GameSetting +{ + Init() + { + this.setEnabled(false); + this.settings.victoryConditions.watch(() => this.maybeUpdate(), ["active"]); + this.settings.map.watch(() => this.onMapChange(), ["map"]); + } + + LegacySerialize(attribs) + { + attribs.settings.RegicideGarrison = this.enabled; + } + + LegacyDeserialize(attribs) + { + this.setEnabled(!!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: binaries/data/mods/public/gamesettings/attributes/Relic.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/Relic.js @@ -0,0 +1,70 @@ +GameSettings.prototype.Attributes.Relic = class 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"]); + } + + LegacySerialize(attribs) + { + if (this.available) + { + attribs.settings.RelicCount = this.count; + attribs.settings.RelicDuration = this.duration; + } + } + + LegacyDeserialize(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) + { + let available = this.available; + this.available = this.settings.victoryConditions.active.has("capture_the_relic"); + if (!available && this.available) + { + this.count = 2; + this.duration = 20; + } + else + { + this.count = this.available ? Math.max(1, count) : 0; + this.duration = this.available ? duration : 0; + } + } + + 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: binaries/data/mods/public/gamesettings/attributes/SeaLevelRise.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/SeaLevelRise.js @@ -0,0 +1,39 @@ +GameSettings.prototype.Attributes.SeaLevelRise = class extends GameSetting +{ + Init() + { + this.min = undefined; + this.max = undefined; + this.value = undefined; + this.settings.map.watch(() => this.onMapChange(), ["map"]); + } + + LegacySerialize(attribs) + { + if (this.value) + attribs.settings.SeaLevelRiseTime = this.value; + } + + LegacyDeserialize(attribs) + { + if (!!this.getLegacySetting(attribs, "SeaLevelRiseTime")) + this.value = this.getLegacySetting(attribs, "SeaLevelRiseTime"); + } + + onMapChange() + { + if (!this.getMapSetting("SeaLevelRise")) + { + this.value = undefined; + return; + } + this.min = mapData.settings.SeaLevelRise.Min; + this.max = mapData.settings.SeaLevelRise.Max; + this.value = mapData.settings.SeaLevelRise.Default; + } + + setValue(val) + { + this.value = Math.max(this.min, Math.min(this.max, Math.round(val))); + } +}; Index: binaries/data/mods/public/gamesettings/attributes/Seeds.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/Seeds.js @@ -0,0 +1,30 @@ +GameSettings.prototype.Attributes.Seeds = class extends GameSetting +{ + Init() + { + this.seed = 0; + this.AIseed = 0; + } + + LegacySerialize(attribs) + { + // Seed is used for map generation and simulation. + attribs.settings.Seed = this.seed; + attribs.settings.AISeed = this.AIseed; + } + + LegacyDeserialize(attribs) + { + // Seed is used for map generation and simulation. + if (!!this.getLegacySetting(attribs, "Seed")) + this.seed = this.getLegacySetting(attribs, "Seed"); + if (!!this.getLegacySetting(attribs, "AISeed")) + this.AIseed = this.getLegacySetting(attribs, "AISeed"); + } + + pickRandomItems() + { + this.seed = randIntExclusive(0, Math.pow(2, 32)); + this.AIseed = randIntExclusive(0, Math.pow(2, 32)); + } +}; Index: binaries/data/mods/public/gamesettings/attributes/StartingResources.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/StartingResources.js @@ -0,0 +1,52 @@ +/** + * TODO: There should be a dialog allowing to specify starting resources per player + */ +GameSettings.prototype.Attributes.StartingResources = class extends GameSetting +{ + Init() + { + this.perPlayer = undefined; + this.setResources(g_StartingResources.Resources[g_StartingResources.Default]); + this.settings.map.watch(() => this.onMapChange(), ["map"]); + } + + LegacySerialize(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; + } + + LegacyDeserialize(attribs) + { + if (!!this.getLegacySetting(attribs, "StartingResources")) + 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(g_StartingResources.Resources[g_StartingResources.Default]); + else + this.setResources(this.getMapSetting("StartingResources")); + } + + setResources(res) + { + this.resources = res; + } +}; Index: binaries/data/mods/public/gamesettings/attributes/StartingTechnologies.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/StartingTechnologies.js @@ -0,0 +1,30 @@ +/** + * TODO: this doesn't have a GUI setting. + */ +GameSettings.prototype.Attributes.StartingTechnologies = class extends GameSetting +{ + Init() + { + this.value = undefined; + this.settings.map.watch(() => this.onMapChange(), ["map"]); + } + + LegacySerialize(attribs) + { + if (this.value) + attribs.settings.StartingTechnologies = this.value; + } + + onMapChange() + { + if (!this.getMapSetting("StartingTechnologies")) + this.setValue(undefined); + else + this.setValue(this.getMapSetting("StartingTechnologies")); + } + + setValue(val) + { + this.value = val; + } +}; Index: binaries/data/mods/public/gamesettings/attributes/TeamPlacement.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/TeamPlacement.js @@ -0,0 +1,71 @@ +GameSettings.prototype.Attributes.TeamPlacement = class extends GameSetting +{ + Init() + { + this.available = undefined; + this.value = undefined; + this.settings.map.watch(() => this.onMapChange(), ["map"]); + } + + LegacySerialize(attribs) + { + if (this.value) + attribs.settings.TeamPlacement = this.value; + } + + LegacyDeserialize(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 (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: binaries/data/mods/public/gamesettings/attributes/TriggerDifficulty.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/TriggerDifficulty.js @@ -0,0 +1,52 @@ +GameSettings.prototype.Attributes.TriggerDifficulty = class extends GameSetting +{ + Init() + { + this.difficulties = loadSettingValuesFile("trigger_difficulties.json"); + this.available = undefined; + this.value = undefined; + this.settings.map.watch(() => this.onMapChange(), ["map"]); + } + + LegacySerialize(attribs) + { + if (this.available) + attribs.settings.TriggerDifficulty = this.value; + } + + LegacyDeserialize(attribs) + { + if (!!this.getLegacySetting(attribs, "TriggerDifficulty")) + 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 = mapData.settings.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]; + } +}; Index: binaries/data/mods/public/gamesettings/attributes/TriggerScripts.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/TriggerScripts.js @@ -0,0 +1,35 @@ +GameSettings.prototype.Attributes.TriggerScripts = class extends GameSetting +{ + Init() + { + this.victory = new Set(); + this.map = new Set(); + this.settings.map.watch(() => this.updateMapScripts(), ["map"]); + this.settings.victoryConditions.watch(() => this.updateVictoryScripts(), ["active"]); + } + + LegacySerialize(attribs) + { + attribs.settings.TriggerScripts = Array.from(this.victory).concat(Array.from(this.map)); + } + + 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.victory = scripts; + } + + updateMapScripts() + { + if (!this.settings.map.data || !this.settings.map.data.settings || + !this.settings.map.data.settings.TriggerScripts) + { + this.map = new Set(); + return; + } + this.map = new Set(this.settings.map.data.settings.TriggerScripts); + } +}; Index: binaries/data/mods/public/gamesettings/attributes/VictoryConditions.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/VictoryConditions.js @@ -0,0 +1,94 @@ +GameSettings.prototype.Attributes.VictoryConditions = class 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); + } + + LegacySerialize(attribs) + { + attribs.settings.VictoryConditions = Array.from(this.active); + } + + LegacyDeserialize(attribs) + { + let legacy = this.getLegacySetting(attribs, "VictoryConditions"); + if (!legacy) + { + this.active = new Set(); + return; + } + 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. + this.active = new Set(); + if (!this.getMapSetting("VictoryConditions")) + return; + // TODO: could be optimised. + for (let cond of this.getMapSetting("VictoryConditions")) + this._add(cond); + } + + _reconstruct(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._reconstruct(active); + this.active = active; + } + + _delete(name) + { + let active = clone(this.active); + active.delete(name); + // TODO: sanity check + this.disabled = this._reconstruct(active); + this.active = active; + } + + setEnabled(name, enabled) + { + if (enabled) + this._add(name); + else + this._delete(name); + } +}; Index: binaries/data/mods/public/gamesettings/attributes/Wonder.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gamesettings/attributes/Wonder.js @@ -0,0 +1,44 @@ +GameSettings.prototype.Attributes.Wonder = class extends GameSetting +{ + Init() + { + this.available = false; + this.duration = 0; + this.settings.victoryConditions.watch(() => this.maybeUpdate(), ["active"]); + this.settings.map.watch(() => this.onMapChange(), ["map"]); + } + + LegacySerialize(attribs) + { + if (this.available) + attribs.settings.WonderDuration = this.duration; + } + + LegacyDeserialize(attribs) + { + if (!!this.getLegacySetting(attribs, "WonderDuration")) + this.setDuration(+this.getLegacySetting(attribs, "WonderDuration")); + } + + onMapChange() + { + if (this.settings.map.type != "scenario") + return; + this.setDuration(+this.getMapSetting("WonderDuration")); + } + + setDuration(duration) + { + let available = this.available; + this.available = this.settings.victoryConditions.active.has("wonder"); + if (!available && this.available) + this.duration = 20; + else + this.duration = this.available ? Math.round(duration) : 0; + } + + maybeUpdate() + { + this.setDuration(this.duration); + } +}; Index: binaries/data/mods/public/gui/autostart/autostart.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/autostart/autostart.js @@ -0,0 +1,45 @@ +class Autostart +{ + constructor(data) + { + this.settings = new GameSettings(); + this.settings.Init(); + let mapCache = new MapCache(); + this.settings.map.mapCache = mapCache; + this.settings.LegacyDeserialize(data); + + this.player = 1; + } + + startGame() + { + this.settings.prepareForLaunch(); + + // TODO: this doesn't actually work, + // because it doesn't listen to the net "start" message. + if (Engine.HasNetClient()) + { + Engine.SetNetworkGameAttributes(this.settings.LegacySerialize()); + Engine.SetRankedGame(this.settings.rating.enabled); + Engine.StartNetworkGame(); + } + else + { + Engine.StartGame(this.settings.LegacySerialize(), this.player); + this.switchToLoadingPage(); + } + } + + switchToLoadingPage() + { + Engine.SwitchGuiPage("page_loading.xml", { + "attribs": this.settings.LegacySerialize(), + "playerAssignments": { + "local": { + "name": "autostart", + "player": this.player + } + } + }); + } +} Index: binaries/data/mods/public/gui/autostart/autostart.xml =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/autostart/autostart.xml @@ -0,0 +1,16 @@ + + + +