diff --git a/common/constants/stats.js b/common/constants/stats.js index 1c94bd4eb0..a2ef2fad27 100644 --- a/common/constants/stats.js +++ b/common/constants/stats.js @@ -237,6 +237,15 @@ export const STATS_DATA = { suffix: "", color: "3", }, + combat_wisdom: { + name: "Combat Wisdom", + nameLore: "Combat Wisdom", + nameShort: "Combat Wisdom", + nameTiny: "CW", + symbol: "☯", + suffix: "", + color: "3", + }, mana_regen: { name: "Mana Regen", nameLore: "Mana Regen", diff --git a/public/resources/scss/stats.scss b/public/resources/scss/stats.scss index 7120e55cb4..d0c688d48e 100644 --- a/public/resources/scss/stats.scss +++ b/public/resources/scss/stats.scss @@ -1547,7 +1547,8 @@ a.additional-player-stat:hover { } .missing-pet, -.missing-accessory { +.missing-accessory, +.missing-power { filter: grayscale(0.8); transition: filter 0.2s; @@ -2646,6 +2647,23 @@ bonus-stats { } } +.stats-tuning { + bonus-stats p { + margin: 0; + } +} + +.selected-power { + display: flex; + align-items: center; + gap: 4px; + font-size: 20px; + + .piece { + margin-top: 0; + } +} + #techno-support { text-align: center; padding: 20px; diff --git a/public/resources/ts/calculate-player-stats.ts b/public/resources/ts/calculate-player-stats.ts index c397047dc5..6aa31032e7 100644 --- a/public/resources/ts/calculate-player-stats.ts +++ b/public/resources/ts/calculate-player-stats.ts @@ -84,6 +84,26 @@ export function getPlayerStats() { } } + // Thaumaturgist power + if (calculated.selected_power) { + for (const [name, value] of Object.entries(calculated.selected_power.stats)) { + if (!allowedStats.includes(name)) { + continue; + } + + stats[name].thaumaturgist_power = value; + } + } + + // Tuning points + for (const [name, value] of Object.entries(calculated.tuning_points.distribution)) { + if (!allowedStats.includes(name)) { + continue; + } + + stats[name].tuning_points = value; + } + // Skill bonus stats for (const [skill, data] of Object.entries(calculated.levels)) { const bonusStats: ItemStats = getBonusStat(data.level, `skill_${skill}` as BonusType, data.maxLevel); diff --git a/public/resources/ts/development-defer.ts b/public/resources/ts/development-defer.ts index a41ef35884..37755853ee 100644 --- a/public/resources/ts/development-defer.ts +++ b/public/resources/ts/development-defer.ts @@ -37,6 +37,12 @@ document.addEventListener("click", (e) => { } else if (element.hasAttribute("data-upgrade-accessory-index")) { item = calculated.missingAccessories.upgrades[parseInt(element.getAttribute("data-upgrade-accessory-index") as string)]; + } else if (element.hasAttribute("data-power-selected")) { + item = calculated.selected_power; + } else if (element.hasAttribute("data-power-index")) { + item = calculated.unlocked_powers[parseInt(element.getAttribute("data-power-index") as string)]; + } else if (element.hasAttribute("data-locked-power-index")) { + item = calculated.locked_powers[parseInt(element.getAttribute("data-locked-power-index") as string)]; } console.log(item); diff --git a/public/resources/ts/globals.d.ts b/public/resources/ts/globals.d.ts index 78bc70ad56..d45580dee7 100644 --- a/public/resources/ts/globals.d.ts +++ b/public/resources/ts/globals.d.ts @@ -572,6 +572,30 @@ declare const calculated: SkyCryptPlayer & { amount: number; }[]; reaper_peppers_eaten: number; + selected_power: Item & { + stats: { + [key in StatName]: number; + }; + }; + unlocked_powers: (Item & { + power_type: string; + stats: { + [key in StatName]: number; + }; + })[]; + locked_powers: (Item & { + power_type: string; + stats: { + [key in StatName]: number; + }; + })[]; + tuning_points: { + distribution: { + [key in StatName]: number; + }; + total: number; + used: number; + }; skyblock_level: Level; }; diff --git a/public/resources/ts/stats-defer.ts b/public/resources/ts/stats-defer.ts index deb2a7013a..b5862d97ee 100644 --- a/public/resources/ts/stats-defer.ts +++ b/public/resources/ts/stats-defer.ts @@ -248,6 +248,12 @@ function fillLore(element: HTMLElement) { } else if (element.hasAttribute("data-upgrade-accessory-index")) { item = calculated.missingAccessories.upgrades[parseInt(element.getAttribute("data-upgrade-accessory-index") as string)]; + } else if (element.hasAttribute("data-power-selected")) { + item = calculated.selected_power; + } else if (element.hasAttribute("data-power-index")) { + item = calculated.unlocked_powers[parseInt(element.getAttribute("data-power-index") as string)]; + } else if (element.hasAttribute("data-locked-power-index")) { + item = calculated.locked_powers[parseInt(element.getAttribute("data-locked-power-index") as string)]; } if (item == undefined) { diff --git a/src/constants.js b/src/constants.js index bde41edffa..df9b3e44be 100644 --- a/src/constants.js +++ b/src/constants.js @@ -20,3 +20,4 @@ export * from "./constants/skins-animations.js"; export * from "./constants/tags.js"; export * from "./constants/trophy-fish.js"; export * from "./constants/accessories.js"; +export * from "./constants/powers.js"; diff --git a/src/constants/powers.js b/src/constants/powers.js new file mode 100644 index 0000000000..b2fcf7b083 --- /dev/null +++ b/src/constants/powers.js @@ -0,0 +1,392 @@ +export const POWERS = { + scorching: { + type: "Marvelous Stone", + texture_path: "/item/SCORCHED_BOOKS", + stats: { + strength: 8.4, + crit_damage: 9.6, + bonus_attack_speed: 1.8, + }, + unique: { + ferocity: 7, + }, + }, + healthy: { + type: "Grandiose Stone", + texture_path: "/item/VITAMIN_DEATH", + stats: { + health: 33.6, + }, + unique: { + health: 200, + }, + }, + slender: { + type: "Grandiose Stone", + texture_path: "/item/HAZMAT_ENDERMAN", + stats: { + health: 8.4, + defense: 6, + speed: 0.6, + strength: 6, + intelligence: 7.2, + crit_damage: 6, + bonus_attack_speed: 1.1, + }, + unique: { + defense: 100, + strength: 50, + }, + }, + strong: { + type: "Grandiose Stone", + texture_path: "/item/MANDRAA", + stats: { + strength: 12, + crit_damage: 12, + }, + unique: { + strength: 25, + crit_damage: 25, + }, + }, + bizarre: { + type: "Master Stone", + texture_path: "/item/ECCENTRIC_PAINTING", + stats: { + strength: -2.4, + intelligence: 43.2, + crit_damage: -2.4, + }, + unique: { + ability_damage: 5, + }, + }, + bubba: { + type: "Master Stone", + texture_path: "/item/BUBBA_BLISTER", + stats: { + strength: 6, + crit_damage: 10.8, + defense: -9.6, + true_defense: 1.2, + bonus_attack_speed: 1.8, + health: 5.1, + crit_chance: 0.9, + }, + unique: { + combat_wisdom: 2, + }, + }, + demonic: { + type: "Master Stone", + texture_path: "/item/HORNS_OF_TORMENT", + stats: { + strength: 5.5, + intelligence: 27.725, + }, + unique: { + crit_damage: 50, + }, + }, + hurtful: { + type: "Master Stone", + texture_path: "/item/MAGMA_URCHIN", + stats: { + strength: 4.8, + crit_damage: 19.2, + }, + unique: { + bonus_attack_speed: 15, + }, + }, + pleasant: { + type: "Master Stone", + texture_path: "/item/PRECIOUS_PEARL", + stats: { + health: 13.45, + defense: 14.4, + }, + }, + adept: { + type: "Advanced Stone", + texture_path: "/item/END_STONE_SHULKER", + stats: { + health: 16.8, + defense: 9.6, + intelligence: 3.6, + }, + unique: { + health: 100, + defense: 40, + }, + }, + bloody: { + type: "Advanced Stone", + texture_path: "/item/BEATING_HEART", + stats: { + strength: 10.8, + intelligence: 3.6, + crit_damage: 10.8, + }, + unique: { + bonus_attack_speed: 10, + }, + }, + crumbly: { + type: "Advanced Stone", + texture_path: "/item/CHOCOLATE_CHIP", + stats: { + mending: 1.8, + intelligence: 5.4, + true_defense: 0.6, + vitality: 2.4, + health: 10.1, + }, + unique: { + speed: 25, + }, + }, + forceful: { + type: "Advanced Stone", + texture_path: "/item/ACACIA_BIRDHOUSE", + stats: { + health: 1.7, + strength: 18, + crit_damage: 4.8, + }, + unique: { + ferocity: 4, + }, + }, + itchy: { + type: "Advanced Stone", + texture_path: "/item/FURBALL", + stats: { + speed: 0.6, + strength: 7.2, + crit_damage: 8.4, + bonus_attack_speed: 2.15, + }, + unique: { + strength: 15, + crit_damage: 15, + }, + }, + mythical: { + type: "Advanced Stone", + texture_path: "/item/OBSIDIAN_TABLET", + stats: { + health: 5.7, + defense: 4.05, + speed: 0.95, + strength: 4.05, + intelligence: 6.1, + crit_chance: 1.65, + crit_damage: 4.05, + }, + unique: { + health: 150, + strength: 40, + }, + }, + shaded: { + type: "Advanced Stone", + texture_path: "/item/DARK_ORB", + stats: { + speed: 0.6, + strength: 4.8, + crit_damage: 18, + }, + unique: { + bonus_attack_speed: 3, + ferocity: 3, + }, + }, + sighted: { + type: "Advanced Stone", + texture_path: "/item/ENDER_MONOCLE", + stats: { + intelligence: 36, + }, + unique: { + ability_damage: 3, + }, + }, + silky: { + type: "Intermediate Stone", + texture_path: "/item/LUXURIOUS_SPOOL", + stats: { + speed: 0.6, + crit_damage: 22.8, + }, + unique: { + bonus_attack_speed: 5, + }, + }, + sweet: { + type: "Intermediate Stone", + texture_path: "/item/ROCK_CANDY", + stats: { + health: 15.1, + defense: 10.8, + speed: 1.2, + }, + unique: { + speed: 5, + }, + }, + sanguisuge: { + type: "Starter", + texture_path: "/item/DISPLACED_LEECH", + stats: { + strength: 12, + vitality: 1.2, + crit_damage: 4.8, + health: 5.1, + }, + unique: { + intelligence: 100, + }, + }, + commando: { + type: "Intermediate", + id: 280, + stats: { + health: 5.02, + defense: 2.4, + strength: 8.4, + crit_chance: 0.475, + crit_damage: 8.4, + }, + unlocked_at: 15, + }, + disciplined: { + type: "Intermediate", + id: 267, + stats: { + health: 5.02, + defense: 2.4, + strength: 7.2, + crit_chance: 1.45, + crit_damage: 7.2, + }, + unlocked_at: 15, + }, + inspired: { + type: "Intermediate", + id: 351, + Damage: 4, + stats: { + health: 1.65, + defense: 1.2, + strength: 4.8, + intelligence: 16.2, + crit_chance: 0.95, + crit_damage: 3.6, + }, + unlocked_at: 15, + }, + ominous: { + type: "Intermediate", + id: 410, + stats: { + health: 5.02, + speed: 0.95, + strength: 3.6, + intelligence: 6.1, + crit_chance: 1.45, + crit_damage: 3.6, + bonus_attack_speed: 0.9, + }, + unlocked_at: 15, + }, + prepared: { + type: "Intermediate", + id: 297, + stats: { + health: 12.4, + speed: 11.3, + strength: 1.95, + crit_chance: 0.4, + crit_damage: 0.95, + }, + unlocked_at: 15, + }, + fortuitous: { + type: "Starter", + id: 266, + stats: { + health: 3.35, + defense: 1.2, + strength: 4.8, + crit_chance: 4.35, + crit_damage: 4.8, + }, + unlocked_at: 0, + }, + pretty: { + type: "Starter", + id: 38, + Damage: 8, + stats: { + health: 1.65, + defense: 1.2, + speed: 0.65, + strength: 4.8, + intelligence: 10.8, + crit_chance: 0.475, + crit_damage: 1.2, + }, + unlocked_at: 0, + }, + protected: { + type: "Starter", + id: 307, + stats: { + health: 11.75, + defense: 10.8, + strength: 2.4, + crit_chance: 0.475, + crit_damage: 1.2, + }, + unlocked_at: 0, + }, + simple: { + type: "Starter", + id: 1, + stats: { + health: 5.02, + defense: 3.6, + speed: 1.2, + strength: 3.6, + intelligence: 5.4, + crit_chance: 1.45, + crit_damage: 3.6, + }, + unlocked_at: 0, + }, + warrior: { + type: "Starter", + id: 264, + stats: { + health: 3.35, + defense: 1.2, + strength: 8.4, + crit_chance: 2.4, + crit_damage: 6, + }, + unlocked_at: 0, + }, +}; + +export const POWER_TUNING_MULTIPLIERS = { + health: 5, + defense: 1, + speed: 1.5, + strength: 1, + crit_chance: 0.2, + crit_damage: 1, + bonus_attack_speed: 0.3, + intelligence: 2, +}; diff --git a/src/helper.js b/src/helper.js index fc069aeecd..433b51e1e6 100644 --- a/src/helper.js +++ b/src/helper.js @@ -1055,6 +1055,30 @@ export function getAnimatedTexture(item) { return deepResults[0] ?? false; } +/** + * @param {string} enrichment + * @returns string + * @description takes an enrichment name and returns the corresponding stat name + */ +export function enrichmentToStatName(enrichment) { + switch (enrichment) { + case "walk_speed": + return "speed"; + + case "critical_chance": + return "crit_chance"; + + case "critical_damage": + return "crit_damage"; + + case "attack_speed": + return "bonus_attack_speed"; + + default: + return enrichment; + } +} + /** * Returns the price of the item. Returns 0 if the item is not found or if the item argument is falsy. * @param {string} item - The ID of the item to retrieve the price for. diff --git a/src/lib.js b/src/lib.js index f44c5f3952..c6f6dd41f3 100644 --- a/src/lib.js +++ b/src/lib.js @@ -882,6 +882,73 @@ function getMinionSlots(minions) { return output; } +function addPowerLoreStat(lore, stat, value) { + const statData = constants.STATS_DATA[stat]; + + lore.push( + `§${statData.color}${value >= 0 ? "+" : ""}${Math.round(value).toLocaleString()} ${statData.symbol} ${ + statData.nameLore + }` + ); +} + +function getPower(name, magicalPower) { + const powerMultiplier = 29.97 * Math.pow(Math.log(0.0019 * magicalPower + 1), 1.2); + const displayName = helper.titleCase(name); + + const itemData = { + display_name: displayName, + tag: { + display: { + name: displayName, + }, + }, + stats: {}, + }; + + if (name in constants.POWERS) { + const info = constants.POWERS[name]; + + const stats = {}; + const lore = [`§8${info.type} Power`, "", "§7Stats:"]; + + for (const [name, value] of Object.entries(info.stats)) { + stats[name] = value * powerMultiplier; + + addPowerLoreStat(lore, name, stats[name]); + } + + const combined = { ...stats }; + + if (info.unique) { + lore.push("", "§7Unique Power Bonus:"); + + for (const [name, value] of Object.entries(info.unique)) { + if (!combined[name]) { + combined[name] = value; + } else { + combined[name] += value; + } + + addPowerLoreStat(lore, name, value); + } + } + + lore.push("", `You Have: §6${magicalPower} Magical Power`); + + itemData.id = info.id; + itemData.rarity = "uncommon"; + itemData.Damage = info.Damage; + itemData.texture_path = info.texture_path; + itemData.tag.display.Lore = lore; + itemData.power_type = info.type; + itemData.stats = combined; + itemData.unlocked_at = info.unlocked_at; + } + + return helper.generateItem(itemData); +} + export const getItems = async ( profile, customTextures = false, @@ -2207,6 +2274,71 @@ export async function getStats( active: userProfile.nether_island_player_data?.abiphone?.active_contacts?.length || 0, }; + output.magical_power = { + total: 0, + }; + + for (const [rarity, value] of Object.entries(constants.MAGICAL_POWER)) { + output.magical_power[rarity] = items.accessory_rarities[rarity] * value || 0; + } + + output.magical_power.hegemony = items.accessory_rarities.hegemony + ? constants.MAGICAL_POWER[items.accessory_rarities.hegemony.rarity] + : 0; + + if (items.accessory_rarities.abicase) { + output.magical_power.abicase = Math.floor(output.abiphone.active / 2); + } + + for (const value of Object.values(output.magical_power)) { + output.magical_power.total += value; + } + + output.selected_power = null; + output.unlocked_powers = []; + output.locked_powers = []; + + output.tuning_points = { + distribution: {}, + total: Math.floor(output.magical_power.total / 10), + used: 0, + }; + + if ("accessory_bag_storage" in userProfile) { + const selectedPower = userProfile.accessory_bag_storage.selected_power; + + if (selectedPower) { + output.selected_power = getPower(selectedPower, output.magical_power.total); + } + + const unlockedPowers = new Set(userProfile.accessory_bag_storage.unlocked_powers); + + for (const name of Object.keys(constants.POWERS)) { + if (name === selectedPower) { + continue; + } + + const power = getPower(name, output.magical_power.total); + const unlocked = + ("unlocked_at" in power && output.levels.combat.level >= power.unlocked_at) || unlockedPowers.has(name); + + output[unlocked ? "unlocked_powers" : "locked_powers"].push(power); + } + + const tuning = userProfile.accessory_bag_storage.tuning?.slot_0; + + if (tuning) { + for (const [name, value] of Object.entries(tuning)) { + if (!value) continue; + + const stat = helper.enrichmentToStatName(name); + + output.tuning_points.used += value; + output.tuning_points.distribution[stat] = value * constants.POWER_TUNING_MULTIPLIERS[stat]; + } + } + } + output.skyblock_level = { xp: userProfile.leveling?.experience || 0, level: Math.floor(userProfile.leveling?.experience / 100 || 0), diff --git a/views/stats.ejs b/views/stats.ejs index c832764e7e..6a9d73ea0a 100644 --- a/views/stats.ejs +++ b/views/stats.ejs @@ -253,25 +253,6 @@ function formatEnrichment(string) { return enrichment } -function enrichmentToStatName(enrichment) { - switch (enrichment) { - case 'walk_speed': - return 'speed' - - case 'critical_chance': - return 'crit_chance' - - case 'critical_damage': - return 'crit_damage' - - case 'attack_speed': - return 'bonus_attack_speed' - - default: - return enrichment - } -} - function getEnrichments(accessories) { const enrichmentCounts = {} const filteredAccessories = accessories @@ -291,7 +272,7 @@ function getEnrichments(accessories) { Enrichments: <% for (const [enrichment, amount] of Object.entries(enrichmentCounts)) { - const stat = enrichmentToStatName(enrichment) + const stat = helper.enrichmentToStatName(enrichment) %> "> <%= amount %>× <%= formatEnrichment(enrichment) %> @@ -978,6 +959,7 @@ const metaDescription = getMetaDescription() Armor <% if(!items.no_inventory){ %>Weapons<% } %> <% if(!items.no_inventory){ %>Accessories<% } %> + <% if(!items.no_inventory){ %>Power<% } %> <% if(calculated.pets.length > 0){ %>Pets<% } %> <% if(!items.no_inventory){ %>Inventory<% } %> Skills @@ -995,7 +977,7 @@ const metaDescription = getMetaDescription() const notAvailable = []; if(items.no_inventory) - notAvailable.push('Weapons', 'Accessories', 'Inventory', 'Storage'); + notAvailable.push('Weapons', 'Accessories', 'Power', 'Inventory', 'Storage'); if(items.no_personal_vault) notAvailable.push('Personal Vault'); @@ -1155,41 +1137,6 @@ const metaDescription = getMetaDescription() Recombobulated: <%= items.accessories.filter(a => a.isUnique && a.extra?.recombobulated).length %> / <%= constants.RECOMBABLE_ACCESSORIES_COUNT %>
- <% - const rarities = items.accessory_rarities; - const player_magical_power = {} - - for (const rarity in constants.MAGICAL_POWER) { - player_magical_power[rarity] = 0 - player_magical_power[rarity] += rarities[rarity] * constants.MAGICAL_POWER[rarity]; - } - - const mp_hegemony = rarities.hegemony ? constants.MAGICAL_POWER[rarities.hegemony.rarity] : 0 - const mp_total = Object.values(player_magical_power).reduce((a, b) => a + b) + mp_hegemony + Math.floor(calculated.abiphone.active / 2); - %> - );' class='grey-text'>Abicase = +<%= Math.floor(calculated.abiphone.active / 2).toLocaleString() %> MP
- <% } %> -
- Total: <%= mp_total.toLocaleString() %> Magical Power - "> - Magical Power: <%= mp_total.toLocaleString() %> -

<% if(items.accessories.find(a => !a.isInactive) != undefined){ %>
@@ -1255,6 +1202,73 @@ const metaDescription = getMetaDescription() <% } %>
+
+ +

Power

+ Magical Power: <%= calculated.magical_power.total.toLocaleString() %> +

Selected Power

+ <% if (calculated.selected_power) { %> +
+
+ <% itemIcon(calculated.selected_power, ['piece-icon']); %> +
+
+ <%= calculated.selected_power.display_name %> +
+
+
+ <% } else { %> +

+ <%= calculated.display_name %> hasn't selected a power. +

+ <% } %> +

Other Powers

+
+ <% for (const [i, power] of calculated.unlocked_powers.entries()) { %> +
+ <% itemIcon(power, ['piece-icon']); %> +
+ <% } %> +
+ <% if (calculated.locked_powers.length) { %> + +
+ <% for (const [i, power] of calculated.locked_powers.entries()) { %> +
+ <% itemIcon(power, ['piece-icon']); %> +
+ <% } %> +
+ <% } %> +
+

Stats Tuning

+ Used Points: + <%= calculated.tuning_points.used %> / <%= calculated.tuning_points.total %> +
+
+
<% } %> <% if(calculated.pets.length > 0){ %>