const ci_save_order = ["name", "lore", "tier", "set", "slots", "type", "material", "drop", "quest", "nDam", "fDam", "wDam", "aDam", "tDam", "eDam", "atkSpd", "hp", "fDef", "wDef", "aDef", "tDef", "eDef", "lvl", "classReq", "strReq", "dexReq", "intReq", "defReq", "agiReq", "str", "dex", "int", "agi", "def", "id", "skillpoints", "reqs", "nDam_", "fDam_", "wDam_", "aDam_", "tDam_", "eDam_", "majorIds", "hprPct", "mr", "sdPct", "mdPct", "ls", "ms", "xpb", "lb", "ref", "thorns", "expd", "spd", "atkTier", "poison", "hpBonus", "spRegen", "eSteal", "hprRaw", "sdRaw", "mdRaw", "fDamPct", "wDamPct", "aDamPct", "tDamPct", "eDamPct", "fDefPct", "wDefPct", "aDefPct", "tDefPct", "eDefPct", "spPct1", "spRaw1", "spPct2", "spRaw2", "spPct3", "spRaw3", "spPct4", "spRaw4", "rainbowRaw", "sprint", "sprintReg", "jh", "lq", "gXp", "gSpd", "durability", "duration", "charges"]; const nonRolled_strings = ["name", "lore", "tier", "set", "type", "material", "drop", "quest", "majorIds", "classReq", "atkSpd", "displayName", "nDam", "fDam", "wDam", "aDam", "tDam", "eDam", "nDam_", "fDam_", "wDam_", "aDam_", "tDam_", "eDam_", "durability", "duration"]; //omitted restrict - it's always "Custom Item" //omitted displayName - either it's the same as name (repetitive) or it's "Custom Item" //omitted category - can always get this from type //omitted fixId - we will denote this early in the string. //omitted "nDam_", "fDam_", "wDam_", "aDam_", "tDam_", "eDam_" - will be calculated on display // NOTE: DO NOT DELETE ENTRIES FROM ARRAYS FOR BACKWARDS COMPAT REASONS!!! // TODO: Add an exclude list /** * @param {Map} custom - the statMap of the CI * @param {boolean} verbose - if we want lore and majorIds to display */ function encodeCustom(custom, verbose) { if (custom) { if (custom.statMap) { custom = custom.statMap; } let hash = "1"; //version 1 if (custom.has("fixID") && custom.get("fixID")) { hash += "1"; } else { hash += "0"; } for (const i in ci_save_order) { let id = ci_save_order[i]; if (rolledIDs.includes(id)) { let val_min = custom.get("minRolls").has(id) ? custom.get("minRolls").get(id) : 0; let val_max = custom.get("maxRolls").has(id) ? custom.get("maxRolls").get(id) : 0; // 0 - both pos // 1 - min neg max pos // 2 - min pos max neg (how?) // 3 - min neg max neg let sign = (Boolean(val_min / Math.abs(val_min) < 0) | 0) + 2 * (Boolean(val_max / Math.abs(val_max) < 0) | 0); //console.log(id + ": " + sign); let min_len = Math.max(1, Math.ceil(log(64, Math.abs(val_min) + 1))); let max_len = Math.max(1, Math.ceil(log(64, Math.abs(val_max) + 1))); let len = Math.max(min_len, max_len); val_min = Math.abs(val_min); val_max = Math.abs(val_max); if (val_min != 0 || val_max != 0) { if (custom.get("fixID")) { hash += Base64.fromIntN(i, 2) + Base64.fromIntN(len, 2) + sign + Base64.fromIntN(val_min, len); } else { hash += Base64.fromIntN(i, 2) + Base64.fromIntN(len, 2) + sign + Base64.fromIntN(val_min, len) + Base64.fromIntN(val_max, len); } } } else { let damages = ["nDam", "eDam", "tDam", "wDam", "fDam", "aDam"]; //"nDam_", "eDam_", "tDam_", "wDam_", "fDam_", "aDam_" let val = custom.get(id); if (id == "majorIds") { if (val.length > 0) { val = val[0]; } else { val = ""; } } if (typeof (val) === "string" && val !== "") { if ((damages.includes(id) && val === "0-0") || (!verbose && ["lore", "majorIds", "quest", "materials", "drop", "set"].includes(id))) { continue; } if (id === "type") { hash += Base64.fromIntN(i, 2) + Base64.fromIntN(types.indexOf(val.substring(0, 1).toUpperCase() + val.slice(1)), 1); } else if (id === "tier") { hash += Base64.fromIntN(i, 2) + Base64.fromIntN(tiers.indexOf(val), 1); } else if (id === "atkSpd") { hash += Base64.fromIntN(i, 2) + Base64.fromIntN(attackSpeeds.indexOf(val), 1); } else if (id === "classReq") { hash += Base64.fromIntN(i, 2) + Base64.fromIntN(classes.indexOf(val), 1); } else { hash += Base64.fromIntN(i, 2) + Base64.fromIntN(val.replaceAll(" ", "%20").length, 2) + val.replaceAll(" ", "%20"); //values cannot go above 4096 chars!!!! Is this ok? } } else if (typeof (val) === "number" && val != 0) { let len = Math.max(1, Math.ceil(log(64, Math.abs(val)))); let sign = Boolean(val / Math.abs(val) < 0) | 0; //console.log(sign); //hash += Base64.fromIntN(i,2) + Base64.fromIntN(val,Math.max(1,Math.ceil(log(64,Math.abs(val))))) + "_"; hash += Base64.fromIntN(i, 2) + Base64.fromIntN(len, 2) + sign + Base64.fromIntN(Math.abs(val), len); } } } return hash; } return ""; } function getCustomFromHash(hash) { let name = hash.slice(); let statMap; console.log("decoding"); try { if (name.slice(0, 3) === "CI-") { name = name.substring(3); } else { throw new Error("Not a custom item!"); } //probably change vers and fixID to be encoded and decoded to/from B64 in the future let version = name.charAt(0); let fixID = Boolean(parseInt(name.charAt(1), 10)); let tag = name.substring(2); statMap = new Map(); statMap.set("minRolls", new Map()); statMap.set("maxRolls", new Map()); if (version === "1") { //do the things if (fixID) { statMap.set("fixID", true); } while (tag !== "") { let id = ci_save_order[Base64.toInt(tag.slice(0, 2))]; let len = Base64.toInt(tag.slice(2, 4)); if (rolledIDs.includes(id)) { let sign = parseInt(tag.slice(4, 5), 10); let minRoll = Base64.toInt(tag.slice(5, 5 + len)); if (!fixID) { let maxRoll = Base64.toInt(tag.slice(5 + len, 5 + 2 * len)); if (sign > 1) { maxRoll *= -1; } if (sign % 2 == 1) { minRoll *= -1; } statMap.get("minRolls").set(id, minRoll); statMap.get("maxRolls").set(id, maxRoll); tag = tag.slice(5 + 2 * len); } else { if (sign != 0) { minRoll *= -1; } statMap.get("minRolls").set(id, minRoll); statMap.get("maxRolls").set(id, minRoll); tag = tag.slice(5 + len); } } else { let val; if (nonRolled_strings.includes(id)) { if (id === "tier") { val = tiers[Base64.toInt(tag.charAt(2))]; len = -1; } else if (id === "type") { val = types[Base64.toInt(tag.charAt(2))]; len = -1; } else if (id === "atkSpd") { val = attackSpeeds[Base64.toInt(tag.charAt(2))]; len = -1; } else if (id === "classReq") { val = classes[Base64.toInt(tag.charAt(2))]; len = -1; } else { //general case val = tag.slice(4, 4 + len).replaceAll("%20", " "); } tag = tag.slice(4 + len); } else { let sign = parseInt(tag.slice(4, 5), 10); val = Base64.toInt(tag.slice(5, 5 + len)); if (sign == 1) { val *= -1; } tag = tag.slice(5 + len); } if (id === "majorIds") { val = [val]; console.log(val); } statMap.set(id, val); } } statMap.set("hash", "CI-" + name); return new Custom(statMap); } } catch (error) { //console.log(statMap); return undefined; } } /** An object representing a Custom Item. Mostly for vanity purposes. * @dep Requires the use of nonRolledIDs and rolledIDs from display_constants.js. * @dep Requires the use of attackSpeeds from build.js. */ class Custom { /** * @description Construct a custom item (CI) from a statMap. * @param {statMap}: A map with keys from rolledIDs or nonRolledIDs or minRolls/maxRolls and values befitting the keys. minRolls and maxRolls are their own maps and have the same keys, but with minimum and maximum values (for rolls). * */ constructor(statMap) { this.statMap = statMap; // TODO patch // this.statMap.set("majorIds", [this.statMap.get("majorIds")]); this.initCustomStats(); } setHash(hash) { let ihash = hash.slice(); if (ihash.slice(0, 3) !== "CI-") { ihash = "CI-" + hash; } this.hash = ihash; this.statMap.set("hash", ihash); } updateName(name) { this.name = name; this.displayName = name; } /* Get all stats for this CI. * Stores in this.statMap. * Follows the expandedItem item structure, similar to a crafted item. * TODO: Check if this is even useful */ initCustomStats() { //this.setHashVerbose(); //do NOT move sethash from here please console.log(this.statMap); for (const id of ci_save_order) { if (rolledIDs.includes(id)) { if (!(this.statMap.get("minRolls").has(id) && this.statMap.get("minRolls").get(id))) { this.statMap.get("minRolls").set(id, 0); this.statMap.get("maxRolls").set(id, 0); } } else { if (nonRolled_strings.includes(id)) { if (!(this.statMap.has(id) && this.statMap.get(id))) { this.statMap.set(id, ""); } } else { if (!(this.statMap.has(id) && this.statMap.get(id))) { this.statMap.set(id, 0); } } } } let type = this.statMap.get("type").toLowerCase(); console.log(type); if (weaponTypes.includes(type)) { for (const n of ["nDam", "eDam", "tDam", "wDam", "fDam", "aDam"]) { if (!(this.statMap.has(n) && this.statMap.get(n))) { this.statMap.set(n, "0-0"); } } } else { for (const n of ["nDam", "eDam", "tDam", "wDam", "fDam", "aDam"]) { if (this.statMap.has(n)) { this.statMap.delete(n); } } } if (this.statMap.get("type")) { this.statMap.set("type", this.statMap.get("type").toLowerCase()); if (armorTypes.includes(this.statMap.get("type"))) { this.statMap.set("category", "armor"); } else if (accessoryTypes.includes(this.statMap.get("type"))) { this.statMap.set("category", "accessory"); } else if (weaponTypes.includes(this.statMap.get("type"))) { this.statMap.set("category", "weapon"); } else if (consumableTypes.includes(this.statMap.get("type"))) { this.statMap.set("category", "consumable"); } else if (tomeTypes.includes(this.statMap.get("type"))) { this.statMap.set("category", "tome"); } } if (this.statMap.get("tier") === "Crafted") { this.statMap.set("crafted", true); for (const e of skp_elements) { this.statMap.set(e + "DamLow", this.statMap.get(e + "Dam")); } this.statMap.set("nDamLow", this.statMap.get("nDam")); this.statMap.set("hpLow", this.statMap.get("hp")); for (const e of skp_order) { this.statMap.get("minRolls").set(e, this.statMap.get(e)); this.statMap.get("maxRolls").set(e, this.statMap.get(e)); } // for (const e of ["durability", "duration"]) { // if (this.statMap.get(e) === "") { // this.statMap.set(e, [0,0]); // } else { // this.statMap.set(e, [this.statMap.get(e).split("-")[0],this.statMap.get(e).split("-")[1]]) // } // } this.statMap.set("lvlLow", this.statMap.get("lvl")); if (this.statMap.get("category") === "weapon") { //this is for powder purposes. //users will likely not stick to the 0.9,1.1 rule because custom item. We will get around this by breaking everything and rewarding users for sticking to 0.9,1.1. this.statMap.set("nDamBaseLow", Math.floor((parseFloat(this.statMap.get("nDamLow")) + parseFloat(this.statMap.get("nDam"))) / 2)); this.statMap.set("nDamBaseHigh", Math.floor((parseFloat(this.statMap.get("nDamLow")) + parseFloat(this.statMap.get("nDam"))) / 2)); for (const e in skp_elements) { this.statMap.set(skp_elements[e] + "DamBaseLow", Math.floor((parseFloat(this.statMap.get(skp_elements[e] + "DamLow")) + parseFloat(this.statMap.get(skp_elements[e] + "Dam"))) / 2)); this.statMap.set(skp_elements[e] + "DamBaseHigh", Math.floor((parseFloat(this.statMap.get(skp_elements[e] + "DamLow")) + parseFloat(this.statMap.get(skp_elements[e] + "Dam"))) / 2)); } this.statMap.set("ingredPowders", []); } } if (this.statMap.get("category") !== "weapon") { this.statMap.set("atkSpd", ""); for (const n in ["nDam", "eDam", "tDam", "wDam", "fDam", "aDam"]) { //this.statMap.set(n,""); } } else { } if (this.statMap.get("name") && this.statMap.get("name") !== "") { this.statMap.set("displayName", this.statMap.get("name")); } else { this.statMap.set("displayName", "Custom Item"); } this.statMap.set("powders", []); this.statMap.set("reqs", [this.statMap.get("strReq"), this.statMap.get("dexReq"), this.statMap.get("intReq"), this.statMap.get("defReq"), this.statMap.get("agiReq")]); this.statMap.set("skillpoints", [this.statMap.get("str"), this.statMap.get("dex"), this.statMap.get("int"), this.statMap.get("def"), this.statMap.get("agi")]); this.statMap.set("restrict", "Custom Item") } }