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() %>
-
Selected Power
+ <% if (calculated.selected_power) { %> ++ <%= calculated.display_name %> hasn't selected a power. +
+ <% } %> +Other Powers
+Locked Powers
+Stats Tuning
+ Used Points: + <%= calculated.tuning_points.used %> / <%= calculated.tuning_points.total %> + +