diff --git a/DolSettingsExport.json b/DolSettingsExport.json index 0383fda5e95f586093a60b7b3fd8f6624ce857c4..9e04088679d0dbf7a2f86bdf09554a29460cdf43 100644 --- a/DolSettingsExport.json +++ b/DolSettingsExport.json @@ -6,11 +6,10 @@ DolSettingsExport = { "gender":"m", "gender_body":"f", "ballsExist":true, - "freckles":false - }, - "skinColor":{ - "natural":"light", - "range":0 + "freckles":false, + "skin": { + "color": "light" + } }, "bodysize":2, "penissize":0, diff --git a/devTools/typings.d.ts b/devTools/typings.d.ts index 2e64844ae8a840c2ca0ba355124e11c8af16c861..4f5e4ea2a3d7f165732c9480ca63197221fcc45f 100644 --- a/devTools/typings.d.ts +++ b/devTools/typings.d.ts @@ -13,7 +13,7 @@ declare var random: any; // Add our extensions interface ObjectConstructor { hasOwn(object: any, property: any): boolean; - deepMerge(objects: any): object; + deepMerge(...objects): object; find(objects: any): object; } interface NumberConstructor { diff --git a/game/03-JavaScript/00-traversal.js b/game/03-JavaScript/00-traversal.js new file mode 100644 index 0000000000000000000000000000000000000000..86459f623909a024897b6faae7f236e2ab0e8d60 --- /dev/null +++ b/game/03-JavaScript/00-traversal.js @@ -0,0 +1,82 @@ +// @ts-check + +/** + * @param {object?} source The tree to traverse. + * @param {string} key The key of the current source node. + * @param {string[]} parents List of keys that when matched to a node's key traverses its children. + * @param {function(object, key):object} callback + * @returns {object} + */ +function traverse(source, key, parents, callback) { + console.debug("Traversing: Source", source, "Key", key, "Parents", parents); + // Ensure node is setup correctly for later travesal or configuration + const output = {}; + if (source == null) { + console.debug(`Source node ${key} is empty`); + return null; + } + Object.entries(source).forEach(([key, value]) => { + const isParent = parents.includes(key) || key === "root"; + if (isParent) { + const result = traverse(value, key, parents, callback); + if (result == null) { + return; + } + console.debug(`Adding branch to output at ${key}:`, result); + output[key] = result; + return; + } + const result = callback(source, key); + if (result == null) { + return; + } + console.debug(`Adding value to output at ${key}:`, result); + output[key] = result; + }); + console.debug(`Output for node ${key}:`, output); + return output; +} +window.traverse = traverse; + +/** + * @param {object?} source The tree to traverse. + * @param {object?} target The tree to compare with the source. + * @param {string} key The key of the current source node. + * @param {string[]} parents List of keys that when matched to a node's key traverses its children. + * @param {function(object, object, key):object} callback + * @returns {object} + */ +function traversePair(source, target, key, parents, callback) { + console.debug("Traversing: Source", source, "Target", target, "Key", key, "Parents", parents); + // Ensure node is setup correctly for later travesal or configuration + const output = {}; + if (source == null) { + console.debug(`Source node ${key} is empty`); + return null; + } + if (target == null) { + console.debug(`Target node ${key} is empty`); + return null; + } + Object.entries(source).forEach(([key, value]) => { + const isParent = parents.includes(key) || key === "root"; + if (isParent) { + const result = traversePair(value, target[key], key, parents, callback); + if (result == null) { + return; + } + console.debug(`Adding branch to output at ${key}:`, result); + output[key] = result; + return; + } + const result = callback(source, target, key); + if (result == null) { + return; + } + console.debug(`Adding value to output at ${key}:`, result); + output[key] = result; + }); + console.debug(`Output for node ${key}:`, output); + return output; +} +window.traversePair = traversePair; diff --git a/game/03-JavaScript/save.js b/game/03-JavaScript/save.js index 2154756d02d4594f5ebab1f23ebd78da40bdb8f5..42cf6cc1438c52fe72abe22d89b824b78d77ce4a 100644 --- a/game/03-JavaScript/save.js +++ b/game/03-JavaScript/save.js @@ -1,3 +1,5 @@ +/* globals traverse, traversePair */ + const DoLSave = ((Story, Save) => { "use strict"; @@ -543,13 +545,13 @@ window.importSettings = function (data, type) { let reader; switch (type) { case "text": - V.importString = document.getElementById("settingsDataInput").value; + V.importString = document.getElementById("settingsDataInput")?.textContent; Wikifier.wikifyEval('<<displaySettings "importConfirmDetails">>'); break; case "file": reader = new FileReader(); reader.addEventListener("load", function (e) { - V.importString = e.target.result; + V.importString = e.target?.result; Wikifier.wikifyEval('<<displaySettings "importConfirmDetails">>'); }); reader.readAsBinaryString(data[0]); @@ -560,125 +562,142 @@ window.importSettings = function (data, type) { } }; -const importSettingsData = function (data) { - let S = null; - const result = data; - if (result != null && result != null) { - // console.log("json",JSON.parse(result)); - S = JSON.parse(result); - if (V.passage === "Start" && S.starting != null) { - S.starting = settingsConvert(false, "starting", S.starting); - } - if (S.general != null) { - S.general = settingsConvert(false, "general", S.general); - } +/** + * Using the incoming configuration object, replace all active variables (V | $ | State.variables) + * + * @param {string} data + */ +function importSettingsData(data) { + if (data == null) { + return; + } + // console.log("json",JSON.parse(result)); + const overrides = JSON.parse(data); + if (V.passage === "Start" && overrides.starting != null) { + overrides.starting = settingsConvert(false, "starting", overrides.starting); + } + if (overrides.general != null) { + overrides.general = settingsConvert(false, "general", overrides.general); + } - if (V.passage === "Start" && S.starting != null) { - const listObject = settingsObjects("starting"); - const listKey = Object.keys(listObject); - const namedObjects = ["player"]; - - for (let i = 0; i < listKey.length; i++) { - if (namedObjects.includes(listKey[i]) && S.starting[listKey[i]] != null) { - const itemKey = Object.keys(listObject[listKey[i]]); - for (let j = 0; j < itemKey.length; j++) { - if (V[listKey[i]][itemKey[j]] != null && S.starting[listKey[i]][itemKey[j]] != null) { - if (validateValue(listObject[listKey[i]][itemKey[j]], S.starting[listKey[i]][itemKey[j]])) { - V[listKey[i]][itemKey[j]] = S.starting[listKey[i]][itemKey[j]]; - } - } - } - } else if (!namedObjects.includes(listKey[i])) { - if (V[listKey[i]] != null && S.starting[listKey[i]] != null) { - if (validateValue(listObject[listKey[i]], S.starting[listKey[i]])) { - V[listKey[i]] = S.starting[listKey[i]]; - } - } - } - } + /** + * @param {object} source + * @param {object} target + * @param {string} key + * @returns {any?} + */ + const validateAndSet = (source, target, key) => { + if (!validateValue(source[key], target[key])) { + console.debug(`Validation fail - Key ${key} Source`, source, "Target", target); + return null; } + return target[key]; + }; - if (S.general != null) { - const listObject = settingsObjects("general"); - const listKey = Object.keys(listObject); - const namedObjects = ["map", "shopDefaults", "options"]; - // correct swapped min/max values - if (S.general.breastsizemin > S.general.breastsizemax) { - const temp = S.general.breastsizemin; - S.general.breastsizemin = S.general.breastsizemax; - S.general.breastsizemax = temp; - } - if (S.general.penissizemin > S.general.penissizemax) { - const temp = S.general.penissizemin; - S.general.penissizemin = S.general.penissizemax; - S.general.penissizemax = temp; - } + /** + * @param {object} source + * @param {object} target + * @param {string} key + * @returns {any?} + */ + const setValue = (source, target, key) => { + target[key] = source[key]; + return target[key]; + }; + + if (V.passage === "Start" && overrides.starting != null) { + const startingConfig = settingsObjects("starting"); + + traversePair(startingConfig, overrides.starting, "root", settingContainers, validateAndSet); + traversePair(overrides.starting, V, "root", settingContainers, setValue); + } + + if (overrides.general != null) { + const listObject = settingsObjects("general"); + const listKey = Object.keys(listObject); + const namedObjects = ["map", "shopDefaults", "options"]; + // correct swapped min/max values + if (overrides.general.breastsizemin > overrides.general.breastsizemax) { + const temp = overrides.general.breastsizemin; + overrides.general.breastsizemin = overrides.general.breastsizemax; + overrides.general.breastsizemax = temp; + } + if (overrides.general.penissizemin > overrides.general.penissizemax) { + const temp = overrides.general.penissizemin; + overrides.general.penissizemin = overrides.general.penissizemax; + overrides.general.penissizemax = temp; + } - for (let i = 0; i < listKey.length; i++) { - if (namedObjects.includes(listKey[i]) && S.general[listKey[i]] != null) { - const itemKey = Object.keys(listObject[listKey[i]]); - for (let j = 0; j < itemKey.length; j++) { - if (V[listKey[i]][itemKey[j]] != null && S.general[listKey[i]][itemKey[j]] != null) { - if (validateValue(listObject[listKey[i]][itemKey[j]], S.general[listKey[i]][itemKey[j]])) { - V[listKey[i]][itemKey[j]] = S.general[listKey[i]][itemKey[j]]; - } + for (let i = 0; i < listKey.length; i++) { + if (namedObjects.includes(listKey[i]) && overrides.general[listKey[i]] != null) { + const itemKey = Object.keys(listObject[listKey[i]]); + for (let j = 0; j < itemKey.length; j++) { + if (V[listKey[i]][itemKey[j]] != null && overrides.general[listKey[i]][itemKey[j]] != null) { + if (validateValue(listObject[listKey[i]][itemKey[j]], overrides.general[listKey[i]][itemKey[j]])) { + V[listKey[i]][itemKey[j]] = overrides.general[listKey[i]][itemKey[j]]; } } - } else if (!namedObjects.includes(listKey[i])) { - if (V[listKey[i]] != null && S.general[listKey[i]] != null) { - if (validateValue(listObject[listKey[i]], S.general[listKey[i]])) { - V[listKey[i]] = S.general[listKey[i]]; - } + } + } else if (!namedObjects.includes(listKey[i])) { + if (V[listKey[i]] != null && overrides.general[listKey[i]] != null) { + if (validateValue(listObject[listKey[i]], overrides.general[listKey[i]])) { + V[listKey[i]] = overrides.general[listKey[i]]; } } } } + } - if (S.npc != null) { - const listObject = settingsObjects("npc"); - // eslint-disable-next-line no-var - const listKey = Object.keys(listObject); - // eslint-disable-next-line no-var - for (let i = 0; i < V.NPCNameList.length; i++) { - if (S.npc[V.NPCNameList[i]] != null) { - // eslint-disable-next-line no-var - for (let j = 0; j < listKey.length; j++) { - // Overwrite to allow for "none" default value in the start passage to allow for rng to decide - if ( - V.passage === "Start" && - ["pronoun", "gender", "skincolour"].includes(listKey[j]) && - S.npc[V.NPCNameList[i]][listKey[j]] === "none" - ) { - V.NPCName[i][listKey[j]] = S.npc[V.NPCNameList[i]][listKey[j]]; - } else if (validateValue(listObject[listKey[j]], S.npc[V.NPCNameList[i]][listKey[j]])) { - V.NPCName[i][listKey[j]] = S.npc[V.NPCNameList[i]][listKey[j]]; - } - // Prevent the changing of gender with pregnant npc's - if (V.NPCName[i].pregnancy.type) { - V.NPCName[i].gender = "f"; - } + if (overrides.npc != null) { + const listObject = settingsObjects("npc"); + // eslint-disable-next-line no-var + const listKey = Object.keys(listObject); + // eslint-disable-next-line no-var + for (let i = 0; i < V.NPCNameList.length; i++) { + if (overrides.npc[V.NPCNameList[i]] != null) { + // eslint-disable-next-line no-var + for (let j = 0; j < listKey.length; j++) { + // Overwrite to allow for "none" default value in the start passage to allow for rng to decide + if ( + V.passage === "Start" && + ["pronoun", "gender", "skincolour"].includes(listKey[j]) && + overrides.npc[V.NPCNameList[i]][listKey[j]] === "none" + ) { + V.NPCName[i][listKey[j]] = overrides.npc[V.NPCNameList[i]][listKey[j]]; + } else if (validateValue(listObject[listKey[j]], overrides.npc[V.NPCNameList[i]][listKey[j]])) { + V.NPCName[i][listKey[j]] = overrides.npc[V.NPCNameList[i]][listKey[j]]; + } + // Prevent the changing of gender with pregnant npc's + if (V.NPCName[i].pregnancy.type) { + V.NPCName[i].gender = "f"; } } } } } -}; +} +window.importSettingsData = importSettingsData; -function validateValue(keys, value) { +/** + * @param {object} configuration + * @param {object} value + * @returns {boolean} + */ +function validateValue(configuration, value) { // console.log("validateValue", keys, value); - const keyArray = Object.keys(keys); + const keyArray = Object.keys(configuration); let valid = false; if (keyArray.length === 0) { valid = true; } if (keyArray.includes("min")) { - if (keys.min <= value && keys.max >= value) { + if (configuration.min <= value && configuration.max >= value) { valid = true; } } if (keyArray.includes("decimals") && value != null) { // eslint-disable-next-line eqeqeq - if (value.toFixed(keys.decimals) != value) { + if (value.toFixed(configuration.decimals) != value) { valid = false; } } @@ -693,7 +712,7 @@ function validateValue(keys, value) { } } if (keyArray.includes("strings") && value != null) { - if (keys.strings.includes(value)) { + if (configuration.strings.includes(value)) { valid = true; } } @@ -702,7 +721,7 @@ function validateValue(keys, value) { window.validateValue = validateValue; function exportSettings(data, type) { - const S = { + const output = { general: { map: {}, shopDefaults: {}, @@ -712,38 +731,23 @@ function exportSettings(data, type) { }; let listObject; let listKey; - let namedObjects; if (V.passage === "Start") { - S.starting = { - player: {}, - }; - listObject = settingsObjects("starting"); - listKey = Object.keys(listObject); - namedObjects = ["player"]; - - for (let i = 0; i < listKey.length; i++) { - if (namedObjects.includes(listKey[i]) && V[listKey[i]] != null) { - const itemKey = Object.keys(listObject[listKey[i]]); - for (let j = 0; j < itemKey.length; j++) { - if (V[listKey[i]][itemKey[j]] != null) { - if (validateValue(listObject[listKey[i]][itemKey[j]], V[listKey[i]][itemKey[j]])) { - S.starting[listKey[i]][itemKey[j]] = V[listKey[i]][itemKey[j]]; - } - } - } - } else if (!namedObjects.includes(listKey[i])) { - if (V[listKey[i]] != null) { - if (validateValue(listObject[listKey[i]], V[listKey[i]])) { - S.starting[listKey[i]] = V[listKey[i]]; - } - } + const startingConfig = settingsObjects("starting"); + const startingOutput = traversePair(startingConfig, V, "root", settingContainers, (source, target, key) => { + console.debug(source, target, key); + if (!validateValue(source[key], target[key])) { + console.debug(`Target ${key} does not contain a valid value:`, target[key], "configuration:", source[key]); + return null; } - } + return target[key]; + }); + + output.starting = startingOutput; } listObject = settingsObjects("general"); listKey = Object.keys(listObject); - namedObjects = ["map", "shopDefaults", "options"]; + const namedObjects = ["map", "shopDefaults", "options"]; for (let i = 0; i < listKey.length; i++) { if (namedObjects.includes(listKey[i]) && V[listKey[i]] != null) { @@ -751,14 +755,14 @@ function exportSettings(data, type) { for (let j = 0; j < itemKey.length; j++) { if (V[listKey[i]][itemKey[j]] != null) { if (validateValue(listObject[listKey[i]][itemKey[j]], V[listKey[i]][itemKey[j]])) { - S.general[listKey[i]][itemKey[j]] = V[listKey[i]][itemKey[j]]; + output.general[listKey[i]][itemKey[j]] = V[listKey[i]][itemKey[j]]; } } } } else if (!namedObjects.includes(listKey[i])) { if (V[listKey[i]] != null) { if (validateValue(listObject[listKey[i]], V[listKey[i]])) { - S.general[listKey[i]] = V[listKey[i]]; + output.general[listKey[i]] = V[listKey[i]]; } } } @@ -766,24 +770,24 @@ function exportSettings(data, type) { listObject = settingsObjects("npc"); listKey = Object.keys(listObject); for (let i = 0; i < V.NPCNameList.length; i++) { - S.npc[V.NPCNameList[i]] = {}; + output.npc[V.NPCNameList[i]] = {}; for (let j = 0; j < listKey.length; j++) { // Overwrite to allow for "none" default value in the start passage to allow for rng to decide if (V.passage === "Start" && ["pronoun", "gender", "skincolour"].includes(listKey[j]) && V.NPCName[i][listKey[j]] === "none") { - S.npc[V.NPCNameList[i]][listKey[j]] = V.NPCName[i][listKey[j]]; + output.npc[V.NPCNameList[i]][listKey[j]] = V.NPCName[i][listKey[j]]; } else if (validateValue(listObject[listKey[j]], V.NPCName[i][listKey[j]])) { - S.npc[V.NPCNameList[i]][listKey[j]] = V.NPCName[i][listKey[j]]; + output.npc[V.NPCNameList[i]][listKey[j]] = V.NPCName[i][listKey[j]]; } } } if (V.passage === "Start") { - S.starting = settingsConvert(true, "starting", S.starting); + output.starting = settingsConvert(true, "starting", output.starting); } - S.general = settingsConvert(true, "general", S.general); + output.general = settingsConvert(true, "general", output.general); // console.log(S); - const result = JSON.stringify(S); + const result = JSON.stringify(output); if (type === "text") { const textArea = document.getElementById("settingsDataInput"); textArea.value = result; @@ -794,6 +798,8 @@ function exportSettings(data, type) { } window.exportSettings = exportSettings; +const settingContainers = ["player", "skin"]; + function settingsObjects(type) { let result; /* boolLetter type also requires the bool type aswell */ @@ -903,6 +909,13 @@ function settingsObjects(type) { textMap: { m: "Masculine", f: "Feminine", a: "Androgynous" }, randomize: "characterAppearance", }, + skin: { + color: { + strings: ["light", "medium", "dark", "gyaru", "ylight", "ymedium", "ydark", "ygyaru"], + randomize: "characterAppearance", + displayName: "Natural Skintone:", + }, + }, ballsExist: { bool: true, displayName: "Balls:", textMap: { true: "Existent", false: "Nonexistent" }, randomize: "characterAppearance" }, freckles: { bool: true, @@ -935,13 +948,6 @@ function settingsObjects(type) { textMap: { 0: "Slender", 1: "Slim", 2: "Modest", 3: "Cushioned" }, randomize: "characterAppearance", }, - skin: { - color: { - strings: ["light", "medium", "dark", "gyaru", "ylight", "ymedium", "ydark", "ygyaru"], - randomize: "characterAppearance", - displayName: "Natural Skintone:", - }, - }, }, }; break; @@ -1336,58 +1342,64 @@ window.loadExternalExportFile = function () { }); }; -window.randomizeSettings = function (filter) { - const settingsResult = {}; - const settingContainers = ["player"]; - const randomizeSettingLoop = function (settingsObject, mainObject, subObject) { - if (mainObject && !settingsResult[mainObject]) { - settingsResult[mainObject] = {}; - } - if (subObject) { - if (!settingsResult[mainObject][subObject]) settingsResult[mainObject][subObject] = {}; - } - Object.entries(settingsObject).forEach(setting => { - if (settingContainers.includes(setting[0])) { - randomizeSettingLoop(setting[1], mainObject, setting[0]); - } else if ((!filter && setting[1].randomize) || (filter && filter === setting[1].randomize)) { - if (subObject) { - settingsResult[mainObject][subObject][setting[0]] = randomizeSettingSet(setting[1]); - } else { - settingsResult[mainObject][setting[0]] = randomizeSettingSet(setting[1]); - } - } - }); - }; - const randomNumber = function (min, max, decimals = 0) { - const decimalsMult = Math.pow(10, decimals); - const minMult = min * decimalsMult; - const maxMult = (max + 1) * decimalsMult; - const rn = Math.floor(Math.random() * (maxMult - minMult)) / decimalsMult + min; - return parseFloat(rn.toFixed(decimals)); - }; - const randomizeSettingSet = function (setting) { - let result; - const keys = Object.keys(setting); - if (keys.includes("min")) { - result = randomNumber(setting.min, setting.max, setting.decimals); - } - if (keys.includes("strings")) { - result = setting.strings.pluck(); - } - if (keys.includes("boolLetter")) { - result = ["t", "f"].pluck(); - } - if (keys.includes("bool")) { - result = [true, false].pluck(); +/** + * @param {string} filter + * @returns {string} + */ +function randomizeSettings(filter) { + const result = {}; + + /** + * @param {object} source + * @param {string} key + * @returns {any?} + */ + const setRandomValue = (source, key) => { + const value = source[key]; + if ((!filter && value.randomize) || (filter && filter === value.randomize)) { + return randomizeSettingSet(value); } - return result; + return null; }; + if (V.passage === "Start") { - randomizeSettingLoop(settingsObjects("starting"), "starting"); + const startingConfig = settingsObjects("starting"); + const starting = traverse(startingConfig, "root", settingContainers, setRandomValue); + result.starting = starting; } - randomizeSettingLoop(settingsObjects("general"), "general"); - return JSON.stringify(settingsResult); + const generalConfig = settingsObjects("general"); + const general = traverse(generalConfig, "root", settingContainers, setRandomValue); + result.general = general; + + return JSON.stringify(result); +} +window.randomizeSettings = randomizeSettings; + +const randomNumber = function (min, max, decimals = 0) { + const decimalsMult = Math.pow(10, decimals); + const minMult = min * decimalsMult; + const maxMult = (max + 1) * decimalsMult; + const rn = Math.floor(Math.random() * (maxMult - minMult)) / decimalsMult + min; + return parseFloat(rn.toFixed(decimals)); +}; + +const randomizeSettingSet = function (setting) { + let result; + const keys = Object.keys(setting); + if (keys.includes("min")) { + result = randomNumber(setting.min, setting.max, setting.decimals); + } + if (keys.includes("strings")) { + result = setting.strings.pluck(); + } + if (keys.includes("boolLetter")) { + result = ["t", "f"].pluck(); + } + if (keys.includes("bool")) { + result = [true, false].pluck(); + } + return result; }; // !!Hack warning!! Don't use it maybe? diff --git a/game/04-Variables/presets.twee b/game/04-Variables/presets.twee index c21efc557a342a71acc457a74c7c84d2e2dbe076..b2b07efbc9aa4b7d8b86b952aa511e2a31bae16a 100644 --- a/game/04-Variables/presets.twee +++ b/game/04-Variables/presets.twee @@ -335,14 +335,14 @@ <<set _displayName to _validatorObject["options"][$_label]["displayName"]>> <<set _currentValue to (_altValue2 isnot undefined ? _altValue2 : $_value) is false ? "Enabled" : "Disabled">> <<print _displayName>> <span class="green"><<print _currentValue .toString().toUpperFirst()>></span> - <<case "natural" "range">> - <<set _object to _validatorObject["skinColor"][$_label]>> + <<case "skin">> + <<set _object to _validatorObject.player.skin>> <<if _object isnot undefined>> - <<set _valid to validateValue(_object, $_value)>> + <<set _valid to validateValue(_object.color, $_value.color)>> <</if>> - <<set _displayName to _validatorObject["skinColor"][$_label]["displayName"]>> - <<set _currentValue to (_altValue2 isnot undefined ? _altValue2 : $_value)>> - <<print _displayName>> <span class="green"><<print _currentValue .toString().toUpperFirst()>></span> + <<set _displayName to "Skin colour">> + <<set _currentValue to (_altValue2 isnot undefined ? _altValue2 : $_value.color)>> + <<print _displayName>> <span class="green"><<print _currentValue.toString().toUpperFirst()>></span> <<case "movement" "top" "markers">> <<set _valid to false>> <<if _object isnot undefined>>