diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 785bd94b1d..4d9db6224f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,7 +32,7 @@ Before contributing to SkyCrypt, make sure you install the development environme 3. Go to `developer.hypixel.net/dashboard`. Click `Create API Key` and copy the result. 4. Open `credentials.json` and input your Hypixel API key into the `hypixel_api_key` field. 5. In the `dbUrl` field, input your MongoDB url. In the `dbName` field, input the name of the database you would like to use. -6. (optional) If you are not using the default Redis port or you are using Redis remotely, you can configure the Redis URL with the `redisUrl` field in `credentials.json` +6. (optional) If you are not using the default Redis port or you are using Redis remotely, you can configure the Redis URL with the `redisUrl` field in `credentials.json`. Also `discord_webhook` if you want to send error remotely, useful in production to detect bugs. 7. Making sure your Mongo and Redis instances are running, run `pnpm start` for production or `pnpm dev` for development in the project directory. You should now be able to access the site at http://localhost:32464/ ### VS-Code diff --git a/common/constants.js b/common/constants.js index 8d7c199836..88d4d275fb 100644 --- a/common/constants.js +++ b/common/constants.js @@ -4,3 +4,4 @@ export * from "./constants/items.js"; export * from "./constants/potions.js"; export * from "./constants/rewards.js"; export * from "./constants/stats.js"; +export * from "./constants/misc.js"; diff --git a/common/constants/bonuses.js b/common/constants/bonuses.js index 65fa15bf32..6d3c07f0b2 100644 --- a/common/constants/bonuses.js +++ b/common/constants/bonuses.js @@ -39,7 +39,9 @@ export const STATS_BONUS = { 51: { health: 0 }, }, skill_social: {}, - skill_carpentry: {}, + skill_carpentry: { + 1: { health: 1 }, + }, skill_runecrafting: {}, // Slayers slayer_zombie: { @@ -83,9 +85,27 @@ export const STATS_BONUS = { 3: { health: 4 }, 4: { true_defense: 1 }, 5: { health: 5 }, - 6: {}, + 6: { strength: 2 }, 7: { health: 6 }, 8: { true_defense: 2 }, 9: { health: 7 }, }, + HOTM_perk_mining_speed: { + 1: { mining_speed: 20 }, + }, + HOTM_perk_mining_speed_2: { + 1: { mining_speed: 40 }, + }, + HOTM_perk_mining_fortune: { + 1: { mining_fortune: 5 }, + }, + HOTM_perk_mining_fortune_2: { + 1: { mining_fortune: 5 }, + }, + HOTM_perk_mining_madness: { + 1: { mining_speed: 50, mining_fortune: 50 }, + }, + HOTM_perk_mining_experience: { + 1: { mining_wisdom: 0.1 }, + }, }; diff --git a/common/constants/enchantments.js b/common/constants/enchantments.js index 7cc3f7b02e..d223bb124c 100644 --- a/common/constants/enchantments.js +++ b/common/constants/enchantments.js @@ -58,6 +58,7 @@ export const MAX_ENCHANTS = new Set([ "Mana Steel III", "Mana Vampire X", "Overload V", + "Pesterminator V", "Piercing I", "Piscary VI", "Power VII", @@ -84,7 +85,7 @@ export const MAX_ENCHANTS = new Set([ "Spiked Hook VI", "Strong Mana X", "Sugar Rush III", - "Sunder V", + "Sunder VI", "Syphon V", "Tabasco III", "Thorns III", diff --git a/common/constants/misc.js b/common/constants/misc.js new file mode 100644 index 0000000000..c5d219b78d --- /dev/null +++ b/common/constants/misc.js @@ -0,0 +1,23 @@ +export const HARP_QUEST = { + song_hymn_joy_best_completion: 1, + song_frere_jacques_best_completion: 1, + song_amazing_grace_best_completion: 1, + song_brahms_best_completion: 2, + song_happy_birthday_best_completion: 2, + song_greensleeves_best_completion: 2, + song_jeopardy_best_completion: 3, + song_minuet_best_completion: 3, + song_joy_world_best_completion: 3, + song_pure_imagination_best_completion: 4, + song_vie_en_rose_best_completion: 4, + song_fire_and_flames_best_completion: 1, + song_pachelbel_best_completion: 1, +}; + +export const FORBIDDEN_STATS = { + permanent_speed: 1, + permanent_intelligence: 2, + permanent_health: 2, + permanent_defense: 1, + permanent_strength: 1, +}; diff --git a/common/constants/potions.js b/common/constants/potions.js index 5e35c24803..3e8d35b88a 100644 --- a/common/constants/potions.js +++ b/common/constants/potions.js @@ -1,5 +1,3 @@ -import { SYMBOLS } from "./stats.js"; - export const POTION_COLORS = { 0: "375cc4", // None 1: "cb5ba9", // Regeneration @@ -21,1098 +19,230 @@ export const POTION_COLORS = { export const POTION_EFFECTS = { true_defense: { - 1: { - name: "True Resistance I Potion", - description: `Increases ${SYMBOLS.true_defense} True Defense, which reduces true damage you receive.`, - bonus: { - true_defense: 5, - }, - }, - 2: { - name: "True Resistance II Potion", - description: `Increases ${SYMBOLS.true_defense} True Defense, which reduces true damage you receive.`, - bonus: { - true_defense: 10, - }, - }, - 3: { - name: "True Resistance III Potion", - description: `Increases ${SYMBOLS.true_defense} True Defense, which reduces true damage you receive.`, - bonus: { - true_defense: 15, - }, - }, - 4: { - name: "True Resistance IV Potion", - description: `Increases ${SYMBOLS.true_defense} True Defense, which reduces true damage you receive.`, - bonus: { - true_defense: 20, - }, + name: "True Resistance Potion", + bonuses: { + true_defense: [5, 10, 15, 20], }, }, strength: { - 1: { - name: "Strength I Potion", - description: `Increases ${SYMBOLS.strength} Strength.`, - bonus: { - strength: 5, - }, - }, - 2: { - name: "Strength II Potion", - description: `Increases ${SYMBOLS.strength} Strength.`, - bonus: { - strength: 12.5, - }, - }, - 3: { - name: "Strength III Potion", - description: `Increases ${SYMBOLS.strength} Strength.`, - bonus: { - strength: 20, - }, - }, - 4: { - name: "Strength IV Potion", - description: `Increases ${SYMBOLS.strength} Strength.`, - bonus: { - strength: 30, - }, - }, - 5: { - name: "Strength V Potion", - description: `Increases ${SYMBOLS.strength} Strength.`, - bonus: { - strength: 40, - }, - }, - 6: { - name: "Strength VI Potion", - description: `Increases ${SYMBOLS.strength} Strength.`, - bonus: { - strength: 50, - }, - }, - 7: { - name: "Strength VII Potion", - description: `Increases ${SYMBOLS.strength} Strength.`, - bonus: { - strength: 60, - }, - }, - 8: { - name: "Strength VIII Potion", - description: `Increases ${SYMBOLS.strength} Strength.`, - bonus: { - strength: 75, - }, + name: "Strength Potion", + bonuses: { + strength: [5, 12.5, 20, 30, 40, 50, 60, 75], }, }, regeneration: { - 1: { - name: "Regeneration I Potion", - description: `Grants ${SYMBOLS.health_regen} Health Regen.`, - bonus: { - health_regen: 5, - }, - }, - 2: { - name: "Regeneration II Potion", - description: `Grants ${SYMBOLS.health_regen} Health Regen.`, - bonus: { - health_regen: 10, - }, - }, - 3: { - name: "Regeneration III Potion", - description: `Grants ${SYMBOLS.health_regen} Health Regen.`, - bonus: { - health_regen: 15, - }, - }, - 4: { - name: "Regeneration IV Potion", - description: `Grants ${SYMBOLS.health_regen} Health Regen.`, - bonus: { - health_regen: 20, - }, - }, - 5: { - name: "Regeneration V Potion", - description: `Grants ${SYMBOLS.health_regen} Health Regen.`, - bonus: { - health_regen: 25, - }, - }, - 6: { - name: "Regeneration VI Potion", - description: `Grants ${SYMBOLS.health_regen} Health Regen.`, - bonus: { - health_regen: 30, - }, - }, - 7: { - name: "Regeneration VII Potion", - description: `Grants ${SYMBOLS.health_regen} Health Regen.`, - bonus: { - health_regen: 40, - }, - }, - 8: { - name: "Regeneration VIII Potion", - description: `Grants ${SYMBOLS.health_regen} Health Regen.`, - bonus: { - health_regen: 50, - }, - }, - 9: { - name: "Regeneration IX Potion", - description: `Grants ${SYMBOLS.health_regen} Health Regen.`, - bonus: { - health_regen: 63, - }, + name: "Regeneration Potion", + bonuses: { + health_regen: [5, 10, 15, 20, 25, 30, 40, 50, 63], }, }, enchanting_xp_boost: { - 1: { - name: "Enchanting XP Boost I Potion", - description: `Grants +5 ${SYMBOLS.enchanting_wisdom} Enchanting Wisdom.`, - bonus: { - enchanting_wisdom: 5, - }, - }, - 2: { - name: "Enchanting XP Boost II Potion", - description: `Grants +10 ${SYMBOLS.enchanting_wisdom} Enchanting Wisdom.`, - bonus: { - enchanting_wisdom: 10, - }, - }, - 3: { - name: "Enchanting XP Boost III Potion", - description: `Grants +20 ${SYMBOLS.enchanting_wisdom} Enchanting Wisdom.`, - bonus: { - enchanting_wisdom: 20, - }, + name: "Enchanting XP Boost Potion", + bonuses: { + enchanting_wisdom: [5, 10, 20], }, }, stun: { - 1: { - name: "Stun I Potion", - description: - "When applied to yourself, your hits have a 10% chance to stun enemies for 1s. When splashed, enemies are stunned for 1s.", - bonus: {}, - }, - 2: { - name: "Stun II Potion", - description: - "When applied to yourself, your hits have a 20% chance to stun enemies for 1s. When splashed, enemies are stunned for 1.25s.", - bonus: {}, - }, - 3: { - name: "Stun III Potion", - description: - "When applied to yourself, your hits have a 30% chance to stun enemies for 1s. When splashed, enemies are stunned for 1.5s.", - bonus: {}, - }, - 4: { - name: "Stun IV Potion", - description: - "When applied to yourself, your hits have a 40% chance to stun enemies for 1s. When splashed, enemies are stunned for 1.75s.", - bonus: {}, - }, + name: "Stun Potion", + bonuses: {}, }, experience: { - 1: { - name: "Experience I Potion", - description: "Gain 10% more experience orbs.", - bonus: {}, - }, - 2: { - name: "Experience II Potion", - description: "Gain 20% more experience orbs.", - bonus: {}, - }, - 3: { - name: "Experience III Potion", - description: "Gain 30% more experience orbs.", - bonus: {}, - }, - 4: { - name: "Experience IV Potion", - description: "Gain 40% more experience orbs.", - bonus: { - combat_wisdom: 10, - }, + name: "Experience Potion", + bonuses: { + combat_wisdom: [0, 0, 0, 10], }, }, rabbit: { - 1: { - name: "Rabbit I Potion", - description: `Grants Jump Boost I and +10 ${SYMBOLS.speed} Speed.`, - bonus: { - speed: 10, - }, - }, - 2: { - name: "Rabbit II Potion", - description: `Grants Jump Boost I and +20 ${SYMBOLS.speed} Speed.`, - bonus: { - speed: 20, - }, - }, - 3: { - name: "Rabbit III Potion", - description: `Grants Jump Boost II and +30 ${SYMBOLS.speed} Speed.`, - bonus: { - speed: 30, - }, - }, - 4: { - name: "Rabbit IV Potion", - description: `Grants Jump Boost II and +40 ${SYMBOLS.speed} Speed.`, - bonus: { - speed: 40, - }, - }, - 5: { - name: "Rabbit V Potion", - description: `Grants Jump Boost III and +50 ${SYMBOLS.speed} Speed.`, - bonus: { - speed: 50, - }, - }, - 6: { - name: "Rabbit VI Potion", - description: `Grants Jump Boost III and +60 ${SYMBOLS.speed} Speed.`, - bonus: { - speed: 60, - }, + name: "Rabbit Potion", + bonuses: { + speed: [10, 20, 30, 40, 50, 60], }, }, magic_find: { - 1: { - name: "Magic Find I Potion", - description: `Increases the chanfe of finding rare items.`, - bonus: { - magic_find: 10, - }, - }, - 2: { - name: "Magic Find II Potion", - description: `Increases the chanfe of finding rare items.`, - bonus: { - magic_find: 25, - }, - }, - 3: { - name: "Magic Find III Potion", - description: `Increases the chanfe of finding rare items.`, - bonus: { - magic_find: 50, - }, - }, - 4: { - name: "Magic Find IV Potion", - description: `Increases the chanfe of finding rare items.`, - bonus: { - magic_find: 75, - }, + name: "Magic Find Potion", + bonuses: { + magic_find: [10, 25, 50, 75], }, }, night_vision: { - 1: { - name: "Night Vision Potion", - description: `Grants greater visiblity at night.`, - bonus: {}, - }, + name: "Night Vision Potion", + bonuses: {}, }, absorption: { - 1: { - name: "Absorption I Potion", - description: `Grants a boost to absorption health.`, - bonus: {}, - }, - 2: { - name: "Absorption II Potion", - description: `Grants a boost to absorption health.`, - bonus: {}, - }, - 3: { - name: "Absorption III Potion", - description: `Grants a boost to absorption health.`, - bonus: {}, - }, - 4: { - name: "Absorption IV Potion", - description: `Grants a boost to absorption health.`, - bonus: {}, - }, - 5: { - name: "Absorption V Potion", - description: `Grants a boost to absorption health.`, - bonus: {}, - }, - 6: { - name: "Absorption VI Potion", - description: `Grants a boost to absorption health.`, - bonus: {}, - }, - 7: { - name: "Absorption VII Potion", - description: `Grants a boost to absorption health.`, - bonus: {}, - }, - 8: { - name: "Absorption VIII Potion", - description: `Grants a boost to absorption health.`, - bonus: {}, - }, + name: "Absorption Potion", + bonuses: {}, }, water_breathing: { - 1: { - name: "Water Breathing I Potion", - description: `Grants a chance of not taking drowning damage.`, - bonus: {}, - }, - 2: { - name: "Water Breathing II Potion", - description: `Grants a chance of not taking drowning damage.`, - bonus: {}, - }, - 3: { - name: "Water Breathing III Potion", - description: `Grants a chance of not taking drowning damage.`, - bonus: {}, - }, - 4: { - name: "Water Breathing IV Potion", - description: `Grants a chance of not taking drowning damage.`, - bonus: {}, - }, - 5: { - name: "Water Breathing V Potion", - description: `Grants a chance of not taking drowning damage.`, - bonus: {}, - }, - 6: { - name: "Water Breathing VI Potion", - description: `Grants a chance of not taking drowning damage.`, - bonus: {}, - }, + name: "Water Breathing Potion", + bonuses: {}, }, combat_xp_boost: { - 1: { - name: "Combat XP Boost I Potion", - description: `Grants +5 ${SYMBOLS.combat_wisdom} Combat Wisdom.`, - bonus: { - combat_wisdom: 5, - }, - }, - 2: { - name: "Combat XP Boost II Potion", - description: `Grants +10 ${SYMBOLS.combat_wisdom} Combat Wisdom.`, - bonus: { - combat_wisdom: 10, - }, - }, - 3: { - name: "Combat XP Boost III Potion", - description: `Grants +20 ${SYMBOLS.combat_wisdom} Combat Wisdom.`, - bonus: { - combat_wisdom: 20, - }, + name: "Combat XP Boost Potion", + bonuses: { + combat_wisdom: [5, 10, 20], }, }, fire_resistance: { - 1: { - name: "Fire Resistance Potion", - description: `Receive a 10% reduced damage from fire and lava.`, - bonus: {}, - }, + name: "Fire Resistance Potion", + bonuses: {}, }, jump_boost: { - 1: { - name: "Jump Boost I Potion", - description: `Increases your jump height.`, - bonus: {}, - }, - 2: { - name: "Jump Boost II Potion", - description: `Increases your jump height.`, - bonus: {}, - }, - 3: { - name: "Jump Boost III Potion", - description: `Increases your jump height.`, - bonus: {}, - }, - 4: { - name: "Jump Boost IV Potion", - description: `Increases your jump height.`, - bonus: {}, - }, + name: "Jump Boost Potion", + bonuses: {}, }, resistance: { - 1: { - name: "Resistance I Potion", - description: `Increases ${SYMBOLS.defense} Defense.`, - bonus: { - defense: 5, - }, - }, - 2: { - name: "Resistance II Potion", - description: `Increases ${SYMBOLS.defense} Defense.`, - bonus: { - defense: 10, - }, - }, - 3: { - name: "Resistance III Potion", - description: `Increases ${SYMBOLS.defense} Defense.`, - bonus: { - defense: 15, - }, - }, - 4: { - name: "Resistance IV Potion", - description: `Increases ${SYMBOLS.defense} Defense.`, - bonus: { - defense: 20, - }, - }, - 5: { - name: "Resistance V Potion", - description: `Increases ${SYMBOLS.defense} Defense.`, - bonus: { - defense: 30, - }, - }, - 6: { - name: "Resistance VI Potion", - description: `Increases ${SYMBOLS.defense} Defense.`, - bonus: { - defense: 40, - }, - }, - 7: { - name: "Resistance VII Potion", - description: `Increases ${SYMBOLS.defense} Defense.`, - bonus: { - defense: 50, - }, - }, - 8: { - name: "Resistance VIII Potion", - description: `Increases ${SYMBOLS.defense} Defense.`, - bonus: { - defense: 66, - }, + name: "Resistance Potion", + bonuses: { + defense: [5, 10, 15, 20, 30, 40, 50, 66], }, }, fishing_xp_boost: { - 1: { - name: "Fishing XP Boost I Potion", - description: `Grants +5 ${SYMBOLS.fishing_wisdom} Fishing Wisdom.`, - bonus: { - fishing_wisdom: 5, - }, - }, - 2: { - name: "Fishing XP Boost II Potion", - description: `Grants +10 ${SYMBOLS.fishing_wisdom} Fishing Wisdom.`, - bonus: { - fishing_wisdom: 10, - }, - }, - 3: { - name: "Fishing XP Boost III Potion", - description: `Grants +20 ${SYMBOLS.fishing_wisdom} Fishing Wisdom.`, - bonus: { - fishing_wisdom: 20, - }, + name: "Fishing XP Boost Potion", + bonuses: { + fishing_wisdom: [5, 10, 20], }, }, agility: { - 1: { - name: "Agility I Potion", - description: `Grants a ${SYMBOLS.speed} Speed boost and increases the chance for mob attacks to miss.`, - bonus: { - speed: 10, - }, - }, - 2: { - name: "Agility II Potion", - description: `Grants a ${SYMBOLS.speed} Speed boost and increases the chance for mob attacks to miss.`, - bonus: { - speed: 20, - }, - }, - 3: { - name: "Agility III Potion", - description: `Grants a ${SYMBOLS.speed} Speed boost and increases the chance for mob attacks to miss.`, - bonus: { - speed: 30, - }, - }, - 4: { - name: "Agility IV Potion", - description: `Grants a ${SYMBOLS.speed} Speed boost and increases the chance for mob attacks to miss.`, - bonus: { - speed: 40, - }, + name: "Agility Potion", + bonuses: { + speed: [10, 20, 30, 40], }, }, archery: { - 1: { - name: "Archery I Potion", - description: `Increases bow damage by 12.5%.`, - bonus: {}, - }, - 2: { - name: "Archery II Potion", - description: `Increases bow damage by 25%.`, - bonus: {}, - }, - 3: { - name: "Archery III Potion", - description: `Increases bow damage by 50%.`, - bonus: {}, - }, - 4: { - name: "Archery IV Potion", - description: `Increases bow damage by 75%.`, - bonus: {}, - }, + name: "Archery Potion", + bonuses: {}, }, critical: { - 1: { - name: "Critical I Potion", - description: `Increases ${SYMBOLS.crit_chance} Crit Chance by 10% and ${SYMBOLS.crit_damage} Crit Damage by 10%.`, - bonus: { - crit_chance: 10, - crit_damage: 10, - }, - }, - 2: { - name: "Critical II Potion", - description: `Increases ${SYMBOLS.crit_chance} Crit Chance by 15% and ${SYMBOLS.crit_damage} Crit Damage by 20%.`, - bonus: { - crit_chance: 15, - crit_damage: 20, - }, - }, - 3: { - name: "Critical III Potion", - description: `Increases ${SYMBOLS.crit_chance} Crit Chance by 20% and ${SYMBOLS.crit_damage} Crit Damage by 30%.`, - bonus: { - crit_chance: 20, - crit_damage: 30, - }, - }, - 4: { - name: "Critical IV Potion", - description: `Increases ${SYMBOLS.crit_chance} Crit Chance by 25% and ${SYMBOLS.crit_damage} Crit Damage by 40%.`, - bonus: { - crit_chance: 25, - crit_damage: 40, - magic_find: 10, - }, + name: "Critical Potion", + bonuses: { + crit_chance: [10, 15, 20, 25], + crit_damage: [10, 20, 30, 40], + magic_find: [0, 0, 0, 10], }, }, speed: { - 1: { - name: "Speed I Potion", - description: `Grants + ${SYMBOLS.speed} Speed.`, - bonus: { - speed: 5, - }, - }, - 2: { - name: "Speed II Potion", - description: `Grants + ${SYMBOLS.speed} Speed.`, - bonus: { - speed: 10, - }, - }, - 3: { - name: "Speed III Potion", - description: `Grants + ${SYMBOLS.speed} Speed.`, - bonus: { - speed: 15, - }, - }, - 4: { - name: "Speed IV Potion", - description: `Grants + ${SYMBOLS.speed} Speed.`, - bonus: { - speed: 20, - }, - }, - 5: { - name: "Speed V Potion", - description: `Grants + ${SYMBOLS.speed} Speed.`, - bonus: { - speed: 25, - }, - }, - 6: { - name: "Speed VI Potion", - description: `Grants + ${SYMBOLS.speed} Speed.`, - bonus: { - speed: 30, - }, - }, - 7: { - name: "Speed VII Potion", - description: `Grants + ${SYMBOLS.speed} Speed.`, - bonus: { - speed: 35, - }, - }, - 8: { - name: "Speed VIII Potion", - description: `Grants + ${SYMBOLS.speed} Speed.`, - bonus: { - speed: 48, - }, + name: "Speed Potion", + bonuses: { + speed: [5, 10, 15, 20, 25, 30, 35, 48], }, }, farming_xp_boost: { - 1: { - name: "Farming XP Boost I Potion", - description: `Grants +5 ${SYMBOLS.farming_wisdom} Farming Wisdom.`, - bonus: { - farming_wisdom: 5, - }, - }, - 2: { - name: "Farming XP Boost II Potion", - description: `Grants +10 ${SYMBOLS.farming_wisdom} Farming Wisdom.`, - bonus: { - farming_wisdom: 10, - }, - }, - 3: { - name: "Farming XP Boost III Potion", - description: `Grants +20 ${SYMBOLS.farming_wisdom} Farming Wisdom.`, - bonus: { - farming_wisdom: 20, - }, + name: "Farming XP Boost Potion", + bonuses: { + farming_wisdom: [5, 10, 20], }, }, adrenaline: { - 1: { - name: "Adrenaline I Potion", - description: `Grants 300 absorption and +40 ${SYMBOLS.speed} Speed.`, - bonus: { - speed: 5, - }, - }, - 2: { - name: "Adrenaline II Potion", - description: `Grants 300 absorption and +40 ${SYMBOLS.speed} Speed.`, - bonus: { - speed: 10, - }, - }, - 3: { - name: "Adrenaline III Potion", - description: `Grants 300 absorption and +40 ${SYMBOLS.speed} Speed.`, - bonus: { - speed: 15, - }, - }, - 4: { - name: "Adrenaline IV Potion", - description: `Grants 300 absorption and +40 ${SYMBOLS.speed} Speed.`, - bonus: { - speed: 20, - }, - }, - 5: { - name: "Adrenaline V Potion", - description: `Grants 300 absorption and +40 ${SYMBOLS.speed} Speed.`, - bonus: { - speed: 25, - }, - }, - 6: { - name: "Adrenaline VI Potion", - description: `Grants 300 absorption and +40 ${SYMBOLS.speed} Speed.`, - bonus: { - speed: 30, - }, - }, - 7: { - name: "Adrenaline VII Potion", - description: `Grants 300 absorption and +40 ${SYMBOLS.speed} Speed.`, - bonus: { - speed: 35, - }, - }, - 8: { - name: "Adrenaline VIII Potion", - description: `Grants 300 absorption and +40 ${SYMBOLS.speed} Speed.`, - bonus: { - speed: 40, - }, + name: "Adrenaline Potion", + bonuses: { + speed: [5, 10, 15, 20, 25, 30, 35, 40], }, }, spelunker: { - 1: { - name: "Spelunker I Potion", - description: `Increases 5 ${SYMBOLS.mining_fortune} Mining Fortune.`, - bonus: { - mining_fortune: 5, - }, - }, - 2: { - name: "Spelunker II Potion", - description: `Increases 10 ${SYMBOLS.mining_fortune} Mining Fortune.`, - bonus: { - mining_fortune: 10, - }, - }, - 3: { - name: "Spelunker III Potion", - description: `Increases 15 ${SYMBOLS.mining_fortune} Mining Fortune.`, - bonus: { - mining_fortune: 15, - }, - }, - 4: { - name: "Spelunker IV Potion", - description: `Increases 20 ${SYMBOLS.mining_fortune} Mining Fortune.`, - bonus: { - mining_fortune: 20, - }, - }, - 5: { - name: "Spelunker V Potion", - description: `Increases 25 ${SYMBOLS.mining_fortune} Mining Fortune.`, - bonus: { - mining_fortune: 25, - }, + name: "Spelunker Potion", + bonuses: { + mining_fortune: [5, 10, 15, 20, 25], }, }, dodge: { - 1: { - name: "Dodge I Potion", - description: `Mobs attacks have a 10% chance to miss.`, - bonus: {}, - }, - 2: { - name: "Dodge II Potion", - description: `Mobs attacks have a 20% chance to miss.`, - bonus: {}, - }, - 3: { - name: "Dodge III Potion", - description: `Mobs attacks have a 30% chance to miss.`, - bonus: {}, - }, - 4: { - name: "Dodge IV Potion", - description: `Mobs attacks have a 40% chance to miss.`, - bonus: {}, - }, + name: "Dodge Potion", + bonuses: {}, }, spirit: { - 1: { - name: "Spirit I Potion", - description: `Grants +10 ${SYMBOLS.speed} Speed and +10 ${SYMBOLS.crit_damage} Crit Damage.`, - bonus: { - speed: 10, - crit_damage: 10, - }, - }, - 2: { - name: "Spirit II Potion", - description: `Grants +20 ${SYMBOLS.speed} Speed and +20 ${SYMBOLS.crit_damage} Crit Damage.`, - bonus: { - speed: 20, - crit_damage: 20, - }, - }, - 3: { - name: "Spirit III Potion", - description: `Grants +30 ${SYMBOLS.speed} Speed and +30 ${SYMBOLS.crit_damage} Crit Damage.`, - bonus: { - speed: 30, - crit_damage: 30, - }, - }, - 4: { - name: "Spirit IV Potion", - description: `Grants +40 ${SYMBOLS.speed} Speed and +40 ${SYMBOLS.crit_damage} Crit Damage.`, - bonus: { - speed: 40, - crit_damage: 40, - }, + name: "Spirit Potion", + bonuses: { + speed: [10, 20, 30, 40], + crit_damage: [10, 20, 30, 40], }, }, pet_luck: { - 1: { - name: "Pet Luck I Potion", - description: `Increases how many pets you can find and gives you better luck in crafting pets.`, - bonus: { - pet_luck: 5, - }, - }, - 2: { - name: "Pet Luck II Potion", - description: `Increases how many pets you can find and gives you better luck in crafting pets.`, - bonus: { - pet_luck: 10, - }, - }, - 3: { - name: "Pet Luck III Potion", - description: `Increases how many pets you can find and gives you better luck in crafting pets.`, - bonus: { - pet_luck: 15, - }, - }, - 4: { - name: "Pet Luck IV Potion", - description: `Increases how many pets you can find and gives you better luck in crafting pets.`, - bonus: { - pet_luck: 20, - }, + name: "Pet Luck Potion", + bonuses: { + pet_luck: [5, 10, 15, 20], }, }, mining_xp_boost: { - 1: { - name: "Mining XP Boost I Potion", - description: `Grants +5 ${SYMBOLS.mining_wisdom} Mining Wisdom.`, - bonus: { - mining_wisdom: 5, - }, - }, - 2: { - name: "Mining XP Boost II Potion", - description: `Grants +10 ${SYMBOLS.mining_wisdom} Mining Wisdom.`, - bonus: { - mining_wisdom: 10, - }, - }, - 3: { - name: "Mining XP Boost III Potion", - description: `Grants +20 ${SYMBOLS.mining_wisdom} Mining Wisdom.`, - bonus: { - mining_wisdom: 20, - }, + name: "Mining XP Boost Potion", + bonuses: { + mining_wisdom: [5, 10, 20], }, }, haste: { - 1: { - name: "Haste I Potion", - description: `Increases your mining speed by 20%.`, - bonus: {}, - }, - 2: { - name: "Haste II Potion", - description: `Increases your mining speed by 40%.`, - bonus: {}, - }, - 3: { - name: "Haste III Potion", - description: `Increases your mining speed by 60%.`, - bonus: {}, - }, - 4: { - name: "Haste IV Potion", - description: `Increases your mining speed by 80%.`, - bonus: {}, - }, + name: "Haste Potion", + bonuses: {}, }, burning: { - 1: { - name: "Burning I Potion", - description: `Increases the duration of fire damage that you inflict on enemies by 5%.`, - bonus: {}, - }, - 2: { - name: "Burning II Potion", - description: `Increases the duration of fire damage that you inflict on enemies by 10%.`, - bonus: {}, - }, - 3: { - name: "Burning III Potion", - description: `Increases the duration of fire damage that you inflict on enemies by 15%.`, - bonus: {}, - }, - 4: { - name: "Burning IV Potion", - description: `Increases the duration of fire damage that you inflict on enemies by 20%.`, - bonus: {}, - }, + name: "Burning Potion", + bonuses: {}, }, mana: { - 1: { - name: "Mana I Potion", - description: `Grants 1 ${SYMBOLS.intelligence} Mana per second.`, - bonus: {}, - }, - 2: { - name: "Mana II Potion", - description: `Grants 2 ${SYMBOLS.intelligence} Mana per second.`, - bonus: {}, - }, - 3: { - name: "Mana III Potion", - description: `Grants 3 ${SYMBOLS.intelligence} Mana per second.`, - bonus: {}, - }, - 4: { - name: "Mana IV Potion", - description: `Grants 4 ${SYMBOLS.intelligence} Mana per second.`, - bonus: {}, - }, - 5: { - name: "Mana V Potion", - description: `Grants 5 ${SYMBOLS.intelligence} Mana per second.`, - bonus: {}, - }, - 6: { - name: "Mana VI Potion", - description: `Grants 6 ${SYMBOLS.intelligence} Mana per second.`, - bonus: {}, - }, - 7: { - name: "Mana VI Potion", - description: `Grants 7 ${SYMBOLS.intelligence} Mana per second.`, - bonus: {}, - }, - 8: { - name: "Mana VII Potion", - description: `Grants 8 ${SYMBOLS.intelligence} Mana per second.`, - bonus: {}, - }, + name: "Mana Potion", + bonuses: {}, }, foraging_xp_boost: { - 1: { - name: "Foraging XP Boost III Potion", - description: `Grants +5 ${SYMBOLS.foraging_wisdom} Foraging Wisdom.`, - bonus: { - foraging_wisdom: 5, - }, - }, - 2: { - name: "Foraging XP Boost III Potion", - description: `Grants +10 ${SYMBOLS.foraging_wisdom} Foraging Wisdom.`, - bonus: { - foraging_wisdom: 10, - }, - }, - 3: { - name: "Foraging XP Boost III Potion", - description: `Grants +20 ${SYMBOLS.foraging_wisdom} Foraging Wisdom.`, - bonus: { - foraging_wisdom: 20, - }, + name: "Foraging XP Boost Potion", + bonuses: { + foraging_wisdom: [5, 10, 20], }, }, alchemy_xp_boost: { - 1: { - name: "Alchemy XP Boost I Potion", - description: `Grants +5 ${SYMBOLS.alchemy_wisdom} Alchemy Wisdom.`, - bonus: { - alchemy_wisdom: 5, - }, - }, - 2: { - name: "Alchemy XP Boost II Potion", - description: `Grants +10 ${SYMBOLS.alchemy_wisdom} Alchemy Wisdom.`, - bonus: { - alchemy_wisdom: 10, - }, - }, - 3: { - name: "Alchemy XP Boost III Potion", - description: `Grants +20 ${SYMBOLS.alchemy_wisdom} Alchemy Wisdom.`, - bonus: { - alchemy_wisdom: 20, - }, + name: "Alchemy XP Boost Potion", + bonuses: { + alchemy_wisdom: [5, 10, 20], }, }, invisibility: { - 1: { - name: "Invisibility Potion", - description: `Grants invisibility from players and mobs.`, - bonus: {}, - }, + name: "Invisibility Potion", + bonuses: {}, }, jerry_candy: { - 1: { - name: "Jerry Candy", - description: `Grants +100 ${SYMBOLS.health} Health, +20 ${SYMBOLS.strength} Strength, +2 ${SYMBOLS.ferocity} Ferocity, +100 ${SYMBOLS.intelligence} Inteligence, +3 ${SYMBOLS.magic_find} Magic Find`, - bonus: { - health: 100, - strength: 20, - ferocity: 2, - intelligence: 100, - magic_find: 3, - }, + name: "Jerry Candy", + bonuses: { + health: [100], + strength: [20], + ferocity: [2], + intelligence: [100], + magic_find: [3], }, }, GABAGOEY: { - 1: { - name: "Gabagoey Mixin", - description: `Increases your ${SYMBOLS.true_defense} True Defense by 5.`, - bonus: { - true_defense: 5, - }, + name: "Gabagoey Mixin", + bonuses: { + true_defense: [5], }, }, END_PORTAL_FUMES: { - 1: { - name: "End Portal Fumes", - description: `${SYMBOLS.soulflow} Soulflow conversions provide +30% more ${SYMBOLS.overflow_mana} Overflow.`, - bonus: {}, - }, + name: "End Portal Fumes", + bonuses: {}, }, SPIDER_EGG: { - 1: { - name: "Spider Egg Mixin", - description: `Gain 5% dodge chance!`, - bonus: {}, - }, + name: "Spider Egg Mixin", + bonuses: {}, }, ZOMBIE_BRAIN: { - 1: { - name: "Zombie Brain Mixin", - description: `Gain +10 ${SYMBOLS.ferocity} Ferocity!`, - bonus: { - ferocity: 10, - }, + name: "Zombie Brain Mixin", + bonuses: { + ferocity: [10], }, }, WOLF_FUR: { - 1: { - name: "Wolf Fur Mixin", - description: `Gain +7 ${SYMBOLS.magic_find} Magic Find when slaying monsters in one hit!`, - bonus: { - magic_find: 7, - }, + name: "Wolf Fur Mixin", + bonuses: { + magic_find: [7], }, }, mushed_glowy_tonic: { - 1: { - name: "Mushed Glowy Tonic", - description: `Grants ${SYMBOLS.fishing_speed} Fishing Speed.`, - bonus: { - fishing_speed: 30, - }, + name: "Mushed Glowy Tonic", + bonuses: { + fishing_speed: [30], }, }, DEEPTERROR: { - 1: { - name: "Deepterror Mixin", - description: `Gain +210 ${SYMBOLS.health} Health and +40 ${SYMBOLS.defense} Defense while in The End or Crimson Isle.`, - bonus: {}, - }, + name: "Deepterror Mixin", + bonuses: {}, + }, + goblin_king_scent: { + name: "Goblin King Scent", + bonuses: {}, }, }; diff --git a/common/constants/stats.js b/common/constants/stats.js index 1c94bd4eb0..852f16556e 100644 --- a/common/constants/stats.js +++ b/common/constants/stats.js @@ -227,16 +227,6 @@ export const STATS_DATA = { suffix: "", color: "a", }, - // todo: remove this once #1639 is merged and update wisdom values - wisdom: { - name: "Wisdom", - nameLore: "Wisdom", - nameShort: "Wisdom", - nameTiny: "W", - symbol: "☯", - suffix: "", - color: "3", - }, mana_regen: { name: "Mana Regen", nameLore: "Mana Regen", @@ -264,8 +254,118 @@ export const STATS_DATA = { suffix: "", color: "2", }, + alchemy_wisdom: { + name: "Alchemy Wisdom", + nameLore: "Alchemy Wisdom", + nameShort: "Alchemy Wisdom", + nameTiny: "AW", + symbol: "☯", + suffix: "", + color: "3", + }, + carpentry_wisdom: { + name: "Carpentry Wisdom", + nameLore: "Carpentry Wisdom", + nameShort: "Carpentry Wisdom", + nameTiny: "CW", + symbol: "☯", + suffix: "", + color: "3", + }, + combat_wisdom: { + name: "Combat Wisdom", + nameLore: "Combat Wisdom", + nameShort: "Combat Wisdom", + nameTiny: "CW", + symbol: "☯", + suffix: "", + color: "3", + }, + enchanting_wisdom: { + name: "Enchanting Wisdom", + nameLore: "Enchanting Wisdom", + nameShort: "Enchanting Wisdom", + nameTiny: "EW", + symbol: "☯", + suffix: "", + color: "3", + }, + farming_wisdom: { + name: "Farming Wisdom", + nameLore: "Farming Wisdom", + nameShort: "Farming Wisdom", + nameTiny: "FW", + symbol: "☯", + suffix: "", + color: "3", + }, + fishing_wisdom: { + name: "Fishing Wisdom", + nameLore: "Fishing Wisdom", + nameShort: "Fishing Wisdom", + nameTiny: "FW", + symbol: "☯", + suffix: "", + color: "3", + }, + foraging_wisdom: { + name: "Foraging Wisdom", + nameLore: "Foraging Wisdom", + nameShort: "Foraging Wisdom", + nameTiny: "FW", + symbol: "☯", + suffix: "", + color: "3", + }, + mining_wisdom: { + name: "Mining Wisdom", + nameLore: "Mining Wisdom", + nameShort: "Mining Wisdom", + nameTiny: "MW", + symbol: "☯", + suffix: "", + color: "3", + }, + runecrafting_wisdom: { + name: "Runecrafting Wisdom", + nameLore: "Runecrafting Wisdom", + nameShort: "Runecrafting Wisdom", + nameTiny: "RW", + symbol: "☯", + suffix: "", + color: "3", + }, + social_wisdom: { + name: "Social Wisdom", + nameLore: "Social Wisdom", + nameShort: "Social Wisdom", + nameTiny: "SW", + symbol: "☯", + suffix: "", + color: "3", + }, + bonus_pest_chance: { + name: "Bonus Pest Chance", + nameLore: "Bonus Pest Chance", + nameShort: "Bonus Pest Chance", + nameTiny: "BPC", + symbol: "ൠ", + }, }; +export const HIDDEN_STATS = [ + "alchemy_wisdom", + "carpentry_wisdom", + "combat_wisdom", + "enchanting_wisdom", + "farming_wisdom", + "fishing_wisdom", + "foraging_wisdom", + "mining_wisdom", + "runecrafting_wisdom", + "social_wisdom", +]; + const symbols = { powder: "᠅", soulflow: "⸎", diff --git a/package.json b/package.json index b8b7dafe74..af9b378f4b 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "sharp": "^0.30.7", "sitemap": "^7.1.1", "skinview3d": "^2.2.1", - "skyhelper-networth": "^1.14.1", + "skyhelper-networth": "^1.17.4", "tippy.js": "^6.3.7", "twemoji": "^14.0.2", "upng-js": "^2.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ba59db86c5..ca3a438659 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,5 +1,9 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + dependencies: apng2gif-bin: specifier: ^1.7.1 @@ -101,8 +105,8 @@ dependencies: specifier: ^2.2.1 version: 2.2.1 skyhelper-networth: - specifier: ^1.14.1 - version: 1.14.1 + specifier: ^1.17.4 + version: 1.17.4 tippy.js: specifier: ^6.3.7 version: 6.3.7 @@ -3066,6 +3070,7 @@ packages: /memory-pager@1.5.0: resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} + requiresBuild: true dev: false optional: true @@ -4138,8 +4143,8 @@ packages: three: 0.136.0 dev: false - /skyhelper-networth@1.14.1: - resolution: {integrity: sha512-UsRTBiR3Q7ZjeDaT4VlMYq1z27gb1I3SZ7qGowWGNK6TyoSFAIeEbB/JWmaLmfj8Az6gh4GRXS2aHP0JDoVxHg==} + /skyhelper-networth@1.17.4: + resolution: {integrity: sha512-dIuCtjSyQHpPuZllbkArVU8niJI1XhfQQod9hCdruu6K0LcakuIQ+crnJ1uLWnjUtitlBcT6nq5Yxsb04OqM4A==} dependencies: axios: 0.27.2 prismarine-nbt: 2.2.1 @@ -4262,6 +4267,7 @@ packages: /sparse-bitfield@3.0.3: resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} + requiresBuild: true dependencies: memory-pager: 1.5.0 dev: false @@ -4875,4 +4881,4 @@ packages: /yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - dev: true \ No newline at end of file + dev: true diff --git a/public/resourcepacks/FurfSky_Reborn_1_7/assets/minecraft/mcpatcher/cit/equipment/belts/implosion_belt/implosion_belt.png b/public/resourcepacks/FurfSky_Reborn_1_7/assets/minecraft/mcpatcher/cit/equipment/belts/implosion_belt/implosion_belt.png index 3002e0e1d2..f95ce8640c 100644 Binary files a/public/resourcepacks/FurfSky_Reborn_1_7/assets/minecraft/mcpatcher/cit/equipment/belts/implosion_belt/implosion_belt.png and b/public/resourcepacks/FurfSky_Reborn_1_7/assets/minecraft/mcpatcher/cit/equipment/belts/implosion_belt/implosion_belt.png differ diff --git a/public/resourcepacks/FurfSky_Reborn_1_7/assets/minecraft/mcpatcher/cit/equipment/cloaks/annihilation_cloak/annihilation_cloak.png b/public/resourcepacks/FurfSky_Reborn_1_7/assets/minecraft/mcpatcher/cit/equipment/cloaks/annihilation_cloak/annihilation_cloak.png index 5d5516d094..2326a8e4e0 100644 Binary files a/public/resourcepacks/FurfSky_Reborn_1_7/assets/minecraft/mcpatcher/cit/equipment/cloaks/annihilation_cloak/annihilation_cloak.png and b/public/resourcepacks/FurfSky_Reborn_1_7/assets/minecraft/mcpatcher/cit/equipment/cloaks/annihilation_cloak/annihilation_cloak.png differ diff --git a/public/resourcepacks/FurfSky_Reborn_1_7/assets/minecraft/mcpatcher/cit/equipment/gloves/flaming_fist/flaming_fist.png b/public/resourcepacks/FurfSky_Reborn_1_7/assets/minecraft/mcpatcher/cit/equipment/gloves/flaming_fist/flaming_fist.png index 71d43f3606..ad11f24ef2 100644 Binary files a/public/resourcepacks/FurfSky_Reborn_1_7/assets/minecraft/mcpatcher/cit/equipment/gloves/flaming_fist/flaming_fist.png and b/public/resourcepacks/FurfSky_Reborn_1_7/assets/minecraft/mcpatcher/cit/equipment/gloves/flaming_fist/flaming_fist.png differ diff --git a/public/resourcepacks/FurfSky_Reborn_1_7/assets/minecraft/mcpatcher/cit/equipment/necklaces/delirium_necklace/delirium_necklace.png b/public/resourcepacks/FurfSky_Reborn_1_7/assets/minecraft/mcpatcher/cit/equipment/necklaces/delirium_necklace/delirium_necklace.png index d9c5710a77..f8665036d5 100644 Binary files a/public/resourcepacks/FurfSky_Reborn_1_7/assets/minecraft/mcpatcher/cit/equipment/necklaces/delirium_necklace/delirium_necklace.png and b/public/resourcepacks/FurfSky_Reborn_1_7/assets/minecraft/mcpatcher/cit/equipment/necklaces/delirium_necklace/delirium_necklace.png differ diff --git a/public/resourcepacks/FurfSky_Reborn_1_7/assets/minecraft/mcpatcher/cit/item/other/the_rift/living_metal_heart/living_metal_heart.png b/public/resourcepacks/FurfSky_Reborn_1_7/assets/minecraft/mcpatcher/cit/item/other/the_rift/living_metal_heart/living_metal_heart.png index 22f653ee87..f0aa480e5f 100644 Binary files a/public/resourcepacks/FurfSky_Reborn_1_7/assets/minecraft/mcpatcher/cit/item/other/the_rift/living_metal_heart/living_metal_heart.png and b/public/resourcepacks/FurfSky_Reborn_1_7/assets/minecraft/mcpatcher/cit/item/other/the_rift/living_metal_heart/living_metal_heart.png differ diff --git a/public/resourcepacks/FurfSky_Reborn_1_7/assets/minecraft/mcpatcher/cit/item/weapons/dungeons/ranged/bonemerang/bonemerang_fragged.png b/public/resourcepacks/FurfSky_Reborn_1_7/assets/minecraft/mcpatcher/cit/item/weapons/dungeons/ranged/bonemerang/bonemerang_fragged.png index 6f0050fe1c..ab84fad56d 100644 Binary files a/public/resourcepacks/FurfSky_Reborn_1_7/assets/minecraft/mcpatcher/cit/item/weapons/dungeons/ranged/bonemerang/bonemerang_fragged.png and b/public/resourcepacks/FurfSky_Reborn_1_7/assets/minecraft/mcpatcher/cit/item/weapons/dungeons/ranged/bonemerang/bonemerang_fragged.png differ diff --git a/public/resourcepacks/FurfSky_Reborn_1_7/assets/minecraft/mcpatcher/cit/item/weapons/dungeons/ranged/last_breath/last_breath_fragged.png b/public/resourcepacks/FurfSky_Reborn_1_7/assets/minecraft/mcpatcher/cit/item/weapons/dungeons/ranged/last_breath/last_breath_fragged.png index 9652d719f5..0b0b4275a2 100644 Binary files a/public/resourcepacks/FurfSky_Reborn_1_7/assets/minecraft/mcpatcher/cit/item/weapons/dungeons/ranged/last_breath/last_breath_fragged.png and b/public/resourcepacks/FurfSky_Reborn_1_7/assets/minecraft/mcpatcher/cit/item/weapons/dungeons/ranged/last_breath/last_breath_fragged.png differ diff --git a/public/resources/scss/stats.scss b/public/resources/scss/stats.scss index 8d34e2ee9b..16d7f1e551 100644 --- a/public/resources/scss/stats.scss +++ b/public/resources/scss/stats.scss @@ -882,19 +882,23 @@ a.additional-player-stat:hover { } } -.stat-health, -.stat-strength, -.stat-ferocity, -.stat-ability-damage, +.color-ability-damage, +.color-ferocity, .color-health, +.color-health-regen, .color-strength, -.color-ferocity, -.color-ability-damage { +.stat-ability-damage, +.stat-ferocity, +.stat-health, +.stat-health-regen, +.stat-strength { color: var(--§c); } .stat-defense, -.color-defense { +.stat-mending, +.color-defense, +.color-mending { color: var(--§a); } @@ -920,16 +924,37 @@ a.additional-player-stat:hover { color: var(--§b); } +.stat-alchemy-wisdom, +.color-alchemy-wisdom, +.stat-carpentry-wisdom, +.color-carpentry-wisdom, +.stat-combat-wisdom, +.color-combat-wisdom, +.stat-enchanting-wisdom, +.color-carpentry-wisdom, +.stat-farming-wisdom, +.color-farming-wisdom, +.stat-fishing-wisdom, +.color-fishing-wisdom, +.stat-foraging-wisdom, +.color-foraging-wisdom, +.stat-mining-wisdom, +.color-mining-wisdom, +.stat-runecrafting-wisdom, +.color-runecrafting-wisdom, .stat-sea-creature-chance, -.color-sea-creature-chance { +.color-sea-creature-chance, +.stat-social-wisdom, +.color-social-wisdom { color: var(--§3); } .stat-magic-find, -.color-magic-find { +.color-magic-find, +.stat-fishing-speed, +.color-fishing-speed { color: var(--§b); } - .stat-pet-luck, .color-pet-luck { color: var(--§d); @@ -970,6 +995,11 @@ a.additional-player-stat:hover { color: var(--§f); } +.stat-vitality, +.color-vitality { + color: var(--§4); +} + .stat-container { margin-top: 35px; position: relative; @@ -1683,7 +1713,7 @@ a.additional-player-stat:hover { inventory-view { display: grid; place-content: center center; - --inventory-width: clamp(300px, calc(100vw - 40px), 700px); + --inventory-width: clamp(300px, calc(100vw - 40px), 780px); gap: calc(var(--inventory-width) * 0.01); padding: calc(var(--inventory-width) * 0.01); grid-template-columns: repeat(9, calc(var(--inventory-width) * 0.1)); diff --git a/public/resources/ts/calculate-player-stats.ts b/public/resources/ts/calculate-player-stats.ts index c397047dc5..daceb18623 100644 --- a/public/resources/ts/calculate-player-stats.ts +++ b/public/resources/ts/calculate-player-stats.ts @@ -1,5 +1,5 @@ import * as helper from "../../../common/helper.js"; -import { STATS_BONUS } from "../../../common/constants.js"; +import { STATS_BONUS, FORBIDDEN_STATS, HARP_QUEST, POTION_EFFECTS } from "../../../common/constants.js"; export function getPlayerStats() { const stats: PlayerStats = { @@ -22,12 +22,85 @@ export function getPlayerStats() { farming_fortune: { base: 0 }, foraging_fortune: { base: 0 }, pristine: { base: 0 }, + fishing_speed: { base: 0 }, + health_regen: { base: 100 }, + vitality: { base: 100 }, + mending: { base: 100 }, + combat_wisdom: { base: 0 }, + mining_wisdom: { base: 0 }, + farming_wisdom: { base: 0 }, + foraging_wisdom: { base: 0 }, + fishing_wisdom: { base: 0 }, + enchanting_wisdom: { base: 0 }, + alchemy_wisdom: { base: 0 }, + carpentry_wisdom: { base: 0 }, + runecrafting_wisdom: { base: 0 }, + social_wisdom: { base: 0 }, }; const allowedStats = Object.keys(stats); + // Skyblock Level + if (calculated.skyblock_level.level && calculated.skyblock_level.level > 0) { + stats.health.skyblock_level = calculated.skyblock_level.level * 5; + stats.strength.skyblock_level = + Math.floor(calculated.skyblock_level.level / 5) + Math.floor(calculated.skyblock_level.level / 10) * 5; + } + + // Bestiary + if (calculated?.bestiary?.milestone !== undefined) { + stats.strength.bestiary = calculated.bestiary.milestone * 2; + } + + // Unique Pets + if (calculated.pets.pet_score.amount !== undefined) { + stats.magic_find.pet_score = calculated.pets.pet_score.amount; + } + + // Jacob's Farming Shop + if (calculated?.farming?.perks?.double_drops !== undefined) { + stats.farming_fortune.jacob_double_drops = calculated.farming.perks.double_drops * 4; + } + + // Slayer Completion + if (calculated.slayer && calculated.slayer.slayers) { + for (const value of Object.values(calculated.slayer.slayers ?? {})) { + stats.combat_wisdom.slayer ??= 0; + for (const tier in value.kills) { + if (parseInt(tier) <= 3) { + stats.combat_wisdom.slayer += 1; + continue; + } + + stats.combat_wisdom.slayer += 2; + } + } + } + + // Heart of the Mountain + for (const ability of items.hotm) { + const item = ability as Item; + if (item.tag?.ExtraAttributes === undefined || item.tag.ExtraAttributes?.enabled === false) { + continue; + } + + const maxLevel = item.tag.ExtraAttributes?.max_level as number; + const level = item.tag.ExtraAttributes?.level as number; + const id = item.tag.ExtraAttributes?.id as string; + + const bonusStats: ItemStats = getBonusStat(level, `HOTM_perk_${id}` as BonusType, maxLevel); + for (const [name, value] of Object.entries(bonusStats)) { + if (!allowedStats.includes(name)) { + continue; + } + + stats[name].heart_of_the_mountain ??= 0; + stats[name].heart_of_the_mountain += value; + } + } + // Active armor stats - for (const piece of items.armor) { + for (const piece of items.armor.armor) { const bonusStats: ItemStats = helper.getStatsFromItem(piece as Item); for (const [name, value] of Object.entries(bonusStats)) { @@ -40,8 +113,34 @@ export function getPlayerStats() { } } + // Harp Quest + for (const harp in calculated.harp_quest || {}) { + const harpID = harp as HarpQuestSongs; + if (harpID.endsWith("_best_completion") === false) { + continue; + } + + stats.intelligence.harp ??= 0; + stats.intelligence.harp += HARP_QUEST[harpID]; + } + + // Essence Shop + for (const perk in calculated.perks ?? {}) { + if (perk in FORBIDDEN_STATS === false) { + continue; + } + + const name = perk.split("_")[1]; + if (!allowedStats.includes(name)) { + continue; + } + + stats[name].essence_shop ??= 0; + stats[name].essence_shop += calculated.perks[perk]; + } + // Active equipment stats - for (const piece of items.equipment) { + for (const piece of items.equipment.equipment) { const bonusStats: ItemStats = helper.getStatsFromItem(piece as Item); for (const [name, value] of Object.entries(bonusStats)) { @@ -56,7 +155,7 @@ export function getPlayerStats() { // Active pet stats { - const activePet = calculated.pets.find((pet) => pet.active); + const activePet = calculated.pets.pets.find((pet) => pet.active); if (activePet) { for (const [name, value] of Object.entries(activePet.stats)) { @@ -71,7 +170,7 @@ export function getPlayerStats() { } // Active accessories stats - for (const item of items.accessories.filter((item) => !(item as Item).isInactive)) { + for (const item of items.accessories.accessories.filter((item) => !(item as Item).isInactive)) { const bonusStats: ItemStats = helper.getStatsFromItem(item as Item); for (const [name, value] of Object.entries(bonusStats)) { @@ -85,7 +184,7 @@ export function getPlayerStats() { } // Skill bonus stats - for (const [skill, data] of Object.entries(calculated.levels)) { + for (const [skill, data] of Object.entries(calculated.skills.skills)) { const bonusStats: ItemStats = getBonusStat(data.level, `skill_${skill}` as BonusType, data.maxLevel); for (const [name, value] of Object.entries(bonusStats)) { @@ -117,40 +216,30 @@ export function getPlayerStats() { } // Slayer bonus stats - for (const [slayer, data] of Object.entries(calculated.slayers)) { - const bonusStats: ItemStats = getBonusStat( - data.level.currentLevel, - `slayer_${slayer}` as BonusType, - data.level.maxLevel - ); - - for (const [name, value] of Object.entries(bonusStats)) { - if (!allowedStats.includes(name)) { - continue; - } - - stats[name][`slayer_${slayer}`] ??= 0; - stats[name][`slayer_${slayer}`] += value; - } - } - - // Fairy souls - if (calculated.fairy_exchanges) { - const bonusStats: ItemStats = getFairyBonus(calculated.fairy_exchanges); + if (calculated.slayer?.slayers !== undefined) { + for (const [slayer, data] of Object.entries(calculated.slayer.slayers)) { + const bonusStats: ItemStats = getBonusStat( + data.level.currentLevel, + `slayer_${slayer}` as BonusType, + data.level.maxLevel + ); + + for (const [name, value] of Object.entries(bonusStats)) { + if (!allowedStats.includes(name)) { + continue; + } - for (const [name, value] of Object.entries(bonusStats)) { - if (!allowedStats.includes(name)) { - continue; + stats[name][`slayer_${slayer}`] ??= 0; + stats[name][`slayer_${slayer}`] += value; } - - stats[name].fairy_souls ??= 0; - stats[name].fairy_souls += value; } } // New year cake bag { - const cakeBag = items.accessory_bag.find((x) => (x as Item).tag?.ExtraAttributes?.id === "NEW_YEAR_CAKE_BAG"); + const cakeBag = items.accessories.accessories.find( + (x) => (x as Item).tag?.ExtraAttributes?.id === "NEW_YEAR_CAKE_BAG" + ); if (cakeBag && (cakeBag as Backpack).containsItems) { const totalCakes = (cakeBag as Backpack).containsItems.filter((x) => x.display_name).length; @@ -162,19 +251,40 @@ export function getPlayerStats() { } if (calculated.century_cakes) { - for (const century_cake of calculated.century_cakes) { - if (!allowedStats.includes(century_cake.stat)) { + for (const centuryCake of calculated.century_cakes) { + if (!allowedStats.includes(centuryCake.stat)) { continue; } - stats[century_cake.stat].cakes ??= 0; - stats[century_cake.stat].cakes += century_cake.amount; + stats[centuryCake.stat].cakes ??= 0; + stats[centuryCake.stat].cakes += centuryCake.amount; } } // Reaper peppers - if (calculated.reaper_peppers_eaten > 0) { - stats.health.reaper_peppers = calculated.reaper_peppers_eaten; + if (calculated?.misc?.uncategorized?.reaper_peppers_eaten?.raw !== undefined) { + stats.health.reaper_peppers = calculated.misc.uncategorized.reaper_peppers_eaten.raw as number; + } + + // Active Potion Effects + if (calculated?.misc?.effects?.active) { + for (const effect of calculated.misc.effects.active) { + if (effect.effect in POTION_EFFECTS === false) { + console.log(`Unknown potion effect: ${effect.effect}`); + continue; + } + + const effectLevel = effect.level; + const effectID = effect.effect as PotionEffectIDs; + for (const [name, value] of Object.entries(POTION_EFFECTS[effectID].bonuses)) { + if (!allowedStats.includes(name)) { + continue; + } + + stats[name].potion ??= 0; + stats[name].potion += value[effectLevel - 1]; + } + } } return stats; @@ -215,20 +325,3 @@ function getBonusStat(level: number, key: BonusType, max: number) { return bonus; } - -function getFairyBonus(fairyExchanges: number) { - const bonus: ItemStats = {}; - - bonus.speed = Math.floor(fairyExchanges / 10); - bonus.health = 0; - bonus.defense = 0; - bonus.strength = 0; - - for (let i = 0; i < fairyExchanges; i++) { - bonus.health += 3 + Math.floor(i / 2); - bonus.defense += (i + 1) % 5 == 0 ? 2 : 1; - bonus.strength += (i + 1) % 5 == 0 ? 2 : 1; - } - - return bonus; -} diff --git a/public/resources/ts/development-defer.ts b/public/resources/ts/development-defer.ts index a41ef35884..9eb767f122 100644 --- a/public/resources/ts/development-defer.ts +++ b/public/resources/ts/development-defer.ts @@ -28,15 +28,13 @@ document.addEventListener("click", (e) => { const itemId = element.getAttribute("data-item-id") as string; item = ALL_ITEMS.get(itemId) as Item; } else if (element.hasAttribute("data-pet-index")) { - item = calculated.pets[parseInt(element.getAttribute("data-pet-index") as string)]; + item = calculated.pets.pets[parseInt(element.getAttribute("data-pet-index") as string)]; } else if (element.hasAttribute("data-missing-pet-index")) { - item = calculated.missingPets[parseInt(element.getAttribute("data-missing-pet-index") as string)]; + item = calculated.pets.missing[parseInt(element.getAttribute("data-missing-pet-index") as string)]; } else if (element.hasAttribute("data-missing-accessory-index")) { - item = - calculated.missingAccessories.missing[parseInt(element.getAttribute("data-missing-accessory-index") as string)]; + item = calculated.accessories.missing[parseInt(element.getAttribute("data-missing-accessory-index") as string)]; } else if (element.hasAttribute("data-upgrade-accessory-index")) { - item = - calculated.missingAccessories.upgrades[parseInt(element.getAttribute("data-upgrade-accessory-index") as string)]; + item = calculated.accessories.upgrades[parseInt(element.getAttribute("data-upgrade-accessory-index") as string)]; } console.log(item); diff --git a/public/resources/ts/elements/inventory-view.ts b/public/resources/ts/elements/inventory-view.ts index 9dd1bffa34..7aaec02c35 100644 --- a/public/resources/ts/elements/inventory-view.ts +++ b/public/resources/ts/elements/inventory-view.ts @@ -15,6 +15,9 @@ export class InventoryView extends LitElement { preview = false; protected render(): TemplateResult[] { + // TODO: fix types for items + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore let inventory = items[this.inventoryType] ?? []; let pagesize = 5 * 9; @@ -47,9 +50,11 @@ export class InventoryView extends LitElement { pagesize = 7 * 9; } else if (this.inventoryType === "bingo_card") { pagesize = 6 * 9; + } else if (this.inventoryType === "museum") { + pagesize = 6 * 9; } - inventory.forEach((item, index) => { + inventory.forEach((item: Item, index: number) => { if (index % pagesize === 0 && index !== 0) { itemTemplateResults.push(html`
`); } diff --git a/public/resources/ts/elements/player-stat.ts b/public/resources/ts/elements/player-stat.ts index c8f28a9f5e..4ea2d75812 100644 --- a/public/resources/ts/elements/player-stat.ts +++ b/public/resources/ts/elements/player-stat.ts @@ -1,7 +1,7 @@ import { html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; import * as helper from "../../../../common/helper.js"; -import { STATS_DATA } from "../../../../common/constants.js"; +import { STATS_DATA, HIDDEN_STATS } from "../../../../common/constants.js"; @customElement("player-stat") export class PlayerStat extends LitElement { @@ -29,6 +29,10 @@ export class PlayerStat extends LitElement { const tooltip = this.getTooltip(this.data, name, suffix, value, this.special); + if (HIDDEN_STATS.includes(this.stat) && value === 0) { + return undefined; + } + return html`
diff --git a/public/resources/ts/elements/skill-component.ts b/public/resources/ts/elements/skill-component.ts index c1336c1632..fb67c143a5 100644 --- a/public/resources/ts/elements/skill-component.ts +++ b/public/resources/ts/elements/skill-component.ts @@ -61,8 +61,8 @@ export class SkillComponent extends LitElement { ${skillName} ${level.level >= 0 ? level.level : "?"}
-
- ${"runecrafting" in calculated.levels +
+ ${this.isAPIEnabled() ? html`
${this.hovering ? this.getHoverText(level, this.type) : this.getMainText(level, this.type)}
` @@ -78,7 +78,7 @@ export class SkillComponent extends LitElement { switch (this.type) { case "skill": - return calculated.levels[this.skill]; + return calculated.skills.skills[this.skill]; case "dungeon": if (this.skill === "catacombs") { @@ -88,7 +88,7 @@ export class SkillComponent extends LitElement { } case "dungeon_class": - return calculated.dungeons.classes[this.skill].experience; + return calculated.dungeons.classes.classes[this.skill].level; case "skyblock_level": return calculated.skyblock_level; @@ -105,7 +105,10 @@ export class SkillComponent extends LitElement { let mainText = formatNumber(level.xpCurrent, true); if (type === "skyblock_level") { - level.progress = this.getProgress(level, type); + level.progress = + level.level === level.maxLevel && level.maxExperience + ? level.xpCurrent / level.maxExperience + : level.xpCurrent / level.xpForNext; const skillBar = document.querySelector(`.skill-bar[data-skill="Skyblock Level"]`); if (skillBar) { @@ -117,7 +120,12 @@ export class SkillComponent extends LitElement { } if (level.xpForNext && level.xpForNext !== Infinity) { - if (level.level === level.maxLevel && level.xpCurrent === level.xpForNext) { + if (level.level === level.maxLevel && level.xpCurrent === level.maxExperience) { + return mainText; + } + + if (level.level === level.maxLevel && level.maxExperience) { + mainText += ` / ${level.maxExperience.toLocaleString()} XP`; return mainText; } @@ -134,7 +142,7 @@ export class SkillComponent extends LitElement { let hoverText = level.xpCurrent.toLocaleString(); if (type === "skyblock_level") { hoverText = `${level.level} / ${level.maxLevel} Level`; - level.progress = this.getProgress(level, type); + level.progress = level.xpCurrent / level.xpForNext; const skillBar = document.querySelector(`.skill-bar[data-skill="Skyblock Level"]`) as HTMLElement; if (skillBar) { @@ -150,15 +158,24 @@ export class SkillComponent extends LitElement { return hoverText; } - /** - * @returns the progress to maxing the skill - */ - private getProgress(level: Level, type: string): number { - if (type === "skyblock_level" && level.level === level.maxLevel && level.maxExperience) { - return level.xpCurrent / level.maxExperience; + private isAPIEnabled(): boolean { + if (this.type === "skill" && "runecrafting" in calculated.skills.skills === false) { + return false; + } + + if (this.type === "dungeon" && "catacombs" in calculated.dungeons === false) { + return false; + } + + if (this.type === "dungeon_class" && "mage" in calculated.dungeons.classes.classes === false) { + return false; + } + + if (this.type === "skyblock_level" && "skyblock_level" in calculated === false) { + return false; } - return level.level / level.maxLevel; + return true; } // disable shadow root diff --git a/public/resources/ts/globals.d.ts b/public/resources/ts/globals.d.ts index 7871a4157f..f7012ea8a6 100644 --- a/public/resources/ts/globals.d.ts +++ b/public/resources/ts/globals.d.ts @@ -54,7 +54,34 @@ interface ProcessedTheme { declare function applyProcessedTheme(processedTheme: ProcessedTheme): void; -declare const items: { [key: string]: (ItemSlot | Item | Backpack)[] }; +declare const items: { + accessories: { + accessories: (ItemSlot | Item | Backpack)[]; + }; + equipment: { + equipment: (ItemSlot | Item | Backpack)[]; + set_name: string; + set_rarity: string; + }; + armor: { + armor: (ItemSlot | Item | Backpack)[]; + set_name: string; + set_rarity: string; + }; + hotm: (ItemSlot | Item | Backpack)[]; + inventory: (ItemSlot | Item | Backpack)[]; + enderchest: (ItemSlot | Item | Backpack)[]; + accessory_bag: (ItemSlot | Item | Backpack)[]; + fishing_bag: (ItemSlot | Item | Backpack)[]; + quiver: (ItemSlot | Item | Backpack)[]; + potion_bag: (ItemSlot | Item | Backpack)[]; + personal_vault: (ItemSlot | Item | Backpack)[]; + wardrobe_inventory: (ItemSlot | Item | Backpack)[]; + candy_bag: (ItemSlot | Item | Backpack)[]; + storage: (ItemSlot | Item | Backpack)[]; + bingo_card: (ItemSlot | Item | Backpack)[]; + museum: (ItemSlot | Item | Backpack)[]; +}; type StatName = | "health" @@ -75,7 +102,21 @@ type StatName = | "mining_fortune" | "farming_fortune" | "foraging_fortune" - | "pristine"; + | "pristine" + | "fishing_speed" + | "health_regen" + | "vitality" + | "mending" + | "combat_wisdom" + | "mining_wisdom" + | "farming_wisdom" + | "foraging_wisdom" + | "fishing_wisdom" + | "enchanting_wisdom" + | "alchemy_wisdom" + | "carpentry_wisdom" + | "runecrafting_wisdom" + | "social_wisdom"; interface DisplayItem { display_name: string; @@ -104,6 +145,7 @@ interface Item extends DisplayItem, ItemSlot { tag: ItemTag; texture_pack?: Pack; isInactive?: boolean; + containsItems: Item[]; } interface ItemTag { @@ -190,11 +232,14 @@ declare const calculated: SkyCryptPlayer & { }; current_area: string; deaths: { - amount: number; - entityId: string; - entityName: string; - type: "deaths"; - }[]; + deaths: { + amount: number; + entity_id: string; + entity_name: string; + type: "deaths"; + }[]; + total: number; + }; dungeons: { boss_collections: { [key: string]: { @@ -252,9 +297,12 @@ declare const calculated: SkyCryptPlayer & { visited: boolean; }; classes: { - [key: string]: { - current: boolean; - experience: Level; + selected_class: string; + classes: { + [key: string]: { + level: Level; + current: boolean; + }; }; }; dungeonsWeight: number; @@ -343,7 +391,7 @@ declare const calculated: SkyCryptPlayer & { progress: number; total: number; }; - farming: { + farming?: { contests: { all_contests: { claimed: boolean; @@ -400,17 +448,25 @@ declare const calculated: SkyCryptPlayer & { tag: string; } | null; kills: { - amount: number; - entityId: string; - entityName: string; - type: "kills"; - }[]; + kills: { + amount: number; + entity_id: string; + entity_name: string; + type: "kills"; + }[]; + total: number; + }; last_updated: SkyCryptRelativeTime; level_caps: { [key: string]: number; }; - levels: { - [key: string]: Level; + skills: { + skills: { + [key: string]: Level; + }; + averageSkillLevel: number; + averageSkillLevelWithoutProgress: number; + totalSkillXp: number; }; members: SkyCryptPlayer[]; mining: { @@ -494,19 +550,52 @@ declare const calculated: SkyCryptPlayer & { most_winter_magma_damage_dealt: number; most_winter_snowballs_hit: number; }; + uncategorized?: { + [key: string]: { + raw?: number; + formatted?: string; + maxed?: boolean; + }; + }; + effects: { + active: { + effect: string; + level: number; + modifiers: { + key: string; + amp: number; + }[]; + ticks_remaining: number; + infinite: boolean; + }[]; + inactive: string[]; + disabled: string[]; + }; }; - missingPets: PetBase[]; - missingAccessories: { + accessories: { [key in "missing" | "upgrades"]: DisplayItem[]; }; petScore: number; pet_bonus: { [key in StatName]?: number; }; - pet_score_bonus: { - [key in StatName]?: number; + pets: { + pets: Pet[]; + missing: PetBase[]; + pet_score: { + total: number; + amount: number; + bonus: { + magic_find: number; + }; + }; + amount_pets: number; + total_pets: number; + total_pet_skins: number; + amount_pet_skins: number; + total_candy_used: number; + total_pet_xp: number; }; - pets: Pet[]; profile: Profile; profiles: { [key: string]: Profile & { @@ -524,29 +613,32 @@ declare const calculated: SkyCryptPlayer & { zombie: number; blaze: number; }; - slayer_xp: number; - slayers: { - [key in SlayerName]: { - boss_kills_tier_0?: number; - boss_kills_tier_1?: number; - boss_kills_tier_2?: number; - boss_kills_tier_3?: number; - claimed_levels: { - [key: string]: true; - }; - kills: { - [key: number]: number; - }; - level: { - currentLevel: number; - maxLevel: number; - progress: number; - weight: { weight: number; weight_overflow: number }; - xp: number; - xpForNext: number; + slayer: { + slayers?: { + [key in SlayerName]: { + boss_kills_tier_0?: number; + boss_kills_tier_1?: number; + boss_kills_tier_2?: number; + boss_kills_tier_3?: number; + claimed_levels: { + [key: string]: true; + }; + kills: { + [key: number]: number; + }; + level: { + currentLevel: number; + maxLevel: number; + progress: number; + weight: { weight: number; weight_overflow: number }; + xp: number; + xpForNext: number; + }; + xp?: number; }; - xp?: number; }; + total_slayer_xp: number; + total_coins_spent: number; }; social: { DISCORD: string; @@ -572,8 +664,20 @@ declare const calculated: SkyCryptPlayer & { stat: string; amount: number; }[]; - reaper_peppers_eaten: number; skyblock_level: Level; + bestiary?: { + categories: BestiaryCategory[]; + tiersUnlocked: number; + totalTiers: number; + milestone: number; + maxMilestone: number; + }; + harp_quest: { + [key: string]: number; + }; + perks: { + [key: string]: number; + }; }; interface SkyCryptRelativeTime { @@ -686,7 +790,13 @@ type BonusType = | "slayer_spider" | "slayer_wolf" | "slayer_enderman" - | "slayer_blaze"; + | "slayer_blaze" + | "HOTM_perk_mining_speed" + | "HOTM_perk_mining_speed_2" + | "HOTM_perk_mining_fortune" + | "HOTM_perk_mining_fortune_2" + | "HOTM_perk_mining_madness" + | "HOTM_perk_mining_experience"; type StatsBonus = { [key in BonusType]: StatBonusType; @@ -703,3 +813,70 @@ type ColorCode = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "a" interface RarityColors { [key: string]: ColorCode; } + +interface BestiaryCategory { + name: string; + texture: string; + mobs: BestiaryMob[]; + mobsUnlocked: number; + mobsMaxed: number; +} + +interface BestiaryMob { + name: string; + texture: string; + kills: number; + nextTierKills: number; + maxKills: number; + tier: number; + maxTier: number; +} + +type HarpQuestSongs = + | "song_hymn_joy_best_completion" + | "song_frere_jacques_best_completion" + | "song_amazing_grace_best_completion" + | "song_brahms_best_completion" + | "song_happy_birthday_best_completion" + | "song_greensleeves_best_completion" + | "song_jeopardy_best_completion" + | "song_minuet_best_completion" + | "song_joy_world_best_completion" + | "song_pure_imagination_best_completion" + | "song_vie_en_rose_best_completion" + | "song_fire_and_flames_best_completion" + | "song_pachelbel_best_completion"; + +type PotionEffectIDs = + | "true_defense" + | "strength" + | "regeneration" + | "enchanting_xp_boost" + | "stun" + | "experience" + | "rabbit" + | "magic_find" + | "water_breathing" + | "combat_xp_boost" + | "fire_resistance" + | "jump_boost" + | "resistance" + | "fishing_xp_boost" + | "agility" + | "archery" + | "critical" + | "speed" + | "farming_xp_boost" + | "adrenaline" + | "spelunker" + | "dodge" + | "spirit" + | "pet_luck" + | "mining_xp_boost" + | "haste" + | "burning" + | "mana" + | "foraging_xp_boost" + | "alchemy_xp_boost" + | "jerry_candy" + | "night_vision"; diff --git a/public/resources/ts/stats-defer.ts b/public/resources/ts/stats-defer.ts index f6aca5596e..73e7410ff9 100644 --- a/public/resources/ts/stats-defer.ts +++ b/public/resources/ts/stats-defer.ts @@ -113,8 +113,8 @@ tippy(".interactive-tooltip", { export const ALL_ITEMS = new Map( [ - items.armor, - items.equipment, + items.armor.armor, + items.equipment.equipment, items.inventory, items.enderchest, items.accessory_bag, @@ -127,11 +127,12 @@ export const ALL_ITEMS = new Map( items.storage, items.hotm, items.bingo_card, + items.museum, ] .flat() .flatMap((item) => { if ("containsItems" in item) { - return [item, ...item.containsItems]; + return [item, ...item.containsItems, ...item.containsItems.map((i) => i.containsItems ?? [])].flat(); } else { return item; } @@ -242,15 +243,13 @@ function fillLore(element: HTMLElement) { const itemId = element.getAttribute("data-item-id") as string; item = ALL_ITEMS.get(itemId) as Item; } else if (element.hasAttribute("data-pet-index")) { - item = calculated.pets[parseInt(element.getAttribute("data-pet-index") as string)]; + item = calculated.pets.pets[parseInt(element.getAttribute("data-pet-index") as string)]; } else if (element.hasAttribute("data-missing-pet-index")) { - item = calculated.missingPets[parseInt(element.getAttribute("data-missing-pet-index") as string)]; + item = calculated.pets.missing[parseInt(element.getAttribute("data-missing-pet-index") as string)]; } else if (element.hasAttribute("data-missing-accessory-index")) { - item = - calculated.missingAccessories.missing[parseInt(element.getAttribute("data-missing-accessory-index") as string)]; + item = calculated.accessories.missing[parseInt(element.getAttribute("data-missing-accessory-index") as string)]; } else if (element.hasAttribute("data-upgrade-accessory-index")) { - item = - calculated.missingAccessories.upgrades[parseInt(element.getAttribute("data-upgrade-accessory-index") as string)]; + item = calculated.accessories.upgrades[parseInt(element.getAttribute("data-upgrade-accessory-index") as string)]; } if (item == undefined) { @@ -744,7 +743,8 @@ if (showStats != null) { for (const element of document.querySelectorAll(".kills-deaths-container .show-all.enabled")) { const parent = element.parentElement as HTMLElement; - const kills = calculated[element.getAttribute("data-type") as "kills" | "deaths"]; + const type = element.getAttribute("data-type") as "kills" | "deaths"; + const kills = type === "kills" ? calculated.kills.kills : calculated.deaths.deaths; element.addEventListener("click", () => { parent.style.maxHeight = parent.offsetHeight + "px"; @@ -765,7 +765,7 @@ for (const element of document.querySelectorAll(".kills-deaths-container .show-a statSeparator.className = "stat-separator"; killRank.innerHTML = "#" + (index + 11) + " "; - killEntity.innerHTML = kill.entityName; + killEntity.innerHTML = kill.entity_name; killAmount.innerHTML = kill.amount.toLocaleString(); statSeparator.innerHTML = ": "; diff --git a/public/resources/ts/themes.ts b/public/resources/ts/themes.ts index 0e60d0f229..c90458ff65 100644 --- a/public/resources/ts/themes.ts +++ b/public/resources/ts/themes.ts @@ -1,4 +1,4 @@ -export const themes = new Map>(); +export const THEMES = new Map>(); /** * converts a hex color to it's rgb components @@ -33,12 +33,12 @@ async function fetchTheme(urlString: string): Promise { } export function getTheme(urlString: string): Promise { - const themeFromMap = themes.get(urlString); + const themeFromMap = THEMES.get(urlString); if (themeFromMap !== undefined) { return themeFromMap; } else { const themeFromFetch = fetchTheme(urlString); - themes.set(urlString, themeFromFetch); + THEMES.set(urlString, themeFromFetch); return themeFromFetch; } } diff --git a/src/app.js b/src/app.js index 3bfd3630f8..f3ed52a49b 100644 --- a/src/app.js +++ b/src/app.js @@ -1,6 +1,6 @@ // this file never runs on the master thread import * as lib from "./lib.js"; -import { getFileHashes, getFileHash, hashedDirectories } from "./hashes.js"; +import { getFileHashes, getFileHash, HASHED_DIRECTORIES } from "./hashes.js"; import fetch from "node-fetch"; import express from "express"; @@ -11,6 +11,7 @@ import bodyParser from "body-parser"; import "axios-debug-log"; import fs from "fs-extra"; +import axios from "axios"; import path from "path"; import * as renderer from "./renderer.js"; @@ -42,6 +43,8 @@ import * as itemRoute from "./routes/item.js"; import * as headRoute from "./routes/head.js"; import * as leatherRoute from "./routes/leather.js"; import * as potionRoute from "./routes/potion.js"; +import * as stats from "./stats.js"; +import { SkyCryptError } from "./constants/error.js"; const folderPath = helper.getFolderPath(); @@ -66,7 +69,7 @@ if (process.env.NODE_ENV == "development") { watch("public/resources/css", { recursive: true }, async (evt, name) => { const [, , directory, fileName] = name.split(/\/|\\/); - if (hashedDirectories.includes(directory)) { + if (HASHED_DIRECTORIES.includes(directory)) { fileHashes[directory][fileName] = await getFileHash(name); } }); @@ -248,7 +251,7 @@ app.all("/stats/:player/:profile?", async (req, res, next) => { const debugId = helper.generateDebugId("stats"); const timeStarted = Date.now(); - // console.debug(`${debugId}: stats page was called.`); + console.debug(`${debugId}: stats page was called.`); const paramPlayer = req.params.player .toLowerCase() @@ -268,13 +271,18 @@ app.all("/stats/:player/:profile?", async (req, res, next) => { debugId, }); - const bingoProfile = - profile.game_mode === "bingo" ? await lib.getBingoProfile(db, paramPlayer, { cacheOnly, debugId }) : {}; - const items = await lib.getItems(profile.members[profile.uuid], bingoProfile, true, req.cookies.pack, { + const museum = await lib.getMuseum(db, profile, { cacheOnly, debugId }); + for (const member in museum) { + profile.members[member].museum = museum[member]; + } + + const paramBingo = + profile.game_mode === "bingo" ? await lib.getBingoProfile(db, paramPlayer, { cacheOnly, debugId }) : null; + const items = await stats.getItems(profile.members[profile.uuid], paramBingo, true, req.cookies.pack, { cacheOnly, debugId, }); - const calculated = await lib.getStats(db, profile, bingoProfile, allProfiles, items, req.cookies.pack, { + const calculated = await lib.getStats(db, profile, paramBingo, allProfiles, items, req.cookies.pack, { cacheOnly, debugId, }); @@ -284,8 +292,8 @@ app.all("/stats/:player/:profile?", async (req, res, next) => { "http://textures.minecraft.net/texture/b4bd832813ac38e68648938d7a32f6ba29801aaf317404367f214b78b4d4754c"; } - // console.debug(`${debugId}: starting page render.`); - // const renderStart = Date.now(); + console.debug(`${debugId}: starting page render.`); + const renderStart = Date.now(); if (req.cookies.pack) { process.send({ type: "selected_pack", id: req.cookies.pack }); @@ -306,15 +314,53 @@ app.all("/stats/:player/:profile?", async (req, res, next) => { page: "stats", }, (err, html) => { - if (err) console.error(err); - // else console.debug(`${debugId}: page successfully rendered. (${Date.now() - renderStart}ms)`); + if (err) { + throw new SkyCryptError(err); + } + console.debug(`${debugId}: page successfully rendered. (${Date.now() - renderStart}ms)`); res.set("X-Debug-ID", `${debugId}`); res.set("X-Process-Time", `${Date.now() - timeStarted}`); res.send(html); } ); } catch (e) { + if (e instanceof SkyCryptError === false) { + const webhookUrl = credentials.discord_webhook; + if (webhookUrl !== undefined) { + let description = ""; + if (playerUsername) { + description += `Username: \`${playerUsername}\`\n`; + } + + if (req.params) { + description += `Options: \`${JSON.stringify(req.params)}\`\n`; + } + + if (paramProfile) { + description += `Profile: \`${paramProfile}\`\n`; + } + + description += `Link: https://sky.shiiyu.moe/stats/${paramPlayer}${paramProfile ? `/${paramProfile}` : ""}\n`; + description += `\`\`\`${e.stack}\`\`\``; + + const embed = { + title: "Error", + description: description, + color: 16711680, + fields: [], + footer: { + text: `by @duckysolucky`, + icon_url: "https://imgur.com/tgwQJTX.png", + }, + }; + + axios.post(webhookUrl, { embeds: [embed] }).catch((error) => { + console.log(error); + }); + } + } + const favorites = parseFavorites(req.cookies.favorite); console.debug(`${debugId}: an error has occurred.`); diff --git a/src/constants.js b/src/constants.js index 55fd1f2e68..2b3bc4fe58 100644 --- a/src/constants.js +++ b/src/constants.js @@ -22,3 +22,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/museum.js"; diff --git a/src/constants/accessories.js b/src/constants/accessories.js index 4c2fc99736..eaa83737cb 100644 --- a/src/constants/accessories.js +++ b/src/constants/accessories.js @@ -75,6 +75,8 @@ const accessoryUpgrades = [ ["AGARIMOO_TALISMAN", "AGARIMOO_RING", "AGARIMOO_ARTIFACT"], ["BLOOD_DONOR_TALISMAN", "BLOOD_DONOR_RING", "BLOOD_DONOR_ARTIFACT"], ["LUSH_TALISMAN", "LUSH_RING", "LUSH_ARTIFACT"], + ["ANITA_TALISMAN", "ANITA_RING", "ANITA_ARTIFACT"], + ["PESTHUNTER_BADGE", "PESTHUNTER_RING", "PESTHUNTER_ARTIFACT"], ]; const ignoredAccessories = [ @@ -103,9 +105,10 @@ const ignoredAccessories = [ "WARDING_TRINKET", "RING_OF_BROKEN_LOVE", "GARLIC_FLAVORED_GUMMY_BEAR", + "GENERAL_MEDALLION", ]; -export const accessoryAliases = { +export const ACCESSORY_ALIASES = { WEDDING_RING_0: ["WEDDING_RING_1"], WEDDING_RING_2: ["WEDDING_RING_3"], WEDDING_RING_4: ["WEDDING_RING_5", "WEDDING_RING_6"], @@ -190,7 +193,7 @@ export function getAllAccessories() { const output = items.reduce((accessory, item) => { if (ignoredAccessories.includes(item.id)) return accessory; - if (Object.values(accessoryAliases).find((list) => list.includes(item.id))) return accessory; + if (Object.values(ACCESSORY_ALIASES).find((list) => list.includes(item.id))) return accessory; accessory.push({ ...item, diff --git a/src/constants/bestiary.js b/src/constants/bestiary.js index 69f00181ac..ebb267284c 100644 --- a/src/constants/bestiary.js +++ b/src/constants/bestiary.js @@ -2138,6 +2138,82 @@ export const BESTIARY = { }, ], }, + garden: { + name: "Garden", + texture: "/head/f4880d2c1e7b86e87522e20882656f45bafd42f94932b2c5e0d6ecaa490cb4c", + mobs: [ + { + name: "Beetle", + texture: "/head/35590d5326a65d55b2bc60c5cd194c13d6125658d3d4c60ece1d9becfacea93c", + cap: 250, + mobs: ["pest_beetle_1"], + bracket: 6, + }, + { + name: "Cricket", + texture: "/head/7b50d6e6bf907fa4e3c44f465cd2c4f79124b5703a2df22fac6376b1b91703cf", + cap: 250, + mobs: ["pest_cricket_1"], + bracket: 6, + }, + { + name: "Earthworm", + texture: "/head/6403ba4027a333d8d2fd32ab59d1cfdbaa7d908d80d2381db2a69cbe65450ad8", + cap: 250, + mobs: ["pest_worm_1"], + bracket: 6, + }, + { + name: "Fly", + texture: "/head/9d90e777826a52461368e26d1b2e19bfa1ba582d602483e545f4124d0f731842", + cap: 250, + mobs: ["pest_fly_1"], + bracket: 6, + }, + { + name: "Locust", + texture: "/head/4b24a482a32db1ea78fb98060b0c2fa4a373cbd18a68edddeb7419455a59cda9", + cap: 250, + mobs: ["pest_locust_1"], + bracket: 6, + }, + { + name: "Mite", + texture: "/head/be6baf6431a9daa2ca604d5a3c26e9a761d5952f0817174a4fe0b764616e21ff", + cap: 250, + mobs: ["pest_mite_1"], + bracket: 6, + }, + { + name: "Mosquito", + texture: "/head/52a9fe05bc663efcd12e56a3ccc5ec035bf577b78708548b6f4ffcf1d30eccfe", + cap: 250, + mobs: ["pest_mosquito_1"], + bracket: 6, + }, + { + name: "Moth", + texture: "/head/65485c4b34e5b5470be94de100e61f7816f81bc5a11dfdf0eccf890172da5d0a", + cap: 250, + mobs: ["pest_moth_1"], + bracket: 6, + }, + { + name: "Rat", + texture: "/head/a8abb471db0ab78703011979dc8b40798a941f3a4dec3ec61cbeec2af8cffe8", + cap: 250, + mobs: ["pest_rat_1"], + bracket: 6, + }, + { + name: "Slug", + texture: "/head/7a79d0fd677b54530961117ef84adc206e2cc5045c1344d61d776bf8ac2fe1ba", + cap: 250, + mobs: ["pest_slug_1"], + bracket: 6, + }, + ], + }, }; export const BESTIARY_BRACKETS = { diff --git a/src/constants/bingo.js b/src/constants/bingo.js index 82c1b513ef..5b853c96c1 100644 --- a/src/constants/bingo.js +++ b/src/constants/bingo.js @@ -1,112 +1,9 @@ -import * as helper from "../helper.js"; - -function getBingoItemId(item, completed) { - if (item?.tiers !== undefined) { - const completed = item.progress >= item.tiers[0]; - - return completed ? 133 : 42; - } - - return completed ? 351 : 339; -} - -function getBingoItemDamage(item, completed) { - if (item?.tiers !== undefined) { - return 0; - } - - return completed ? 10 : 0; -} - -function formatBingOItemLore(item, bingoData, { completed = false, custom = false }) { - const output = []; - // Personal Goal - if (item.tiers === undefined && item.id !== undefined) { - if (custom === false) { - output.push("§8Personal Goal"); - output.push(""); - } - if (Array.isArray(item.lore)) { - output.push(...item.lore); - } else { - output.push(item.lore); - } - - if (custom === false) { - output.push(""); - output.push("§7Reward"); - output.push(`§61 Bingo Point`); - output.push(""); - if (item.requiredAmount) { - output.push(`§7Progress:`); - output.push( - `§a${completed ? item.requiredAmount.toLocaleString() : "NaN"} §7/ §6${item.requiredAmount.toLocaleString()}` - ); - output.push(""); - } - - if (completed) { - output.push("§aGOAL REACHED"); - } else { - output.push("§cYou have not reached this goal!"); - } - } - } else { - // Community Goal - output.push("§8Community Goal"); - output.push(""); - const total = item.progress; - const nextTierAmount = item.tiers.find((tier) => tier > item.progress) || item.tiers[item.tiers.length - 1]; - const nextTier = item.tiers.indexOf(item.tiers.find((tier) => tier > item.progress)) || item.tiers.length; - const percentage = (total / nextTierAmount) * 100; - output.push( - `§7Progress to ${item.name} ${helper.romanize(nextTier)}: §e${ - percentage > 100 ? 100 : percentage.toLocaleString() - }§6%` - ); - - let progress = ""; - for (let i = 0; i < 20; i++) { - if (i < (total / nextTierAmount) * 20) { - progress += "§a§l§m⎯"; - } else { - progress += "§7§l§m⎯"; - } - } - progress += `§r §e ${total.toLocaleString()} §6/ §e${helper.formatNumber(nextTierAmount)}`; - - output.push(progress); - - const index = bingoData.filter((goal) => goal.tiers !== undefined).indexOf(item); - - output.push(""); - output.push("§7Contribution Rewards"); - output.push(...bingoCommunityRewards[index].description); - output.push(""); - output.push( - "§7§oCommunity Goals are", - "§7§ocollaborative - anyone with a", - "§7§oBingo profile can help to reach", - "§7§othe goal!", - "", - "§7§oThe more you contribute", - "§7§otowards the goal, the more you", - "§7§owill be rewarded!" - ); - - if (percentage >= 100) { - output.push(""); - output.push("§aGOAL REACHED"); - } - } - - return output; -} - -const bingoCardSlots = [ +export const BINGO_CARD_SLOTS = [ { - slots: [1, 10, 19, 28, 37], - name: "Row #", + positions: [1, 10, 19, 28, 37], + display_name: "Row #", + id: 160, + Damage: 0, lore: [ "§7Completed all of the goals in the", "§7row to the right to earn a", @@ -115,13 +12,13 @@ const bingoCardSlots = [ "§7Bonus Reward", "§65 Bingo Points", ], - id: 160, - damage: 0, rarity: "legendary", }, { - slots: [7, 16, 25, 34, 43], - name: "Row #", + positions: [7, 16, 25, 34, 43], + display_name: "Row #", + id: 160, + Damage: 0, lore: [ "§7Completed all of the goals in the", "§7row to the left to earn a", @@ -130,13 +27,13 @@ const bingoCardSlots = [ "§7Bonus Reward", "§65 Bingo Points", ], - id: 160, - damage: 0, rarity: "legendary", }, { - slots: [46], - name: "Diagonal", + positions: [46], + display_name: "Diagonal", + id: 160, + Damage: 0, lore: [ "§7Completed all of the goals in the", "§7diagonal from bottom-left to", @@ -146,13 +43,13 @@ const bingoCardSlots = [ "§7Bonus Reward", "§610 Bingo Points", ], - id: 160, - damage: 0, rarity: "legendary", }, { - slots: [52], - name: "Community Diagonal", + positions: [52], + display_name: "Community Diagonal", + id: 160, + Damage: 0, lore: [ "§7Reach §aTier I §7for all of the", "§6Community Goals §7to earn a", @@ -161,13 +58,13 @@ const bingoCardSlots = [ "§7Bonus Reward", "§65 Bingo Points", ], - id: 160, - damage: 0, rarity: "legendary", }, { - slots: [47, 48, 49, 50, 51], - name: "Column #", + positions: [47, 48, 49, 50, 51], + display_name: "Column #", + id: 160, + Damage: 0, lore: [ "§7Completed all of the goals in the", "§7column above to earn a bonus", @@ -176,12 +73,12 @@ const bingoCardSlots = [ "§7Bonus Reward", "§65 Bingo Points", ], - id: 160, - damage: 0, rarity: "legendary", }, + /* + TODO: implement once data get's added to the API { - slots: [44], + positions: [44], name: "Item Transfer", lore: [ "§7Transfer up to §a10 items §7from", @@ -191,11 +88,10 @@ const bingoCardSlots = [ "§7Items Transferred: §aNaN / 10", ], id: 54, - damage: 0, rarity: "uncommon", }, { - slots: [53], + positions: [53], name: "Bingo Shop", lore: [ "§7Spend §6Bingo Points §7on", @@ -212,12 +108,12 @@ const bingoCardSlots = [ "§7Bingo Rank: §cUnknown", ], id: 388, - damage: 0, rarity: "divine", }, + */ ]; -const bingoCommunityRewards = [ +export const BINGO_COMMUNITY_REWARDS = [ { description: [ "§fTop §e1% §8- §69 Bingo Points", @@ -264,60 +160,3 @@ const bingoCommunityRewards = [ ], }, ]; - -const bingoPositions = [3, 4, 5, 6, 7, 12, 13, 14, 15, 16, 21, 22, 23, 24, 25, 30, 31, 32, 33, 34, 39, 40, 41, 42, 43]; - -export function getBingoItems(userProfile, bingoData) { - const bingoGoals = userProfile.completed_goals; - const output = []; - - // NOTE: Not sure why but without this it doesn't work - for (let i = 0; i < 6 * 9; i++) { - output[i] = helper.generateItem({ - id: undefined, - }); - } - - for (const itemData of bingoCardSlots) { - for (const slot of itemData.slots) { - const name = - itemData.name.endsWith("#") === true ? `${itemData.name}${itemData.slots.indexOf(slot) + 1}` : itemData.name; - - output[slot] = helper.generateItem({ - display_name: name, - id: itemData.id, - Damage: itemData.damage, - rarity: itemData.rarity || "common", - tag: { - display: { - Name: name, - Lore: formatBingOItemLore(itemData, bingoData, { completed: false, custom: true }), - }, - }, - position: slot, - }); - } - } - - for (const item of bingoData) { - const position = bingoPositions[bingoData.indexOf(item)] - 1; - const completed = item?.tiers ? item.progress >= item.tiers[0] : bingoGoals.includes(item.id) || false; - - output[position] = helper.generateItem({ - display_name: item.name, - id: getBingoItemId(item, completed), - Damage: getBingoItemDamage(item, completed), - rarity: "uncommon", - tag: { - display: { - Name: item.name, - Lore: formatBingOItemLore(item, bingoData, { completed: completed, custom: false }), - }, - }, - position: position, - completed: completed, - }); - } - - return output; -} diff --git a/src/constants/collections.js b/src/constants/collections.js index cfe5dea15f..b015c73c0f 100644 --- a/src/constants/collections.js +++ b/src/constants/collections.js @@ -1,540 +1,230 @@ -export const COLLECTION_TYPES = ["farming", "mining", "combat", "foraging", "fishing"]; - -export const COLLECTION_DATA = [ - { - type: "farming", - skyblockId: "WHEAT", - name: "Wheat", - id: 296, - damage: 0, - maxTier: 9, - }, - { - type: "farming", - skyblockId: "CARROT_ITEM", - name: "Carrot", - id: 391, - damage: 0, - maxTier: 9, - }, - { - type: "farming", - skyblockId: "POTATO_ITEM", - name: "Potato", - id: 392, - damage: 0, - maxTier: 9, - }, - { - type: "farming", - skyblockId: "PUMPKIN", - name: "Pumpkin", - id: 86, - damage: 0, - maxTier: 9, - }, - { - type: "farming", - skyblockId: "MELON", - name: "Melon", - id: 360, - damage: 0, - maxTier: 9, - }, - { - type: "farming", - skyblockId: "SEEDS", - name: "Seeds", - id: 295, - damage: 0, - maxTier: 6, - }, - { - type: "farming", - skyblockId: "MUSHROOM_COLLECTION", - name: "Mushroom", - id: 40, - damage: 0, - maxTier: 9, - }, - { - type: "farming", - skyblockId: "INK_SACK:3", - name: "Cocoa Beans", - id: 351, - damage: 3, - maxTier: 9, - }, - { - type: "farming", - skyblockId: "CACTUS", - name: "Cactus", - id: 81, - damage: 0, - maxTier: 9, - }, - { - type: "farming", - skyblockId: "SUGAR_CANE", - name: "Sugar Cane", - id: 338, - damage: 0, - maxTier: 9, - }, - { - type: "farming", - skyblockId: "FEATHER", - name: "Feather", - id: 288, - damage: 0, - maxTier: 9, - }, - { - type: "farming", - skyblockId: "LEATHER", - name: "Leather", - id: 334, - damage: 0, - maxTier: 10, - }, - { - type: "farming", - skyblockId: "PORK", - name: "Raw Porkchop", - id: 319, - damage: 0, - maxTier: 9, - }, - { - type: "farming", - skyblockId: "RAW_CHICKEN", - name: "Raw Chicken", - id: 365, - damage: 0, - maxTier: 9, - }, - { - type: "farming", - skyblockId: "MUTTON", - name: "Mutton", - id: 423, - damage: 0, - maxTier: 9, - }, - { - type: "farming", - skyblockId: "RABBIT", - name: "Raw Rabbit", - id: 411, - damage: 0, - maxTier: 9, - }, - { - type: "farming", - skyblockId: "NETHER_STALK", - name: "Nether Wart", - id: 372, - damage: 0, - maxTier: 9, - }, - - { - type: "mining", - skyblockId: "COBBLESTONE", - name: "Cobblestone", - id: 4, - damage: 0, - maxTier: 9, - }, - { - type: "mining", - skyblockId: "COAL", - name: "Coal", - id: 263, - damage: 0, - maxTier: 9, - }, - { - type: "mining", - skyblockId: "IRON_INGOT", - name: "Iron Ingot", - id: 265, - damage: 0, - maxTier: 9, - }, - { - type: "mining", - skyblockId: "GOLD_INGOT", - name: "Gold Ingot", - id: 266, - damage: 0, - maxTier: 9, - }, - { - type: "mining", - skyblockId: "DIAMOND", - name: "Diamond", - id: 264, - damage: 0, - maxTier: 9, - }, - { - type: "mining", - skyblockId: "INK_SACK:4", - name: "Lapis Lazuli", - id: 351, - damage: 4, - maxTier: 9, - }, - { - type: "mining", - skyblockId: "EMERALD", - name: "Emerald", - id: 388, - damage: 0, - maxTier: 9, - }, - { - type: "mining", - skyblockId: "REDSTONE", - name: "Redstone", - id: 331, - damage: 0, - maxTier: 11, - }, - { - type: "mining", - skyblockId: "QUARTZ", - name: "Nether Quartz", - id: 406, - damage: 0, - maxTier: 9, - }, - { - type: "mining", - skyblockId: "OBSIDIAN", - name: "Obsidian", - id: 49, - damage: 0, - maxTier: 9, - }, - { - type: "mining", - skyblockId: "SAND:1", - name: "Red Sand", - id: 12, - damage: 1, - maxTier: 8, - }, - { - type: "mining", - skyblockId: "GLOWSTONE_DUST", - name: "Glowstone", - id: 348, - damage: 0, - maxTier: 9, - }, - { - type: "mining", - skyblockId: "GRAVEL", - name: "Gravel", - id: 13, - damage: 0, - maxTier: 9, - }, - { - type: "mining", - skyblockId: "ICE", - name: "Ice", - id: 79, - damage: 0, - maxTier: 10, - }, - { - type: "mining", - skyblockId: "NETHERRACK", - name: "Netherrack", - id: 87, - damage: 0, - maxTier: 3, - }, - { - type: "mining", - skyblockId: "SAND", - name: "Sand", - id: 12, - damage: 0, - maxTier: 7, - }, - { - type: "mining", - skyblockId: "ENDER_STONE", - name: "End Stone", - id: 121, - damage: 0, - maxTier: 9, - }, - { - type: "mining", - skyblockId: "MITHRIL_ORE", - name: "Mithril", - id: 138, - damage: 0, - maxTier: 9, - }, - { - type: "mining", - skyblockId: "MYCEL", - name: "Mycelium", - id: 110, - damage: 0, - maxTier: 10, - }, - { - type: "mining", - skyblockId: "HARD_STONE", - name: "Hard Stone", - id: 1, - damage: 0, - maxTier: 7, - }, - { - type: "mining", - skyblockId: "GEMSTONE_COLLECTION", - name: "Gemstone", - texture: "aac15f6fcf2ce963ef4ca71f1a8685adb97eb769e1d11194cbbd2e964a88978c", - maxTier: 11, - }, - { - type: "mining", - skyblockId: "SULPHUR_ORE", - name: "Sulphur", - id: 348, - damage: 0, - maxTier: 9, - }, - { - type: "combat", - skyblockId: "ROTTEN_FLESH", - name: "Rotten Flesh", - id: 367, - damage: 0, - maxTier: 9, - }, - { - type: "combat", - skyblockId: "BONE", - name: "Bone", - id: 352, - damage: 0, - maxTier: 9, - }, - { - type: "combat", - skyblockId: "STRING", - name: "String", - id: 287, - damage: 0, - maxTier: 9, - }, - { - type: "combat", - skyblockId: "SPIDER_EYE", - name: "Spider Eye", - id: 375, - damage: 0, - maxTier: 9, - }, - { - type: "combat", - skyblockId: "SULPHUR", - name: "Gunpowder", - id: 289, - damage: 0, - maxTier: 9, - }, - { - type: "combat", - skyblockId: "ENDER_PEARL", - name: "Ender Pearl", - id: 368, - damage: 0, - maxTier: 9, - }, - { - type: "combat", - skyblockId: "GHAST_TEAR", - name: "Ghast Tear", - id: 370, - damage: 0, - maxTier: 9, - }, - { - type: "combat", - skyblockId: "SLIME_BALL", - name: "Slimeball", - id: 341, - damage: 0, - maxTier: 9, - }, - { - type: "combat", - skyblockId: "BLAZE_ROD", - name: "Blaze Rod", - id: 369, - damage: 0, - maxTier: 9, - }, - { - type: "combat", - skyblockId: "MAGMA_CREAM", - name: "Magma Cream", - id: 378, - damage: 0, - maxTier: 9, - }, - { - type: "combat", - skyblockId: "CHILI_PEPPER", - name: "Chili Pepper", - texture: "f859c8df1109c08a756275f1d2887c2748049fe33877769a7b415d56eda469d8", - maxTier: 9, - }, - - { - type: "foraging", - skyblockId: "LOG", - name: "Oak Wood", - id: 17, - damage: 0, - maxTier: 9, - }, - { - type: "foraging", - skyblockId: "LOG:1", - name: "Spruce Wood", - id: 17, - damage: 1, - maxTier: 9, - }, - { - type: "foraging", - skyblockId: "LOG:2", - name: "Birch Wood", - id: 17, - damage: 2, - maxTier: 9, - }, - { - type: "foraging", - skyblockId: "LOG_2:1", - name: "Dark Oak Wood", - id: 162, - damage: 1, - maxTier: 9, - }, - { - type: "foraging", - skyblockId: "LOG_2", - name: "Acacia Wood", - id: 162, - damage: 0, - maxTier: 9, - }, - { - type: "foraging", - skyblockId: "LOG:3", - name: "Jungle Wood", - id: 17, - damage: 3, - maxTier: 9, - }, - - { - type: "fishing", - skyblockId: "RAW_FISH", - name: "Raw Fish", - id: 349, - damage: 0, - maxTier: 9, - }, - { - type: "fishing", - skyblockId: "RAW_FISH:1", - name: "Raw Salmon", - id: 349, - damage: 1, - maxTier: 9, - }, - { - type: "fishing", - skyblockId: "RAW_FISH:2", - name: "Clownfish", - id: 349, - damage: 2, - maxTier: 3, - }, - { - type: "fishing", - skyblockId: "RAW_FISH:3", - name: "Pufferfish", - id: 349, - damage: 3, - maxTier: 6, - }, - { - type: "fishing", - skyblockId: "PRISMARINE_SHARD", - name: "Prismarine Shard", - id: 409, - damage: 0, - maxTier: 5, - }, - { - type: "fishing", - skyblockId: "PRISMARINE_CRYSTALS", - name: "Prismarine Crystals", - id: 410, - damage: 0, - maxTier: 7, - }, - { - type: "fishing", - skyblockId: "CLAY_BALL", - name: "Clay", - id: 337, - damage: 0, - maxTier: 5, - }, - { - type: "fishing", - skyblockId: "WATER_LILY", - name: "Lily Pad", - id: 111, - damage: 0, - maxTier: 9, - }, - { - type: "fishing", - skyblockId: "INK_SACK", - name: "Ink Sac", - id: 351, - damage: 0, - maxTier: 9, - }, - { - type: "fishing", - skyblockId: "SPONGE", - name: "Sponge", - id: 19, - damage: 0, - maxTier: 9, - }, - { - type: "fishing", - skyblockId: "MAGMA_FISH", - name: "Magmafish", - texture: "f56b5955b295522c9689481960c01a992ca1c7754cf4ee313c8dd0c356d335f", - maxTier: 12, +export const BOSS_COLLECTIONS = [ + { + name: "Bonzo", + texture: "/head//12716ecbf5b8da00b05f316ec6af61e8bd02805b21eb8e440151468dc656549c", + rewards: [ + { + name: "Red Nose", + required: 25, + }, + { + name: "Bonzo's Mask", + required: 50, + }, + { + name: "Golden Bonzo Head", + required: 100, + }, + { + name: "Bonzo's Staff", + required: 150, + }, + { + name: "Recombobulator 3000", + required: 250, + }, + { + name: "Diamond Bonzo Head", + required: 1000, + }, + ], + }, + { + name: "Scarf", + texture: "/head/7de7bbbdf22bfe17980d4e20687e386f11d59ee1db6f8b4762391b79a5ac532d", + rewards: [ + { + name: "Red Scarf", + required: 25, + }, + { + name: "Scarf's Thesis", + required: 50, + }, + { + name: "Golden Scarf Head", + required: 100, + tier: 3, + }, + { + name: "Adaptive Blade", + required: 150, + }, + { + name: "Recombobulator 3000", + required: 250, + }, + { + name: "Diamond Scarf Head", + required: 1000, + }, + ], + }, + { + name: "Professor", + texture: "/head/9971cee8b833a62fc2a612f3503437fdf93cad692d216b8cf90bbb0538c47dd8", + rewards: [ + { + name: "Suspicious Vial", + required: 25, + }, + { + name: "Adaptive Leggings", + required: 50, + }, + { + name: "Golden Professor Head", + required: 100, + tier: 3, + }, + { + name: "Adaptive Chestplate", + required: 150, + }, + { + name: "Recombobulator 3000", + required: 250, + }, + { + name: "Diamond Professor Head", + required: 1000, + }, + ], + }, + { + name: "Thorn", + texture: "/head/8b6a72138d69fbbd2fea3fa251cabd87152e4f1c97e5f986bf685571db3cc0", + rewards: [ + { + name: "Spirit Stone", + required: 50, + }, + { + name: "Golden Thorn Head", + required: 100, + }, + { + name: "Spirit Bow", + required: 150, + tier: 3, + }, + { + name: "Recombobulator 3000", + required: 250, + }, + { + name: "Spirit Boots", + required: 400, + }, + { + name: "Diamond Thorn Head", + required: 1000, + }, + ], + }, + { + name: "Livid", + texture: "/head/c1007c5b7114abec734206d4fc613da4f3a0e99f71ff949cedadc99079135a0b", + rewards: [ + { + name: "Dark Orb", + required: 50, + }, + { + name: "Golden Livid Head", + required: 100, + }, + { + name: "Livid Dagger", + required: 150, + tier: 3, + }, + { + name: "Recombobulator 3000", + required: 250, + }, + { + name: "Last Breath", + required: 500, + }, + { + name: "Shadow Assasin Chestplate", + required: 750, + }, + { + name: "Diamond Livid Head", + required: 1000, + }, + ], + }, + { + name: "Sadan", + texture: "/head/fa06cb0c471c1c9bc169af270cd466ea701946776056e472ecdaeb49f0f4a4dc", + rewards: [ + { + name: "Giant Tooth", + required: 50, + }, + { + name: "Golden Sadan Head", + required: 100, + }, + { + name: "Necromancer Lord Helmet", + required: 150, + tier: 3, + }, + { + name: "Recombobulator 3000", + required: 250, + }, + { + name: "Necromancer Lord Chestplate", + required: 500, + }, + { + name: "Necromancer Sword", + required: 750, + }, + { + name: "Diamond Sadan Head", + required: 1000, + }, + ], + }, + { + name: "Necron", + texture: "/head/a435164c05cea299a3f016bbbed05706ebb720dac912ce4351c2296626aecd9a", + rewards: [ + { + name: "Wither Blood", + required: 50, + }, + { + name: "Golden Necron Head", + required: 100, + }, + { + name: "Wither Helmet", + required: 150, + tier: 3, + }, + { + name: "Recombobulator 3000", + required: 250, + }, + { + name: "Wither Leggings", + required: 500, + }, + { + name: "Wither Chestplate", + required: 750, + }, + { + name: "Diamond Necron Head", + required: 1000, + }, + ], }, ]; diff --git a/src/constants/dungeons.js b/src/constants/dungeons.js index 464de09af6..5dcc849ed7 100644 --- a/src/constants/dungeons.js +++ b/src/constants/dungeons.js @@ -43,275 +43,6 @@ export const DUNGEONS = { collection: "catacombs_7", }, }, - boss_collections: { - catacombs_1: { - boss: "bonzo", - max_tiers: 6, - rewards: { - red_nose: { - name: "Red Nose", - required: 25, - tier: 1, - }, - bonzo_mask: { - name: "Bonzo's Mask", - required: 50, - tier: 2, - }, - gold_bonzo_head: { - name: "Golden Bonzo Head", - required: 100, - tier: 3, - }, - bonzo_staff: { - name: "Bonzo's Staff", - required: 150, - tier: 4, - }, - recombobulator: { - name: "Recombobulator 3000", - required: 250, - tier: 5, - }, - diamond_bonzo_head: { - name: "Diamond Bonzo Head", - required: 1000, - tier: 6, - }, - }, - }, - catacombs_2: { - boss: "scarf", - max_tiers: 6, - rewards: { - red_scarf: { - name: "Red Scarf", - required: 25, - tier: 1, - }, - scarf_thesis: { - name: "Scarf's Thesis", - required: 50, - tier: 2, - }, - gold_scarf_head: { - name: "Golden Scarf Head", - required: 100, - tier: 3, - }, - stone_blade: { - name: "Adaptive Blade", - required: 150, - tier: 4, - }, - recombobulator: { - name: "Recombobulator 3000", - required: 250, - tier: 5, - }, - diamond_scarf_head: { - name: "Diamond Scarf Head", - required: 1000, - tier: 6, - }, - }, - }, - catacombs_3: { - boss: "professor", - max_tiers: 6, - rewards: { - suspicious_vial: { - name: "Suspicious Vial", - required: 25, - tier: 1, - }, - adaptive_leggings: { - name: "Adaptive Leggings", - required: 50, - tier: 2, - }, - gold_professor_head: { - name: "Golden Professor Head", - required: 100, - tier: 3, - }, - adaptive_chestplate: { - name: "Adaptive Chestplate", - required: 150, - tier: 4, - }, - recombobulator: { - name: "Recombobulator 3000", - required: 250, - tier: 5, - }, - diamond_professor_head: { - name: "Diamond Professor Head", - required: 1000, - tier: 6, - }, - }, - }, - catacombs_4: { - boss: "thorn", - max_tiers: 6, - rewards: { - spirit_decoy: { - name: "Spirit Stone", - required: 50, - tier: 1, - }, - gold_thorn_head: { - name: "Golden Thorn Head", - required: 100, - tier: 2, - }, - spirit_bow: { - name: "Spirit Bow", - required: 150, - tier: 3, - }, - recombobulator: { - name: "Recombobulator 3000", - required: 250, - tier: 4, - }, - spirit_boots: { - name: "Spirit Boots", - required: 400, - tier: 5, - }, - diamond_thorn_head: { - name: "Diamond Thorn Head", - required: 1000, - tier: 6, - }, - }, - }, - catacombs_5: { - boss: "livid", - max_tiers: 6, - rewards: { - dark_orb: { - name: "Dark Orb", - required: 50, - tier: 1, - }, - gold_livid_head: { - name: "Golden Livid Head", - required: 100, - tier: 2, - }, - livid_dagger: { - name: "Livid Dagger", - required: 150, - tier: 3, - }, - recombobulator: { - name: "Recombobulator 3000", - required: 250, - tier: 4, - }, - last_breath: { - name: "Last Breath", - required: 500, - tier: 5, - }, - shadow_chestplate: { - name: "Shadow Assasin Chestplate", - required: 750, - tier: 6, - }, - diamond_livid_head: { - name: "Diamond Livid Head", - required: 1000, - tier: 7, - }, - }, - }, - catacombs_6: { - boss: "sadan", - max_tiers: 7, - rewards: { - giant_tooth: { - name: "Giant Tooth", - required: 50, - tier: 1, - }, - gold_sadan_head: { - name: "Golden Sadan Head", - required: 100, - tier: 2, - }, - necromancer_helmet: { - name: "Necromancer Lord Helmet", - required: 150, - tier: 3, - }, - recombobulator: { - name: "Recombobulator 3000", - required: 250, - tier: 4, - }, - necromancer_chestplate: { - name: "Necromancer Lord Chestplate", - required: 500, - tier: 5, - }, - necromancer_sword: { - name: "Necromancer Sword", - required: 750, - tier: 6, - }, - diamond_sadan_head: { - name: "Diamond Sadan Head", - required: 1000, - tier: 7, - }, - }, - }, - catacombs_7: { - boss: "necron", - max_tiers: 7, - rewards: { - wither_blood: { - name: "Wither Blood", - required: 50, - tier: 1, - }, - gold_necron_head: { - name: "Golden Necron Head", - required: 100, - tier: 2, - }, - wither_helmet: { - name: "Wither Helmet", - required: 150, - tier: 3, - }, - recombobulator: { - name: "Recombobulator 3000", - required: 250, - tier: 4, - }, - wither_leggings: { - name: "Wither Leggings", - required: 500, - tier: 5, - }, - wither_chestplate: { - name: "Wither Chestplate", - required: 750, - tier: 6, - }, - diamond_necron_head: { - name: "Diamond Necron Head", - required: 1000, - tier: 7, - }, - }, - }, - }, floors: { catacombs_0: { name: "entrance", diff --git a/src/constants/error.js b/src/constants/error.js new file mode 100644 index 0000000000..1e7c2b08c6 --- /dev/null +++ b/src/constants/error.js @@ -0,0 +1,11 @@ +export class SkyCryptError extends Error { + constructor(message, source) { + super(message); + + this.name = "SkyCryptError"; + + if (source) { + this.source = source; + } + } +} diff --git a/src/constants/minions.js b/src/constants/minions.js index ccef4c267f..ba3fb651ba 100644 --- a/src/constants/minions.js +++ b/src/constants/minions.js @@ -30,289 +30,238 @@ export const MINION_SLOTS = { export const MINIONS_MAX_SLOTS = 26; export const MINIONS = { - COBBLESTONE: { - type: "mining", - head: "/head/2f93289a82bd2a06cbbe61b733cfdc1f1bd93c4340f7a90abd9bdda774109071", - tiers: 12, - }, - OBSIDIAN: { - type: "mining", - head: "/head/320c29ab966637cb9aecc34ee76d5a0130461e0c4fdb08cdaf80939fa1209102", - tiers: 12, - }, - GLOWSTONE: { - type: "mining", - head: "/head/20f4d7c26b0310990a7d3a3b45948b95dd4ab407a16a4b6d3b7cb4fba031aeed", - tiers: 12, - }, - GRAVEL: { - type: "mining", - head: "/head/7458507ed31cf9a38986ac8795173c609637f03da653f30483a721d3fbe602d", - }, - SAND: { - type: "mining", - head: "/head/81f8e2ad021eefd1217e650e848b57622144d2bf8a39fbd50dab937a7eac10de", - }, - CLAY: { - type: "fishing", - head: "/head/af9b312c8f53da289060e6452855072e07971458abbf338ddec351e16c171ff8", - }, - ICE: { - type: "mining", - head: "/head/e500064321b12972f8e5750793ec1c823da4627535e9d12feaee78394b86dabe", - tiers: 12, - }, - SNOW: { - type: "mining", - head: "/head/f6d180684c3521c9fc89478ba4405ae9ce497da8124fa0da5a0126431c4b78c3", - tiers: 12, - }, - COAL: { - type: "mining", - head: "/head/425b8d2ea965c780652d29c26b1572686fd74f6fe6403b5a3800959feb2ad935", - tiers: 12, - }, - IRON: { - type: "mining", - head: "/head/af435022cb3809a68db0fccfa8993fc1954dc697a7181494905b03fdda035e4a", - tiers: 12, - }, - GOLD: { - type: "mining", - head: "/head/f6da04ed8c810be29bba53c62e712d65cfb25238117b94d7e85a4615775bf14f", - tiers: 12, - }, - DIAMOND: { - type: "mining", - head: "/head/2354bbe604dfe58bf92e7729730d0c8e37844e831ee3816d7e8427c27a1824a2", - tiers: 12, - }, - LAPIS: { - type: "mining", - head: "/head/64fd97b9346c1208c1db3957530cdfc5789e3e65943786b0071cf2b2904a6b5c", - tiers: 12, - }, - REDSTONE: { - type: "mining", - head: "/head/1edefcf1a89d687a0a4ecf1589977af1e520fc673c48a0434be426612e8faa67", - tiers: 12, - }, - EMERALD: { - type: "mining", - head: "/head/9bf57f3401b130c6b53808f2b1e119cc7b984622dac7077bbd53454e1f65bbf0", - tiers: 12, - }, - MITHRIL: { - type: "mining", - head: "/head/c62fa670ff8599b32ab344195ba15f3ef64c3a8aa8a37821c08375950cb74cd0", - tiers: 12, - }, - QUARTZ: { - type: "mining", - head: "/head/d270093be62dfd3019f908043db570b5dfd366fd5345fccf9da340e75c701a60", - tiers: 12, - }, - ENDER_STONE: { - name: "End Stone", - type: "mining", - head: "/head/7994be3dcfbb4ed0a5a7495b7335af1a3ced0b5888b5007286a790767c3b57e6", - }, - WHEAT: { - type: "farming", - tiers: 12, - head: "/head/bbc571c5527336352e2fee2b40a9edfa2e809f64230779aa01253c6aa535881b", - }, - MELON: { - type: "farming", - tiers: 12, - head: "/head/95d54539ac8d3fba9696c91f4dcc7f15c320ab86029d5c92f12359abd4df811e", - }, - PUMPKIN: { - type: "farming", - tiers: 12, - head: "/head/f3fb663e843a7da787e290f23c8af2f97f7b6f572fa59a0d4d02186db6eaabb7", - }, - CARROT: { - type: "farming", - tiers: 12, - head: "/head/4baea990b45d330998cb0c1f8515c27b24f93bff1df0db056e647f8200d03b9d", - }, - POTATO: { - type: "farming", - tiers: 12, - head: "/head/7dda35a044cb0374b516015d991a0f65bf7d0fb6566e350496642cf2059ff1d9", - }, - MUSHROOM: { - type: "farming", - tiers: 12, - head: "/head/4a3b58341d196a9841ef1526b367209cbc9f96767c24f5f587cf413d42b74a93", - }, - CACTUS: { - type: "farming", - tiers: 12, - head: "/head/ef93ec6e67a6cd272c9a9684b67df62584cb084a265eee3cde141d20e70d7d72", - }, - COCOA: { - type: "farming", - tiers: 12, - head: "/head/acb680e96f6177cd8ffaf27e9625d8b544d720afc50738801818d0e745c0e5f7", - }, - SUGAR_CANE: { - type: "farming", - tiers: 12, - head: "/head/2fced0e80f0d7a5d1f45a1a7217e6a99ea9720156c63f6efc84916d4837fabde", - }, - NETHER_WARTS: { - name: "Nether Wart", - type: "farming", - tiers: 12, - head: "/head/71a4620bb3459c1c2fa74b210b1c07b4a02254351f75173e643a0e009a63f558", - }, - FLOWER: { - type: "foraging", - head: "/head/baa7c59b2f792d8d091aecacf47a19f8ab93f3fd3c48f6930b1c2baeb09e0f9b", - tiers: 12, - }, - FISHING: { - type: "fishing", - head: "/head/53ea0fd89524db3d7a3544904933830b4fc8899ef60c113d948bb3c4fe7aabb1", - }, - ZOMBIE: { - type: "combat", - head: "/head/196063a884d3901c41f35b69a8c9f401c61ac9f6330f964f80c35352c3e8bfb0", - }, - REVENANT: { - type: "combat", - head: "/head/a3dce8555923558d8d74c2a2b261b2b2d630559db54ef97ed3f9c30e9a20aba", - tiers: 12, - }, - SKELETON: { - type: "combat", - head: "/head/2fe009c5cfa44c05c88e5df070ae2533bd682a728e0b33bfc93fd92a6e5f3f64", - }, - CREEPER: { - type: "combat", - head: "/head/54a92c2f8c1b3774e80492200d0b2218d7b019314a73c9cb5b9f04cfcacec471", - }, - SPIDER: { - type: "combat", - head: "/head/e77c4c284e10dea038f004d7eb43ac493de69f348d46b5c1f8ef8154ec2afdd0", - }, - TARANTULA: { - type: "combat", - head: "/head/97e86007064c9ce26eb4bad8ac9aa30aac309e70a9e0b615936318dea40a721", - }, - CAVESPIDER: { - name: "Cave Spider", - type: "combat", - head: "/head/5d815df973bcd01ee8dfdb3bd74f0b7cb8fef2a70559e4faa5905127bbb4a435", - }, - BLAZE: { - type: "combat", - head: "/head/3208fbd64e97c6e00853d36b3a201e4803cae43dcbd6936a3cece050912e1f20", - tiers: 12, - }, - MAGMA_CUBE: { - type: "combat", - head: "/head/18c9a7a24da7e3182e4f62fa62762e21e1680962197c7424144ae1d2c42174f7", - tiers: 12, - }, - ENDERMAN: { - type: "combat", - head: "/head/e460d20ba1e9cd1d4cfd6d5fb0179ff41597ac6d2461bd7ccdb58b20291ec46e", - }, - GHAST: { - type: "combat", - head: "/head/2478547d122ec83a818b46f3b13c5230429559e40c7d144d4ec225f92c1494b3", - tiers: 12, - }, - SLIME: { - type: "combat", - head: "/head/c95eced85db62c922724efca804ea0060c4a87fcdedf2fd5c4f9ac1130a6eb26", - }, - COW: { - type: "farming", - tiers: 12, - head: "/head/c2fd8976e1b64aebfd38afbe62aa1429914253df3417ace1f589e5cf45fbd717", - }, - PIG: { - type: "farming", - tiers: 12, - head: "/head/a9bb5f0c56408c73cfa412345c8fc51f75b6c7311ae60e7099c4781c48760562", - }, - CHICKEN: { - type: "farming", - tiers: 12, - head: "/head/a04b7da13b0a97839846aa5648f5ac6736ba0ca9fbf38cd366916e417153fd7f", - }, - SHEEP: { - type: "farming", - tiers: 12, - head: "/head/fd15d4b8bce708f77f963f1b4e87b1b969fef1766a3e9b67b249c59d5e80e8c5", - }, - RABBIT: { - type: "farming", - tiers: 12, - head: "/head/ef59c052d339bb6305cad370fd8c52f58269a957dfaf433a255597d95e68a373", - }, - OAK: { - type: "foraging", - head: "/head/57e4a30f361204ea9cded3fbff850160731a0081cc452cfe26aed48e97f6364b", - }, - BIRCH: { - type: "foraging", - head: "/head/eb74109dbb88178afb7a9874afc682904cedb3df75978a51f7beeb28f924251", - }, - SPRUCE: { - type: "foraging", - head: "/head/7ba04bfe516955fd43932dcb33bd5eac20b38a231d9fa8415b3fb301f60f7363", - }, - DARK_OAK: { - type: "foraging", - head: "/head/5ecdc8d6b2b7e081ed9c36609052c91879b89730b9953adbc987e25bf16c5581", - }, - ACACIA: { - type: "foraging", - head: "/head/42183eaf5b133b838db13d145247e389ab4b4f33c67846363792dc3d82b524c0", - }, - JUNGLE: { - type: "foraging", - head: "/head/2fe73d981690c1be346a16331819c4e8800859fcdc3e5153718c6ad45861924c", - }, - VOIDLING: { - name: "Voidling", - type: "combat", - head: "/head/3a851ed2ce5c2c0523af772d206d9555e2e1383ec87946e6ff4c51186e29ef7f", - }, - HARD_STONE: { - name: "Hard Stone", - type: "mining", - head: "/head/1e8bab9493708beda34255606d5883b8762746bcbe6c94e8ca78a77a408c8ba8", - tiers: 12, - }, - RED_SAND: { - name: "Red Sand", - type: "mining", - head: "/head/9d24991435e4e7fb1a9ad23db75c80aec300d003ec0c5963e0ed658634027889", - tiers: 12, - }, - MYCELIUM: { - type: "mining", - head: "/head/fc8ebad72b77df3990e07bc869a99a8f8962d3c19c76e39d99553cae4131cc8", - tiers: 12, - }, - INFERNO: { - type: "combat", - head: "/head/665c54366f88fb3280b1c3fc500ce2b799c8dd327ab6d41c9bc959488f5cfd92", - }, - VAMPIRE: { - type: "combat", - head: "/head/5b0c2db42e90f83fae6551c96e83669211a77c2c155c54d1523af3079f9565ed", + farming: { + COCOA: { + texture: "/head/acb680e96f6177cd8ffaf27e9625d8b544d720afc50738801818d0e745c0e5f7", + maxTier: 12, + }, + PUMPKIN: { + maxTier: 12, + texture: "/head/f3fb663e843a7da787e290f23c8af2f97f7b6f572fa59a0d4d02186db6eaabb7", + }, + CHICKEN: { + maxTier: 12, + texture: "/head/a04b7da13b0a97839846aa5648f5ac6736ba0ca9fbf38cd366916e417153fd7f", + }, + MUSHROOM: { + maxTier: 12, + texture: "/head/4a3b58341d196a9841ef1526b367209cbc9f96767c24f5f587cf413d42b74a93", + }, + CACTUS: { + maxTier: 12, + texture: "/head/ef93ec6e67a6cd272c9a9684b67df62584cb084a265eee3cde141d20e70d7d72", + }, + PIG: { + maxTier: 12, + texture: "/head/a9bb5f0c56408c73cfa412345c8fc51f75b6c7311ae60e7099c4781c48760562", + }, + WHEAT: { + maxTier: 12, + texture: "/head/bbc571c5527336352e2fee2b40a9edfa2e809f64230779aa01253c6aa535881b", + }, + COW: { + maxTier: 12, + texture: "/head/c2fd8976e1b64aebfd38afbe62aa1429914253df3417ace1f589e5cf45fbd717", + }, + RABBIT: { + maxTier: 12, + texture: "/head/ef59c052d339bb6305cad370fd8c52f58269a957dfaf433a255597d95e68a373", + }, + SUGAR_CANE: { + maxTier: 12, + texture: "/head/2fced0e80f0d7a5d1f45a1a7217e6a99ea9720156c63f6efc84916d4837fabde", + }, + MELON: { + maxTier: 12, + texture: "/head/95d54539ac8d3fba9696c91f4dcc7f15c320ab86029d5c92f12359abd4df811e", + }, + NETHER_WARTS: { + maxTier: 12, + texture: "/head/71a4620bb3459c1c2fa74b210b1c07b4a02254351f75173e643a0e009a63f558", + }, + CARROT: { + maxTier: 12, + texture: "/head/4baea990b45d330998cb0c1f8515c27b24f93bff1df0db056e647f8200d03b9d", + }, + POTATO: { + maxTier: 12, + texture: "/head/7dda35a044cb0374b516015d991a0f65bf7d0fb6566e350496642cf2059ff1d9", + }, + SHEEP: { + maxTier: 12, + texture: "/head/fd15d4b8bce708f77f963f1b4e87b1b969fef1766a3e9b67b249c59d5e80e8c5", + }, + }, + mining: { + HARD_STONE: { + name: "Hard Stone", + texture: "/head/1e8bab9493708beda34255606d5883b8762746bcbe6c94e8ca78a77a408c8ba8", + maxTier: 12, + }, + RED_SAND: { + name: "Red Sand", + texture: "/head/9d24991435e4e7fb1a9ad23db75c80aec300d003ec0c5963e0ed658634027889", + maxTier: 12, + }, + MYCELIUM: { + texture: "/head/fc8ebad72b77df3990e07bc869a99a8f8962d3c19c76e39d99553cae4131cc8", + maxTier: 12, + }, + COBBLESTONE: { + texture: "/head/2f93289a82bd2a06cbbe61b733cfdc1f1bd93c4340f7a90abd9bdda774109071", + maxTier: 12, + }, + OBSIDIAN: { + texture: "/head/320c29ab966637cb9aecc34ee76d5a0130461e0c4fdb08cdaf80939fa1209102", + maxTier: 12, + }, + GLOWSTONE: { + texture: "/head/20f4d7c26b0310990a7d3a3b45948b95dd4ab407a16a4b6d3b7cb4fba031aeed", + maxTier: 12, + }, + GRAVEL: { + texture: "/head/7458507ed31cf9a38986ac8795173c609637f03da653f30483a721d3fbe602d", + }, + SAND: { + texture: "/head/81f8e2ad021eefd1217e650e848b57622144d2bf8a39fbd50dab937a7eac10de", + }, + ICE: { + texture: "/head/e500064321b12972f8e5750793ec1c823da4627535e9d12feaee78394b86dabe", + maxTier: 12, + }, + SNOW: { + texture: "/head/f6d180684c3521c9fc89478ba4405ae9ce497da8124fa0da5a0126431c4b78c3", + maxTier: 12, + }, + COAL: { + texture: "/head/425b8d2ea965c780652d29c26b1572686fd74f6fe6403b5a3800959feb2ad935", + maxTier: 12, + }, + IRON: { + texture: "/head/af435022cb3809a68db0fccfa8993fc1954dc697a7181494905b03fdda035e4a", + maxTier: 12, + }, + GOLD: { + texture: "/head/f6da04ed8c810be29bba53c62e712d65cfb25238117b94d7e85a4615775bf14f", + maxTier: 12, + }, + DIAMOND: { + texture: "/head/2354bbe604dfe58bf92e7729730d0c8e37844e831ee3816d7e8427c27a1824a2", + maxTier: 12, + }, + LAPIS: { + texture: "/head/64fd97b9346c1208c1db3957530cdfc5789e3e65943786b0071cf2b2904a6b5c", + maxTier: 12, + }, + REDSTONE: { + texture: "/head/1edefcf1a89d687a0a4ecf1589977af1e520fc673c48a0434be426612e8faa67", + maxTier: 12, + }, + EMERALD: { + texture: "/head/9bf57f3401b130c6b53808f2b1e119cc7b984622dac7077bbd53454e1f65bbf0", + maxTier: 12, + }, + MITHRIL: { + texture: "/head/c62fa670ff8599b32ab344195ba15f3ef64c3a8aa8a37821c08375950cb74cd0", + maxTier: 12, + }, + QUARTZ: { + texture: "/head/d270093be62dfd3019f908043db570b5dfd366fd5345fccf9da340e75c701a60", + maxTier: 12, + }, + ENDER_STONE: { + name: "End Stone", + texture: "/head/7994be3dcfbb4ed0a5a7495b7335af1a3ced0b5888b5007286a790767c3b57e6", + }, + }, + combat: { + ZOMBIE: { + texture: "/head/196063a884d3901c41f35b69a8c9f401c61ac9f6330f964f80c35352c3e8bfb0", + }, + REVENANT: { + texture: "/head/a3dce8555923558d8d74c2a2b261b2b2d630559db54ef97ed3f9c30e9a20aba", + maxTier: 12, + }, + SKELETON: { + texture: "/head/2fe009c5cfa44c05c88e5df070ae2533bd682a728e0b33bfc93fd92a6e5f3f64", + }, + CREEPER: { + texture: "/head/54a92c2f8c1b3774e80492200d0b2218d7b019314a73c9cb5b9f04cfcacec471", + }, + SPIDER: { + texture: "/head/e77c4c284e10dea038f004d7eb43ac493de69f348d46b5c1f8ef8154ec2afdd0", + }, + TARANTULA: { + texture: "/head/97e86007064c9ce26eb4bad8ac9aa30aac309e70a9e0b615936318dea40a721", + }, + CAVESPIDER: { + name: "Cave Spider", + texture: "/head/5d815df973bcd01ee8dfdb3bd74f0b7cb8fef2a70559e4faa5905127bbb4a435", + }, + BLAZE: { + texture: "/head/3208fbd64e97c6e00853d36b3a201e4803cae43dcbd6936a3cece050912e1f20", + maxTier: 12, + }, + MAGMA_CUBE: { + texture: "/head/18c9a7a24da7e3182e4f62fa62762e21e1680962197c7424144ae1d2c42174f7", + maxTier: 12, + }, + ENDERMAN: { + texture: "/head/e460d20ba1e9cd1d4cfd6d5fb0179ff41597ac6d2461bd7ccdb58b20291ec46e", + }, + GHAST: { + texture: "/head/2478547d122ec83a818b46f3b13c5230429559e40c7d144d4ec225f92c1494b3", + maxTier: 12, + }, + SLIME: { + texture: "/head/c95eced85db62c922724efca804ea0060c4a87fcdedf2fd5c4f9ac1130a6eb26", + }, + VOIDLING: { + name: "Voidling", + texture: "/head/3a851ed2ce5c2c0523af772d206d9555e2e1383ec87946e6ff4c51186e29ef7f", + }, + INFERNO: { + texture: "/head/665c54366f88fb3280b1c3fc500ce2b799c8dd327ab6d41c9bc959488f5cfd92", + }, + VAMPIRE: { + texture: "/head/5b0c2db42e90f83fae6551c96e83669211a77c2c155c54d1523af3079f9565ed", + }, + }, + foraging: { + OAK: { + texture: "/head/57e4a30f361204ea9cded3fbff850160731a0081cc452cfe26aed48e97f6364b", + }, + BIRCH: { + texture: "/head/eb74109dbb88178afb7a9874afc682904cedb3df75978a51f7beeb28f924251", + }, + SPRUCE: { + texture: "/head/7ba04bfe516955fd43932dcb33bd5eac20b38a231d9fa8415b3fb301f60f7363", + }, + DARK_OAK: { + texture: "/head/5ecdc8d6b2b7e081ed9c36609052c91879b89730b9953adbc987e25bf16c5581", + }, + ACACIA: { + texture: "/head/42183eaf5b133b838db13d145247e389ab4b4f33c67846363792dc3d82b524c0", + }, + JUNGLE: { + texture: "/head/2fe73d981690c1be346a16331819c4e8800859fcdc3e5153718c6ad45861924c", + }, + FLOWER: { + texture: "/head/baa7c59b2f792d8d091aecacf47a19f8ab93f3fd3c48f6930b1c2baeb09e0f9b", + maxTier: 12, + }, + }, + fishing: { + FISHING: { + texture: "/head/53ea0fd89524db3d7a3544904933830b4fc8899ef60c113d948bb3c4fe7aabb1", + }, + CLAY: { + texture: "/head/af9b312c8f53da289060e6452855072e07971458abbf338ddec351e16c171ff8", + }, }, }; let totalUniqueMinions = 0; - for (const minion in MINIONS) { totalUniqueMinions += MINIONS[minion].tiers ?? 11; } diff --git a/src/constants/misc.js b/src/constants/misc.js index 4bd7d0e56c..8c3dee5ef6 100644 --- a/src/constants/misc.js +++ b/src/constants/misc.js @@ -6,20 +6,9 @@ export const BLOCKED_PLAYERS = [ "fc7e31ef7bfe41e7aa5d7e2db14bedd0", // Kazius1 (Admin) ]; -// Number of kills required for each level of expertise -export const EXPERTISE_KILLS_LADDER = [50, 100, 250, 500, 1000, 2500, 5500, 10000, 15000]; - // Walking distance required for each rarity level of the prehistoric egg export const PREHISTORIC_EGG_BLOCKS_WALKED_LADDER = [4000, 10000, 20000, 40000, 100000]; -// Number of S runs required for each level of hecatomb -export const hecatomb_s_runs_ladder = [2, 5, 10, 20, 30, 40, 60, 80, 100]; - -// xp required for each level of champion -export const champion_xp_ladder = [50000, 100000, 250000, 500000, 1000000, 1500000, 2000000, 2500000, 3000000]; - -export const cultivating_crops_ladder = [1000, 5000, 25000, 100000, 300000, 1500000, 5000000, 20000000, 100000000]; - // api names and their max value from the profile upgrades export const PROFILE_UPGRADES = { island_size: 10, @@ -147,6 +136,21 @@ export const RACE_OBJECTIVE_TO_STAT_NAME = { complete_the_crystal_core_nothing_no_return_race: "dungeon_hub_crystal_core_nothing_no_return_best_time", }; +export const CUSTOM_RACE_IDS = { + woods_race_best_time: "foraging_race_best_time", + chicken_race_best_time: "chicken_race_best_time_2", +}; + +export const RACE_NAMES = { + crystal_core: "Crystal Core", + giant_mushroom: "Giant Mushroom", + precursor_ruins: "Precursor Ruins", + foraging_race: "Foraging", + end_race: "End", + chicken_race: "Chicken", + rift_race: "Rift", +}; + export const AREA_NAMES = { dynamic: "Private Island", hub: "Hub", @@ -396,30 +400,30 @@ export const ESSENCE = { }, }; -export const STAT_MAPPINGS = { +export const CENTURY_CAKE_STATS = { walk_speed: "speed", }; export const KUUDRA_TIERS = { none: { name: "Basic", - head: "bfd3e71838c0e76f890213120b4ce7449577736604338a8d28b4c86db2547e71", + head: "/head/bfd3e71838c0e76f890213120b4ce7449577736604338a8d28b4c86db2547e71", }, hot: { name: "Hot", - head: "c0259e8964c3deb95b1233bb2dc82c986177e63ae36c11265cb385180bb91cc0", + head: "/head/c0259e8964c3deb95b1233bb2dc82c986177e63ae36c11265cb385180bb91cc0", }, burning: { name: "Burning", - head: "330f6f6e63b245f839e3ccdce5a5f22056201d0274411dfe5d94bbe449c4ece", + head: "/head/330f6f6e63b245f839e3ccdce5a5f22056201d0274411dfe5d94bbe449c4ece", }, fiery: { name: "Fiery", - head: "bd854393bbf9444542502582d4b5a23cc73896506e2fc739d545bc35bc7b1c06", + head: "/head/bd854393bbf9444542502582d4b5a23cc73896506e2fc739d545bc35bc7b1c06", }, infernal: { name: "Infernal", - head: "82ee25414aa7efb4a2b4901c6e33e5eaa705a6ab212ebebfd6a4de984125c7a0", + head: "/head/82ee25414aa7efb4a2b4901c6e33e5eaa705a6ab212ebebfd6a4de984125c7a0", }, }; @@ -681,3 +685,54 @@ export const BANK_COOLDOWN = { 2: "5 minutes", 3: "None", }; + +export const SLAYER_INFO = { + zombie: { + name: "Revenant Horror", + head: "/head/1fc0184473fe882d2895ce7cbc8197bd40ff70bf10d3745de97b6c2a9c5fc78f", + }, + spider: { + name: "Tarantula Broodfather", + head: "/head/9d7e3b19ac4f3dee9c5677c135333b9d35a7f568b63d1ef4ada4b068b5a25", + }, + wolf: { + name: "Sven Packmaster", + head: "/head/f83a2aa9d3734b919ac24c9659e5e0f86ecafbf64d4788cfa433bbec189e8", + }, + enderman: { + name: "Voidgloom Seraph", + head: "/head/1b09a3752510e914b0bdc9096b392bb359f7a8e8a9566a02e7f66faff8d6f89e", + }, + blaze: { + name: "Inferno Demonlord", + head: "/head/b20657e24b56e1b2f8fc219da1de788c0c24f36388b1a409d0cd2d8dba44aa3b", + }, + vampire: { + name: "Riftstalker Bloodfiend", + head: "/head/5aa29ea961757dc3c90bfabf302c5abe9d308fb4a7d3864e5788ad2cc9160aa2", + }, +}; + +export const MILESTONE_RARITIES = ["common", "uncommon", "rare", "epic", "legendary"]; + +export const PET_MILESTONES = { + sea_creatures_killed: [250, 1000, 2500, 5000, 10000], + ores_mined: [2500, 7500, 20000, 100000, 250000], +}; + +export const ENCHANTMENT_LADDERS = { + // Number of S runs required for each level of hecatomb + hecatomb_s_runs: [2, 5, 10, 20, 30, 40, 60, 80, 100], + + // Number of xp required for each level of champion + champion_xp: [50000, 100000, 250000, 500000, 1000000, 1500000, 2000000, 2500000, 3000000], + + // Number of crops harvested for each level of cultivating crops + cultivating_crops: [1000, 5000, 25000, 100000, 300000, 1500000, 5000000, 20000000, 100000000], + + // Number of kills required for each level of expertise + expertise_kills: [50, 100, 250, 500, 1000, 2500, 5500, 10000, 15000], + + // Number of ores mined required for each level of compact ores + compact_ores: [100, 500, 1500, 5000, 15000, 50000, 150000, 500000, 1000000], +}; diff --git a/src/constants/museum.js b/src/constants/museum.js new file mode 100644 index 0000000000..e4add36dbb --- /dev/null +++ b/src/constants/museum.js @@ -0,0 +1,915 @@ +const categoryInventory = [ + { + display_name: "Go Back", + id: 262, + damage: 0, + rarity: "uncommon", + tag: { + display: { + Lore: [], + }, + }, + position: 48, + }, + { + display_name: "Close", + id: 166, + damage: 0, + rarity: "special", + tag: { + display: { + Lore: [], + }, + }, + position: 49, + }, + { + display_name: "Next Page", + id: 262, + damage: 0, + rarity: "uncommon", + tag: { + display: { + Lore: [], + }, + }, + position: 53, + }, +]; + +export const MUSEUM = { + weapons: [ + "ROGUE_SWORD", + "FANCY_SWORD", + "SPIDER_SWORD", + "UNDEAD_SWORD", + "ARTISANAL_SHORTBOW", + "END_SWORD", + "CLEAVER", + "FLAMING_SWORD", + "WITHER_BOW", + "DECENT_BOW", + "HUNTER_KNIFE", + "SAVANA_BOW", + "PRISMARINE_BOW", + "PRISMARINE_BLADE", + "ENDER_BOW", + "SILVER_FANG", + "CRYPT_DREADLORD_SWORD", + "ZOMBIE_SOLDIER_CUTLASS", + "MACHINE_GUN_BOW", + "MERCENARY_AXE", + "STARLIGHT_WAND", + "ARACK", + "TACTICIAN_SWORD", + "SNIPER_BOW", + "CRYPT_BOW", + "FROZEN_SCYTHE", + "GLACIAL_SCYTHE", + "GOLEM_SWORD", + "RAIDER_AXE", + "EXPLOSIVE_BOW", + "MAGMA_BOW", + "SLIME_BOW", + "EARTH_SHARD", + "VOIDWALKER_KATANA", + "VOIDEDGE_KATANA", + "VORPAL_KATANA", + "ATOMSPLIT_KATANA", + "SINSEEKER_SCYTHE", + "VOID_SWORD", + "DRAGON_SHORTBOW", + "ASPECT_OF_THE_END", + "ASPECT_OF_THE_VOID", + "SCORPION_BOW", + "HURRICANE_BOW", + "END_STONE_BOW", + "REVENANT_SWORD", + "REAPER_SWORD", + "AXE_OF_THE_SHREDDED", + "RECLUSE_FANG", + "CONJURING_SWORD", + "ZOMBIE_COMMANDER_WHIP", + "EDIBLE_MACE", + "ZOMBIE_SWORD", + "ORNATE_ZOMBIE_SWORD", + "FLORID_ZOMBIE_SWORD", + "SCORPION_FOIL", + "END_STONE_SWORD", + "DEATH_BOW", + "GIANT_CLEAVER", + "SPIDER_QUEENS_STINGER", + "VENOMS_TOUCH", + "SHAMAN_SWORD", + "POOCH_SWORD", + "EMERALD_BLADE", + "INK_WAND", + "ZOMBIE_KNIGHT_SWORD", + "FEL_SWORD", + "SOULS_REBOUND", + "JUJU_SHORTBOW", + "LEAPING_SWORD", + "SILK_EDGE_SWORD", + "BONZO_STAFF", + "SILENT_DEATH", + "STONE_BLADE", + "RUNAANS_BOW", + "MOSQUITO_BOW", + "SPIRIT_SWORD", + "FELTHORN_REAPER", + "BONE_REAVER", + "BAT_WAND", + "ITEM_SPIRIT_BOW", + "BONE_BOOMERANG", + "SWORD_OF_REVELATIONS", + "SOUL_WHIP", + "ASPECT_OF_THE_DRAGON", + "PIGMAN_SWORD", + "MIDAS_SWORD", + "MIDAS_STAFF", + "YETI_SWORD", + "LAST_BREATH", + "ICE_SPRAY_WAND", + "LIVID_DAGGER", + "SHADOW_FURY", + "REAPER_SCYTHE", + "GEMSTONE_GAUNTLET", + "DAEDALUS_AXE", + "PHANTOM_ROD", + "VOODOO_DOLL", + "VOODOO_DOLL_WILTED", + "FLOWER_OF_TRUTH", + "BOUQUET_OF_LIES", + "WITHER_CLOAK", + "NECROMANCER_SWORD", + "GIANTS_SWORD", + "TERMINATOR", + "HYPERION", + "ASTRAEA", + "SCYLLA", + "VALKYRIE", + "BLADE_OF_THE_VOLCANO", + "FIRE_VEIL_WAND", + "STAFF_OF_THE_VOLCANO", + "FIRE_FURY_STAFF", + "FIRE_FREEZE_STAFF", + "SULPHUR_BOW", + "WAND_OF_STRENGTH", + "RAGNAROCK_AXE", + "ALCHEMISTS_STAFF", + "SWORD_OF_BAD_HEALTH", + "ENRAGER", + "HOLLOW_WAND", + "FIREDUST_DAGGER", + "BURSTFIRE_DAGGER", + "HEARTFIRE_DAGGER", + "MAWDUST_DAGGER", + "BURSTMAW_DAGGER", + "HEARTMAW_DAGGER", + "DARK_CLAYMORE", + ], + armor: [ + "FARM_SUIT", + "MUSHROOM", + "ANGLER", + "PUMPKIN", + "CACTUS", + "LEAFLET", + "LAPIS_ARMOR", + "MINER_OUTFIT", + "GOLEM_ARMOR", + "TANK_MINER", + "ROTTEN", + "HARDENED_DIAMOND", + "MINERAL", + "FAIRY", + "FARM_ARMOR", + "MERCENARY", + "STARLIGHT", + "ARACHNE", + "BOUNCY", + "HEAVY", + "SKELETON_GRUNT", + "GROWTH", + "SALMON_NEW", + "MONSTER_HUNTER", + "MONSTER_RAIDER", + "SKELETON_SOLDIER", + "ZOMBIE_SOLDIER", + "GOBLIN", + "HEAT", + "FLAME_BREAKER", + "ARMOR_OF_YOG", + "END", + "ARMOR_OF_THE_PACK", + "ARMOR_OF_MAGMA", + "EMERALD_ARMOR", + "EMBER", + "CRYSTAL", + "SKELETON_MASTER", + "ZOMBIE", + "REVENANT", + "REAPER", + "BLAZE", + "FROZEN_BLAZE", + "CHEAP_TUXEDO", + "FANCY_TUXEDO", + "ELEGANT_TUXEDO", + "SPEEDSTER", + "SPONGE", + "MELON", + "CROPIE", + "SQUASH", + "FERMENTO", + "RABBIT", + "SEYMOUR", + "LOTUS", + "MASTIFF", + "TARANTULA", + "SPOOKY", + "SNOW_SUIT", + "NUTCRACKER", + "GLACITE", + "ZOMBIE_KNIGHT", + "ZOMBIE_COMMANDER", + "SHARK_SCALE", + "BAT_PERSON", + "DIVER", + "WEREWOLF", + "ADAPTIVE", + "YOUNG_DRAGON", + "OLD_DRAGON", + "WISE_DRAGON", + "PROTECTOR_DRAGON", + "STRONG_DRAGON", + "UNSTABLE_DRAGON", + "HOLY_DRAGON", + "SUPER_HEAVY", + "SKELETOR", + "SKELETON_LORD", + "ZOMBIE_LORD", + "PERFECT_TIER_12", + "PERFECT_TIER_13", + "SUPERIOR_DRAGON", + "SORROW", + "SHADOW_ASSASSIN", + "FINAL_DESTINATION", + "DIVAN", + "NECROMANCER_LORD", + "WITHER", + "TANK_WITHER", + "WISE_WITHER", + "SPEED_WITHER", + "POWER_WITHER", + "RAMPART", + "REKINDLED_EMBER", + "SHIMMERING_LIGHT", + "BERSERKER", + "LAVA_SEA_CREATURE", + "THUNDER", + "MAGMA_LORD", + "BRONZE_HUNTER", + "SILVER_HUNTER", + "GOLD_HUNTER", + "DIAMOND_HUNTER", + "MAGMA", + "VANQUISHED", + "MOLTEN", + "AURORA", + "CRIMSON", + "FERVOR", + "TERROR", + "HOLLOW", + ], + rarities: [ + "ENCHANTED_JACK_O_LANTERN", + "OBSIDIAN_CHESTPLATE", + "SQUID_BOOTS", + "BONZO_MASK", + "BALLOON_SNAKE", + "BONE_NECKLACE", + "SOULWEAVER_GLOVES", + "SPIRIT_MASK", + "WATER_HYDRA_HEAD", + "VAMPIRE_MASK", + "WITCH_MASK", + "VAMPIRE_WITCH_MASK", + "JERRY_STAFF", + "FARMER_BOOTS", + "RANCHERS_BOOTS", + "STONK_PICKAXE", + "JUNGLE_AXE", + "TREECAPITATOR_AXE", + "MITHRIL_COAT", + "THORNS_BOOTS", + "RADIANT_POWER_ORB", + "MANA_FLUX_POWER_ORB", + "OVERFLUX_POWER_ORB", + "PLASMAFLUX_POWER_ORB", + "WEIRD_TUBA", + "WEIRDER_TUBA", + "ZOMBIE_HEART", + "CRYSTALLIZED_HEART", + "REVIVED_HEART", + "KRAMPUS_HELMET", + "RIFT_NECKLACE_OUTSIDE", + "SUPER_COMPACTOR_3000", + "ROD_OF_THE_SEA", + "PRECURSOR_EYE", + "SUMMONING_RING", + "REAPER_MASK", + "THE_SHREDDER", + "YETI_ROD", + "AUGER_ROD", + "WARDEN_HELMET", + "CROWN_OF_GREED", + "GYROKINETIC_WAND", + "WAND_OF_HEALING", + "WAND_OF_MENDING", + "WAND_OF_RESTORATION", + "WAND_OF_ATONEMENT", + "MITHRIL_DRILL_1", + "MITHRIL_DRILL_2", + "GEMSTONE_DRILL_1", + "GEMSTONE_DRILL_2", + "GEMSTONE_DRILL_3", + "GEMSTONE_DRILL_4", + "TITANIUM_DRILL_1", + "TITANIUM_DRILL_2", + "TITANIUM_DRILL_3", + "TITANIUM_DRILL_4", + "STEEL_CHESTPLATE", + "MENDER_CROWN", + "WITHER_GOGGLES", + "FLAMING_FIST", + "IMPLOSION_BELT", + "GAUNTLET_OF_CONTAGION", + "SCOVILLE_BELT", + "SCOURGE_CLOAK", + "DELIRIUM_NECKLACE", + "LAVA_SHELL_NECKLACE", + "STARTER_LAVA_ROD", + "MAGMA_ROD", + "INFERNO_ROD", + "HELLFIRE_ROD", + "DOJO_BLACK_BELT", + "SYNTHESIZER_V3", + "ANCIENT_CLOAK", + "THEORETICAL_HOE_WHEAT_1", + "THEORETICAL_HOE_WHEAT_2", + "THEORETICAL_HOE_WHEAT_3", + "THEORETICAL_HOE_CARROT_1", + "THEORETICAL_HOE_CARROT_2", + "THEORETICAL_HOE_CARROT_3", + "THEORETICAL_HOE_POTATO_1", + "THEORETICAL_HOE_POTATO_2", + "THEORETICAL_HOE_POTATO_3", + "THEORETICAL_HOE_WARTS_1", + "THEORETICAL_HOE_WARTS_2", + "THEORETICAL_HOE_WARTS_3", + "THEORETICAL_HOE_CANE_1", + "THEORETICAL_HOE_CANE_2", + "THEORETICAL_HOE_CANE_3", + "CACTUS_KNIFE", + "FUNGI_CUTTER", + "COCO_CHOPPER", + "MELON_DICER", + "MELON_DICER_2", + "MELON_DICER_3", + "PUMPKIN_DICER", + "PUMPKIN_DICER_2", + "PUMPKIN_DICER_3", + "SKYMART_VACUUM", + "SKYMART_TURBO_VACUUM", + "SKYMART_HYPER_VACUUM", + "INFINI_VACUUM", + "INFINI_VACUUM_HOOVERIUS", + "SOS_FLARE", + "TACTICAL_INSERTION", + "ANNIHILATION_CLOAK", + "BLAZETEKK_HAM_RADIO", + "DEMONLORD_GAUNTLET", + ], + children: { + GLACIAL_SCYTHE: "FROZEN_SCYTHE", + VOIDEDGE_KATANA: "VOIDWALKER_KATANA", + VORPAL_KATANA: "VOIDEDGE_KATANA", + ATOMSPLIT_KATANA: "VORPAL_KATANA", + ASPECT_OF_THE_VOID: "ASPECT_OF_THE_END", + REAPER_SWORD: "REVENANT_SWORD", + AXE_OF_THE_SHREDDED: "REAPER_SWORD", + ORNATE_ZOMBIE_SWORD: "ZOMBIE_SWORD", + FLORID_ZOMBIE_SWORD: "ORNATE_ZOMBIE_SWORD", + POOCH_SWORD: "SHAMAN_SWORD", + SILK_EDGE_SWORD: "LEAPING_SWORD", + MINERAL: "HARDENED_DIAMOND", + MASTIFF: "GROWTH", + FLAME_BREAKER: "HEAT", + ARMOR_OF_YOG: "FLAME_BREAKER", + REVENANT: "ZOMBIE", + REAPER: "REVENANT", + FROZEN_BLAZE: "BLAZE", + FANCY_TUXEDO: "CHEAP_TUXEDO", + ELEGANT_TUXEDO: "FANCY_TUXEDO", + SHARK_SCALE: "SPONGE", + BAT_PERSON: "SPOOKY", + SILVER_HUNTER: "BRONZE_HUNTER", + GOLD_HUNTER: "SILVER_HUNTER", + DIAMOND_HUNTER: "GOLD_HUNTER", + VANQUISHED: "MAGMA", + CROPIE: "MELON", + SQUASH: "CROPIE", + FERMENTO: "SQUASH", + WITCH_MASK: "VAMPIRE_MASK", + VAMPIRE_WITCH_MASK: "WITCH_MASK", + RANCHERS_BOOTS: "FARMER_BOOTS", + TREECAPITATOR_AXE: "JUNGLE_AXE", + MANA_FLUX_POWER_ORB: "RADIANT_POWER_ORB", + OVERFLUX_POWER_ORB: "MANA_FLUX_POWER_ORB", + PLASMAFLUX_POWER_ORB: "OVERFLUX_POWER_ORB", + CRYSTALLIZED_HEART: "ZOMBIE_HEART", + REVIVED_HEART: "CRYSTALLIZED_HEART", + WAND_OF_MENDING: "WAND_OF_HEALING", + WAND_OF_RESTORATION: "WAND_OF_MENDING", + WAND_OF_ATONEMENT: "WAND_OF_RESTORATION", + MITHRIL_DRILL_2: "MITHRIL_DRILL_1", + GEMSTONE_DRILL_2: "GEMSTONE_DRILL_1", + GEMSTONE_DRILL_3: "GEMSTONE_DRILL_2", + GEMSTONE_DRILL_4: "GEMSTONE_DRILL_3", + TITANIUM_DRILL_2: "TITANIUM_DRILL_1", + TITANIUM_DRILL_3: "TITANIUM_DRILL_2", + TITANIUM_DRILL_4: "TITANIUM_DRILL_3", + DIVAN_DRILL: "TITANIUM_DRILL_4", + INFERNO_ROD: "MAGMA_ROD", + HELLFIRE_ROD: "INFERNO_ROD", + BURSTMAW_DAGGER: "MAWDUST_DAGGER", + HEARTMAW_DAGGER: "BURSTMAW_DAGGER", + BURSTFIRE_DAGGER: "FIREDUST_DAGGER", + HEARTFIRE_DAGGER: "BURSTFIRE_DAGGER", + MELON_DICER_2: "MELON_DICER", + MELON_DICER_3: "MELON_DICER_2", + PUMPKIN_DICER_2: "PUMPKIN_DICER", + PUMPKIN_DICER_3: "PUMPKIN_DICER_2", + THEORETICAL_HOE_WHEAT_2: "THEORETICAL_HOE_WHEAT_1", + THEORETICAL_HOE_WHEAT_3: "THEORETICAL_HOE_WHEAT_2", + THEORETICAL_HOE_CARROT_2: "THEORETICAL_HOE_CARROT_1", + THEORETICAL_HOE_CARROT_3: "THEORETICAL_HOE_CARROT_2", + THEORETICAL_HOE_POTATO_2: "THEORETICAL_HOE_POTATO_1", + THEORETICAL_HOE_POTATO_3: "THEORETICAL_HOE_POTATO_2", + THEORETICAL_HOE_WARTS_2: "THEORETICAL_HOE_WARTS_1", + THEORETICAL_HOE_WARTS_3: "THEORETICAL_HOE_WARTS_2", + THEORETICAL_HOE_CANE_2: "THEORETICAL_HOE_CANE_1", + THEORETICAL_HOE_CANE_3: "THEORETICAL_HOE_CANE_2", + VOODOO_DOLL_WILTED: "VOODOO_DOLL", + WEIRDER_TUBA: "WEIRD_TUBA", + BOUQUET_OF_LIES: "FLOWER_OF_TRUTH", + FELTHORN_REAPER: "BONE_REAVER", + BONE_REAVER: "SPIRIT_SWORD", + MONSTER_RAIDER: "MONSTER_HUNTER", + TANK_WITHER: "WITHER", + WISE_WITHER: "TANK_WITHER", + SPEED_WITHER: "WISE_WITHER", + POWER_WITHER: "SPEED_WITHER", + PERFECT_TIER_13: "PERFECT_TIER_12", + SKYMART_TURBO_VACUUM: "SKYMART_VACUUM", + SKYMART_HYPER_VACUUM: "SKYMART_TURBO_VACUUM", + INFINI_VACUUM: "SKYMART_HYPER_VACUUM", + INFINI_VACUUM_HOOVERIUS: "INFINI_VACUUM", + }, + armor_to_id: { + FARM_SUIT: "FARM_SUIT_HELMET", + MUSHROOM: "MUSHROOM_HELMET", + ANGLER: "ANGLER_HELMET", + PUMPKIN: "PUMPKIN_HELMET", + CACTUS: "CACTUS_HELMET", + LEAFLET: "LEAFLET_HELMET", + LAPIS_ARMOR: "LAPIS_ARMOR_HELMET", + MINER_OUTFIT: "MINER_OUTFIT_HELMET", + GOLEM_ARMOR: "GOLEM_ARMOR_HELMET", + TANK_MINER: "TANK_MINER_HELMET", + ROTTEN: "ROTTEN_HELMET", + HARDENED_DIAMOND: "HARDENED_DIAMOND_HELMET", + MINERAL: "MINERAL_HELMET", + FAIRY: "FAIRY_HELMET", + FARM_ARMOR: "FARM_ARMOR_HELMET", + MERCENARY: "MERCENARY_HELMET", + STARLIGHT: "STARLIGHT_HELMET", + ARACHNE: "ARACHNE_HELMET", + BOUNCY: "BOUNCY_HELMET", + HEAVY: "HEAVY_HELMET", + SKELETON_GRUNT: "SKELETON_GRUNT_HELMET", + GROWTH: "GROWTH_HELMET", + SALMON_NEW: "SALMON_HELMET", + MONSTER_HUNTER: "SKELETON_HELMET", + MONSTER_RAIDER: "SKELETON_HELMET", + SKELETON_SOLDIER: "SKELETON_SOLDIER_HELMET", + ZOMBIE_SOLDIER: "ZOMBIE_SOLDIER_HELMET", + GOBLIN: "GOBLIN_HELMET", + HEAT: "HEAT_HELMET", + FLAME_BREAKER: "FLAME_BREAKER_HELMET", + ARMOR_OF_YOG: "ARMOR_OF_YOG_HELMET", + END: "END_HELMET", + ARMOR_OF_THE_PACK: "HELMET_OF_THE_PACK", + ARMOR_OF_MAGMA: "ARMOR_OF_MAGMA_HELMET", + EMERALD_ARMOR: "EMERALD_ARMOR_HELMET", + EMBER: "EMBER_HELMET", + CRYSTAL: "CRYSTAL_HELMET", + SKELETON_MASTER: "SKELETON_MASTER_HELMET", + ZOMBIE: "ZOMBIE_CHESTPLATE", + REVENANT: "REVENANT_CHESTPLATE", + REAPER: "REAPER_CHESTPLATE", + BLAZE: "BLAZE_HELMET", + FROZEN_BLAZE: "FROZEN_BLAZE_HELMET", + CHEAP_TUXEDO: "CHEAP_TUXEDO_CHESTPLATE", + FANCY_TUXEDO: "FANCY_TUXEDO_CHESTPLATE", + ELEGANT_TUXEDO: "ELEGANT_TUXEDO_CHESTPLATE", + SPEEDSTER: "SPEEDSTER_HELMET", + SPONGE: "SPONGE_HELMET", + MASTIFF: "MASTIFF_HELMET", + TARANTULA: "TARANTULA_HELMET", + SPOOKY: "SPOOKY_HELMET", + SNOW_SUIT: "SNOW_SUIT_HELMET", + NUTCRACKER: "NUTCRACKER_HELMET", + GLACITE: "GLACITE_HELMET", + ZOMBIE_KNIGHT: "ZOMBIE_KNIGHT_HELMET", + ZOMBIE_COMMANDER: "ZOMBIE_COMMANDER_HELMET", + SHARK_SCALE: "SHARK_SCALE_HELMET", + BAT_PERSON: "BAT_PERSON_HELMET", + DIVER: "DIVER_HELMET", + WEREWOLF: "WEREWOLF_HELMET", + ADAPTIVE: "ADAPTIVE_HELMET", + YOUNG_DRAGON: "YOUNG_DRAGON_HELMET", + OLD_DRAGON: "OLD_DRAGON_HELMET", + WISE_DRAGON: "WISE_DRAGON_HELMET", + PROTECTOR_DRAGON: "PROTECTOR_DRAGON_HELMET", + STRONG_DRAGON: "STRONG_DRAGON_HELMET", + UNSTABLE_DRAGON: "UNSTABLE_DRAGON_HELMET", + HOLY_DRAGON: "HOLY_DRAGON_HELMET", + SUPER_HEAVY: "SUPER_HEAVY_HELMET", + SKELETOR: "SKELETOR_HELMET", + SKELETON_LORD: "SKELETON_LORD_HELMET", + ZOMBIE_LORD: "ZOMBIE_LORD_HELMET", + PERFECT_TIER_12: "PERFECT_HELMET_12", + PERFECT_TIER_13: "PERFECT_HELMET_13", + SUPERIOR_DRAGON: "SUPERIOR_DRAGON_HELMET", + SORROW: "SORROW_HELMET", + SHADOW_ASSASSIN: "SHADOW_ASSASSIN_HELMET", + FINAL_DESTINATION: "FINAL_DESTINATION_HELMET", + DIVAN: "DIVAN_HELMET", + NECROMANCER_LORD: "NECROMANCER_LORD_HELMET", + WITHER: "WITHER_HELMET", + TANK_WITHER: "TANK_WITHER_HELMET", + WISE_WITHER: "WISE_WITHER_HELMET", + SPEED_WITHER: "SPEED_WITHER_HELMET", + POWER_WITHER: "POWER_WITHER_HELMET", + RAMPART: "RAMPART_HELMET", + REKINDLED_EMBER: "REKINDLED_EMBER_HELMET", + SHIMMERING_LIGHT: "SHIMMERING_LIGHT_HOOD", + BERSERKER: "BERSERKER_HELMET", + LAVA_SEA_CREATURE: "TAURUS_HELMET", + THUNDER: "THUNDER_HELMET", + MAGMA_LORD: "MAGMA_LORD_HELMET", + BRONZE_HUNTER: "BRONZE_HUNTER_HELMET", + SILVER_HUNTER: "SILVER_HUNTER_HELMET", + GOLD_HUNTER: "GOLD_HUNTER_HELMET", + DIAMOND_HUNTER: "DIAMOND_HUNTER_HELMET", + MAGMA: "MAGMA_NECKLACE", + VANQUISHED: "VANQUISHED_MAGMA_NECKLACE", + MOLTEN: "MOLTEN_NECKLACE", + AURORA: "AURORA_HELMET", + CRIMSON: "CRIMSON_HELMET", + FERVOR: "FERVOR_HELMET", + TERROR: "TERROR_HELMET", + HOLLOW: "HOLLOW_HELMET", + MELON: "MELON_HELMET", + CROPIE: "CROPIE_HELMET", + SQUASH: "SQUASH_HELMET", + FERMENTO: "FERMENTO_HELMET", + RABBIT: "RABBIT_HELMET", + SEYMOUR: "VELVET_TOP_HAT", + LOTUS: "LOTUS_NECKLACE", + }, + inventory: [ + { + display_name: "Museum", + rarity: "rare", + texture_path: "/head/438cf3f8e54afc3b3f91d20a49f324dca1486007fe545399055524c17941f4dc", + tag: { + display: { + Lore: [ + "§7The §9Museum §7is a compendium", + "§7of all of your items in", + "§7SkyBlock. Donate items to your", + "§7Museum to unlock rewards.", + "", + "§7Other players can visit your", + "§7Museum at any time! Display your", + "§7best items proudly for all to", + "§7see.", + "", + ], + }, + }, + position: 4, + progressType: "total", + }, + { + display_name: "Weapons", + id: 276, + damage: 0, + rarity: "uncommon", + tag: { + display: { + Lore: ["§7View all of the §6Weapons §7that", "§7you have donated to the", "§7§9Museum§7!", ""], + }, + }, + position: 19, + inventoryType: "weapons", + containsItems: [ + { + display_name: "Weapons", + id: 276, + damage: 0, + rarity: "uncommon", + tag: { + display: { + Lore: ["§7View all of the §6Weapons §7that", "§7you have donated to the", "§7§9Museum§7!", ""], + }, + }, + position: 4, + }, + ...categoryInventory, + ], + numPagesContainsInventory: 1, + progressType: "weapons", + }, + { + display_name: "Armor Sets", + id: 311, + damage: 0, + rarity: "uncommon", + tag: { + display: { + Lore: ["§7View all of the §9Armor Sets", "§9§7that you have donated to the", "§7§9Museum§7!", ""], + }, + }, + position: 21, + inventoryType: "armor", + containsItems: [ + { + display_name: "Armor Sets", + id: 311, + damage: 0, + rarity: "uncommon", + tag: { + display: { + Lore: ["§7View all of the §9Armor Sets", "§9§7that you have donated to the", "§7§9Museum§7!", ""], + }, + }, + position: 4, + }, + ...categoryInventory, + ], + numPagesContainsInventory: 1, + progressType: "armor", + }, + { + display: "Rarities", + rarity: "uncommon", + texture_path: "/head/86addbd5dedad40999473be4a7f48f6236a79a0dce971b5dbd7372014ae394d", + tag: { + display: { + Lore: ["§7View all of the §5Rarities", "§5§7that you have donated to the", "§7§9Museum§7!", ""], + }, + }, + position: 23, + inventoryType: "rarities", + containsItems: [ + { + display: "Rarities", + rarity: "uncommon", + texture_path: "/head/86addbd5dedad40999473be4a7f48f6236a79a0dce971b5dbd7372014ae394d", + tag: { + display: { + Lore: ["§7View all of the §5Rarities", "§5§7that you have donated to the", "§7§9Museum§7!", ""], + }, + }, + position: 4, + }, + ...categoryInventory, + ], + numPagesContainsInventory: 1, + progressType: "rarities", + }, + { + display_name: "Special Items", + id: 354, + damage: 0, + rarity: "uncommon", + tag: { + display: { + Lore: [ + "§7View all of the §dSpecial Items", + "§d§7that you have donated to the", + "§7§9Museum§7!", + "", + "§7These items don't count towards", + "§7Museum progress and rewards, but", + "§7are cool nonetheless. Items that", + "§7are §9rare §7and §6prestigious", + "§6§7fit into this category, and", + "§7can be displayed in the Main", + "§7room of the Museum.", + "", + ], + }, + }, + position: 25, + inventoryType: "special", + containsItems: [ + { + display_name: "Special Items", + id: 354, + damage: 0, + rarity: "uncommon", + tag: { + display: { + Lore: [ + "§7View all of the §dSpecial Items", + "§d§7that you have donated to the", + "§7§9Museum§7!", + "", + "§7These items don't count towards", + "§7Museum progress and rewards, but", + "§7are cool nonetheless. Items that", + "§7are §9rare §7and §6prestigious", + "§6§7fit into this category, and", + "§7can be displayed in the Main", + "§7room of the Museum.", + "", + ], + }, + }, + position: 4, + }, + ...categoryInventory, + ], + numPagesContainsInventory: 1, + progressType: "special", + }, + { + display_name: "Museum Appraisal", + id: 264, + damage: 0, + rarity: "legendary", + tag: { + display: { + Lore: [ + "§7§6Madame Goldsworth §7offers an", + "§7appraisal service for Museums.", + "§7When unlocked, she will appraise", + "§7the value of your Museum each", + "§7time you add or remove items.", + "", + "§7This service also allows you to", + "§7appear on the §6Top Valued", + "§6§7filter in the §9Museum", + "§9Browser§7.", + "", + ], + }, + }, + position: 40, + progressType: "appraisal", + }, + { + display_name: "Edit NPC Tags", + id: 421, + damage: 0, + rarity: "uncommon", + tag: { + display: { + Lore: [ + "§7Edit the tags that appear above", + "§7your NPC. Show off your SkyBlock", + "§7progress with tags showing your", + "§7highest collection, best Skill,", + "§7and more!", + "", + "§cCOMING SOON", + ], + }, + }, + position: 45, + }, + { + display_name: "Museum Rewards", + id: 41, + damage: 0, + rarity: "legendary", + tag: { + display: { + Lore: [ + "§7Each time you donate an item to", + "§7your Museum, the §bCurator", + "§b§7will reward you.", + "", + "§7§dSpecial Items §7do not count", + "§7towards your Museum rewards", + "§7progress.", + "", + "§7Currently, most rewards are", + "§7§ccoming soon§7, but you can", + "§7view them anyway.", + ], + }, + }, + position: 48, + }, + { + display_name: "Close", + id: 166, + damage: 0, + rarity: "special", + tag: { + display: { + Lore: [], + }, + }, + position: 49, + }, + { + display_name: "Museum Browser", + id: 323, + damage: 0, + rarity: "uncommon", + tag: { + display: { + Lore: ["§7View the Museums of your", "§7friends, top valued players, and", "§7more!"], + }, + }, + position: 50, + }, + ], + item_slots: [ + 10, 11, 12, 13, 14, 15, 16, 19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34, 37, 38, 39, 40, 41, 42, 43, + ], + missing_item: { + weapons: { + display_name: null, + id: 351, + damage: 8, + rarity: "special", + tag: { + display: { + Lore: ["§7Click on this item in your", "§7inventory to add it to your", "§7§9Museum§7!"], + }, + }, + }, + armor: { + display_name: null, + id: 351, + damage: 8, + rarity: "special", + tag: { + display: { + Lore: [ + "§7Click on an armor piece in your", + "§7inventory that belongs to this", + "§7armor set to donate the full set", + "§7to your Museum.", + ], + }, + }, + }, + rarities: { + display_name: null, + id: 351, + damage: 8, + rarity: "special", + tag: { + display: { + Lore: ["§7Click on this item in your", "§7inventory to add it to your", "§7§9Museum§7!"], + }, + }, + }, + special: null, + }, + higher_tier_donated: { + display_name: null, + id: 351, + damage: 10, + rarity: "special", + tag: { + display: { + Lore: ["§7Donated as higher tier"], + }, + }, + }, +}; + +export function getMuseumItems() { + const { armor, weapons, rarities } = MUSEUM; + + return [...armor, ...weapons, ...rarities]; +} diff --git a/src/constants/pet-stats.js b/src/constants/pet-stats.js index d8710c684d..134bfe4c51 100644 --- a/src/constants/pet-stats.js +++ b/src/constants/pet-stats.js @@ -292,7 +292,7 @@ class Eerie extends Pet { get third() { const mult = getValue(this.rarity, { legendary: 0.01 }); - const primalFearKills = this.profile.kills.find((mob) => mob.entityId === "primal_fear")?.amount ?? 0; + const primalFearKills = this.profile.kills.kills.find((mob) => mob.entityId === "primal_fear")?.amount ?? 0; const kills = Math.max(primalFearKills, 150); const killsFormatted = kills >= 150 ? `§a${kills}` : `§c${kills}`; @@ -453,7 +453,7 @@ class Rabbit extends Pet { const mult = getValue(this.rarity, { rare: 0.25, epic: 0.3 }); return { name: "§6Farming Wisdom Boost ", - desc: [`§7Grants §3+${round(this.level * mult, 1)} ${SYMBOLS.wisdom} Farming Wisdom§7.`], + desc: [`§7Grants §3+${round(this.level * mult, 1)} ${SYMBOLS.farming_wisdom} Farming Wisdom§7.`], }; } @@ -706,7 +706,7 @@ class MithrilGolem extends Pet { const mult = getValue(this.rarity, { legendary: 0.2 }); return { name: "§6Danger Averse", - desc: [`§7Increases your combat stats by §a+${round(this.level * mult, 1)}% §7on a Mining Island.`], + desc: [`§7Increases MOST combat stats by §a+${round(this.level * mult, 1)}% §7on mining islands.`], }; } @@ -859,7 +859,7 @@ class Silverfish extends Pet { const mult = getValue(this.rarity, { rare: 0.25, epic: 0.3 }); return { name: "§6Mining Wisdom Boost", - desc: [`§7Grants §3+${round(this.level * mult, 1)} ${SYMBOLS.wisdom} Mining Wisdom§7.`], + desc: [`§7Grants §3+${round(this.level * mult, 1)} ${SYMBOLS.mining_wisdom} Mining Wisdom§7.`], }; } @@ -871,6 +871,57 @@ class Silverfish extends Pet { } } +class Slug extends Pet { + get stats() { + return { + intelligence: this.level * 0.25, + defense: this.level * 0.2, + }; + } + + get abilities() { + const list = [this.first, this.second]; + + if (this.rarity >= LEGENDARY) { + list.push(this.third); + } + return list; + } + + get first() { + const mult = getValue(this.rarity, { epic: 0.2 }); + return { + name: "§6Slow and Steady", + desc: [ + `§7When fishing in the §cCrimson §cIsle§7, §aSlugfish §7take §a${round( + this.level * mult, + 1 + )}% §7less time to catch.`, + ], + }; + } + + get second() { + const mult = getValue(this.rarity, { epic: 0.4 }); + return { + name: "§6Pest Friends", + desc: [`§7Grants §a${round(this.level * mult, 1)} §a${SYMBOLS.bonus_pest_chance} Bonus Pest Chance.`], + }; + } + + get third() { + const mult = getValue(this.rarity, { legendary: 1 }); + return { + name: "§6Repugnant Aroma", + desc: [ + `§7When farming in a plot affected by a §aSprayonator§7, gain §6+${round(this.level * mult, 1)} ${ + SYMBOLS.farming_fortune + } Farming Fortune§7.`, + ], + }; + } +} + class WitherSkeleton extends Pet { get stats() { return { @@ -1119,7 +1170,9 @@ class GoldenDragon extends Pet { get stats() { const stats = {}; if (this.level >= 100) { - const goldCollectionDigits = this.profile?.collections?.GOLD_INGOT?.totalAmount.toString().length ?? 0; + const goldCollectionDigits = this.profile.collections?.mining?.collections + ?.find((collection) => collection.id === "GOLD_INGOT") + ?.amount.toString().length; stats.strength = Math.floor(25 + Math.max(0, this.level - 100) * 0.25) + 10 * goldCollectionDigits; stats.bonus_attack_speed = Math.floor(25 + Math.max(0, this.level - 100) * 0.25); @@ -1457,7 +1510,7 @@ class Guardian extends Pet { const mult = getValue(this.rarity, { rare: 0.25, epic: 0.3 }); return { name: "§6Enchanting Wisdom Boost", - desc: [`§7Grants §3+${round(this.level * mult, 1)} ${SYMBOLS.wisdom} Enchanting Wisdom§7.`], + desc: [`§7Grants §3+${round(this.level * mult, 1)} ${SYMBOLS.enchanting_wisdom} Enchanting Wisdom§7.`], }; } @@ -2212,7 +2265,7 @@ class Wolf extends Pet { const mult = getValue(this.rarity, { legendary: 0.3 }); return { name: "§6Combat Wisdom Boost", - desc: [`§7Grants §3+${round(this.level * mult, 1)} ${SYMBOLS.wisdom} Combat Wisdom§7.`], + desc: [`§7Grants §3+${round(this.level * mult, 1)} ${SYMBOLS.combat_wisdom} Combat Wisdom§7.`], }; } } @@ -2245,7 +2298,7 @@ class GrandmaWolf extends Pet { `§a15 Combo §8(lasts §a${Math.floor((4 + this.level * 0.02) * 10) / 10}s§8)`, `§8+ §b3% §b${SYMBOLS.magic_find} Magic Find`, `§a20 Combo §8(lasts §a${Math.floor((3 + this.level * 0.02) * 10) / 10}s§8)`, - `§8+ §315 ${SYMBOLS.wisdom} Combat Wisdom`, + `§8+ §315 ${SYMBOLS.combat_wisdom} Combat Wisdom`, `§a25 Combo §8(lasts §a${Math.floor((3 + this.level * 0.01) * 10) / 10}s§8)`, `§8+ §b3% §b${SYMBOLS.magic_find} Magic Find`, `§a30 Combo §8(lasts §a${Math.floor((2 + this.level * 0.01) * 10) / 10}s§8)`, @@ -2507,7 +2560,7 @@ class Ocelot extends Pet { const mult = getValue(this.rarity, { common: 0.2, uncommon: 0.25, epic: 0.3 }); return { name: "§6Foraging Wisdom Boost", - desc: [`§7Grants §3+${round(this.level * mult, 1)} ${SYMBOLS.wisdom} Foraging Wisdom§7.`], + desc: [`§7Grants §3+${round(this.level * mult, 1)} ${SYMBOLS.foraging_wisdom} Foraging Wisdom§7.`], }; } @@ -2877,7 +2930,7 @@ class Squid extends Pet { const mult = getValue(this.rarity, { legendary: 0.3 }); return { name: "§6Fishing Wisdom Boost", - desc: [`§7Grants §3+${round(this.level * mult, 1)} ${SYMBOLS.wisdom} Fishing Wisdom§7.`], + desc: [`§7Grants §3+${round(this.level * mult, 1)} ${SYMBOLS.fishing_wisdom} Fishing Wisdom§7.`], }; } } @@ -3646,6 +3699,7 @@ export const PET_STATS = { SCATHA: Scatha, SHEEP: Sheep, SILVERFISH: Silverfish, + SLUG: Slug, SKELETON_HORSE: SkeletonHorse, SKELETON: Skeleton, SNAIL: Snail, diff --git a/src/constants/pets.js b/src/constants/pets.js index 18557b2194..a12d109b23 100644 --- a/src/constants/pets.js +++ b/src/constants/pets.js @@ -560,6 +560,13 @@ export const PET_DATA = { maxLevel: 100, emoji: "🕷️", }, + SLUG: { + head: "/head/7a79d0fd677b54530961117ef84adc206e2cc5045c1344d61d776bf8ac2fe1ba", + type: "farming", + maxTier: "legendary", + maxLevel: 100, + emoji: "🐌", + }, }; export const PET_VALUE = { diff --git a/src/constants/skins-animations.js b/src/constants/skins-animations.js index 641db74295..b3f013a024 100644 --- a/src/constants/skins-animations.js +++ b/src/constants/skins-animations.js @@ -951,6 +951,76 @@ const SKINS = [ source: "firesale", release: new Date("2023-10-28 18:00:00 GMT+1").getTime(), }, + { + id: "PET_SKIN_RAT_GYM_RAT", + name: "Gym", + texture: "/head/e9f05d0552f5418957808d3b098567e9d6294e5257f6c677f22f3401c9616231", + source: "firesale", + release: new Date("2023-11-18 18:00:00 GMT+1").getTime(), + }, + { + id: "PET_SKIN_RAT_JUNK_RAT", + name: "Junk", + texture: "/head/879d59e8cff4831c985988f5701a6689a88ccbcb094a97b61bd408384d093417", + source: "firesale", + release: new Date("2023-11-18 18:00:00 GMT+1").getTime(), + }, + { + id: "PET_SKIN_RAT_KARATE", + name: "Karate", + texture: "/head/c0983ff7f3e7525f32fceab07b64aced104e4c8798294a1dcc262e5283327258", + source: "firesale", + release: new Date("2023-11-18 18:00:00 GMT+1").getTime(), + }, + { + id: "PET_SKIN_RAT_MR_CLAWS", + name: "Mr Claws", + texture: "/head/11787e838120c6cce0570154a406adeefec3f04afdaec915067db829b3588270", + source: "firesale", + release: new Date("2023-11-18 18:00:00 GMT+1").getTime(), + }, + { + id: "PET_SKIN_RAT_NINJA", + name: "Ninja", + head: "/head/337e9a838503d17acdb0d2642e5bcdefa0393887b61bb13698f4368333a6e962", + source: "firesale", + release: new Date("2023-11-18 18:00:00 GMT+1").getTime(), + }, + { + id: "PET_SKIN_RAT_PIRATE", + name: "PiRate", + texture: "/head/d7c7624fc4463749ed516df1bbbaa67c172ffc9f20b6f2d5b80e39ccf03d5f7a", + source: "firesale", + release: new Date("2023-11-18 18:00:00 GMT+1").getTime(), + }, + { + id: "PET_SKIN_RAT_RAT-STRONAUT", + name: "Rat-stronaut", + texture: "/head/22f6f61f863dea14b18ee763d0cc0053bad338ed7755cc52df459c6c9a10a237", + source: "firesale", + release: new Date("2023-11-18 18:00:00 GMT+1").getTime(), + }, + { + id: "PET_SKIN_RAT_SECRAT_SERVICE", + name: "SecRat Service Rat Skin", + texture: "/head/3d248f8e7cb791ece9bcea2dd0381f2eb138f18f02b9657099625735cd4bfc0e", + source: "firesale", + release: new Date("2023-11-18 18:00:00 GMT+1").getTime(), + }, + { + id: "PET_SKIN_RAT_SECURATY_GUARD", + name: "SecuRaty Guard", + texture: "/head/e0da7bef0d3dda928ad34d6f7ca780e08659af043bdb31a09e123ccf6b63ad3c", + source: "firesale", + release: new Date("2023-11-18 18:00:00 GMT+1").getTime(), + }, + { + id: "PET_SKIN_RAT_SQUEAKHEART", + name: "Squeakheart", + texture: "/head/278ca13dc36d2efbc628a8f0265325e589a56475cc6cde1f267e7ed083f1f8ff", + source: "firesale", + release: new Date("2023-11-18 18:00:00 GMT+1").getTime(), + }, ]; /* diff --git a/src/constants/trophy-fish.js b/src/constants/trophy-fish.js index 91e535f008..1824a24431 100644 --- a/src/constants/trophy-fish.js +++ b/src/constants/trophy-fish.js @@ -185,21 +185,4 @@ export const TROPHY_FISH = { }, }; -export const TROPHY_FISH_STAGES = { - 1: { - formatted: `Bronze Hunter`, - type: "bronze", - }, - 2: { - formatted: `Silver Hunter`, - type: "silver", - }, - 3: { - formatted: `Gold Hunter`, - type: "gold", - }, - 4: { - formatted: `Diamond Hunter`, - type: "diamond", - }, -}; +export const TROPHY_FISH_STAGES = ["Bronze Hunter", "Silver Hunter", "Gold Hunter", "Diamond Hunter"]; diff --git a/src/weight/farming-weight.js b/src/constants/weight/farming-weight.js similarity index 50% rename from src/weight/farming-weight.js rename to src/constants/weight/farming-weight.js index f5037549d2..002a038950 100644 --- a/src/weight/farming-weight.js +++ b/src/constants/weight/farming-weight.js @@ -37,7 +37,7 @@ const crops = { }, }; -export function calculateFarmingWeight(profile) { +export function calculateFarmingWeight(userProfile) { const output = { weight: 0, crops: {}, @@ -49,75 +49,82 @@ export function calculateFarmingWeight(profile) { }, }; - if (profile?.collections) { + const farmingCollection = userProfile?.collections?.farming?.collections; + if (farmingCollection !== undefined) { let weight = 0; for (const [name, crop] of Object.entries(crops)) { - let total = profile.collections[name]?.amount ?? 0; - let calculated = total / crop.weight; - weight += calculated; + const { amount = 0 } = farmingCollection.find((a) => a.id === name); + + const calculated = amount / crop.weight; + output.crops[name] = { name: crop.name, weight: calculated, }; + + weight += calculated; } output.weight += weight; - const mushroom_scaling = 90_178.06; + const mushroomScaling = 90_178.06; - let mushroom_collection = profile?.collections?.MUSHROOM_COLLECTION?.amount ?? 0; + const mushroomCollection = farmingCollection.find((a) => a.id === "MUSHROOM_COLLECTION")?.amount ?? 0; - let total = output.weight + mushroom_collection / mushroom_scaling; - let double_break_ratio = total <= 0 ? 0 : (output.crops.CACTUS.weight + output.crops.SUGAR_CANE.weight) / total; - let normal_ratio = total <= 0 ? 0 : (total - output.crops.CACTUS.weight - output.crops.SUGAR_CANE.weight) / total; + const total = output.weight + mushroomCollection / mushroomScaling; + const doubleBreakRatio = total <= 0 ? 0 : (output.crops.CACTUS.weight + output.crops.SUGAR_CANE.weight) / total; + const normalRatio = total <= 0 ? 0 : (total - output.crops.CACTUS.weight - output.crops.SUGAR_CANE.weight) / total; - let mushroom_weight = - double_break_ratio * (mushroom_collection / (2 * mushroom_scaling)) + - normal_ratio * (mushroom_collection / mushroom_scaling); + const mushroomWeight = + doubleBreakRatio * (mushroomCollection / (2 * mushroomScaling)) + + normalRatio * (mushroomCollection / mushroomScaling); output.crops.MUSHROOM = { name: "Mushroom", - weight: mushroom_weight, + weight: mushroomWeight, }; - output.weight += mushroom_weight; + output.weight += mushroomWeight; } output.crop_weight = output.weight; let bonus = 0; - if (profile?.levels?.farming) { - if (profile.levels.farming.level >= 50) { + const farmingSkill = userProfile?.skills?.skills?.farming; + if (farmingSkill) { + if (farmingSkill.level >= 50) { output.bonuses.level.level = 50; output.bonuses.level.weight += 100; bonus += 100; } - if (profile.levels.farming.level >= 60) { + if (farmingSkill.level >= 60) { output.bonuses.level.level = 60; output.bonuses.level.weight += 150; bonus += 150; } } - if (profile?.farming) { - let double_drops = profile.farming?.perks?.double_drops ?? 0; + if (userProfile?.farming) { + const doubleDrops = userProfile.farming?.perks?.double_drops ?? 0; output.bonuses.double_drops = { - double_drops: double_drops, - weight: double_drops * 2, + double_drops: doubleDrops, + weight: doubleDrops * 2, }; - bonus += double_drops * 2; - let gold_medals = profile.farming?.total_badges?.gold ?? 0; - let gold_medal_bonus = Math.min(Math.floor(gold_medals / 50) * 25, 500); + bonus += doubleDrops * 2; + + const goldMedals = userProfile.farming?.total_badges?.gold ?? 0; + const goldMedalBonus = Math.min(Math.floor(goldMedals / 50) * 25, 500); output.bonuses.gold_medals = { - medals: gold_medals, - weight: gold_medal_bonus, + medals: goldMedals, + weight: goldMedalBonus, }; - bonus += gold_medal_bonus; + + bonus += goldMedalBonus; } - if (profile?.minions) { - const farming_minions = [ + if (userProfile?.minions) { + const FARMING_MINIONS = [ "WHEAT", "CARROT", "POTATO", @@ -131,15 +138,17 @@ export function calculateFarmingWeight(profile) { ]; let count = 0; + for (const minion of userProfile.minions.minions.farming.minions) { + if (FARMING_MINIONS.includes(minion.id) === false) { + continue; + } - profile.minions.forEach((minion) => { - if (farming_minions.includes(minion.id)) { - if (minion.maxLevel == 12) { - count++; - bonus += 5; - } + if (minion.maxLevel == 12) { + count++; + bonus += 5; } - }); + } + output.bonuses.minions = { count: count, weight: count * 5, diff --git a/src/constants/weight/lily-weight.js b/src/constants/weight/lily-weight.js new file mode 100644 index 0000000000..fdbcc27684 --- /dev/null +++ b/src/constants/weight/lily-weight.js @@ -0,0 +1,37 @@ +import LilyWeight from "lilyweight"; + +const SKILL_ORDER = ["enchanting", "taming", "alchemy", "mining", "farming", "foraging", "combat", "fishing"]; +const SLAYER_ORDER = ["zombie", "spider", "wolf", "enderman", "blaze"]; + +/** + * converts a dungeon floor into a completion map + * @param {{[key:string]:{stats:{tier_completions:number}}}} floors + * @returns {{[key:string]:number}} + */ +function getTierCompletions(floors = {}) { + return Object.fromEntries( + Object.entries(floors) + .map(([key, value]) => { + if (key === "total" || key === "best") { + return []; + } + + return [key, value.stats.tier_completions ?? 0]; + }) + .filter(([, value]) => value > 0) + ); +} + +export function calculateLilyWeight(userProfile) { + const skillLevels = SKILL_ORDER.map((key) => userProfile.skills.skills[key].uncappedLevel); + const skillXP = SKILL_ORDER.map((key) => userProfile.skills.skills[key].xp); + + const cataCompletions = getTierCompletions(userProfile.dungeons?.catacombs?.floors ?? {}); + const masterCataCompletions = getTierCompletions(userProfile.dungeons?.master_catacombs?.floors ?? {}); + + const cataXP = userProfile.dungeons?.catacombs?.level?.xp ?? 0; + + const slayerXP = SLAYER_ORDER.map((key) => userProfile.slayer?.slayers?.[key]?.level?.xp ?? 0); + + return LilyWeight.getWeightRaw(skillLevels, skillXP, cataCompletions, masterCataCompletions, cataXP, slayerXP); +} diff --git a/src/weight/senither-weight.js b/src/constants/weight/senither-weight.js similarity index 59% rename from src/weight/senither-weight.js rename to src/constants/weight/senither-weight.js index 067e871706..38c2f0e1ad 100644 --- a/src/weight/senither-weight.js +++ b/src/constants/weight/senither-weight.js @@ -1,8 +1,9 @@ +// Weight Calculation by Senither (https://github.com/Senither/) + const level50SkillExp = 55172425; const level60SkillExp = 111672425; // Skill Weight - const skillWeight = { // Maxes out mining at 1,750 points at 60. mining: { @@ -64,36 +65,32 @@ const skillWeight = { }, }; +/** + * Calculates the weight of a skill based on its skill group, level, and experience. + * + * @param {Object} skillGroup - The skill group object containing exponent, divider, and maxLevel properties. + * @param {number} level - The level of the skill. + * @param {number} experience - The experience of the skill. + * @returns {{ weight: number, weight_overflow: number }} The weight and overflow weight of the skill. + */ function calcSkillWeight(skillGroup, level, experience) { - if (skillGroup.exponent == undefined || skillGroup.divider == undefined || level === undefined) { - return { - weight: 0, - weight_overflow: 0, - }; + if (!skillGroup.exponent || !skillGroup.divider || !level) { + return { weight: 0, weight_overflow: 0 }; } - const maxSkillLevelXP = skillGroup.maxLevel == 60 ? level60SkillExp : level50SkillExp; + const maxSkillLevelXP = skillGroup.maxLevel === 60 ? level60SkillExp : level50SkillExp; let base = Math.pow(level * 10, 0.5 + skillGroup.exponent + level / 100) / 1250; - if (experience > maxSkillLevelXP) { - base = Math.round(base); - } - - if (experience <= maxSkillLevelXP) { - return { - weight: base, - weight_overflow: 0, - }; - } + base = experience > maxSkillLevelXP ? Math.round(base) : base; return { weight: base, - weight_overflow: Math.pow((experience - maxSkillLevelXP) / skillGroup.divider, 0.968), + weight_overflow: + experience <= maxSkillLevelXP ? 0 : Math.pow((experience - maxSkillLevelXP) / skillGroup.divider, 0.968), }; } // Dungeons Weight - const dungeonsWeight = { catacombs: 0.0002149604615, healer: 0.0000045254834, @@ -103,24 +100,24 @@ const dungeonsWeight = { tank: 0.0000045254834, }; +/** + * Calculates the weight and weight overflow of a dungeon type based on the level and experience. + * @param {string} type - The type of dungeon. + * @param {number} level - The level of the dungeon. + * @param {number} experience - The experience of the dungeon. + * @returns {{weight: number, weight_overflow: number}} - The weight and weight overflow of the dungeon type. + */ function calcDungeonsWeight(type, level, experience) { if (type.startsWith("master_")) { - return { - weight: 0, - weight_overflow: 0, - }; + return { weight: 0, weight_overflow: 0 }; } const percentageModifier = dungeonsWeight[type]; const level50Experience = 569809640; - const base = Math.pow(level, 4.5) * percentageModifier; if (experience <= level50Experience) { - return { - weight: base, - weight_overflow: 0, - }; + return { weight: base, weight_overflow: 0 }; } const remaining = experience - level50Experience; @@ -129,11 +126,11 @@ function calcDungeonsWeight(type, level, experience) { return { weight: Math.floor(base), weight_overflow: Math.pow(remaining / splitter, 0.968), + total_weight: Math.floor(base) + Math.pow(remaining / splitter, 0.968), }; } // Slayer Weight - const slayerWeight = { zombie: { divider: 2208, @@ -153,45 +150,37 @@ const slayerWeight = { }, }; +/** + * Calculates the weight of a slayer type based on the experience gained. + * @param {string} type - The type of slayer. + * @param {number} experience - The amount of experience gained. + * @returns {{ weight: number, weight_overflow: number }} The weight and overflow weight of the slayer. + */ function calcSlayerWeight(type, experience) { const sw = slayerWeight[type]; if (!sw) { - return { - weight: 0, - weight_overflow: 0, - }; + return { weight: 0, weight_overflow: 0 }; } if (!experience || experience <= 1000000) { - return { - weight: !experience ? 0 : experience / sw.divider, // for some reason experience can be undefined - weight_overflow: 0, - }; + return { weight: experience ? experience / sw.divider : 0, weight_overflow: 0 }; } const base = 1000000 / sw.divider; let remaining = experience - 1000000; - let modifier = sw.modifier; let overflow = 0; while (remaining > 0) { const left = Math.min(remaining, 1000000); - overflow += Math.pow(left / (sw.divider * (1.5 + modifier)), 0.942); modifier += sw.modifier; remaining -= left; } - return { - weight: base, - weight_overflow: overflow, - }; + return { weight: base, weight_overflow: overflow }; } -/* - All weight calculations are provided by Senither (https://github.com/Senither/) - */ export function calculateSenitherWeight(profile) { const output = { @@ -212,13 +201,18 @@ export function calculateSenitherWeight(profile) { }; // skill - for (const skillName in profile.levels) { - const data = profile.levels[skillName]; + for (const skill in profile.skills.skills) { + const skillData = profile.skills.skills[skill]; + + const sw = calcSkillWeight(skillWeight[skill], skillData.unlockableLevelWithProgress, skillData.xp); - const sw = calcSkillWeight(skillWeight[skillName], data.unlockableLevelWithProgress, data.xp); + output.skill.skills[skill] = { + weight: sw.weight, + overflow_weight: sw.weight_overflow, + total_weight: sw.weight + sw.weight_overflow, + }; - output.skill.skills[skillName] = sw.weight + sw.weight_overflow; - output.skill.total += output.skill.skills[skillName]; + output.skill.total += output.skill.skills[skill].total_weight; } // dungeon weight @@ -234,31 +228,34 @@ export function calculateSenitherWeight(profile) { } // dungeon classes - if (dungeons.classes) { - for (const className of Object.keys(dungeons.classes)) { - const dungeonClass = dungeons.classes[className]; - const xp = dungeonClass.experience; + if (dungeons?.classes?.classes) { + for (const className of Object.keys(dungeons.classes.classes)) { + const dungeonClass = dungeons.classes.classes[className]; + const xp = dungeonClass.level; const levelWithProgress = xp.levelWithProgress; const classWeight = calcDungeonsWeight(className, levelWithProgress, xp.xp); - output.dungeon.total += classWeight.weight + classWeight.weight_overflow ?? 0; output.dungeon.classes[className] = classWeight; + + output.dungeon.total += classWeight.weight + classWeight.weight_overflow ?? 0; } } // slayer - for (const slayerName in profile.slayers) { - const data = profile.slayers[slayerName]; - if (data === undefined) { - continue; - } + for (const slayerName in profile.slayer?.slayers) { + const slayer = profile.slayer.slayers[slayerName]; + + const sw = calcSlayerWeight(slayerName, slayer.level.xp); - const sw = calcSlayerWeight(slayerName, data.level.xp); + output.slayer.slayers[slayerName] = { + weight: sw.weight, + overflow_weight: sw.weight_overflow, + total_weight: sw.weight + sw.weight_overflow, + }; - output.slayer.slayers[slayerName] = sw.weight + sw.weight_overflow; - output.slayer.total += output.slayer.slayers[slayerName]; + output.slayer.total += output.slayer.slayers[slayerName].total_weight; } output.overall = [output.dungeon.total, output.skill.total, output.slayer.total] diff --git a/src/credentials.js b/src/credentials.js index a0ceb7259a..91eaec0a3c 100644 --- a/src/credentials.js +++ b/src/credentials.js @@ -43,29 +43,33 @@ function writeFile(newValue) { let hasBeenModified = false; /** @type {Credentials} */ -const credentials = readFile() ?? {}; +const CREDENTIALS = readFile() ?? {}; for (const key in defaultCredentials) { - if (credentials[key] == undefined) { - credentials[key] = defaultCredentials[key]; + if (CREDENTIALS[key] == undefined) { + CREDENTIALS[key] = defaultCredentials[key]; hasBeenModified = true; } } if (hasBeenModified) { - writeFile(credentials); + writeFile(CREDENTIALS); } if (process.env.HYPIXEL_API_KEY) { - credentials.hypixel_api_key = process.env.HYPIXEL_API_KEY; + CREDENTIALS.hypixel_api_key = process.env.HYPIXEL_API_KEY; } if (process.env.MONGO_CONNECTION_STRING) { - credentials.dbUrl = process.env.MONGO_CONNECTION_STRING; + CREDENTIALS.dbUrl = process.env.MONGO_CONNECTION_STRING; } if (process.env.REDIS_CONNECTION_STRING) { - credentials.redisUrl = process.env.REDIS_CONNECTION_STRING; + CREDENTIALS.redisUrl = process.env.REDIS_CONNECTION_STRING; } -export default credentials; +if (process.env.DISCORD_WEBHOOK) { + CREDENTIALS.discord_webhook = process.env.DISCORD_WEBHOOK; +} + +export default CREDENTIALS; diff --git a/src/custom-resources.js b/src/custom-resources.js index e676d24a44..3d251b46e6 100644 --- a/src/custom-resources.js +++ b/src/custom-resources.js @@ -487,7 +487,16 @@ async function loadResourcePacks() { const apng = UPNG.encode(pngFrames, NORMALIZED_SIZE, NORMALIZED_SIZE, 0, pngDelays); await fs.writeFile(textureFile, Buffer.from(apng)); - await execFile(apng2gif, [textureFile, textureFile.replace(".png", ".gif")]); + + try { + if (fs.existsSync(textureFile.replace(".png", ".gif"))) { + await execFile(apng2gif, [textureFile, "-o", textureFile.replace(".png", ".gif")]); + } else { + await execFile(apng2gif, [textureFile, "-o", textureFile]); + } + } catch (error) { + console.log(error); + } } } @@ -554,8 +563,8 @@ export async function getTexture(item, options) { if ( options.ignore_id === false && - (("skyblock_id" in texture && texture.skyblock_id != (item?.tag?.ExtraAttributes?.id ?? "")) || - (!("skyblock_id" in texture) && item?.tag?.ExtraAttributes?.id !== undefined)) + texture.skyblock_id === undefined && + (!texture.match || texture.skyblock_id === item.tag?.ExtraAttributes?.id) ) { continue; } diff --git a/src/hashes.js b/src/hashes.js index 8265f31630..db1134d0d7 100644 --- a/src/hashes.js +++ b/src/hashes.js @@ -6,7 +6,7 @@ import { promisify } from "util"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -export const hashedDirectories = ["css"]; +export const HASHED_DIRECTORIES = ["css"]; export function getFileHash(filename) { return new Promise((resolve, reject) => { @@ -26,7 +26,7 @@ export function getFileHash(filename) { } export function getFileHashes() { - const directoryPromises = hashedDirectories.map(async (directory) => { + const directoryPromises = HASHED_DIRECTORIES.map(async (directory) => { const readdirPromise = promisify(fs.readdir); const fileNames = await readdirPromise(path.join(__dirname, "../public/resources", directory)); @@ -49,8 +49,8 @@ export function getFileHashes() { return Promise.all(directoryPromises).then((directories) => { const directoriesObject = {}; - for (let i = 0; i < hashedDirectories.length; i++) { - directoriesObject[hashedDirectories[i]] = directories[i]; + for (let i = 0; i < HASHED_DIRECTORIES.length; i++) { + directoriesObject[HASHED_DIRECTORIES[i]] = directories[i]; } return directoriesObject; diff --git a/src/helper.js b/src/helper.js index eb0f754142..aecea7041d 100644 --- a/src/helper.js +++ b/src/helper.js @@ -624,9 +624,9 @@ export function getClusterId(fullName = false) { return cluster.isWorker ? `w${cluster.worker.id}` : "m"; } -export const generateDebugId = (endpointName = "unknown") => { +export function generateDebugId(endpointName = "unknown") { return `${getClusterId()}/${endpointName}_${Date.now()}.${Math.floor(Math.random() * 9000 + 1000)}`; -}; +} export function generateUUID() { let u = "", @@ -661,22 +661,22 @@ export function parseItemGems(gems, rarity) { const parsed = []; for (const [key, value] of Object.entries(gems)) { - const slot_type = key.split("_")[0]; + const slotType = key.split("_")[0]; - if (slots.ignore.includes(key) || (slots.special.includes(slot_type) && key.endsWith("_gem"))) { + if (slots.ignore.includes(key) || (slots.special.includes(slotType) && key.endsWith("_gem"))) { continue; } - if (slots.special.includes(slot_type)) { + if (slots.special.includes(slotType)) { parsed.push({ - slot_type, + slot_type: slotType, slot_number: +key.split("_")[1], gem_type: gems[`${key}_gem`], gem_tier: value?.quality || value, }); - } else if (slots.normal.includes(slot_type)) { + } else if (slots.normal.includes(slotType)) { parsed.push({ - slot_type, + slot_type: slotType, slot_number: +key.split("_")[1], gem_type: key.split("_")[0], gem_tier: value?.quality || value, @@ -713,19 +713,19 @@ export function generateGemLore(type, tier, rarity) { // Gem stats if (rarity) { - const gemstone_stats = GEMSTONES[type.toUpperCase()]?.stats?.[tier.toUpperCase()]; - if (gemstone_stats) { - Object.keys(gemstone_stats).forEach((stat) => { - let stat_value = gemstone_stats[stat][rarityNameToInt(rarity)]; + const gemstoneStats = GEMSTONES[type.toUpperCase()]?.stats?.[tier.toUpperCase()]; + if (gemstoneStats) { + Object.keys(gemstoneStats).forEach((stat) => { + let statValue = gemstoneStats[stat][rarityNameToInt(rarity)]; // Fallback since skyblock devs didn't code all gemstone stats for divine rarity yet // ...they didn't expect people to own divine tier items other than divan's drill - if (rarity.toUpperCase() === "DIVINE" && stat_value === null) { - stat_value = gemstone_stats[stat][rarityNameToInt("MYTHIC")]; + if (rarity.toUpperCase() === "DIVINE" && statValue === null) { + statValue = gemstoneStats[stat][rarityNameToInt("MYTHIC")]; } - if (stat_value) { - stats.push(["§", STATS_DATA[stat].color, "+", stat_value, " ", STATS_DATA[stat].symbol].join("")); + if (statValue) { + stats.push(["§", STATS_DATA[stat].color, "+", statValue, " ", STATS_DATA[stat].symbol].join("")); } else { stats.push("§c§oMISSING VALUE§r"); } @@ -775,7 +775,7 @@ export function generateItem(data) { }; } - const default_data = { + const DEFAULT_DATA = { id: 389, Damage: 0, Count: 1, @@ -798,8 +798,24 @@ export function generateItem(data) { data.rarity = data.rarity.toLowerCase(); } + if (data.name && (data.display_name === undefined || data.display_name?.length === 0)) { + data.display_name = data.name; + } + + if (!data.rarity && data.tier) { + data.rarity = data.tier.toLowerCase(); + } + + if (data.item_id) { + data.id = data.item_id; + } + + if (data.damage) { + data.Damage = data.damage; + } + // Setting tag.display.Name using display_name if not specified - if (data.display_name && !data.tag.display.Name) { + if (data.display_name && !data.tag?.display?.Name) { data.tag = data.tag ?? {}; data.tag.display = data.tag.display ?? {}; const rarityColor = data.rarity ? `§${RARITY_COLORS[data.rarity ?? "common"]}` : ""; @@ -807,7 +823,7 @@ export function generateItem(data) { } // Creating final item - return Object.assign(default_data, data); + return Object.assign(DEFAULT_DATA, data); } /** @@ -1008,14 +1024,20 @@ export function romanize(num) { } export async function getBingoGoals(db, cacheOnly = false) { - const output = await db.collection("bingoData").findOne({ _id: "cardData" }); + const cachedBingoData = await db.collection("bingoData").findOne({ _id: "cardData" }); + const output = cachedBingoData?.output; if (cacheOnly === true) { return output; } - // 12 hours cache - if (output === null || output.last_save + 43200000 < Date.now()) { + // Check if it's the first day of the month and the data hasn't been updated in the last 24 hours, + // or if the data hasn't been updated in the last 12 hours (in case Hypixel changes goals). + if ( + output == null || + output.last_save + 43200000 < Date.now() || + (new Date().getUTCDate() === 1 && output.last_save + 86400000 < Date.now()) + ) { const { data: output } = await axios.get("https://api.hypixel.net/resources/skyblock/bingo"); output.last_save = Date.now(); @@ -1074,12 +1096,29 @@ export function getCommitHash() { */ } -export function RGBtoHex(rgb) { +export function rgbToHex(rgb) { const [r, g, b] = rgb.split(",").map((c) => parseInt(c.trim())); return [r, g, b].map((c) => c.toString(16).padStart(2, "0")).join(""); } +/** + * Returns a formatted progress bar string based on the given amount and total. + * + * @param {number} amount - The current amount. + * @param {number} total - The total amount. + * @param {string} [color="a"] - The color of the progress bar. + * @returns {string} The formatted progress bar string. + */ +export function formatProgressBar(amount, total, completedColor = "a", missingColor = "f") { + const barLength = 25; + const progress = Math.min(1, amount / total); + const progressBars = Math.floor(progress * barLength); + const emptyBars = barLength - progressBars; + + return `${`§${completedColor}§l§m-`.repeat(progressBars)}${`§${missingColor}§l§m-`.repeat(emptyBars)}§r`; +} + /** * Adds lore to an item's display tag. * diff --git a/src/helper/item.js b/src/helper/item.js index 110fb4654e..0517c781f8 100644 --- a/src/helper/item.js +++ b/src/helper/item.js @@ -28,7 +28,7 @@ export async function getItemData(query = {}) { query.damage = new Number(split[1]); } - dbItem = Object.assign(item, await db.collection("items").findOne({ id: query.skyblockId })); + dbItem = { ...item, ...(await db.collection("items").findOne({ id: query.skyblockId })) }; } if (query.name !== undefined) { diff --git a/src/leaderboards.js b/src/leaderboards.js index f25553e5be..534e2604c6 100644 --- a/src/leaderboards.js +++ b/src/leaderboards.js @@ -1,10 +1,9 @@ -import { COLLECTION_DATA } from "./constants/collections.js"; +import { db } from "./mongo.js"; import moment from "moment"; import momentDurationFormat from "moment-duration-format"; +import { getLevelByXp } from "./stats/skills/leveling.js"; momentDurationFormat(moment); -import { getLevelByXp } from "./lib.js"; - const defaultOptions = { mappedBy: "uuid", sortedBy: -1, @@ -82,14 +81,20 @@ export default (name) => { } } - if (lbName.startsWith("collection_")) { - const collectionName = lbName.split("_").slice(1).join("_").toUpperCase(); - const collectionData = COLLECTION_DATA.filter((a) => a.skyblockId == collectionName); - - if (collectionData.length > 0) { - options["name"] = collectionData[0].name + " Collection"; + async () => { + const { collections: COLLECTION_DATA } = await db.collection("collections").findOne({ _id: "collections" }); + if (lbName.startsWith("collection_")) { + const collectionName = lbName.split("_").slice(1).join("_").toUpperCase(); + const collectionData = Object.values(COLLECTION_DATA) + .map((a) => a.items) + .flat() + .find((a) => a.id === collectionName); + + if (collectionData?.length > 0) { + options["name"] = collectionData[0].name + " Collection"; + } } - } + }; if (lbName.includes("_best_time") || lbName.includes("_fastest_time")) { options["sortedBy"] = 1; diff --git a/src/lib.js b/src/lib.js index da0d2b22e5..b87a9c7e77 100644 --- a/src/lib.js +++ b/src/lib.js @@ -1,3747 +1,194 @@ -import retry from "async-retry"; -import axios from "axios"; -import _ from "lodash"; -import minecraftData from "minecraft-data"; -import { getItemNetworth, getPreDecodedNetworth } from "skyhelper-networth"; -import moment from "moment"; -import sanitize from "mongo-sanitize"; -import path from "path"; -import nbt from "prismarine-nbt"; -import { fileURLToPath } from "url"; -import util from "util"; -import { v4 } from "uuid"; - -import * as stats from "./stats.js"; -import * as constants from "./constants.js"; -import credentials from "./credentials.js"; -import { getTexture } from "./custom-resources.js"; -import * as helper from "./helper.js"; -import { db } from "./mongo.js"; -import { redisClient } from "./redis.js"; -import { calculateLilyWeight } from "./weight/lily-weight.js"; -import { calculateSenitherWeight } from "./weight/senither-weight.js"; -import { getLeaderboardPosition } from "./helper/leaderboards.js"; -import { calculateFarmingWeight } from "./weight/farming-weight.js"; - -const mcData = minecraftData("1.8.9"); -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const hypixel = axios.create({ - baseURL: "https://api.hypixel.net/", -}); -const parseNbt = util.promisify(nbt.parse); - -function getMinMax(profiles, min, ...path) { - let output = null; - - const compareValues = profiles.map((a) => helper.getPath(a, ...path)).filter((a) => !isNaN(a)); - - if (compareValues.length == 0) { - return output; - } - - if (min) { - output = Math.min(...compareValues); - } else { - output = Math.max(...compareValues); - } - - if (isNaN(output)) { - return null; - } - - return output; -} - -function getMax(profiles, ...path) { - return getMinMax(profiles, false, ...path); -} - -function getAllKeys(profiles, ...path) { - return _.uniq([].concat(...profiles.map((a) => _.keys(helper.getPath(a, ...path))))); -} - -/** - * gets the xp table for the given type - * @param {string} type - * @returns {{[key: number]: number}} - */ -function getXpTable(type) { - switch (type) { - case "runecrafting": - return constants.RUNECRAFTING_XP; - case "social": - return constants.SOCIAL_XP; - case "dungeoneering": - return constants.DUNGEONEERING_XP; - case "hotm": - return constants.HOTM_XP; - case "skyblock_level": - return constants.SKYBLOCK_XP; - default: - return constants.LEVELING_XP; - } -} - -/** - * estimates the xp based on the level - * @param {number} uncappedLevel - * @param {{type?: string, cap?: number, skill?: string}} extra - * @param type the type of levels (used to determine which xp table to use) - * @param cap override the cap highest level the player can reach - * @param skill the key of default_skill_caps - */ -function getXpByLevel(uncappedLevel, extra = {}) { - const xpTable = getXpTable(extra.type); - - if (typeof uncappedLevel !== "number" || isNaN(uncappedLevel)) { - uncappedLevel = 0; - } - - /** the level that this player is caped at */ - const levelCap = - extra.cap ?? - Math.max(uncappedLevel, constants.DEFAULT_SKILL_CAPS[extra.skill]) ?? - Math.max(...Object.keys(xpTable).map((a) => Number(a))); - - /** the maximum level that any player can achieve (used for gold progress bars) */ - const maxLevel = constants.MAXED_SKILL_CAPS[extra.skill] ?? levelCap; - - /** the amount of xp over the amount required for the level (used for calculation progress to next level) */ - const xpCurrent = 0; - - /** the sum of all levels including level */ - let xp = 0; - - for (let x = 1; x <= uncappedLevel; x++) { - xp += xpTable[x]; - } - - /** the level as displayed by in game UI */ - const level = Math.min(levelCap, uncappedLevel); - - /** the amount amount of xp needed to reach the next level (used for calculation progress to next level) */ - const xpForNext = level < maxLevel ? Math.ceil(xpTable[level + 1]) : Infinity; - - /** the fraction of the way toward the next level */ - const progress = level < maxLevel ? 0.05 : 0; - - /** a floating point value representing the current level for example if you are half way to level 5 it would be 4.5 */ - const levelWithProgress = level + progress; - - return { - xp, - level, - maxLevel, - xpCurrent, - xpForNext, - progress, - levelCap, - uncappedLevel, - levelWithProgress, - }; -} - -/** - * gets the level and some other information from an xp amount - * @param {number} xp - * @param {{type?: string, cap?: number, skill?: string, ignoreCap?: boolean, infinite?: boolean }} extra - * @param type the type of levels (used to determine which xp table to use) - * @param cap override the cap highest level the player can reach - * @param skill the id of the skill (used to determine the default cap) - * @param ignoreCap whether to ignore the in-game cap or not - * @param infinite repeats the last level's experience requirement infinitely - * @param skill the key of default_skill_caps - */ -export function getLevelByXp(xp, extra = {}) { - const xpTable = getXpTable(extra.type); - - if (typeof xp !== "number" || isNaN(xp)) { - xp = 0; - } - - /** the level that this player is caped at */ - const levelCap = - extra.cap ?? constants.DEFAULT_SKILL_CAPS[extra.skill] ?? Math.max(...Object.keys(xpTable).map(Number)); - - /** the level ignoring the cap and using only the table */ - let uncappedLevel = 0; - - /** the amount of xp over the amount required for the level (used for calculation progress to next level) */ - let xpCurrent = xp; - - /** like xpCurrent but ignores cap */ - let xpRemaining = xp; - - while (xpTable[uncappedLevel + 1] <= xpRemaining) { - uncappedLevel++; - xpRemaining -= xpTable[uncappedLevel]; - if (uncappedLevel <= levelCap) { - xpCurrent = xpRemaining; - } - } - - /** adds support for infinite leveling (dungeoneering and skyblock level) */ - if (extra.infinite) { - const maxExperience = Object.values(xpTable).at(-1); - - uncappedLevel += Math.floor(xpRemaining / maxExperience); - xpRemaining %= maxExperience; - xpCurrent = xpRemaining; - } - - /** the maximum level that any player can achieve (used for gold progress bars) */ - const maxLevel = - extra.ignoreCap && uncappedLevel >= levelCap ? uncappedLevel : constants.MAXED_SKILL_CAPS[extra.skill] ?? levelCap; - - /** the maximum amount of experience that any player can acheive (used for skyblock level gold progress bar) */ - const maxExperience = constants.MAXED_SKILL_XP[extra.skill]; - - // not sure why this is floored but I'm leaving it in for now - xpCurrent = Math.floor(xpCurrent); - - /** the level as displayed by in game UI */ - const level = extra.ignoreCap ? uncappedLevel : Math.min(levelCap, uncappedLevel); - - /** the amount amount of xp needed to reach the next level (used for calculation progress to next level) */ - const xpForNext = - level < maxLevel ? Math.ceil(xpTable[level + 1] ?? Object.values(xpTable).at(-1)) : maxExperience ?? Infinity; - - /** the fraction of the way toward the next level */ - const progress = level >= maxLevel ? (extra.ignoreCap ? 1 : 0) : Math.max(0, Math.min(xpCurrent / xpForNext, 1)); - - /** a floating point value representing the current level for example if you are half way to level 5 it would be 4.5 */ - const levelWithProgress = level + progress; - - /** a floating point value representing the current level ignoring the in-game unlockable caps for example if you are half way to level 5 it would be 4.5 */ - const unlockableLevelWithProgress = extra.cap ? Math.min(uncappedLevel + progress, maxLevel) : levelWithProgress; - - console.log(xpForNext); - - return { - xp, - level, - maxLevel, - xpCurrent, - maxExperience, - xpForNext, - progress, - levelCap, - uncappedLevel, - levelWithProgress, - unlockableLevelWithProgress, - }; -} - -function getSlayerLevel(slayer, slayerName) { - const { xp = 0, claimed_levels } = slayer; - - let currentLevel = 0; - let progress = 0; - let xpForNext = 0; - - if (constants.SLAYER_XP[slayerName] === undefined) { - return { - currentLevel, - xp: 0, - maxLevel: 0, - progress, - xpForNext, - }; - } - - const maxLevel = Math.max(...Object.keys(constants.SLAYER_XP[slayerName])); - - for (const level_name in claimed_levels) { - // Ignoring legacy levels for zombie - if (slayerName === "zombie" && ["level_7", "level_8", "level_9"].includes(level_name)) { - continue; - } - - const level = parseInt(level_name.split("_")[1]); - - if (level > currentLevel) { - currentLevel = level; - } - } - - if (currentLevel < maxLevel) { - const nextLevel = constants.SLAYER_XP[slayerName][currentLevel + 1]; - - progress = xp / nextLevel; - xpForNext = nextLevel; - } else { - progress = 1; - } - - return { currentLevel, xp, maxLevel, progress, xpForNext }; -} - -function getPetLevel(petExp, offsetRarity, maxLevel) { - const rarityOffset = constants.PET_RARITY_OFFSET[offsetRarity]; - const levels = constants.PET_LEVELS.slice(rarityOffset, rarityOffset + maxLevel - 1); - - const xpMaxLevel = levels.reduce((a, b) => a + b, 0); - let xpTotal = 0; - let level = 1; - - let xpForNext = Infinity; - - for (let i = 0; i < maxLevel; i++) { - xpTotal += levels[i]; - - if (xpTotal > petExp) { - xpTotal -= levels[i]; - break; - } else { - level++; - } - } - - let xpCurrent = Math.floor(petExp - xpTotal); - let progress; - - if (level < maxLevel) { - xpForNext = Math.ceil(levels[level - 1]); - progress = Math.max(0, Math.min(xpCurrent / xpForNext, 1)); - } else { - level = maxLevel; - xpCurrent = petExp - levels[maxLevel - 1]; - xpForNext = 0; - progress = 1; - } - - return { - level, - xpCurrent, - xpForNext, - progress, - xpMaxLevel, - }; -} - -async function getBackpackContents(arraybuf) { - const buf = Buffer.from(arraybuf); - - let data = await parseNbt(buf); - data = nbt.simplify(data); - - const items = data.i; - - for (const [index, item] of items.entries()) { - item.isInactive = true; - item.inBackpack = true; - item.item_index = index; - } - - return items; -} - -// Process items returned by API -async function processItems(base64, source, customTextures = false, packs, cacheOnly = false) { - // API stores data as base64 encoded gzipped Minecraft NBT data - const buf = Buffer.from(base64, "base64"); - - let data = await parseNbt(buf); - data = nbt.simplify(data); - - let items = data.i; - - // Check backpack contents and add them to the list of items - for (const [index, item] of items.entries()) { - if ( - item.tag?.display?.Name.includes("Backpack") || - ["NEW_YEAR_CAKE_BAG", "BUILDERS_WAND", "BASKET_OF_SEEDS"].includes(item.tag?.ExtraAttributes?.id) - ) { - let backpackData; - - for (const key of Object.keys(item.tag.ExtraAttributes)) { - if (key.endsWith("_data")) backpackData = item.tag.ExtraAttributes[key]; - } - - if (!Array.isArray(backpackData)) { - continue; - } - - const backpackContents = await getBackpackContents(backpackData); - - for (const backpackItem of backpackContents) { - backpackItem.backpackIndex = index; - } - - item.containsItems = []; - - items.push(...backpackContents); - } - - if ( - item.tag?.ExtraAttributes?.id?.includes("PERSONAL_COMPACTOR_") || - item.tag?.ExtraAttributes?.id?.includes("PERSONAL_DELETOR_") - ) { - item.containsItems = []; - for (const key in item.tag.ExtraAttributes) { - if (key.startsWith("personal_compact_") || key.startsWith("personal_deletor_")) { - const hypixelItem = await db.collection("items").findOne({ id: item.tag.ExtraAttributes[key] }); - - const itemData = { - Count: 1, - Damage: hypixelItem?.damage ?? 3, - id: hypixelItem?.item_id ?? 397, - itemIndex: item.containsItems.length, - glowing: hypixelItem?.glowing ?? false, - display_name: hypixelItem?.name ?? _.startCase(item.tag.ExtraAttributes[key].replace(/_/g, " ")), - rarity: hypixelItem?.tier ?? "common", - categories: [], - }; - - if (hypixelItem?.texture !== undefined) { - itemData.texture_path = `/head/${hypixelItem.texture}`; - } - - if (itemData.id >= 298 && itemData.id <= 301) { - const type = ["helmet", "chestplate", "leggings", "boots"][itemData.id - 298]; - - if (hypixelItem?.color !== undefined) { - const color = helper.RGBtoHex(hypixelItem.color) ?? "955e3b"; - - itemData.texture_path = `/leather/${type}/${color}`; - } - } - - if (hypixelItem === null) { - itemData.texture_path = "/head/bc8ea1f51f253ff5142ca11ae45193a4ad8c3ab5e9c6eec8ba7a4fcb7bac40"; - } - - item.containsItems.push(itemData); - } - } - } - } - - for (const item of items) { - // Get extra info about certain things - if (item.tag?.ExtraAttributes != undefined) { - item.extra = { - hpbs: 0, - }; - } - - if (item.tag?.ExtraAttributes?.rarity_upgrades != undefined) { - const { rarity_upgrades } = item.tag.ExtraAttributes; - - if (rarity_upgrades > 0) { - item.extra.recombobulated = true; - } - } - - if (item.tag?.ExtraAttributes?.model != undefined) { - item.extra.model = item.tag.ExtraAttributes.model; - } - - if (item.tag?.ExtraAttributes?.hot_potato_count != undefined) { - item.extra.hpbs = item.tag.ExtraAttributes.hot_potato_count; - } - - if (item.tag?.ExtraAttributes?.expertise_kills != undefined) { - const { expertise_kills } = item.tag.ExtraAttributes; - - if (expertise_kills > 0) { - item.extra.expertise_kills = expertise_kills; - } - } - - if (item.tag?.ExtraAttributes?.hecatomb_s_runs != undefined) { - const { hecatomb_s_runs } = item.tag.ExtraAttributes; - - if (hecatomb_s_runs > 0) { - item.extra.hecatomb_s_runs = hecatomb_s_runs; - } - } - - if (item.tag?.ExtraAttributes?.champion_combat_xp != undefined) { - const { champion_combat_xp } = item.tag.ExtraAttributes; - - if (champion_combat_xp > 0) { - item.extra.champion_combat_xp = champion_combat_xp; - } - } - - if (item.tag?.ExtraAttributes?.farmed_cultivating != undefined) { - const { farmed_cultivating } = item.tag.ExtraAttributes; - - if (farmed_cultivating > 0) { - item.extra.farmed_cultivating = item.tag?.ExtraAttributes?.mined_crops?.toString() ?? farmed_cultivating; - } - } - - if (item.tag?.ExtraAttributes?.blocks_walked != undefined) { - const { blocks_walked } = item.tag.ExtraAttributes; - - if (blocks_walked > 0) { - item.extra.blocks_walked = blocks_walked; - } - } - - if (item.tag?.ExtraAttributes?.timestamp != undefined) { - const timestamp = item.tag.ExtraAttributes.timestamp; - - if (!isNaN(timestamp)) { - item.extra.timestamp = timestamp; - } else { - item.extra.timestamp = Date.parse(timestamp + " EDT"); - } - - item.extra.timestamp; - } - - if (item.tag?.ExtraAttributes?.spawnedFor != undefined) { - item.extra.spawned_for = item.tag.ExtraAttributes.spawnedFor.replaceAll("-", ""); - } - - if (item.tag?.ExtraAttributes?.baseStatBoostPercentage != undefined) { - item.extra.base_stat_boost = item.tag.ExtraAttributes.baseStatBoostPercentage; - } - - if (item.tag?.ExtraAttributes?.item_tier != undefined) { - item.extra.floor = item.tag.ExtraAttributes.item_tier; - } - - if (item.tag?.ExtraAttributes?.winning_bid != undefined) { - item.extra.price_paid = item.tag.ExtraAttributes.winning_bid; - } - - if (item.tag?.ExtraAttributes?.modifier != undefined) { - item.extra.reforge = item.tag.ExtraAttributes.modifier; - } - - if (item.tag?.ExtraAttributes?.ability_scroll != undefined) { - item.extra.ability_scroll = item.tag.ExtraAttributes.ability_scroll; - } - - if (item.tag?.ExtraAttributes?.mined_crops != undefined) { - item.extra.crop_counter = item.tag.ExtraAttributes.mined_crops; - } - - if (item.tag?.ExtraAttributes?.petInfo != undefined) { - item.tag.ExtraAttributes.petInfo = JSON.parse(item.tag.ExtraAttributes.petInfo); - } - - if (item.tag?.ExtraAttributes?.gems != undefined) { - item.extra.gems = item.tag.ExtraAttributes.gems; - } - - if (item.tag?.ExtraAttributes?.skin != undefined) { - item.extra.skin = item.tag.ExtraAttributes.skin; - } - - if (item.tag?.ExtraAttributes?.petInfo?.skin != undefined) { - item.extra.skin = `PET_SKIN_${item.tag.ExtraAttributes.petInfo.skin}`; - } - - // Set custom texture for colored leather armor - if (typeof item.id === "number" && item.id >= 298 && item.id <= 301) { - const color = item.tag?.display?.color?.toString(16).padStart(6, "0") ?? "955e3b"; - - const type = ["helmet", "chestplate", "leggings", "boots"][item.id - 298]; - - item.texture_path = `/leather/${type}/${color}`; - } - - // Set custom texture for colored potions - if (item.id == 373) { - const color = constants.POTION_COLORS[item.Damage % 16]; - - const type = item.Damage & 16384 ? "splash" : "normal"; - - item.texture_path = `/potion/${type}/${color}`; - } - - // Set raw display name without color and formatting codes - if (item.tag?.display?.Name != undefined) { - item.display_name = helper.getRawLore(item.tag.display.Name); - } - - // Resolve skull textures to their image path - if ( - Array.isArray(item.tag?.SkullOwner?.Properties?.textures) && - item.tag.SkullOwner.Properties.textures.length > 0 - ) { - try { - const json = JSON.parse(Buffer.from(item.tag.SkullOwner.Properties.textures[0].Value, "base64").toString()); - const url = json.textures.SKIN.url; - const uuid = url.split("/").pop(); - - item.texture_path = `/head/${uuid}?v6`; - } catch (e) { - console.error(e); - } - } - - const animatedTexture = helper.getAnimatedTexture(item); - - // Gives animated texture on certain items, will be overwritten by custom textures - if (animatedTexture) { - item.texture_path = animatedTexture.texture; - } - - // Uses animated skin texture - if (item?.extra?.skin != undefined && constants.ANIMATED_ITEMS?.[item.extra.skin]) { - item.texture_path = constants.ANIMATED_ITEMS[item.extra.skin].texture; - } - - if (item.tag?.ExtraAttributes?.skin == undefined && customTextures) { - const customTexture = await getTexture(item, { - ignore_id: false, - pack_ids: packs, - }); - - if (customTexture) { - item.animated = customTexture.animated; - item.texture_path = "/" + customTexture.path; - item.texture_pack = customTexture.pack.config; - item.texture_pack.base_path = - "/" + path.relative(path.resolve(__dirname, "..", "public"), customTexture.pack.base_path); - } - } - - if (source !== undefined) { - item.extra ??= {}; - item.extra.source = source - .split(" ") - .map((a) => a.charAt(0).toUpperCase() + a.slice(1)) - .join(" "); - } - - // Lore stuff - const itemLore = item?.tag?.display?.Lore ?? []; - const lore_raw = [...itemLore]; - - const lore = lore_raw != null ? lore_raw.map((a) => (a = helper.getRawLore(a))) : []; - - item.rarity = null; - item.categories = []; - - if (lore.length > 0) { - // item categories, rarity, recombobulated, dungeon, shiny - const itemType = helper.parseItemTypeFromLore(lore, item); - - for (const key in itemType) { - item[key] = itemType[key]; - } - - // fix custom maps texture - if (item.id == 358) { - item.id = 395; - item.Damage = 0; - } - } - - // Set HTML lore to be displayed on the website - if (itemLore.length > 0) { - if (item.extra?.recombobulated) { - itemLore.push("§8(Recombobulated)"); - } - - if (item.extra?.gems) { - itemLore.push( - "", - "§7Applied Gemstones:", - ...helper.parseItemGems(item.extra.gems, item.rarity).map((gem) => `§7 - ${gem.lore}`) - ); - } - - if (item.extra?.expertise_kills) { - const expertise_kills = item.extra.expertise_kills; - - if (lore_raw) { - itemLore.push("", `§7Expertise Kills: §c${expertise_kills.toLocaleString()}`); - if (expertise_kills >= 15000) { - itemLore.push(`§8MAXED OUT!`); - } else { - let toNextLevel = 0; - for (const e of constants.EXPERTISE_KILLS_LADDER) { - if (expertise_kills < e) { - toNextLevel = e - expertise_kills; - break; - } - } - itemLore.push(`§8${toNextLevel.toLocaleString()} kills to tier up!`); - } - } - } - - if (item.extra?.hecatomb_s_runs) { - const hecatomb_s_runs = item.extra.hecatomb_s_runs; - - if (lore_raw) { - itemLore.push("", `§7Hecatomb Runs: §c${hecatomb_s_runs.toLocaleString()}`); - if (hecatomb_s_runs >= 100) { - itemLore.push(`§8MAXED OUT!`); - } else { - let toNextLevel = 0; - for (const e of constants.hecatomb_s_runs_ladder) { - if (hecatomb_s_runs < e) { - toNextLevel = e - hecatomb_s_runs; - break; - } - } - itemLore.push(`§8${toNextLevel.toLocaleString()} runs to tier up!`); - } - } - } - - if (item.extra?.champion_combat_xp) { - const champion_combat_xp = Math.floor(item.extra.champion_combat_xp); - - if (lore_raw) { - itemLore.push("", `§7Champion XP: §c${champion_combat_xp.toLocaleString()}`); - if (champion_combat_xp >= 3000000) { - itemLore.push(`§8MAXED OUT!`); - } else { - let toNextLevel = 0; - for (const e of constants.champion_xp_ladder) { - if (champion_combat_xp < e) { - toNextLevel = Math.floor(e - champion_combat_xp); - break; - } - } - itemLore.push(`§8${toNextLevel.toLocaleString()} xp to tier up!`); - } - } - } - - if (item.extra?.farmed_cultivating) { - const farmed_cultivating = Math.floor(item.extra.farmed_cultivating); - - if (lore_raw) { - itemLore.push("", `§7Cultivating Crops: §c${farmed_cultivating.toLocaleString()}`); - if (farmed_cultivating >= 100000000) { - itemLore.push(`§8MAXED OUT!`); - } else { - let toNextLevel = 0; - for (const e of constants.cultivating_crops_ladder) { - if (farmed_cultivating < e) { - toNextLevel = Math.floor(e - farmed_cultivating); - break; - } - } - itemLore.push(`§8${toNextLevel.toLocaleString()} crops to tier up!`); - } - } - } - - if (item.extra?.blocks_walked) { - const blocks_walked = item.extra.blocks_walked; - - if (lore_raw) { - itemLore.push("", `§7Blocks Walked: §c${blocks_walked.toLocaleString()}`); - if (blocks_walked >= 100000) { - itemLore.push(`§8MAXED OUT!`); - } else { - let toNextLevel = 0; - for (const e of constants.PREHISTORIC_EGG_BLOCKS_WALKED_LADDER) { - if (blocks_walked < e) { - toNextLevel = e - blocks_walked; - break; - } - } - itemLore.push(`§8Walk ${toNextLevel.toLocaleString()} blocks to tier up!`); - } - } - } - - if (item.tag?.display?.color) { - const hex = item.tag.display.color.toString(16).padStart(6, "0"); - itemLore.push("", `§7Color: #${hex.toUpperCase()}`); - } - - if (item.extra?.timestamp) { - itemLore.push("", `§7Obtained: §c`); - } - - if (item.extra?.spawned_for) { - if (!item.extra.timestamp) { - itemLore.push(""); - } - - const spawnedFor = item.extra.spawned_for; - const spawnedForUser = await helper.resolveUsernameOrUuid(spawnedFor, db, cacheOnly); - - itemLore.push(`§7By: §c${spawnedForUser.display_name}`); - } - - if (item.extra?.base_stat_boost) { - itemLore.push( - "", - `§7Dungeon Item Quality: ${item.extra.base_stat_boost == 50 ? "§6" : "§c"}${item.extra.base_stat_boost}/50%` - ); - } - - if (item.extra?.floor) { - itemLore.push(`§7Obtained From: §bFloor ${item.extra.floor}`); - } - - if (item.extra?.price_paid) { - itemLore.push("", `§7Price Paid at Dark Auction: §6${item.extra.price_paid.toLocaleString()} Coins`); - } - } - - if (item?.tag || item?.exp) { - if (item.tag?.ExtraAttributes?.id === "PET") { - item.tag.ExtraAttributes.petInfo = - JSON.stringify(item.tag.ExtraAttributes.petInfo) ?? item.tag.ExtraAttributes.petInfo; - } - - const ITEM_PRICE = await getItemNetworth(item, { cache: true }); - - if (ITEM_PRICE?.price > 0) { - itemLore.push( - "", - `§7Item Value: §6${Math.round(ITEM_PRICE.price).toLocaleString()} Coins §7(§6${helper.formatNumber( - ITEM_PRICE.price - )}§7)` - ); - } - - if (item.tag?.ExtraAttributes?.id === "PET") { - item.tag.ExtraAttributes.petInfo = - typeof item.tag.ExtraAttributes.petInfo === "string" - ? JSON.parse(item.tag.ExtraAttributes.petInfo) - : item.tag.ExtraAttributes.petInfo; - } - } - - if (!("display_name" in item) && "id" in item) { - const vanillaItem = mcData.items[item.id]; - - if ("displayName" in vanillaItem) { - item.display_name = vanillaItem.displayName; - } - } - } - - for (const item of items) { - if (item.inBackpack) { - items[item.backpackIndex].containsItems.push(Object.assign({}, item)); - } - } - - items = items.filter((a) => !a.inBackpack); - - return items; -} - -function getMinions(coopMembers) { - const minions = []; - - const craftedGenerators = []; - - for (const member in coopMembers) { - if (!("crafted_generators" in coopMembers[member])) { - continue; - } - - craftedGenerators.push(...coopMembers[member].crafted_generators); - } - - for (const generator of craftedGenerators) { - const split = generator.split("_"); - - const minionLevel = parseInt(split.pop()); - const minionName = split.join("_"); - - const minion = minions.find((a) => a.id == minionName); - - if (minion == undefined) { - minions.push( - Object.assign({ id: minionName, maxLevel: 0, levels: [minionLevel] }, constants.MINIONS[minionName]) - ); - } else { - minion.levels.push(minionLevel); - } - } - - for (const minion in constants.MINIONS) { - if (minions.find((a) => a.id == minion) == undefined) { - minions.push(Object.assign({ id: minion, levels: [], maxLevel: 0 }, constants.MINIONS[minion])); - } - } - - for (const minion of minions) { - minion.levels = _.uniq(minion.levels.sort((a, b) => a - b)); - minion.maxLevel = minion.levels.length > 0 ? Math.max(...minion.levels) : 0; - minion.tiers = minion.tiers != null ? minion.tiers : 11; - - if (!("name" in minion)) { - minion.name = _.startCase(_.toLower(minion.id)); - } - } - - return minions; -} - -function getMinionSlots(minions) { - let uniqueMinions = 0; - - for (const minion of minions) { - uniqueMinions += minion.levels.length; - } - - const output = { currentSlots: 5, toNext: 5 }; - - const uniquesRequired = Object.keys(constants.MINION_SLOTS).sort((a, b) => parseInt(a) - parseInt(b)); - - for (const [index, uniques] of uniquesRequired.entries()) { - if (parseInt(uniques) <= uniqueMinions) { - continue; - } - - output.currentSlots = constants.MINION_SLOTS[uniquesRequired[index - 1]]; - output.toNextSlot = uniquesRequired[index] - uniqueMinions; - break; - } - - return output; -} - -export const getItems = async ( - profile, - bingoProfile, - customTextures = false, - packs, - options = { cacheOnly: false, debugId: `${helper.getClusterId()}/unknown@getItems` } -) => { - const output = {}; - - // console.debug(`${options.debugId}: getItems called.`); - // const timeStarted = Date.now(); - - // Process inventories returned by API - const armor = - "inv_armor" in profile - ? await processItems(profile.inv_armor.data, "armor", customTextures, packs, options.cacheOnly) - : []; - const equipment = - "equippment_contents" in profile - ? await processItems(profile.equippment_contents.data, "equipment", customTextures, packs, options.cacheOnly) - : []; - const inventory = - "inv_contents" in profile - ? await processItems(profile.inv_contents.data, "inventory", customTextures, packs, options.cacheOnly) - : []; - const wardrobe_inventory = - "wardrobe_contents" in profile - ? await processItems(profile.wardrobe_contents.data, "wardrobe", customTextures, packs, options.cacheOnly) - : []; - let enderchest = - "ender_chest_contents" in profile - ? await processItems(profile.ender_chest_contents.data, "ender chest", customTextures, packs, options.cacheOnly) - : []; - const accessory_bag = - "talisman_bag" in profile - ? await processItems(profile.talisman_bag.data, "accessory bag", customTextures, packs, options.cacheOnly) - : []; - const fishing_bag = - "fishing_bag" in profile - ? await processItems(profile.fishing_bag.data, "fishing bag", customTextures, packs, options.cacheOnly) - : []; - const quiver = - "quiver" in profile - ? await processItems(profile.quiver.data, "quiver", customTextures, packs, options.cacheOnly) - : []; - const potion_bag = - "potion_bag" in profile - ? await processItems(profile.potion_bag.data, "potion bag", customTextures, packs, options.cacheOnly) - : []; - const candy_bag = - "candy_inventory_contents" in profile - ? await processItems(profile.candy_inventory_contents.data, "candy bag", customTextures, packs, options.cacheOnly) - : []; - const personal_vault = - "personal_vault_contents" in profile - ? await processItems( - profile.personal_vault_contents.data, - "personal vault", - customTextures, - packs, - options.cacheOnly - ) - : []; - - let storage = []; - if (profile.backpack_contents) { - const storageSize = Math.max(18, Object.keys(profile.backpack_contents).length); - for (let slot = 0; slot < storageSize; slot++) { - storage.push({}); - - if (profile.backpack_contents[slot] && profile.backpack_icons[slot]) { - const icon = await processItems( - profile.backpack_icons[slot].data, - "storage", - customTextures, - packs, - options.cacheOnly - ); - const items = await processItems( - profile.backpack_contents[slot].data, - "storage", - customTextures, - packs, - options.cacheOnly - ); - - for (const [index, item] of items.entries()) { - item.isInactive = true; - item.inBackpack = true; - item.item_index = index; - } - - const storageUnit = icon[0]; - storageUnit.containsItems = items; - storage[slot] = storageUnit; - } - } - } - - const wardrobeColumns = wardrobe_inventory.length / 4; - - const wardrobe = []; - - for (let i = 0; i < wardrobeColumns; i++) { - const page = Math.floor(i / 9); - - const wardrobeSlot = []; - - for (let j = 0; j < 4; j++) { - const index = 36 * page + (i % 9) + j * 9; - - if (helper.getId(wardrobe_inventory[index]).length > 0) { - wardrobeSlot.push(wardrobe_inventory[index]); - } else { - wardrobeSlot.push(null); - } - } - - if (wardrobeSlot.find((a) => a !== null) != undefined) { - wardrobe.push(wardrobeSlot); - } - } - - let hotm = "mining_core" in profile ? await getHotmItems(profile, packs) : []; - - output.armor = armor.filter((x) => x.rarity); - output.equipment = equipment.filter((x) => x.rarity); - output.wardrobe = wardrobe; - output.wardrobe_inventory = wardrobe_inventory; - output.inventory = inventory; - output.enderchest = enderchest; - output.accessory_bag = accessory_bag; - output.fishing_bag = fishing_bag; - output.quiver = quiver; - output.potion_bag = potion_bag; - output.personal_vault = personal_vault; - output.storage = storage; - output.hotm = hotm; - output.candy_bag = candy_bag; - - output.bingo_card = {}; - if (bingoProfile?.events !== undefined) { - const bingoRes = await helper.getBingoGoals(db); - if (bingoRes === null) { - throw new Error("Failed to fetch bingo goals"); - } - - const bingoData = bingoRes.output; - const bingoProfilev2 = bingoProfile.events.find((profile) => profile.key === bingoData.id); - - output.bingo_card = bingoProfilev2 !== undefined ? constants.getBingoItems(bingoProfilev2, bingoData.goals) : {}; - } - - const allItems = armor.concat( - equipment, - inventory, - enderchest, - accessory_bag, - fishing_bag, - quiver, - potion_bag, - personal_vault, - wardrobe_inventory, - storage, - hotm, - candy_bag - ); - - for (const [index, item] of allItems.entries()) { - item.item_index = index; - item.itemId = v4("itemId"); - - if ("containsItems" in item && Array.isArray(item.containsItems)) { - item.containsItems.forEach((a, idx) => { - a.backpackIndex = item.item_index; - a.itemId = v4("itemId"); - }); - } - } - - // All items not in the inventory or accessory bag should be inactive so they don't contribute to the total stats - enderchest = enderchest.map((a) => Object.assign({ isInactive: true }, a)); - storage = storage.map((a) => Object.assign({ isInactive: true }, a)); - hotm = hotm.map((a) => Object.assign({ isInactive: true }, a)); - - // Add candy bag contents as backpack contents to candy bag - for (const item of allItems) { - if (helper.getId(item) == "TRICK_OR_TREAT_BAG") { - item.containsItems = candy_bag; - } - } - - const accessories = []; - const accessoryIds = []; - const accessoryRarities = { - common: 0, - uncommon: 0, - rare: 0, - epic: 0, - legendary: 0, - mythic: 0, - special: 0, - very_special: 0, - hegemony: null, - abicase: null, - rift_prism: profile.rift?.access?.consumed_prism ? { consumed: true } : null, - }; - - // Modify accessories on armor and add - for (const accessory of armor.filter((a) => a.categories.includes("accessory"))) { - const id = helper.getId(accessory); - - if (id === "") { - continue; - } - - const insertAccessory = Object.assign({ isUnique: true, isInactive: false }, accessory); - - accessories.push(insertAccessory); - accessoryIds.push({ - id: id, - rarity: insertAccessory.rarity, - }); - } - - // Add accessories from inventory and accessory bag - for (const accessory of accessory_bag.concat(inventory.filter((a) => a.categories.includes("accessory")))) { - const id = helper.getId(accessory); - if (id === "") { - continue; - } - - const insertAccessory = Object.assign({ isUnique: true, isInactive: false }, accessory); - - // mark lower tiers as inactive - if (constants.getUpgradeList(id) !== undefined) { - accessories.find((a) => { - if (constants.getUpgradeList(id).includes(helper.getId(a)) === false) { - return; - } - - insertAccessory.isInactive = true; - insertAccessory.isUnique = false; - }); - } - - // mark accessory inactive if player has two exactly same accessories - accessories.map((a) => { - if (a.isInactive == true) { - return; - } - - if (helper.getId(a) === helper.getId(insertAccessory)) { - insertAccessory.isInactive = true; - a.isInactive = true; - - // give accessories with higher rarity priority, mark lower rarity as inactive - if (constants.RARITIES.indexOf(a.rarity) > constants.RARITIES.indexOf(insertAccessory.rarity)) { - a.isInactive = false; - a.isUnique = true; - insertAccessory.isUnique = false; - } else if (constants.RARITIES.indexOf(insertAccessory.rarity) > constants.RARITIES.indexOf(a.rarity)) { - insertAccessory.isInactive = false; - insertAccessory.isUnique = true; - a.isUnique = false; - } else { - insertAccessory.isInactive = false; - insertAccessory.isUnique = false; - a.isInactive = true; - a.isUnique = true; - } - } - }); - - // mark accessory aliases as inactive - const accessoryAliases = constants.accessoryAliases; - if (id in accessoryAliases || Object.keys(accessoryAliases).find((a) => accessoryAliases[a].includes(id))) { - let accessoryDuplicates = constants.accessoryAliases[id]; - if (accessoryDuplicates === undefined) { - const aliases = Object.keys(accessoryAliases).filter((a) => accessoryAliases[a].includes(id)); - accessoryDuplicates = aliases.concat(constants.accessoryAliases[aliases]); - } - - for (const duplicate of accessoryDuplicates) { - accessory_bag.concat(inventory.filter((a) => a.categories.includes("accessory"))).map((a) => { - if (helper.getId(a) === duplicate) { - a.isInactive = true; - a.isUnique = false; - } - }); - } - } - - accessories.push(insertAccessory); - accessoryIds.push({ - id: id, - rarity: insertAccessory.rarity, - }); - if (insertAccessory.isInactive === false) { - accessoryRarities[insertAccessory.rarity]++; - if (id == "HEGEMONY_ARTIFACT") { - accessoryRarities.hegemony = { rarity: insertAccessory.rarity }; - } - if (id === "ABICASE") { - accessoryRarities.abicase = { model: insertAccessory.extra?.model }; - } - } - } - - // Add inactive accessories from enderchest and backpacks - for (const item of enderchest.concat(storage)) { - // filter out filler or empty slots (such as empty storage slot) - if (!("categories" in item)) { - continue; - } - - let items = [item]; - - if (!item.categories.includes("accessory") && "containsItems" in item && Array.isArray(item.containsItems)) { - items = item.containsItems.slice(0); - } - - for (const accessory of items.filter((a) => a.categories.includes("accessory"))) { - const insertAccessory = Object.assign({ isUnique: false, isInactive: true }, accessory); - - accessories.push(insertAccessory); - } - } - - for (const accessory of accessories) { - accessory.base_name = accessory.display_name; - - if (accessory.tag?.ExtraAttributes?.modifier != undefined) { - accessory.base_name = accessory.display_name.split(" ").slice(1).join(" "); - accessory.reforge = accessory.tag.ExtraAttributes.modifier; - } - - if (accessory.tag?.ExtraAttributes?.talisman_enrichment != undefined) { - accessory.enrichment = accessory.tag.ExtraAttributes.talisman_enrichment.toLowerCase(); - } - - if (accessory.isUnique === false || accessory.isInactive === true) { - const source = accessory.extra?.source; - if (source !== undefined) { - accessory.tag.display.Lore.push("", `§7Location: §c${source}`); - } - } - } - - if (accessoryRarities.rift_prism?.consumed) { - accessoryIds.push({ - id: "RIFT_PRISM", - rarity: "rare", - }); - } - - output.accessories = accessories; - output.accessory_ids = accessoryIds; - output.accessory_rarities = accessoryRarities; - - output.weapons = allItems.filter((a) => a.categories?.includes("weapon")); - output.farming_tools = allItems.filter((a) => a.categories?.includes("farming_tool")); - output.mining_tools = allItems.filter((a) => a.categories?.includes("mining_tool")); - output.fishing_tools = allItems.filter((a) => a.categories?.includes("fishing_tool")); - - output.pets = allItems - .filter((a) => a.tag?.ExtraAttributes?.petInfo) - .map((a) => ({ - uuid: a.tag.ExtraAttributes.uuid, - type: a.tag.ExtraAttributes.petInfo.type, - exp: a.tag.ExtraAttributes.petInfo.exp, - active: a.tag.ExtraAttributes.petInfo.active, - tier: a.tag.ExtraAttributes.petInfo.tier, - heldItem: a.tag.ExtraAttributes.petInfo.heldItem || null, - candyUsed: a.tag.ExtraAttributes.petInfo.candyUsed, - skin: a.tag.ExtraAttributes.petInfo.skin || null, - })); - - for (const item of allItems) { - if (!Array.isArray(item.containsItems)) { - continue; - } - - output.weapons.push(...item.containsItems.filter((a) => a.categories.includes("weapon"))); - output.farming_tools.push(...item.containsItems.filter((a) => a.categories.includes("farming_tool"))); - output.mining_tools.push(...item.containsItems.filter((a) => a.categories.includes("mining_tool"))); - output.fishing_tools.push(...item.containsItems.filter((a) => a.categories.includes("fishing_tool"))); - - output.pets.push( - ...item.containsItems - .filter((a) => a.tag?.ExtraAttributes?.petInfo) - .map((a) => ({ - uuid: a.tag.ExtraAttributes.uuid, - type: a.tag.ExtraAttributes.petInfo.type, - exp: a.tag.ExtraAttributes.petInfo.exp, - active: a.tag.ExtraAttributes.petInfo.active, - tier: a.tag.ExtraAttributes.petInfo.tier, - heldItem: a.tag.ExtraAttributes.petInfo.heldItem || null, - candyUsed: a.tag.ExtraAttributes.petInfo.candyUsed, - skin: a.tag.ExtraAttributes.petInfo.skin || null, - })) - ); - } - - // Check if inventory access disabled by user - if (inventory.length == 0) { - output.no_inventory = true; - } - - // Same for personal vault - if (personal_vault.length == 0) { - output.no_personal_vault = true; - } - - const itemSorter = (a, b) => { - if (a.rarity !== b.rarity) { - return constants.RARITIES.indexOf(b.rarity) - constants.RARITIES.indexOf(a.rarity); - } - - if (b.inBackpack && !a.inBackpack) { - return -1; - } - if (a.inBackpack && !b.inBackpack) { - return 1; - } - - return a.item_index - b.item_index; - }; - - // Sort accessories, weapons and rods by rarity - output.weapons = output.weapons.sort(itemSorter); - output.fishing_tools = output.fishing_tools.sort(itemSorter); - output.farming_tools = output.farming_tools.sort(itemSorter); - output.mining_tools = output.mining_tools.sort(itemSorter); - output.accessories = output.accessories.sort(itemSorter); - - const countsOfId = {}; - - for (const weapon of output.weapons) { - const id = helper.getId(weapon); - - countsOfId[id] = (countsOfId[id] || 0) + 1; - - if (countsOfId[id] > 2 && constants.RARITIES.indexOf(weapon.rarity) < constants.RARITIES.indexOf("legendary")) { - weapon.hidden = true; - } - } - - for (const item of output.fishing_tools) { - const id = helper.getId(item); - - countsOfId[id] = (countsOfId[id] || 0) + 1; - - if (countsOfId[id] > 2) { - item.hidden = true; - } - } - - const swords = output.weapons.filter((a) => a.categories.includes("sword")); - const bows = output.weapons.filter((a) => a.categories.includes("bow")); - - const swordsInventory = swords.filter((a) => a.backpackIndex === undefined); - const bowsInventory = bows.filter((a) => a.backpackIndex === undefined); - const fishingtoolsInventory = output.fishing_tools.filter((a) => a.backpackIndex === undefined); - const farmingtoolsInventory = output.farming_tools.filter((a) => a.backpackIndex === undefined); - const miningtoolsInventory = output.mining_tools.filter((a) => a.backpackIndex === undefined); - - if (swords.length > 0) { - output.highest_rarity_sword = swordsInventory - .filter((a) => a.rarity == swordsInventory[0].rarity) - .sort((a, b) => a.item_index - b.item_index)[0]; - } - - if (bows.length > 0) { - output.highest_rarity_bow = bowsInventory - .filter((a) => a.rarity == bowsInventory[0].rarity) - .sort((a, b) => a.item_index - b.item_index)[0]; - } - - if (output.fishing_tools.length > 0) { - output.highest_rarity_fishing_tool = fishingtoolsInventory - .filter((a) => a.rarity == fishingtoolsInventory[0].rarity) - .sort((a, b) => a.item_index - b.item_index)[0]; - } - - if (output.farming_tools.length > 0) { - output.highest_rarity_farming_tool = farmingtoolsInventory - .filter((a) => a.rarity == farmingtoolsInventory[0].rarity) - .sort((a, b) => a.item_index - b.item_index)[0]; - } - - if (output.mining_tools.length > 0) { - output.highest_rarity_mining_tool = miningtoolsInventory - .filter((a) => a.rarity == miningtoolsInventory[0].rarity) - .sort((a, b) => a.item_index - b.item_index)[0]; - } - - if (armor.filter((x) => x.rarity).length === 1) { - const armorPiece = armor.find((x) => x.rarity); - - output.armor_set = armorPiece.display_name; - output.armor_set_rarity = armorPiece.rarity; - } - - if (equipment.filter((x) => x.rarity).length === 1) { - const equipmentPiece = equipment.find((x) => x.rarity); - - output.equipment_set = equipmentPiece.display_name; - output.equipment_set_rarity = equipmentPiece.rarity; - } - - // Full armor set (4 pieces) - if (armor.filter((x) => x.rarity).length === 4) { - let output_name; - let reforgeName; - - // Getting armor_name - armor.forEach((armorPiece) => { - let name = armorPiece.display_name; - - // Removing skin and stars / Whitelisting a-z and 0-9 - name = name.replace(/[^A-Za-z0-9 -']/g, "").trim(); - - // Removing modifier - if (armorPiece.tag?.ExtraAttributes?.modifier != undefined) { - name = name.split(" ").slice(1).join(" "); - } - - // Converting armor_name to generic name - // Ex: Superior Dragon Helmet -> Superior Dragon Armor - if (/^Armor .*? (Helmet|Chestplate|Leggings|Boots)$/g.test(name)) { - // name starts with Armor and ends with piece name, remove piece name - name = name.replaceAll(/(Helmet|Chestplate|Leggings|Boots)/g, "").trim(); - } else { - // removing old 'Armor' and replacing the piece name with 'Armor' - name = name.replace("Armor", "").replace(" ", " ").trim(); - name = name.replaceAll(/(Helmet|Chestplate|Leggings|Boots)/g, "Armor").trim(); - } - - armorPiece.armor_name = name; - }); - - // Getting full armor reforge (same reforge on all pieces) - if ( - armor.filter( - (a) => - a.tag?.ExtraAttributes?.modifier != undefined && - a.tag?.ExtraAttributes?.modifier == armor[0].tag.ExtraAttributes.modifier - ).length == 4 - ) { - reforgeName = armor[0].display_name - .replace(/[^A-Za-z0-9 -']/g, "") - .trim() - .split(" ")[0]; - } - - // Handling normal sets of armor - if (armor.filter((a) => a.armor_name == armor[0].armor_name).length == 4) { - output_name = armor[0].armor_name; - } - - // Handling special sets of armor (where pieces aren't named the same) - constants.SPECIAL_SETS.forEach((set) => { - if (armor.filter((a) => set.pieces.includes(helper.getId(a))).length == 4) { - output_name = set.name; - } - }); - - // Finalizing the output - if (reforgeName && output_name) { - output_name = reforgeName + " " + output_name; - } - - output.armor_set = output_name; - output.armor_set_rarity = constants.RARITIES[Math.max(...armor.map((a) => helper.rarityNameToInt(a.rarity)))]; - } - - // Full equipment set (4 pieces) - for (const piece of equipment) { - if (piece.rarity == null) delete equipment[equipment.indexOf(piece)]; - } - - if (equipment.filter((x) => x.rarity).length === 4) { - // Getting equipment_name - equipment.forEach((equipmentPiece) => { - let name = equipmentPiece.display_name; - - // Removing skin and stars / Whitelisting a-z and 0-9 - name = name.replace(/[^A-Za-z0-9 -']/g, "").trim(); - - // Removing modifier - if (equipmentPiece.tag?.ExtraAttributes?.modifier != undefined) { - name = name.split(" ").slice(1).join(" "); - } - - equipmentPiece.equipment_name = name; - }); - - output.equipment_set_rarity = - constants.RARITIES[Math.max(...equipment.map((a) => helper.rarityNameToInt(a.rarity)))]; - } - - // console.debug(`${options.debugId}: getItems returned. (${Date.now() - timeStarted}ms)`); - return output; -}; - -async function getLevels(userProfile, hypixelProfile, levelCaps, profileMembers) { - const output = {}; - - let skillLevels; - let totalSkillXp = 0; - let average_level = 0; - - // Apply skill bonuses - if ( - "experience_skill_taming" in userProfile || - "experience_skill_farming" in userProfile || - "experience_skill_mining" in userProfile || - "experience_skill_combat" in userProfile || - "experience_skill_foraging" in userProfile || - "experience_skill_fishing" in userProfile || - "experience_skill_enchanting" in userProfile || - "experience_skill_alchemy" in userProfile || - "experience_skill_carpentry" in userProfile || - "experience_skill_runecrafting" in userProfile || - "experience_skill_social2" in userProfile - ) { - let average_level_no_progress = 0; - - skillLevels = { - taming: getLevelByXp(userProfile.experience_skill_taming, { skill: "taming" }), - farming: getLevelByXp(userProfile.experience_skill_farming, { - skill: "farming", - cap: levelCaps?.farming, - }), - mining: getLevelByXp(userProfile.experience_skill_mining, { skill: "mining" }), - combat: getLevelByXp(userProfile.experience_skill_combat, { skill: "combat" }), - foraging: getLevelByXp(userProfile.experience_skill_foraging, { skill: "foraging" }), - fishing: getLevelByXp(userProfile.experience_skill_fishing, { skill: "fishing" }), - enchanting: getLevelByXp(userProfile.experience_skill_enchanting, { skill: "enchanting" }), - alchemy: getLevelByXp(userProfile.experience_skill_alchemy, { skill: "alchemy" }), - carpentry: getLevelByXp(userProfile.experience_skill_carpentry, { skill: "carpentry" }), - runecrafting: getLevelByXp(userProfile.experience_skill_runecrafting, { - skill: "runecrafting", - type: "runecrafting", - cap: - hypixelProfile.rankText === null - ? constants.NON_RUNECRAFTING_LEVEL_CAP - : constants.DEFAULT_SKILL_CAPS.runecrafting, - }), - social: - Object.keys(profileMembers || {}).length > 0 - ? getLevelByXp( - Object.keys(profileMembers) - .map((member) => profileMembers[member].experience_skill_social2 || 0) - .reduce((a, b) => a + b, 0), - { skill: "social", type: "social" } - ) - : getLevelByXp(userProfile.experience_skill_social2, { - skill: "social", - type: "social", - }), - }; - - for (const skill in skillLevels) { - if (!constants.COSMETIC_SKILLS.includes(skill)) { - average_level += skillLevels[skill].level + skillLevels[skill].progress; - average_level_no_progress += skillLevels[skill].level; - - totalSkillXp += skillLevels[skill].xp; - } - } - - output.average_level = - average_level / (Object.keys(skillLevels).length - Object.keys(constants.COSMETIC_SKILLS).length); - output.average_level_no_progress = - average_level_no_progress / (Object.keys(skillLevels).length - Object.keys(constants.COSMETIC_SKILLS).length); - output.total_skill_xp = totalSkillXp; - - output.levels = Object.assign({}, skillLevels); - } else { - skillLevels = { - farming: hypixelProfile.achievements.skyblock_harvester || 0, - mining: hypixelProfile.achievements.skyblock_excavator || 0, - combat: hypixelProfile.achievements.skyblock_combat || 0, - foraging: hypixelProfile.achievements.skyblock_gatherer || 0, - fishing: hypixelProfile.achievements.skyblock_angler || 0, - enchanting: hypixelProfile.achievements.skyblock_augmentation || 0, - alchemy: hypixelProfile.achievements.skyblock_concoctor || 0, - taming: hypixelProfile.achievements.skyblock_domesticator || 0, - carpentry: 0, - }; - - output.levels = {}; - - let skillsAmount = 0; - - for (const skill in skillLevels) { - output.levels[skill] = getXpByLevel(skillLevels[skill], { skill: skill }); - - if (skillLevels[skill] < 0) { - continue; - } - - skillsAmount++; - average_level += output.levels[skill].level; - - totalSkillXp += output.levels[skill].xp; - } - - output.average_level = average_level / skillsAmount; - output.average_level_no_progress = output.average_level; - output.total_skill_xp = totalSkillXp; - } - - const skillNames = Object.keys(output.levels); - - for (const skill of skillNames) { - output.levels[skill].rank = await getLeaderboardPosition(`skill_${skill}_xp`, output.levels[skill].xp); - } - - output.average_level_rank = await redisClient.zcount([`lb_average_level`, output.average_level, "+inf"]); - - return output; -} - -export async function getStats( - db, - profile, - bingoProfile, - allProfiles, - items, - packs, - options = { cacheOnly: false, debugId: `${helper.getClusterId()}/unknown@getStats` } -) { - const output = {}; - - // console.debug(`${options.debugId}: getStats called.`); - // const timeStarted = Date.now(); - - const userProfile = profile.members[profile.uuid]; - const hypixelProfile = await helper.getRank(profile.uuid, db, options.cacheOnly); - - output.stats = Object.assign({}, constants.BASE_STATS); - - // fairy souls - if (isNaN(userProfile.fairy_souls_collected)) { - userProfile.fairy_souls_collected = 0; - } - - const totalSouls = - profile.game_mode === "island" ? constants.FAIRY_SOULS.max.stranded : constants.FAIRY_SOULS.max.normal; - - output.fairy_souls = { - collected: userProfile.fairy_souls_collected, - total: totalSouls, - progress: userProfile.fairy_souls_collected / totalSouls, - }; - output.fairy_exchanges = userProfile.fairy_exchanges; - - // levels - const levelCaps = { - farming: constants.DEFAULT_SKILL_CAPS.farming + (userProfile.jacob2?.perks?.farming_level_cap || 0), - }; - - const { levels, average_level, average_level_no_progress, total_skill_xp, average_level_rank } = await getLevels( - userProfile, - hypixelProfile, - levelCaps, - profile.members - ); - - output.levels = levels; - output.average_level = average_level; - output.average_level_no_progress = average_level_no_progress; - output.total_skill_xp = total_skill_xp; - output.average_level_rank = average_level_rank; - output.level_caps = levelCaps; - - output.slayer_coins_spent = {}; - - // Apply slayer bonuses - if ("slayer_bosses" in userProfile) { - const slayers = {}; - - if ("slayer_bosses" in userProfile) { - for (const slayerName in userProfile.slayer_bosses) { - const slayer = userProfile.slayer_bosses[slayerName]; - - if (!("claimed_levels" in slayer)) { - continue; - } - - slayers[slayerName] = { - level: getSlayerLevel(slayer, slayerName), - kills: {}, - }; - - for (const property in slayer) { - slayers[slayerName][property] = slayer[property]; - - if (property.startsWith("boss_kills_tier_")) { - const tier = parseInt(property.replace("boss_kills_tier_", "")) + 1; - - slayers[slayerName].kills[tier] = slayer[property]; - - output.slayer_coins_spent[slayerName] = - (output.slayer_coins_spent[slayerName] || 0) + slayer[property] * constants.SLAYER_COST[tier]; - } - } - } - - for (const slayerName in output.slayer_coins_spent) { - output.slayer_coins_spent.total = - (output.slayer_coins_spent.total || 0) + output.slayer_coins_spent[slayerName]; - } - - output.slayer_coins_spent.total = output.slayer_coins_spent.total || 0; - } - - output.slayer_xp = 0; - - for (const slayer in slayers) { - if (slayers[slayer]?.level?.currentLevel == undefined) { - continue; - } - - output.slayer_xp += slayers[slayer].xp || 0; - } - - output.slayers = Object.assign({}, slayers); - } - - if (!items.no_inventory && items.accessory_ids) { - output.missingAccessories = getMissingAccessories(items.accessory_ids); - - for (const key in output.missingAccessories) { - for (const item of output.missingAccessories[key]) { - let price = 0; - - if (item.customPrice === true) { - if (item.upgrade) { - price = (await helper.getItemPrice(item.upgrade.item)) * item.upgrade.cost[item.rarity]; - } - - if (item.id === "POWER_ARTIFACT") { - for (const { slot_type: slot } of item.gemstone_slots) { - price += await helper.getItemPrice(`PERFECT_${slot}_GEM`); - } - } - } else { - price = await helper.getItemPrice(item.id); - } - - item.extra = { price }; - if (price > 0) { - helper.addToItemLore( - item, - `§7Price: §6${Math.round(price).toLocaleString()} Coins §7(§6${helper.formatNumber( - Math.floor(price / helper.getMagicalPower(item.rarity, item.id)) - )} §7per MP)` - ); - } - - item.tag ??= {}; - item.tag.ExtraAttributes ??= {}; - item.tag.ExtraAttributes.id ??= item.id; - item.Damage ??= item.damage; - item.id = item.item_id; - - helper.applyResourcePack(item, packs); - } - - output.missingAccessories[key].sort((a, b) => { - const aPrice = a.extra?.price || 0; - const bPrice = b.extra?.price || 0; - - if (aPrice === 0) return 1; - if (bPrice === 0) return -1; - - return aPrice - bPrice; - }); - } - } - - output.base_stats = Object.assign({}, output.stats); - - output.weapon_stats = {}; - - let killsDeaths = []; - - for (const stat in userProfile.stats) { - if (stat.startsWith("kills_") && userProfile.stats[stat] > 0) { - killsDeaths.push({ type: "kills", entityId: stat.replace("kills_", ""), amount: userProfile.stats[stat] }); - } - - if (stat.startsWith("deaths_") && userProfile.stats[stat] > 0) { - killsDeaths.push({ type: "deaths", entityId: stat.replace("deaths_", ""), amount: userProfile.stats[stat] }); - } - } - - for (const stat of killsDeaths) { - const { entityId } = stat; - - if (entityId in constants.MOB_NAMES) { - stat.entityName = constants.MOB_NAMES[entityId]; - continue; - } - - let entityName = ""; - - entityId.split("_").forEach((split, index) => { - entityName += split.charAt(0).toUpperCase() + split.slice(1); - - if (index < entityId.split("_").length - 1) { - entityName += " "; - } - }); - - stat.entityName = entityName; - } - - if ("kills_guardian_emperor" in userProfile.stats || "kills_skeleton_emperor" in userProfile.stats) { - killsDeaths.push({ - type: "kills", - entityId: "sea_emperor", - entityName: "Sea Emperor", - amount: (userProfile.stats["kills_guardian_emperor"] || 0) + (userProfile.stats["kills_skeleton_emperor"] || 0), - }); - } - - if ("kills_chicken_deep" in userProfile.stats || "kills_zombie_deep" in userProfile.stats) { - killsDeaths.push({ - type: "kills", - entityId: "monster_of_the_deep", - entityName: "Monster of the Deep", - amount: (userProfile.stats["kills_chicken_deep"] || 0) + (userProfile.stats["kills_zombie_deep"] || 0), - }); - } - - const random = Math.random() < 0.01; - - killsDeaths = killsDeaths.filter((a) => { - return !["guardian_emperor", "skeleton_emperor", "chicken_deep", "zombie_deep", random ? "yeti" : null].includes( - a.entityId - ); - }); - - if ("kills_yeti" in userProfile.stats && random) { - killsDeaths.push({ - type: "kills", - entityId: "yeti", - entityName: "Snow Monke", - amount: userProfile.stats["kills_yeti"] || 0, - }); - } - - if ("deaths_yeti" in userProfile.stats) { - killsDeaths.push({ - type: "deaths", - entityId: "yeti", - entityName: "Snow Monke", - amount: userProfile.stats["deaths_yeti"] || 0, - }); - } - - output.kills = killsDeaths.filter((a) => a.type == "kills").sort((a, b) => b.amount - a.amount); - output.deaths = killsDeaths.filter((a) => a.type == "deaths").sort((a, b) => b.amount - a.amount); - - const playerObject = await helper.resolveUsernameOrUuid(profile.uuid, db, options.cacheOnly); - - output.display_name = playerObject.display_name; - - if ("wardrobe_equipped_slot" in userProfile) { - output.wardrobe_equipped_slot = userProfile.wardrobe_equipped_slot; - } - - const userInfo = await db.collection("usernames").findOne({ uuid: profile.uuid }); - - const memberUuids = []; - for (const [uuid, memberProfile] of Object.entries(profile?.members ?? {})) { - if (memberProfile?.coop_invitation?.confirmed === false || memberProfile.deletion_notice?.timestamp !== undefined) { - memberProfile.removed = true; - } - - memberUuids.push(uuid); - } - - const members = await Promise.all( - memberUuids.map(async (a) => { - return { - ...(await helper.resolveUsernameOrUuid(a, db, options.cacheOnly)), - removed: profile.members[a]?.removed || false, - }; - }) - ); - - if (userInfo) { - output.display_name = userInfo.username; - - members.push({ - uuid: profile.uuid, - display_name: userInfo.username, - removed: profile.members[profile.uuid]?.removed || false, - }); - - if ("emoji" in userInfo) { - output.display_emoji = userInfo.emoji; - } - - if ("emojiImg" in userInfo) { - output.display_emoji_img = userInfo.emojiImg; - } - - if (userInfo.username == "jjww2") { - output.display_emoji = constants.randomEmoji(); - } - } - - if (profile.banking?.balance != undefined) { - output.bank = profile.banking.balance; - } - - output.guild = await helper.getGuild(profile.uuid, db, options.cacheOnly); - - output.rank_prefix = helper.renderRank(hypixelProfile); - output.purse = userProfile.coin_purse || 0; - output.uuid = profile.uuid; - output.skin_data = playerObject.skin_data; - - output.profile = { profile_id: profile.profile_id, cute_name: profile.cute_name, game_mode: profile.game_mode }; - output.profiles = {}; - - for (const sbProfile of allProfiles.filter((a) => a.profile_id != profile.profile_id)) { - output.profiles[sbProfile.profile_id] = { - profile_id: sbProfile.profile_id, - cute_name: sbProfile.cute_name, - game_mode: sbProfile.game_mode, - }; - } - - output.members = members.filter((a) => a.uuid != profile.uuid); - output.minions = getMinions(profile.members); - output.minion_slots = getMinionSlots(output.minions); - output.collections = await getCollections(profile.uuid, profile, options.cacheOnly); - output.bestiary = stats.getBestiary(userProfile); - - output.social = hypixelProfile.socials; - - output.dungeons = await getDungeons(userProfile, hypixelProfile); - - output.essence = getEssence(userProfile, hypixelProfile); - - output.fishing = { - total: userProfile.stats.items_fished || 0, - treasure: userProfile.stats.items_fished_treasure || 0, - treasure_large: userProfile.stats.items_fished_large_treasure || 0, - shredder_fished: userProfile.stats.shredder_fished || 0, - shredder_bait: userProfile.stats.shredder_bait || 0, - }; - - // - // FARMING - // - - const farming = { - talked: userProfile.jacob2?.talked || false, - pelts: userProfile.trapper_quest?.pelt_count || 0, - }; - - if (farming.talked) { - // Your current badges - farming.current_badges = { - bronze: userProfile.jacob2.medals_inv.bronze || 0, - silver: userProfile.jacob2.medals_inv.silver || 0, - gold: userProfile.jacob2.medals_inv.gold || 0, - }; - - // Your total badges - farming.total_badges = { - bronze: 0, - silver: 0, - gold: 0, - }; - - // Your current perks - farming.perks = { - double_drops: userProfile.jacob2.perks?.double_drops || 0, - farming_level_cap: userProfile.jacob2.perks?.farming_level_cap || 0, - }; - - // Your amount of unique golds - farming.unique_golds = userProfile.jacob2.unique_golds2?.length || 0; - - // Things about individual crops - farming.crops = {}; - - for (const crop in constants.FARMING_CROPS) { - farming.crops[crop] = constants.FARMING_CROPS[crop]; - - Object.assign(farming.crops[crop], { - attended: false, - unique_gold: userProfile.jacob2.unique_golds2?.includes(crop) || false, - contests: 0, - personal_best: 0, - badges: { - gold: 0, - silver: 0, - bronze: 0, - }, - }); - } - - // Template for contests - const contests = { - attended_contests: 0, - all_contests: [], - }; - - for (const contestId in userProfile.jacob2.contests) { - const data = userProfile.jacob2.contests[contestId]; - - const contestName = contestId.split(":"); - const date = `${contestName[1]}_${contestName[0]}`; - const crop = contestName.slice(2).join(":"); - - if (data.collected < 100) { - continue; // Contests aren't counted in game with less than 100 collection - } - - farming.crops[crop].contests++; - farming.crops[crop].attended = true; - if (farming.crops[crop].personal_best < data.collected) { - farming.crops[crop].personal_best = data.collected; - } - - const contest = { - date: date, - crop: crop, - collected: data.collected, - claimed: data.claimed_rewards || false, - medal: null, - }; - - const placing = {}; - - if (contest.claimed) { - placing.position = data.claimed_position || 0; - placing.percentage = (data.claimed_position / data.claimed_participants) * 100; - const participants = data.claimed_participants; - - // Use the claimed medal if it exists and is valid - // This accounts for the farming mayor increased brackets perk - // Note: The medal brackets are the percentage + 1 extra person - if ( - contest.claimed_medal === "bronze" || - contest.claimed_medal === "silver" || - contest.claimed_medal === "gold" - ) { - contest.medal = contest.claimed_medal; - } else if (placing.position <= participants * 0.05 + 1) { - contest.medal = "gold"; - } else if (placing.position <= participants * 0.25 + 1) { - contest.medal = "silver"; - } else if (placing.position <= participants * 0.6 + 1) { - contest.medal = "bronze"; - } - - // Count the medal if it exists - if (contest.medal) { - farming.total_badges[contest.medal]++; - farming.crops[crop].badges[contest.medal]++; - } - } - - contest.placing = placing; - - contests.attended_contests++; - contests.all_contests.push(contest); - } - - farming.contests = contests; - } - - output.farming = farming; - - // - // ENCHANTING - // - - // simon = Chronomatron - // numbers = Ultrasequencer - // pairings = Superpairs - - const enchanting = { - experimented: - (userProfile.experimentation != null && Object.keys(userProfile.experimentation).length >= 1) || false, - experiments: {}, - }; - - if (enchanting.experimented) { - const enchantingData = userProfile.experimentation; - - for (const game in constants.EXPERIMENTS.games) { - if (enchantingData[game] == null) continue; - if (!Object.keys(enchantingData[game]).length >= 1) { - continue; - } - - const gameData = enchantingData[game]; - const gameConstants = constants.EXPERIMENTS.games[game]; - - const gameOutput = { - name: gameConstants.name, - stats: {}, - tiers: {}, - }; - - for (const key in gameData) { - if (key.startsWith("attempts") || key.startsWith("claims") || key.startsWith("best_score")) { - let statKey = key.split("_"); - let tierValue = parseInt(statKey.pop()); - tierValue = - game === "numbers" ? tierValue + 2 : game === "simon" ? (tierValue === 5 ? 5 : tierValue + 1) : tierValue; - - statKey = statKey.join("_"); - const tierInfo = _.cloneDeep(constants.EXPERIMENTS.tiers[tierValue]); - - if (!gameOutput.tiers[tierValue]) { - gameOutput.tiers[tierValue] = tierInfo; - } - - Object.assign(gameOutput.tiers[tierValue], { - [statKey]: gameData[key], - }); - continue; - } - - if (key == "last_attempt" || key == "last_claimed") { - if (gameData[key] <= 0) continue; - const lastTime = { - unix: gameData[key], - text: moment(gameData[key]).fromNow(), - }; - - gameOutput.stats[key] = lastTime; - continue; - } - - gameOutput.stats[key] = gameData[key]; - } - - enchanting.experiments[game] = gameOutput; - } - - if (!Object.keys(enchanting.experiments).length >= 1) { - enchanting.experimented = false; - } - } - - output.enchanting = enchanting; - - // MINING - - const mining = { - commissions: { - milestone: 0, - completions: hypixelProfile?.achievements?.skyblock_hard_working_miner || 0, - }, - }; - - if (userProfile?.tutorial) { - for (const key of userProfile.tutorial) { - if (key.startsWith("commission_milestone_reward_mining_xp_tier_")) { - const milestoneTier = key.slice(43); - if (mining.commissions.milestone < milestoneTier) mining.commissions.milestone = milestoneTier; - } - } - } - - mining.forge = await getForge(userProfile); - mining.core = getMiningCoreData(userProfile); - - output.mining = mining; - - // CRIMSON ISLES - - const crimsonIsles = { - kuudra_completed_tiers: {}, - dojo: {}, - factions: {}, - total_dojo_points: 0, - }; - - crimsonIsles.factions.selected_faction = userProfile.nether_island_player_data?.selected_faction ?? "None"; - crimsonIsles.factions.mages_reputation = userProfile.nether_island_player_data?.mages_reputation ?? 0; - crimsonIsles.factions.barbarians_reputation = userProfile.nether_island_player_data?.barbarians_reputation ?? 0; - - Object.keys(constants.KUUDRA_TIERS).forEach((key) => { - crimsonIsles.kuudra_completed_tiers[key] = { - name: constants.KUUDRA_TIERS[key].name, - head: constants.KUUDRA_TIERS[key].head, - completions: userProfile.nether_island_player_data?.kuudra_completed_tiers[key] ?? 0, - }; - }); - - Object.keys(constants.DOJO).forEach((key) => { - key = key.replaceAll("dojo_points_", "").replaceAll("dojo_time_", ""); - crimsonIsles.total_dojo_points += userProfile.nether_island_player_data?.dojo[`dojo_points_${key}`] ?? 0; - crimsonIsles.dojo[key.toUpperCase()] = { - name: constants.DOJO[key].name, - id: constants.DOJO[key].itemId, - damage: constants.DOJO[key].damage, - points: userProfile.nether_island_player_data?.dojo[`dojo_points_${key}`] ?? 0, - time: userProfile.nether_island_player_data?.dojo[`dojo_time_${key}`] ?? 0, - }; - }); - - output.crimsonIsles = crimsonIsles; - - output.trophy_fish = getTrophyFish(userProfile); - - output.abiphone = { - contacts: userProfile.nether_island_player_data?.abiphone?.contact_data ?? {}, - active: userProfile.nether_island_player_data?.abiphone?.active_contacts?.length || 0, - }; - - const skyblockExperience = userProfile.leveling?.experience ?? 0; - output.skyblock_level = getLevelByXp(skyblockExperience, { - skill: "skyblock_level", - type: "skyblock_level", - infinite: true, - ignoreCap: true, - }); - - output.skyblock_level.rank = await getLeaderboardPosition("skyblock_level_xp", skyblockExperience); - - // MISC - - const misc = {}; - - output.visited_zones = userProfile.visited_zones || []; - output.visited_modes = userProfile.visited_modes || []; - output.perks = userProfile.perks || {}; - misc.milestones = {}; - misc.objectives = {}; - misc.races = {}; - misc.gifts = {}; - misc.winter = {}; - misc.dragons = {}; - misc.protector = {}; - misc.damage = {}; - misc.burrows = {}; - misc.profile_upgrades = {}; - misc.auctions_sell = {}; - misc.auctions_buy = {}; - misc.claimed_items = {}; - misc.effects = { - active: userProfile.active_effects || [], - paused: userProfile.paused_effects || [], - disabled: userProfile.disabled_potion_effects || [], - }; - - if ("ender_crystals_destroyed" in userProfile.stats) { - misc.dragons["ender_crystals_destroyed"] = userProfile.stats["ender_crystals_destroyed"]; - } - - misc.dragons["last_hits"] = 0; - misc.dragons["deaths"] = 0; - - if (hypixelProfile.claimed_items) { - misc.claimed_items = hypixelProfile.claimed_items; - } - - const burrows = [ - "mythos_burrows_dug_next", - "mythos_burrows_dug_combat", - "mythos_burrows_dug_treasure", - "mythos_burrows_chains_complete", - ]; - - const dug_next = {}; - const dug_combat = {}; - const dug_treasure = {}; - const chains_complete = {}; - - for (const key of burrows) { - if (key in userProfile.stats) { - misc.burrows[key.replace("mythos_burrows_", "")] = { total: userProfile.stats[key] }; - } - } - - misc.profile_upgrades = getProfileUpgrades(profile); - - const auctions_buy = ["auctions_bids", "auctions_highest_bid", "auctions_won", "auctions_gold_spent"]; - const auctions_sell = ["auctions_fees", "auctions_gold_earned"]; - - const auctions_bought = {}; - const auctions_sold = {}; - - for (const key of auctions_sell) { - if (key in userProfile.stats) { - misc.auctions_sell[key.replace("auctions_", "")] = userProfile.stats[key]; - } - } - - for (const key of auctions_buy) { - if (key in userProfile.stats) { - misc.auctions_buy[key.replace("auctions_", "")] = userProfile.stats[key]; - } - } - - misc.objectives.completedRaces = []; - - for (const key in userProfile.objectives) { - if (key.includes("complete_the_")) { - const isCompleted = userProfile.objectives[key].status == "COMPLETE"; - const tierNumber = parseInt("" + key.charAt(key.length - 1)) ?? 0; - const raceName = constants.RACE_OBJECTIVE_TO_STAT_NAME[key.substring(0, key.length - 2)]; - - if (tierNumber == 1 && !isCompleted) { - misc.objectives.completedRaces[raceName] = 0; - } else if (isCompleted && tierNumber > (misc.objectives.completedRaces[raceName] ?? 0)) { - misc.objectives.completedRaces[raceName] = tierNumber; - } - } - } - - for (const key in userProfile.stats) { - if (key.includes("_best_time")) { - misc.races[key] = userProfile.stats[key]; - } else if (key.includes("gifts_")) { - misc.gifts[key] = userProfile.stats[key]; - } else if (key.includes("most_winter")) { - misc.winter[key] = userProfile.stats[key]; - } else if (key.includes("highest_critical_damage")) { - misc.damage[key] = userProfile.stats[key]; - } else if (key.includes("auctions_sold_")) { - auctions_sold[key.replace("auctions_sold_", "")] = userProfile.stats[key]; - } else if (key.includes("auctions_bought_")) { - auctions_bought[key.replace("auctions_bought_", "")] = userProfile.stats[key]; - } else if (key.startsWith("kills_") && key.endsWith("_dragon") && key !== "kills_master_wither_king_dragon") { - misc.dragons["last_hits"] += userProfile.stats[key]; - } else if (key.startsWith("deaths_") && key.endsWith("_dragon") && key !== "deaths_master_wither_king_dragon") { - misc.dragons["deaths"] += userProfile.stats[key]; - } else if (key.includes("kills_corrupted_protector")) { - misc.protector["last_hits"] = userProfile.stats[key]; - } else if (key.includes("deaths_corrupted_protector")) { - misc.protector["deaths"] = userProfile.stats[key]; - } else if (key.startsWith("pet_milestone_")) { - misc.milestones[key.replace("pet_milestone_", "")] = userProfile.stats[key]; - } else if (key.startsWith("mythos_burrows_dug_next_")) { - dug_next[key.replace("mythos_burrows_dug_next_", "").toLowerCase()] = userProfile.stats[key]; - } else if (key.startsWith("mythos_burrows_dug_combat_")) { - dug_combat[key.replace("mythos_burrows_dug_combat_", "").toLowerCase()] = userProfile.stats[key]; - } else if (key.startsWith("mythos_burrows_dug_treasure_")) { - dug_treasure[key.replace("mythos_burrows_dug_treasure_", "").toLowerCase()] = userProfile.stats[key]; - } else if (key.startsWith("mythos_burrows_chains_complete_")) { - chains_complete[key.replace("mythos_burrows_chains_complete_", "").toLowerCase()] = userProfile.stats[key]; - } - } - - for (const key in misc.dragons) { - if (misc.dragons[key] == 0) { - delete misc.dragons[key]; - } - } - - for (const key in misc) { - if (Object.keys(misc[key]).length == 0) { - delete misc[key]; - } - } - - for (const key in dug_next) { - misc.burrows.dug_next[key] = dug_next[key]; - } - - for (const key in dug_combat) { - misc.burrows.dug_combat[key] = dug_combat[key]; - } - - for (const key in dug_treasure) { - misc.burrows.dug_treasure[key] = dug_treasure[key]; - } - - for (const key in chains_complete) { - misc.burrows.chains_complete[key] = chains_complete[key]; - } - - for (const key in auctions_bought) { - misc.auctions_buy["items_bought"] = (misc.auctions_buy["items_bought"] || 0) + auctions_bought[key]; - } - - for (const key in auctions_sold) { - misc.auctions_sell["items_sold"] = (misc.auctions_sell["items_sold"] || 0) + auctions_sold[key]; - } - - misc.uncategorized = {}; - - if ("soulflow" in userProfile) { - misc.uncategorized.soulflow = { - raw: userProfile.soulflow, - formatted: helper.formatNumber(userProfile.soulflow), - }; - } - - if ("fastest_target_practice" in userProfile) { - misc.uncategorized.fastest_target_practice = { - raw: userProfile.fastest_target_practice, - formatted: `${helper.formatNumber(userProfile.fastest_target_practice)}s`, - }; - } - - if ("favorite_arrow" in userProfile) { - misc.uncategorized.favorite_arrow = { - raw: userProfile.favorite_arrow, - formatted: `${userProfile.favorite_arrow - .split("_") - .map((word) => helper.capitalizeFirstLetter(word.toLowerCase())) - .join(" ")}`, - }; - } - - if ("teleporter_pill_consumed" in userProfile) { - misc.uncategorized.teleporter_pill_consumed = { - raw: userProfile.teleporter_pill_consumed, - formatted: userProfile.teleporter_pill_consumed ? "Yes" : "No", - }; - } - - if ("reaper_peppers_eaten" in userProfile) { - misc.uncategorized.reaper_peppers_eaten = { - raw: userProfile.reaper_peppers_eaten, - formatted: helper.formatNumber(userProfile.reaper_peppers_eaten), - maxed: userProfile.reaper_peppers_eaten === constants.MAX_REAPER_PEPPERS_EATEN, - }; - } - - if ("personal_bank_upgrade" in userProfile) { - misc.uncategorized.bank_cooldown = { - raw: userProfile.personal_bank_upgrade, - formatted: constants.BANK_COOLDOWN[userProfile.personal_bank_upgrade] ?? "Unknown", - maxed: userProfile.personal_bank_upgrade === Object.keys(constants.BANK_COOLDOWN).length, - }; - } - - if (bingoProfile?.events !== undefined) { - output.bingo = { - total: bingoProfile.events.length, - points: bingoProfile.events.reduce((a, b) => a + b.points, 0), - completed_goals: bingoProfile.events.reduce((a, b) => a + b.completed_goals.length, 0), - }; - } - - output.misc = misc; - output.auctions_bought = auctions_bought; - output.auctions_sold = auctions_sold; - - const firstJoin = userProfile.first_join; - - const firstJoinText = moment(firstJoin).fromNow(); - - if ("current_area" in userProfile) { - output.current_area = userProfile.current_area; - } - - if ("current_area_updated" in userProfile) { - output.current_area_updated = userProfile.current_area_updated; - } - - output.first_join = { - unix: firstJoin, - text: firstJoinText, - }; - - /* - - WEIGHT - - */ - - output.weight = { - senither: calculateSenitherWeight(output), - lily: calculateLilyWeight(output), - farming: calculateFarmingWeight(output), - }; - - /* - - NETWORTH - - */ - - output.networth = await getPreDecodedNetworth( - userProfile, - { - armor: items.armor, - equipment: items.equipment, - wardrobe: items.wardrobe_inventory, - inventory: items.inventory, - enderchest: items.enderchest, - accessories: items.accessory_bag, - personal_vault: items.personal_vault, - storage: items.storage.concat(items.storage.map((item) => item.containsItems).flat()), - fishing_bag: items.fishing_bag, - potion_bag: items.potion_bag, - candy_inventory: items.candy_bag, - }, - output.bank, - { cache: true, onlyNetworth: true } - ); - - /* - century cake effects - - */ - const centuryCakes = []; - - if (userProfile.temp_stat_buffs) { - for (const cake of userProfile.temp_stat_buffs) { - if (!cake.key.startsWith("cake_")) continue; - let stat = cake.key.replace("cake_", ""); - if (Object.keys(constants.STAT_MAPPINGS).includes(stat)) { - stat = constants.STAT_MAPPINGS[stat]; - } - centuryCakes.push({ - stat: stat == "walk_speed" ? "speed" : stat, - amount: cake.amount, - }); - } - } - - output.century_cakes = centuryCakes; - - output.reaper_peppers_eaten = userProfile.reaper_peppers_eaten ?? 0; - - output.objectives = userProfile.objectives ?? 0; - - output.rift = getRift(userProfile); - - if (!userProfile.pets) { - userProfile.pets = []; - } - userProfile.pets.push(...items.pets); - - if (userProfile.rift?.dead_cats?.montezuma !== undefined) { - userProfile.pets.push(userProfile.rift.dead_cats.montezuma); - userProfile.pets.at(-1).active = false; - } - - for (const pet of userProfile.pets) { - await getItemNetworth(pet, { cache: true, returnItemData: false }); - } - - output.pets = await getPets(userProfile, output); - output.missingPets = await getMissingPets(output.pets, profile.game_mode, output); - output.petScore = getPetScore(output.pets); - - const petScoreRequired = Object.keys(constants.PET_REWARDS).sort((a, b) => parseInt(b) - parseInt(a)); - - output.pet_bonus = {}; - - for (const score of petScoreRequired) { - if (parseInt(score) > output.petScore) { - continue; - } - - output.pet_score_bonus = Object.assign({}, constants.PET_REWARDS[score]); - - break; - } - - for (const pet of output.pets) { - if (pet.price > 0) { - pet.lore += "
"; - pet.lore += helper.renderLore( - `§7Item Value: §6${Math.round(pet.price).toLocaleString()} Coins §7(§6${helper.formatNumber(pet.price)}§7)` - ); - } - - if (!pet.active) { - continue; - } - - for (const stat in pet.stats) { - output.pet_bonus[stat] = (output.pet_bonus[stat] || 0) + pet.stats[stat]; - } - } - - // Apply pet score bonus - for (const stat in output.pet_score_bonus) { - output.stats[stat] += output.pet_score_bonus[stat]; - } - - // console.debug(`${options.debugId}: getStats returned. (${Date.now() - timeStarted}ms)`); - return output; -} - -export async function getPets(profile, calculated) { - let output = []; - - if (!("pets" in profile)) { - return output; - } - - // debug pets - // profile.pets = helper.generateDebugPets("EERIE"); - - for (const pet of profile.pets) { - if (!("tier" in pet)) { - continue; - } - - const petData = constants.PET_DATA[pet.type] ?? { - head: "/head/bc8ea1f51f253ff5142ca11ae45193a4ad8c3ab5e9c6eec8ba7a4fcb7bac40", - type: "???", - maxTier: "legendary", - maxLevel: 100, - emoji: "❓", - }; - - petData.typeGroup = petData.typeGroup ?? pet.type; - - pet.rarity = pet.tier.toLowerCase(); - pet.stats = {}; - pet.ignoresTierBoost = petData.ignoresTierBoost; - /** @type {string[]} */ - const lore = []; - - // Rarity upgrades - if (pet.heldItem == "PET_ITEM_TIER_BOOST" && !pet.ignoresTierBoost) { - pet.rarity = - constants.RARITIES[ - Math.min(constants.RARITIES.indexOf(petData.maxTier), constants.RARITIES.indexOf(pet.rarity) + 1) - ]; - } - if (pet.heldItem == "PET_ITEM_VAMPIRE_FANG" || pet.heldItem == "PET_ITEM_TOY_JERRY") { - if (constants.RARITIES.indexOf(pet.rarity) === constants.RARITIES.indexOf(petData.maxTier) - 1) { - pet.rarity = petData.maxTier; - } - } - - pet.level = getPetLevel(pet.exp, petData.customLevelExpRarityOffset ?? pet.rarity, petData.maxLevel); - - // Get texture - if (typeof petData.head === "object") { - pet.texture_path = petData.head[pet.rarity] ?? petData.head.default; - } else { - pet.texture_path = petData.head; - } - - if (petData.hatching?.level > pet.level.level) { - pet.texture_path = petData.hatching.head; - } - - // eslint-disable-next-line no-prototype-builtins - if (pet.rarity in (petData?.upgrades || {})) { - pet.texture_path = petData.upgrades[pet.rarity]?.head || pet.texture_path; - } - - let petSkin = null; - if (pet.skin && constants.PET_SKINS?.[`PET_SKIN_${pet.skin}`]) { - pet.texture_path = constants.PET_SKINS[`PET_SKIN_${pet.skin}`].texture; - petSkin = constants.PET_SKINS[`PET_SKIN_${pet.skin}`].name; - } - - // Get first row of lore - const loreFirstRow = ["§8"]; - - if (petData.type === "all") { - loreFirstRow.push("All Skills"); - } else { - loreFirstRow.push(helper.capitalizeFirstLetter(petData.type), " ", petData.category ?? "Pet"); - - if (petData.obtainsExp === "feed") { - loreFirstRow.push(", feed to gain XP"); - } - - if (petSkin) { - loreFirstRow.push(`, ${petSkin} Skin`); - } - } - - lore.push(loreFirstRow.join(""), ""); - - // Get name - const petName = - petData.hatching?.level > pet.level.level - ? petData.hatching.name - : petData.name - ? petData.name[pet.rarity] ?? petData.name.default - : helper.titleCase(pet.type.replaceAll("_", " ")); - - const rarity = constants.RARITIES.indexOf(pet.rarity); - - const searchName = pet.type in constants.PET_STATS ? pet.type : "???"; - const petInstance = new constants.PET_STATS[searchName](rarity, pet.level.level, pet.extra, calculated ?? profile); - pet.stats = Object.assign({}, petInstance.stats); - pet.ref = petInstance; - - if (pet.heldItem) { - const { heldItem } = pet; - let heldItemObj = await db.collection("items").findOne({ id: heldItem }); - - if (heldItem in constants.PET_ITEMS) { - for (const stat in constants.PET_ITEMS[heldItem]?.stats) { - pet.stats[stat] = (pet.stats[stat] || 0) + constants.PET_ITEMS[heldItem].stats[stat]; - } - for (const stat in constants.PET_ITEMS[heldItem]?.statsPerLevel) { - pet.stats[stat] = - (pet.stats[stat] || 0) + constants.PET_ITEMS[heldItem].statsPerLevel[stat] * pet.level.level; - } - for (const stat in constants.PET_ITEMS[heldItem]?.multStats) { - if (pet.stats[stat]) { - pet.stats[stat] = (pet.stats[stat] || 0) * constants.PET_ITEMS[heldItem].multStats[stat]; - } - } - if ("multAllStats" in constants.PET_ITEMS[heldItem]) { - for (const stat in pet.stats) { - pet.stats[stat] *= constants.PET_ITEMS[heldItem].multAllStats; - } - } - } - - // push specific pet lore before stats added - if (constants.PET_DATA[pet.type]?.subLore !== undefined) { - lore.push(constants.PET_DATA[pet.type].subLore, " "); - } - - // push pet lore after held item stats added - const stats = pet.ref.lore(pet.stats); - stats.forEach((line) => { - lore.push(line); - }); - - // then the ability lore - const abilities = pet.ref.abilities; - abilities.forEach((ability) => { - lore.push(" ", ability.name); - ability.desc.forEach((line) => { - lore.push(line); - }); - }); - - // now we push the lore of the held items - heldItemObj = constants.PET_ITEMS[heldItem] ?? constants.PET_ITEMS["???"]; - - lore.push("", `§6Held Item: §${constants.RARITY_COLORS[heldItemObj.tier.toLowerCase()]}${heldItemObj.name}`); - - if (heldItem in constants.PET_ITEMS) { - lore.push(constants.PET_ITEMS[heldItem].description); - } - // extra line - lore.push(" "); - } else { - // no held items so push the new stats - const stats = pet.ref.lore(); - stats.forEach((line) => { - lore.push(line); - }); - - const abilities = pet.ref.abilities; - abilities.forEach((ability) => { - lore.push(" ", ability.name); - ability.desc.forEach((line) => { - lore.push(line); - }); - }); - - // extra line - lore.push(" "); - } - - // passive perks text - if (petData.passivePerks) { - lore.push("§8This pet's perks are active even when the pet is not summoned!", ""); - } - - // always gains exp text - if (petData.alwaysGainsExp) { - lore.push("§8This pet gains XP even when not summoned!", ""); - - if (typeof petData.alwaysGainsExp === "string") { - lore.push(`§8This pet only gains XP on the ${petData.alwaysGainsExp}§8!`, ""); - } - } - - if (pet.level.level < petData.maxLevel) { - lore.push(`§7Progress to Level ${pet.level.level + 1}: §e${(pet.level.progress * 100).toFixed(1)}%`); - - const progress = Math.ceil(pet.level.progress * 20); - const numerator = pet.level.xpCurrent.toLocaleString(); - const denominator = helper.formatNumber(pet.level.xpForNext, false); - - lore.push(`§2${"-".repeat(progress)}§f${"-".repeat(20 - progress)} §e${numerator} §6/ §e${denominator}`); - } else { - lore.push("§bMAX LEVEL"); - } - - let progress = Math.floor((pet.exp / pet.level.xpMaxLevel) * 100); - if (isNaN(progress)) { - progress = 0; - } - - lore.push( - "", - `§7Total XP: §e${helper.formatNumber(pet.exp, true, 1)} §6/ §e${helper.formatNumber( - pet.level.xpMaxLevel, - true, - 1 - )} §6(${progress.toLocaleString()}%)` - ); - - if (petData.obtainsExp !== "feed") { - lore.push(`§7Candy Used: §e${pet.candyUsed || 0} §6/ §e10`); - } - - pet.lore = ""; - - for (const line of lore) { - pet.lore += '' + helper.renderLore(line) + ""; - } - - pet.display_name = `${petName}${petSkin ? " ✦" : ""}`; - pet.emoji = petData.emoji; - pet.ref.profile = null; - - output.push(pet); - } - - output = output.sort((a, b) => { - if (a.active === b.active) { - if (a.rarity == b.rarity) { - if (a.type == b.type) { - return a.level.level > b.level.level ? -1 : 1; - } else { - let maxPetA = output - .filter((x) => x.type == a.type && x.rarity == a.rarity) - .sort((x, y) => y.level.level - x.level.level); - - maxPetA = maxPetA.length > 0 ? maxPetA[0].level.level : null; - - let maxPetB = output - .filter((x) => x.type == b.type && x.rarity == b.rarity) - .sort((x, y) => y.level.level - x.level.level); - - maxPetB = maxPetB.length > 0 ? maxPetB[0].level.level : null; - - if (maxPetA && maxPetB && maxPetA == maxPetB) { - return a.type < b.type ? -1 : 1; - } else { - return maxPetA > maxPetB ? -1 : 1; - } - } - } else { - return constants.RARITIES.indexOf(a.rarity) < constants.RARITIES.indexOf(b.rarity) ? 1 : -1; - } - } - - return a.active ? -1 : 1; - }); - - return output; -} - -async function getMissingPets(pets, gameMode, userProfile) { - const profile = { - pets: [], - }; - - const missingPets = []; - - const ownedPetTypes = pets.map((pet) => constants.PET_DATA[pet.type]?.typeGroup || pet.type); - - for (const [petType, petData] of Object.entries(constants.PET_DATA)) { - if ( - ownedPetTypes.includes(petData.typeGroup ?? petType) || - (petData.bingoExclusive === true && gameMode !== "bingo") - ) { - continue; - } - - const key = petData.typeGroup ?? petType; - - missingPets[key] ??= []; - - missingPets[key].push({ - type: petType, - active: false, - exp: helper.getPetExp(constants.PET_DATA[petType].maxTier, constants.PET_DATA[petType].maxLevel), - tier: constants.PET_DATA[petType].maxTier, - candyUsed: 0, - heldItem: null, - skin: null, - uuid: helper.generateUUID(), - }); - } - - for (const pets of Object.values(missingPets)) { - if (pets.length > 1) { - // using exp to find the highest tier - profile.pets.push(pets.sort((a, b) => b.exp - a.exp)[0]); - continue; - } - - profile.pets.push(pets[0]); - } - - profile.rift = userProfile.rift; - profile.collections = userProfile.collections; - - return await getPets(profile); -} - -function getPetScore(pets) { - const highestRarity = {}; - for (const pet of pets) { - if (constants.PET_DATA[pet.type]?.ignoredInPetScoreCalculation === true) { - continue; - } - - if (!(pet.type in highestRarity) || constants.PET_VALUE[pet.rarity] > highestRarity[pet.type]) { - highestRarity[pet.type] = constants.PET_VALUE[pet.rarity]; - } - } - - const highestLevel = {}; - for (const pet of pets) { - if (constants.PET_DATA[pet.type]?.ignoredInPetScoreCalculation === true) { - continue; - } - - if (!(pet.type in highestLevel) || pet.level.level > highestLevel[pet.type]) { - if (constants.PET_DATA[pet.type] && pet.level.level < constants.PET_DATA[pet.type].maxLevel) { - continue; - } - - highestLevel[pet.type] = 1; - } - } - - const output = - Object.values(highestRarity).reduce((a, b) => a + b, 0) + Object.values(highestLevel).reduce((a, b) => a + b, 0); - - return output; -} - -/** - * Checks if an accessory is present in an array of accessories. - * - * @param {Object[]} accessories - The array of accessories to search. - * @param {Object|string} accessory - The accessory object or ID to find. - * @param {Object} [options] - The options object. - * @param {boolean} [options.ignoreRarity=false] - Whether to ignore the rarity of the accessory when searching. - * @returns {boolean} True if the accessory is found, false otherwise. - */ -function hasAccessory(accessories, accessory, options = { ignoreRarity: false }) { - const id = typeof accessory === "object" ? accessory.id : accessory; - - if (options.ignoreRarity === false) { - return accessories.some( - (a) => a.id === id && constants.RARITIES.indexOf(a.rarity) >= constants.RARITIES.indexOf(accessory.rarity) - ); - } else { - return accessories.some((a) => a.id === id); - } -} - -/** - * Finds an accessory in an array of accessories by its ID. - * - * @param {Object[]} accessories - The array of accessories to search. - * @param {string} accessory - The ID of the accessory to find. - * @returns {Object|undefined} The accessory object if found, or undefined if not found. - */ -function getAccessory(accessories, accessory) { - return accessories.find((a) => a.id === accessory); -} - -function getMissingAccessories(accessories) { - const ACCESSORIES = constants.getAllAccessories(); - const unique = ACCESSORIES.map(({ id, tier: rarity }) => ({ id, rarity })); - - for (const { id } of unique) { - if (id in constants.accessoryAliases === false) continue; - - for (const duplicate of constants.accessoryAliases[id]) { - if (hasAccessory(accessories, duplicate, { ignoreRarity: true }) === true) { - getAccessory(accessories, duplicate).id = id; - } - } - } - - let missing = unique.filter((accessory) => hasAccessory(accessories, accessory) === false); - for (const { id } of missing) { - const upgrades = constants.getUpgradeList(id); - if (upgrades === undefined) { - continue; - } - - for (const upgrade of upgrades.filter((item) => upgrades.indexOf(item) > upgrades.indexOf(id))) { - if (hasAccessory(accessories, upgrade) === true) { - missing = missing.filter((item) => item.id !== id); - } - } - } - - const upgrades = []; - const other = []; - for (const { id, rarity } of missing) { - const ACCESSORY = ACCESSORIES.find((a) => a.id === id && a.tier === rarity); - - const object = { - ...ACCESSORY, - display_name: ACCESSORY.name ?? null, - rarity: rarity, - }; - - if ((constants.getUpgradeList(id) && constants.getUpgradeList(id)[0] !== id) || ACCESSORY.rarities) { - upgrades.push(object); - } else { - other.push(object); - } - } - - return { - missing: other, - upgrades: upgrades, - }; -} - -export async function getCollections(uuid, profile, cacheOnly = false) { - const output = {}; - - const userProfile = profile.members[uuid]; - - if (!("unlocked_coll_tiers" in userProfile) || !("collection" in userProfile)) { - return output; - } - - const members = {}; - - (await Promise.all(Object.keys(profile.members).map((a) => helper.resolveUsernameOrUuid(a, db, cacheOnly)))).forEach( - (a) => (members[a.uuid] = a.display_name) - ); - - for (const collection of userProfile.unlocked_coll_tiers) { - const split = collection.split("_"); - const tier = Math.max(0, parseInt(split.pop())); - const type = split.join("_"); - const amount = userProfile.collection[type] || 0; - const amounts = []; - let totalAmount = 0; - - for (const member in profile.members) { - const memberProfile = profile.members[member]; - - if ("collection" in memberProfile) { - amounts.push({ username: members[member], amount: memberProfile.collection[type] || 0 }); - } - } - - for (const memberAmount of amounts) { - totalAmount += memberAmount.amount; - } - - if (!(type in output) || tier > output[type].tier) { - output[type] = { tier, amount, totalAmount, amounts }; - } - - const collectionData = constants.COLLECTION_DATA.find((a) => a.skyblockId == type); - - for (const tier of collectionData?.tiers ?? []) { - if (totalAmount >= tier.amountRequired) { - output[type].tier = Math.max(tier.tier, output[type].tier); - } - } - } - - return output; -} - -export function getTrophyFish(userProfile) { - const output = { - total_caught: 0, - stage: { - name: null, - progress: null, - }, - fish: [], - }; - - for (const key of Object.keys(constants.TROPHY_FISH)) { - const id = key.toLowerCase(); - const caught = (userProfile.trophy_fish && userProfile.trophy_fish[id]) || 0; - const caughtBronze = (userProfile.trophy_fish && userProfile.trophy_fish[`${id}_bronze`]) || 0; - const caughtSilver = (userProfile.trophy_fish && userProfile.trophy_fish[`${id}_silver`]) || 0; - const caughtGold = (userProfile.trophy_fish && userProfile.trophy_fish[`${id}_gold`]) || 0; - const caughtDiamond = (userProfile.trophy_fish && userProfile.trophy_fish[`${id}_diamond`]) || 0; - - const highestType = - caughtDiamond > 0 ? "diamond" : caughtGold > 0 ? "gold" : caughtSilver > 0 ? "silver" : "bronze"; - - output.fish.push({ - id: key, - name: constants.TROPHY_FISH[key].display_name, - texture: constants.TROPHY_FISH[key].textures[highestType], - description: constants.TROPHY_FISH[key].description, - caught: { - total: caught, - bronze: caughtBronze, - silver: caughtSilver, - gold: caughtGold, - diamond: caughtDiamond, - highestType: highestType, - }, - }); - } - - output.total_caught = userProfile.trophy_fish?.total_caught || 0; - - const { type: stageType, formatted: stageFormatted } = - constants.TROPHY_FISH_STAGES[(userProfile.trophy_fish?.rewards || []).length] || {}; - const { type: stageProgressType } = constants.TROPHY_FISH_STAGES[ - (userProfile.trophy_fish?.rewards || []).length + 1 - ] || { - type: stageType, - }; - - const stageProgress = - stageType === "diamond" - ? null - : stageType - ? `${ - Object.keys(userProfile.trophy_fish).filter( - (a) => a.endsWith(stageProgressType) && userProfile.trophy_fish[a] > 0 - ).length - } / ${Object.keys(constants.TROPHY_FISH).length}` - : null; - - output.stage = { - name: stageFormatted || "None", - type: stageType, - progress: stageProgress, - }; - - return output; -} - -function getRift(userProfile) { - if (!("rift" in userProfile) || (userProfile.visited_zones && userProfile.visited_zones.includes("rift") === false)) { - return null; - } - - const rift = userProfile.rift; - - const killedEyes = []; - for (const [key, data] of constants.RIFT_EYES.entries()) { - data.unlocked = rift.wither_cage?.killed_eyes && rift.wither_cage.killed_eyes[key] !== undefined; - - killedEyes.push(data); - } - - const timecharms = []; - for (const [key, data] of constants.RIFT_TIMECHARMS.entries()) { - data.unlocked = rift.gallery?.secured_trophies && rift.gallery.secured_trophies[key]?.type !== undefined; - data.unlocked_at = rift.gallery?.secured_trophies && rift.gallery.secured_trophies[key]?.timestamp; +import { getPreDecodedNetworth } from "skyhelper-networth"; +import sanitize from "mongo-sanitize"; +import retry from "async-retry"; +import axios from "axios"; - timecharms.push(data); - } +import * as constants from "./constants.js"; +import credentials from "./credentials.js"; +import * as helper from "./helper.js"; +import * as stats from "./stats.js"; +import { SkyCryptError } from "./constants/error.js"; - return { - motes: { - purse: userProfile.motes_purse ?? 0, - lifetime: userProfile.stats.rift_lifetime_motes_earned ?? 0, - orbs: userProfile.stats.rift_motes_orb_pickup ?? 0, - }, - enigma: { - souls: rift.enigma.found_souls?.length ?? 0, - total_souls: constants.RIFT_ENIGMA_SOULS, - }, - wither_cage: { - killed_eyes: killedEyes, - }, - timecharms: { - timecharms: timecharms, - obtained_timecharms: timecharms.filter((a) => a.unlocked).length, - }, - dead_cats: { - montezuma: rift?.dead_cats?.montezuma ?? {}, - found_cats: rift?.dead_cats?.found_cats ?? [], - }, - castle: { - grubber_stacks: rift.castle?.grubber_stacks ?? 0, - max_burgers: constants.MAX_GRUBBER_STACKS, - }, - }; -} +const hypixel = axios.create({ + baseURL: "https://api.hypixel.net/", +}); -export async function getDungeons(userProfile, hypixelProfile) { +export async function getStats( + db, + profile, + bingoProfile, + allProfiles, + items, + packs, + options = { cacheOnly: false, debugId: `${helper.getClusterId()}/unknown@getStats` } +) { const output = {}; - const dungeons = userProfile.dungeons; - if (dungeons == null || Object.keys(dungeons).length === 0) return output; - - const dungeons_data = constants.DUNGEONS; - if (dungeons.dungeon_types == null || Object.keys(dungeons.dungeon_types).length === 0) return output; - - // Main Dungeons Data - for (const type of Object.keys(dungeons.dungeon_types)) { - const dungeon = dungeons.dungeon_types[type]; - if (dungeon == null || Object.keys(dungeon).length === 0) { - output[type] = { visited: false }; - continue; - } + console.debug(`${options.debugId}: getStats called.`); + const timeStarted = Date.now(); - const floors = {}; - - for (const key of Object.keys(dungeon)) { - if (typeof dungeon[key] != "object") continue; - for (const floor of Object.keys(dungeon[key])) { - if (!floors[floor]) { - floors[floor] = { - name: `floor_${floor}`, - icon_texture: "908fc34531f652f5be7f27e4b27429986256ac422a8fb59f6d405b5c85c76f7", - stats: {}, - }; - } - - const id = `${type}_${floor}`; // Floor ID - if (dungeons_data.floors[id]) { - if (dungeons_data.floors[id].name) { - floors[floor].name = dungeons_data.floors[id].name; - } - if (dungeons_data.floors[id].texture) { - floors[floor].icon_texture = dungeons_data.floors[id].texture; - } - } - - if (key.startsWith("most_damage")) { - if (!floors[floor].most_damage || dungeon[key][floor] > floors[floor].most_damage.value) { - floors[floor].most_damage = { - class: key.replace("most_damage_", ""), - value: dungeon[key][floor], - }; - } - } else if (key === "best_runs") { - floors[floor][key] = dungeon[key][floor]; - } else { - floors[floor].stats[key] = dungeon[key][floor]; - } - } - } + const userProfile = profile.members[profile.uuid]; + const hypixelProfile = await helper.getRank(profile.uuid, db, options.cacheOnly); - const dungeon_id = `dungeon_${type}`; // Dungeon ID - const highest_floor = dungeon.highest_tier_completed || 0; - output[type] = { - id: dungeon_id, - visited: true, - level: getLevelByXp(dungeon.experience, { - type: "dungeoneering", - skill: "dungeoneering", - ignoreCap: true, - infinite: true, - }), - highest_floor: - dungeons_data.floors[`${type}_${highest_floor}`] && dungeons_data.floors[`${type}_${highest_floor}`].name - ? dungeons_data.floors[`${type}_${highest_floor}`].name - : `floor_${highest_floor}`, - floors: floors, - completions: Object.values(floors).reduce((a, b) => a + (b.stats?.tier_completions ?? 0), 0), - }; + output.stats = Object.assign({}, constants.BASE_STATS); - output[type].level.rank = await getLeaderboardPosition(`dungeons_${type}_xp`, dungeon.experience); - } + output.fairy_souls = stats.getFairySouls(userProfile, profile); - // Classes - output.classes = {}; - output.class_average = {}; + output.skills = await stats.getSkills(userProfile, hypixelProfile, profile.members); - let used_classes = false; - const current_class = dungeons.selected_dungeon_class || "none"; - for (const className of Object.keys(dungeons.player_classes)) { - const data = dungeons.player_classes[className]; + output.slayer = stats.getSlayer(userProfile); - if (!data.experience) { - data.experience = 0; - } + output.base_stats = Object.assign({}, output.stats); - output.classes[className] = { - experience: getLevelByXp(data.experience, { - type: "dungeoneering", - skill: "dungeoneering", - ignoreCap: true, - infinite: true, - }), - current: false, - }; + output.kills = stats.getKills(userProfile); + output.deaths = stats.getDeaths(userProfile); - output.classes[className].experience.rank = await getLeaderboardPosition( - `dungeons_class_${className}_xp`, - data.experience - ); + const playerObject = await helper.resolveUsernameOrUuid(profile.uuid, db, options.cacheOnly); - if (data.experience > 0) { - used_classes = true; - } - if (className == current_class) { - output.classes[className].current = true; - } + output.display_name = playerObject.display_name; - output.class_average.experience ??= 0; - output.class_average.experience += output.classes[className].experience.xp; + if ("wardrobe_equipped_slot" in userProfile) { + output.wardrobe_equipped_slot = userProfile.wardrobe_equipped_slot; } - output.used_classes = used_classes; - output.class_average.avrg_level = Object.keys(output.classes) - .map((key) => output.classes[key].experience.level / Object.keys(output.classes).length) - .reduce((a, b) => a + b, 0); - output.class_average.avrg_level_with_progress = Object.keys(output.classes) - .map((key) => output.classes[key].experience.levelWithProgress / Object.keys(output.classes).length) - .reduce((a, b) => a + b, 0); - output.class_average.max = - Object.keys(output.classes).filter((key) => output.classes[key].experience.level >= 50).length === - Object.keys(output.classes).length; - - output.selected_class = current_class; - output.secrets_found = hypixelProfile.achievements.skyblock_treasure_hunter || 0; - - if (!output.catacombs.visited) return output; - - // Boss Collections - const collection_data = dungeons_data.boss_collections; - const boss_data = dungeons_data.bosses; - const collections = {}; - - for (const coll_id in collection_data) { - const coll = collection_data[coll_id]; - const boss = boss_data[coll.boss]; - - for (const floor_id of boss.floors) { - // This can be done much better. But I didn't want to deal with it. - const a = floor_id.split("_"); - const dung_floor = a.pop(); - const dung_name = a.join("_"); - - // I can't put these two into a single if. Welp, doesn't seem like a problem. - if (output[dung_name] == null || !output[dung_name]?.visited) continue; - if (output[dung_name].floors[dung_floor] == null) continue; - - const data = output[dung_name].floors[dung_floor]; - const num = data.stats.tier_completions || 0; - - if (num <= 0) continue; - - if (!collections[coll_id]) { - collections[coll_id] = { - name: boss.name, - texture: boss.texture, - tier: 0, - maxed: false, - killed: num, - floors: {}, - unclaimed: 0, - claimed: [], - }; - } else { - collections[coll_id].killed += num; - } - - collections[coll_id].floors[floor_id] = num; - } - - if (!collections[coll_id]) { - continue; - } - - for (const rewardId in coll.rewards) { - const reward = coll.rewards[rewardId]; - if (collections[coll_id].killed >= reward.required) { - collections[coll_id].tier = reward.tier; - if (rewardId != "coming_soon") collections[coll_id].unclaimed++; - } else { - break; - } + const userInfo = await db.collection("usernames").findOne({ uuid: profile.uuid }); - if (collections[coll_id].tier == coll.max_tiers) { - collections[coll_id].maxed = true; - } + const memberUuids = []; + for (const [uuid, memberProfile] of Object.entries(profile?.members ?? {})) { + if (memberProfile?.coop_invitation?.confirmed === false || memberProfile.deletion_notice?.timestamp !== undefined) { + memberProfile.removed = true; } - } - const tasks = userProfile.tutorial; - for (const i in tasks) { - if (!tasks[i].startsWith("boss_collection_claimed")) continue; - const task = tasks[i].split("_").splice(3); - - if (!Object.keys(boss_data).includes(task[0])) continue; - const boss = boss_data[task[0]]; - - if (!Object.keys(collection_data).includes(boss.collection)) continue; - const coll = collection_data[boss.collection]; - - const item = coll.rewards[task.splice(1).join("_")]; - - if (item == null || boss == null) continue; - collections[boss.collection].claimed.push(item.name); - collections[boss.collection].unclaimed--; + memberUuids.push(uuid); } - if (Object.keys(collections).length === 0) { - output.unlocked_collections = false; - } else { - output.unlocked_collections = true; - } + const members = await Promise.all( + memberUuids.map(async (a) => { + return { + ...(await helper.resolveUsernameOrUuid(a, db, options.cacheOnly)), + removed: profile.members[a]?.removed || false, + }; + }) + ); - output.boss_collections = collections; + if (userInfo) { + output.display_name = userInfo.username; - // Journal Entries - const JOURNAL_CONSTANTS = constants.DUNGEONS.journals; - const journals = { - pages_collected: 0, - journals_completed: 0, - total_pages: 0, - journal_entries: dungeons.dungeon_journal.unlocked_journals, - }; + members.push({ + uuid: profile.uuid, + display_name: userInfo.username, + removed: profile.members[profile.uuid]?.removed || false, + }); - if (dungeons.dungeon_journal.unlocked_journals !== undefined) { - for (const entryID of dungeons.dungeon_journal.unlocked_journals) { - journals.journals_completed += 1; - journals.pages_collected += JOURNAL_CONSTANTS[entryID]?.pages || 0; + if ("emoji" in userInfo) { + output.display_emoji = userInfo.emoji; } - } - - for (const journal in JOURNAL_CONSTANTS) { - journals.total_pages += JOURNAL_CONSTANTS[journal].pages; - } - output.journals = journals; - - // Level Bonuses (Only Catacombs Item Boost right now) - for (const name in constants.DUNGEONS.level_bonuses) { - const level_stats = constants.DUNGEONS.level_bonuses[name]; - const steps = Object.keys(level_stats) - .sort((a, b) => Number(a) - Number(b)) - .map((a) => Number(a)); - - let level = 0; - switch (name) { - case "dungeon_catacombs": - level = output.catacombs.level.level; - output.catacombs.bonuses = { - item_boost: 0, - }; - break; - default: - continue; + if ("emojiImg" in userInfo) { + output.display_emoji_img = userInfo.emojiImg; } - for (let x = steps[0]; x <= steps[steps.length - 1]; x += 1) { - if (level < x) { - break; - } - - const level_step = steps - .slice() - .reverse() - .find((a) => a <= x); - - const level_bonus = level_stats[level_step]; - - for (const bonus in level_bonus) { - switch (name) { - case "dungeon_catacombs": - output.catacombs.bonuses[bonus] += level_bonus[bonus]; - } - } + if (userInfo.username == "jjww2") { + output.display_emoji = constants.randomEmoji(); } } - return output; -} - -function getEssence(userProfile, hypixelProfile) { - /** @type {{[key:string]: number}} */ - const output = {}; - - for (const essence in constants.ESSENCE) { - output[essence] = userProfile?.[`essence_${essence}`] ?? 0; - } + output.guild = await helper.getGuild(profile.uuid, db, options.cacheOnly); - return output; -} + output.rank_prefix = helper.renderRank(hypixelProfile); + output.uuid = profile.uuid; + output.skin_data = playerObject.skin_data; -function getHotmItems(userProfile, packs) { - const data = userProfile.mining_core; - const output = []; + output.profile = { profile_id: profile.profile_id, cute_name: profile.cute_name, game_mode: profile.game_mode }; + output.profiles = {}; - // Filling the space with empty items - for (let index = 0; index < 7 * 9; index++) { - output.push(helper.generateItem()); + for (const sbProfile of allProfiles.filter((a) => a.profile_id != profile.profile_id)) { + output.profiles[sbProfile.profile_id] = { + profile_id: sbProfile.profile_id, + cute_name: sbProfile.cute_name, + game_mode: sbProfile.game_mode, + }; } - if (!data) { - return output; - } + output.members = members.filter((a) => a.uuid != profile.uuid); - const hotmLevelData = data.experience ? getLevelByXp(data.experience, { type: "hotm" }) : 0; - const nodes = data.nodes - ? Object.fromEntries(Object.entries(data.nodes).filter(([key, value]) => !key.startsWith("toggle_"))) - : {}; - const toggles = data.nodes - ? Object.fromEntries(Object.entries(data.nodes).filter(([key, value]) => key.startsWith("toggle_"))) - : {}; - const mcdata = getMiningCoreData(userProfile); - - // Check for missing node classes - for (const nodeId in nodes) { - if (constants.HOTM.nodes[nodeId] == undefined) { - throw new Error(`Missing Heart of the Mountain node: ${nodeId}`); - } - } + output.minions = stats.getMinions(profile); - // Processing nodes - for (const nodeId in constants.HOTM.nodes) { - const enabled = toggles[`toggle_${nodeId}`] ?? true; - const level = nodes[nodeId] ?? 0; - const node = new constants.HOTM.nodes[nodeId]({ - level, - enabled, - nodes, - hotmLevelData, - selectedPickaxeAbility: data.selected_pickaxe_ability, - }); + output.bestiary = stats.getBestiary(userProfile); - output[node.position7x9 - 1] = helper.generateItem({ - display_name: node.name, - id: node.itemData.id, - Damage: node.itemData.Damage, - glowing: node.itemData.glowing, - tag: { - display: { - Name: node.displayName, - Lore: node.lore, - }, - }, - position: node.position7x9, - }); - } + output.social = hypixelProfile.socials; - // Processing HotM tiers - for (let tier = 1; tier <= constants.HOTM.tiers; tier++) { - const hotm = new constants.HOTM.hotm(tier, hotmLevelData); - - output[hotm.position7x9 - 1] = helper.generateItem({ - display_name: `Tier ${tier}`, - id: hotm.itemData.id, - Damage: hotm.itemData.Damage, - glowing: hotm.itemData.glowing, - tag: { - display: { - Name: hotm.displayName, - Lore: hotm.lore, - }, - }, - position: hotm.position7x9, - }); - } + output.dungeons = await stats.getDungeons(userProfile, hypixelProfile); - // Processing HotM items (stats, hc crystals, reset) - for (const itemClass of constants.HOTM.items) { - const item = new itemClass({ - resources: { - token_of_the_mountain: mcdata.tokens, - mithril_powder: mcdata.powder.mithril, - gemstone_powder: mcdata.powder.gemstone, - }, - crystals: mcdata.crystal_nucleus.crystals, - last_reset: mcdata.hotm_last_reset, - }); + output.fishing = stats.getFishing(userProfile); - output[item.position7x9 - 1] = helper.generateItem({ - display_name: helper.removeFormatting(item.displayName), - id: item.itemData.id, - Damage: item.itemData.Damage, - glowing: item.itemData.glowing, - texture_path: item.itemData?.texture_path, - tag: { - display: { - Name: item.displayName, - Lore: item.lore, - }, - }, - position: item.position7x9, - }); - } + output.farming = stats.getFarming(userProfile); - // Processing textures - output.forEach(async (item) => { - const customTexture = await getTexture(item, { - ignore_id: false, - pack_ids: packs, - }); + output.enchanting = stats.getEnchanting(userProfile); - if (customTexture) { - item.animated = customTexture.animated; - item.texture_path = "/" + customTexture.path; - item.texture_pack = customTexture.pack.config; - item.texture_pack.base_path = - "/" + path.relative(path.resolve(__dirname, "..", "public"), customTexture.pack.base_path); - } - }); + output.mining = await stats.getMining(userProfile, hypixelProfile); - return output; -} + output.crimson_isle = stats.getCrimsonIsle(userProfile); -function getMiningCoreData(userProfile) { - const output = {}; - const data = userProfile.mining_core; + output.collections = await stats.getCollections( + profile.uuid, + profile, + output.dungeons, + output.crimson_isle, + options.cacheOnly + ); - if (!data) { - return null; - } + output.skyblock_level = await stats.getSkyBlockLevel(userProfile); - output.tier = getLevelByXp(data.experience, { type: "hotm" }); + output.visited_zones = userProfile.player_data.visited_zones || []; - const totalTokens = helper.calcHotmTokens(output.tier.level, data.nodes?.special_0 || 0); - output.tokens = { - total: totalTokens, - spent: data.tokens_spent || 0, - available: totalTokens - (data.tokens_spent || 0), - }; + output.visited_modes = userProfile.player_data.visited_modes || []; - output.selected_pickaxe_ability = data.selected_pickaxe_ability - ? constants.HOTM.names[data.selected_pickaxe_ability] - : null; - - output.powder = { - mithril: { - total: (data.powder_mithril || 0) + (data.powder_spent_mithril || 0), - spent: data.powder_spent_mithril || 0, - available: data.powder_mithril || 0, - }, - gemstone: { - total: (data.powder_gemstone || 0) + (data.powder_spent_gemstone || 0), - spent: data.powder_spent_gemstone || 0, - available: data.powder_gemstone || 0, - }, - }; + output.perks = userProfile.player_data.perks || {}; - const crystals_completed = data.crystals - ? Object.values(data.crystals) - .filter((x) => x.total_placed) - .map((x) => x.total_placed) - : []; - output.crystal_nucleus = { - times_completed: crystals_completed.length > 0 ? Math.min(...crystals_completed) : 0, - crystals: data.crystals || {}, - goblin: data.biomes?.goblin ?? null, - precursor: data.biomes?.precursor ?? null, - }; + output.harp_quest = userProfile.quests?.harp_quest || {}; - output.daily_ores = { - mined: data.daily_ores_mined, - day: data.daily_ores_mined_day, - ores: { - mithril: { - day: data.daily_ores_mined_day_mithril_ore, - count: data.daily_ores_mined_mithril_ore, - }, - gemstone: { - day: data.daily_ores_mined_day_gemstone, - count: data.daily_ores_mined_gemstone, - }, - }, - }; + output.misc = stats.getMisc(profile, userProfile, hypixelProfile); - output.hotm_last_reset = data.last_reset || 0; + output.bingo = stats.getBingoData(bingoProfile); - output.crystal_hollows_last_access = data.greater_mines_last_access || 0; + output.user_data = stats.getUserData(userProfile); - output.daily_effect = { - effect: data.current_daily_effect || null, - last_changed: data.current_daily_effect_last_changed || null, - }; + output.currencies = stats.getCurrenciesData(userProfile, profile); - output.nodes = data.nodes || {}; + output.weight = stats.getWeight(output); - return output; -} + output.accessories = await stats.getMissingAccessories(output, items, packs); -async function getForge(userProfile) { - const output = {}; + output.networth = + (await getPreDecodedNetworth( + userProfile, + { + armor: items.armor.armor, + equipment: items.equipment.equipment, + wardrobe: items.wardrobe_inventory, + inventory: items.inventory, + enderchest: items.enderchest, + accessories: items.accessory_bag, + personal_vault: items.personal_vault, + storage: items.storage.concat(items.storage.map((item) => item.containsItems).flat()), + fishing_bag: items.fishing_bag, + potion_bag: items.potion_bag, + candy_inventory: items.candy_bag, + museum: [], + }, + output.currencies.bank, + { cache: true, onlyNetworth: true, v2Endpoint: true } + )) ?? {}; - if (userProfile?.forge?.forge_processes?.forge_1) { - const forge = Object.values(userProfile.forge.forge_processes.forge_1); - const processes = []; - for (const item of forge) { - const forgeItem = { - id: item.id, - slot: item.slot, - timeFinished: 0, - timeFinishedText: "", - }; + output.temp_stats = stats.getTempStats(userProfile); - if (item.id in constants.FORGE_TIMES) { - let forgeTime = constants.FORGE_TIMES[item.id] * 60 * 1000; // convert minutes to milliseconds - const quickForge = userProfile.mining_core?.nodes?.forge_time; - if (quickForge != null) { - forgeTime *= constants.QUICK_FORGE_MULTIPLIER[quickForge]; - } - const dbObject = await db.collection("items").findOne({ id: item.id }); - - forgeItem.name = item.id == "PET" ? "[Lvl 1] Ammonite" : dbObject ? dbObject.name : item.id; - const timeFinished = item.startTime + forgeTime; - forgeItem.timeFinished = timeFinished; - forgeItem.timeFinishedText = moment(timeFinished).fromNow(); - } else { - forgeItem.id = `UNKNOWN-${item.id}`; - } - processes.push(forgeItem); - } - output.processes = processes; - } + output.rift = stats.getRift(userProfile); - return output; -} + output.pets = await stats.getPets(userProfile, output, items, profile); + + console.debug(`${options.debugId}: getStats returned. (${Date.now() - timeStarted}ms)`); -function getProfileUpgrades(profile) { - const output = {}; - for (const upgrade in constants.PROFILE_UPGRADES) { - output[upgrade] = 0; - } - if (profile.community_upgrades?.upgrade_states != undefined) { - for (const u of profile.community_upgrades.upgrade_states) { - output[u.upgrade] = Math.max(output[u.upgrade] || 0, u.tier); - } - } return output; } @@ -3751,8 +198,8 @@ export async function getProfile( paramProfile, options = { cacheOnly: false, debugId: `${helper.getClusterId()}/unknown@getProfile` } ) { - // console.debug(`${options.debugId}: getProfile called.`); - // const timeStarted = Date.now(); + console.debug(`${options.debugId}: getProfile called.`); + const timeStarted = Date.now(); if (paramPlayer.length != 32) { try { @@ -3761,7 +208,7 @@ export async function getProfile( paramPlayer = uuid; } catch (e) { console.error(e); - throw e; + throw new SkyCryptError(e); } } @@ -3810,7 +257,7 @@ export async function getProfile( profileObject.last_update = Date.now(); response = await retry( async () => { - return await hypixel.get("skyblock/profiles", { params }); + return await hypixel.get("v2/skyblock/profiles", { params }); }, { retries: 2 } ); @@ -3818,25 +265,25 @@ export async function getProfile( const { data } = response; if (!data.success) { - throw new Error("Request to Hypixel API failed. Please try again!"); + throw new SkyCryptError("Request to Hypixel API failed. Please try again!"); } if (data.profiles == null) { - throw new Error("Player has no SkyBlock profiles."); + throw new SkyCryptError("Player has no SkyBlock profiles."); } allSkyBlockProfiles = data.profiles; } catch (e) { if (e?.response?.data?.cause != undefined) { - throw new Error(`Hypixel API Error: ${e.response.data.cause}.`); + throw new SkyCryptError(`Hypixel API Error: ${e.response.data.cause}.`); } - throw e; + throw new SkyCryptError(e); } } if (allSkyBlockProfiles.length == 0) { - throw new Error("Player has no SkyBlock profiles."); + throw new SkyCryptError("Player has no SkyBlock profiles."); } for (const profile of allSkyBlockProfiles) { @@ -3868,7 +315,7 @@ export async function getProfile( if (memberCount == 0) { if (paramProfile) { - throw new Error("Uh oh, this SkyBlock profile has no players."); + throw new SkyCryptError("Uh oh, this SkyBlock profile has no players."); } continue; @@ -3878,7 +325,7 @@ export async function getProfile( } if (profiles.length == 0) { - throw new Error("No data returned by Hypixel API, please try again!"); + throw new SkyCryptError("No data returned by Hypixel API, please try again!"); } let profile; @@ -3937,7 +384,7 @@ export async function getProfile( profile = profiles[0]; if (!profile) { - throw new Error("Couldn't find any Skyblock profile that belongs to this player."); + throw new SkyCryptError("Couldn't find any Skyblock profile that belongs to this player."); } } @@ -3978,14 +425,14 @@ export async function getProfile( console.error(e); } - updateLeaderboardPositions(db, paramPlayer, allSkyBlockProfiles).catch(console.error); + // updateLeaderboardPositions(db, paramPlayer, allSkyBlockProfiles).catch(console.error); db.collection("profileStore") .updateOne({ uuid: sanitize(paramPlayer) }, { $set: insertProfileStore }, { upsert: true }) .catch(console.error); } - // console.debug(`${options.debugId}: getProfile returned. (${Date.now() - timeStarted}ms)`); + console.debug(`${options.debugId}: getProfile returned. (${Date.now() - timeStarted}ms)`); return { profile: profile, allProfiles: allSkyBlockProfiles, uuid: paramPlayer }; } @@ -4035,7 +482,7 @@ export async function getBingoProfile( const { data } = response; if (!data.success) { - throw new Error("Request to Hypixel API failed. Please try again!"); + throw new SkyCryptError("Request to Hypixel API failed. Please try again!"); } profileData = data; @@ -4052,7 +499,7 @@ export async function getBingoProfile( } if (e?.response?.data?.cause != undefined) { - throw new Error(`Hypixel API Error: ${e.response.data.cause}.`); + throw new SkyCryptError(`Hypixel API Error: ${e.response.data.cause}.`); } throw e; @@ -4063,221 +510,57 @@ export async function getBingoProfile( return profileData; } -async function updateLeaderboardPositions(db, uuid, allProfiles) { - if (constants.BLOCKED_PLAYERS.includes(uuid)) { - return; - } - - const hypixelProfile = await helper.getRank(uuid, db, true); - - const memberProfiles = []; - - for (const singleProfile of allProfiles) { - const userProfile = singleProfile.members[uuid]; - - if (userProfile == null) { - continue; - } - - userProfile.levels = await getLevels(userProfile, hypixelProfile); - - let totalSlayerXp = 0; - - userProfile.slayer_xp = 0; - - if (userProfile.slayer_bosses != undefined) { - for (const slayer in userProfile.slayer_bosses) { - totalSlayerXp += userProfile.slayer_bosses[slayer].xp || 0; - } - - userProfile.slayer_xp = totalSlayerXp; - - for (const mountMob in constants.MOB_MOUNTS) { - const mounts = constants.MOB_MOUNTS[mountMob]; - - userProfile.stats[`kills_${mountMob}`] = 0; - userProfile.stats[`deaths_${mountMob}`] = 0; +export async function getMuseum( + db, + paramProfile, + options = { cacheOnly: false, debugId: `${helper.getClusterId()}/unknown@getProfile` } +) { + console.debug(`${options.debugId}: getMuseum called.`); + const timeStarted = Date.now(); - for (const mount of mounts) { - userProfile.stats[`kills_${mountMob}`] += userProfile.stats[`kills_${mount}`] || 0; - userProfile.stats[`deaths_${mountMob}`] += userProfile.stats[`deaths_${mount}`] || 0; + const profileID = paramProfile.profile_id; + if (profileID.length !== 36) { + throw new SkyCryptError("Invalid profile ID."); + } - delete userProfile.stats[`kills_${mount}`]; - delete userProfile.stats[`deaths_${mount}`]; - } - } - } + let museumData = await db.collection("museumCache").findOne({ profile_id: profileID }); - userProfile.skyblock_level = { - xp: userProfile.leveling?.experience || 0, - level: Math.floor(userProfile.leveling?.experience / 100 || 0), - }; + if (!options.cacheOnly && (museumData == undefined || museumData.last_save < Date.now() - 1000 * 60 * 5)) { + try { + const params = { + key: credentials.hypixel_api_key, + profile: profileID, + }; - userProfile.pet_score = 0; + const response = await retry( + async () => { + return await hypixel.get("skyblock/museum", { params }); + }, + { retries: 2 } + ); - const maxPetRarity = {}; - if (Array.isArray(userProfile.pets)) { - for (const pet of userProfile.pets) { - if (!("tier" in pet)) { - continue; - } + const { data } = response; - maxPetRarity[pet.type] = Math.max(maxPetRarity[pet.type] || 0, constants.PET_VALUE[pet.tier.toLowerCase()]); + if (data === undefined || data.success === false) { + throw new SkyCryptError("Request to Hypixel API failed. Please try again!"); } - for (const key in maxPetRarity) { - userProfile.pet_score += maxPetRarity[key]; + if (data.members === null || Object.keys(data.members).length === 0) { + return null; } - } - - memberProfiles.push({ - profile_id: singleProfile.profile_id, - data: userProfile, - }); - } - - const values = {}; - - values["pet_score"] = getMax(memberProfiles, "data", "pet_score"); - - values["fairy_souls"] = getMax(memberProfiles, "data", "fairy_souls_collected"); - values["average_level"] = getMax(memberProfiles, "data", "levels", "average_level"); - values["total_skill_xp"] = getMax(memberProfiles, "data", "levels", "total_skill_xp"); - - for (const skill of getAllKeys(memberProfiles, "data", "levels", "levels")) { - values[`skill_${skill}_xp`] = getMax(memberProfiles, "data", "levels", "levels", skill, "xp"); - } - - values[`skyblock_level_xp`] = getMax(memberProfiles, "data", "skyblock_level", "xp"); - values["slayer_xp"] = getMax(memberProfiles, "data", "slayer_xp"); - for (const slayer of getAllKeys(memberProfiles, "data", "slayer_bosses")) { - for (const key of getAllKeys(memberProfiles, "data", "slayer_bosses", slayer)) { - if (!key.startsWith("boss_kills_tier")) { - continue; + museumData = { museum: data.members, last_save: Date.now() }; + db.collection("museumCache").updateOne({ profile_id: profileID }, { $set: museumData }, { upsert: true }); + } catch (e) { + console.log(e); + if (e?.response?.data?.cause != undefined) { + throw new SkyCryptError(`Hypixel API Error: ${e.response.data.cause}.`); } - const tier = key.split("_").pop(); - - values[`${slayer}_slayer_boss_kills_tier_${tier}`] = getMax(memberProfiles, "data", "slayer_bosses", slayer, key); - } - - values[`${slayer}_slayer_xp`] = getMax(memberProfiles, "data", "slayer_bosses", slayer, "xp"); - } - - for (const item of getAllKeys(memberProfiles, "data", "collection")) { - values[`collection_${item.toLowerCase()}`] = getMax(memberProfiles, "data", "collection", item); - } - - for (const stat of getAllKeys(memberProfiles, "data", "stats")) { - values[stat] = getMax(memberProfiles, "data", "stats", stat); - } - - // Dungeons (Mainly Catacombs now.) - for (const stat of getAllKeys(memberProfiles, "data", "dungeons", "dungeon_types", "catacombs")) { - switch (stat) { - case "best_runs": - case "highest_tier_completed": - break; - case "experience": - values[`dungeons_catacombs_xp`] = getMax( - memberProfiles, - "data", - "dungeons", - "dungeon_types", - "catacombs", - "experience" - ); - break; - default: - for (const floor of getAllKeys(memberProfiles, "data", "dungeons", "dungeon_types", "catacombs", stat)) { - const floorId = `catacombs_${floor}`; - if (!constants.DUNGEONS.floors[floorId] || !constants.DUNGEONS.floors[floorId].name) continue; - - const floorName = constants.DUNGEONS.floors[floorId].name; - values[`dungeons_catacombs_${floorName}_${stat}`] = getMax( - memberProfiles, - "data", - "dungeons", - "dungeon_types", - "catacombs", - stat, - floor - ); - } - } - } - - for (const dungeonClass of getAllKeys(memberProfiles, "data", "dungeons", "player_classes")) { - values[`dungeons_class_${dungeonClass}_xp`] = getMax( - memberProfiles, - "data", - "dungeons", - "player_classes", - dungeonClass, - "experience" - ); - } - - values[`dungeons_secrets_found`] = hypixelProfile.achievements.skyblock_treasure_hunter || 0; - - const multi = redisClient.pipeline(); - - for (const key in values) { - if (values[key] == null) { - continue; - } - - multi.zadd(`lb_${key}`, values[key], uuid); - } - for (const singleProfile of allProfiles) { - if (singleProfile.banking?.balance != undefined) { - multi.zadd(`lb_bank`, singleProfile.banking.balance, singleProfile.profile_id); - } - - const minionCrafts = []; - - for (const member in singleProfile.members) { - if (Array.isArray(singleProfile.members[member].crafted_generators)) { - minionCrafts.push(...singleProfile.members[member].crafted_generators); - } + throw new SkyCryptError(`Hypixel API Error: Failed to fetch Museum data.`); } - - multi.zadd(`lb_unique_minions`, _.uniq(minionCrafts).length, singleProfile.profile_id); - } - - try { - await multi.exec(); - } catch (e) { - console.error(e); - } -} - -async function init() { - const response = await axios("https://api.hypixel.net/resources/skyblock/collections"); - - if (!response?.data?.collections) { - return; } - for (const type in response.data.collections) { - for (const itemType in response.data.collections[type].items) { - const item = response.data.collections[type].items[itemType]; - try { - const collectionData = constants.COLLECTION_DATA.find((a) => a.skyblockId == itemType); - - collectionData.maxTier = item.maxTiers; - collectionData.tiers = item.tiers; - } catch (e) { - if (e instanceof TypeError) { - //Collection Data filter error - } else { - //Throw exception unchanged - throw e; - } - } - } - } + console.debug(`${options.debugId}: getMuseum returned. (${Date.now() - timeStarted}ms)`); + return museumData.museum; } - -init(); diff --git a/src/master.js b/src/master.js index 693ba4ce35..ef887ffdfc 100644 --- a/src/master.js +++ b/src/master.js @@ -3,6 +3,7 @@ await import("./scripts/init-collections.js"); await import("./scripts/init-custom-resources.js"); await Promise.all([ + import("./scripts/update-collections.js"), import("./scripts/cap-leaderboards.js"), import("./scripts/clear-favorite-cache.js"), import("./scripts/update-bazaar.js"), diff --git a/src/mongo.js b/src/mongo.js index 206549031f..069eb7e7db 100644 --- a/src/mongo.js +++ b/src/mongo.js @@ -1,3 +1,4 @@ +/* eslint-disable */ import { MongoClient } from "mongodb"; import credentials from "./credentials.js"; diff --git a/src/redis.js b/src/redis.js index 8a53f19e72..cbee1a4872 100644 --- a/src/redis.js +++ b/src/redis.js @@ -1,3 +1,4 @@ +/* eslint-disable */ import Redis from "ioredis"; import credentials from "./credentials.js"; diff --git a/src/routes/api/accessories.js b/src/routes/api/accessories.js index 476e0d251f..31fe10e74b 100644 --- a/src/routes/api/accessories.js +++ b/src/routes/api/accessories.js @@ -4,6 +4,7 @@ import express from "express"; import { tableify } from "../api.js"; import { db } from "../../mongo.js"; +import { getItems } from "../../stats.js"; const router = express.Router(); @@ -17,7 +18,7 @@ router.use(async (req, res, next) => { const { profile, uuid } = await lib.getProfile(db, req.player, req.profile, req.options); const userProfile = profile.members[uuid]; - const items = await lib.getItems(userProfile, false, undefined, req.options); + const items = await getItems(userProfile, false, undefined, req.options); const accessories = items.accessories .filter((a) => a.isUnique) diff --git a/src/routes/api/armor.js b/src/routes/api/armor.js index 5b6fdb86c9..757fda0646 100644 --- a/src/routes/api/armor.js +++ b/src/routes/api/armor.js @@ -4,6 +4,7 @@ import express from "express"; import { tableify } from "../api.js"; import { db } from "../../mongo.js"; +import { getItems } from "../../stats.js"; const router = express.Router(); @@ -17,7 +18,7 @@ router.use(async (req, res, next) => { const { profile, uuid } = await lib.getProfile(db, req.player, req.profile, req.options); const userProfile = profile.members[uuid]; - const items = await lib.getItems(userProfile, false, undefined, req.options); + const items = await getItems(userProfile, false, undefined, req.options); const output = []; diff --git a/src/routes/api/cakebag.js b/src/routes/api/cakebag.js index fed49f7aed..be7ff969a6 100644 --- a/src/routes/api/cakebag.js +++ b/src/routes/api/cakebag.js @@ -4,6 +4,7 @@ import express from "express"; import { tableify, handleError } from "../api.js"; import { db } from "../../mongo.js"; +import { getItems } from "../../stats.js"; const router = express.Router(); @@ -17,7 +18,7 @@ router.use(async (req, res, next) => { const { profile, uuid } = await lib.getProfile(db, req.player, req.profile, { cacheOnly: true }); const userProfile = profile.members[uuid]; - const items = await lib.getItems(userProfile, false, undefined, req.options); + const items = await getItems(userProfile, false, undefined, req.options); const allItems = items.armor.concat(items.inventory, items.accessory_bag, items.enderchest); diff --git a/src/routes/api/collections.js b/src/routes/api/collections.js index 3997684be2..3b023de7b4 100644 --- a/src/routes/api/collections.js +++ b/src/routes/api/collections.js @@ -4,6 +4,7 @@ import express from "express"; import { tableify } from "../api.js"; import { db } from "../../mongo.js"; +import * as stats from "../../stats.js"; const router = express.Router(); @@ -16,7 +17,7 @@ router.use(async (req, res, next) => { try { const { profile, uuid } = await lib.getProfile(db, req.player, req.profile, req.options); - const collections = await lib.getCollections(uuid, profile, req.options.cacheOnly); + const collections = await stats.getCollections(uuid, profile, req.options.cacheOnly); res.send( tableify( diff --git a/src/routes/api/items.js b/src/routes/api/items.js index 0cc8e761ce..d4cc893b31 100644 --- a/src/routes/api/items.js +++ b/src/routes/api/items.js @@ -4,6 +4,7 @@ import express from "express"; import { tableify } from "../api.js"; import { db } from "../../mongo.js"; +import { getItems } from "../../stats.js"; const router = express.Router(); @@ -17,7 +18,7 @@ router.use(async (req, res, next) => { const { profile, uuid } = await lib.getProfile(db, req.player, req.profile, req.options); const userProfile = profile.members[uuid]; - const items = await lib.getItems(userProfile, false, undefined, req.options); + const items = await getItems(userProfile, false, undefined, req.options); const allItems = items.inventory.concat(items.enderchest); diff --git a/src/routes/api/pets.js b/src/routes/api/pets.js index c7dcfe5143..31659215d0 100644 --- a/src/routes/api/pets.js +++ b/src/routes/api/pets.js @@ -4,6 +4,7 @@ import express from "express"; import { tableify } from "../api.js"; import { db } from "../../mongo.js"; +import { getPets } from "../../stats.js"; const router = express.Router(); @@ -17,7 +18,7 @@ router.use(async (req, res, next) => { const { profile, uuid } = await lib.getProfile(db, req.player, req.profile, req.options); const userProfile = profile.members[uuid]; - const pets = await lib.getPets(userProfile); + const pets = await getPets(userProfile, userProfile, [], profile); for (const pet of pets) { delete pet.lore; diff --git a/src/routes/api/skills.js b/src/routes/api/skills.js index 0ddd3257a7..b1b402b02a 100644 --- a/src/routes/api/skills.js +++ b/src/routes/api/skills.js @@ -4,6 +4,7 @@ import express from "express"; import { tableify } from "../api.js"; import { db } from "../../mongo.js"; +import { getItems } from "../../stats.js"; const router = express.Router(); @@ -18,20 +19,20 @@ router.use(async (req, res, next) => { const bingoProfile = await lib.getBingoProfile(db, req.player, req.options); const userProfile = profile.members[uuid]; - const items = await lib.getItems(userProfile, bingoProfile, false, undefined, req.options); + const items = await getItems(userProfile, bingoProfile, false, undefined, req.options); const calculated = await lib.getStats(db, profile, bingoProfile, allProfiles, items, req.options); const response = []; - for (const skill in calculated.levels) { - const pushArr = [helper.titleCase(skill), calculated.levels[skill].level.toString()]; + for (const skill in calculated.skills.skills) { + const pushArr = [helper.titleCase(skill), calculated.skills.skills[skill].level.toString()]; if ("progress" in req.query) { pushArr.push( - calculated.levels[skill].maxLevel, - calculated.levels[skill].xp, - calculated.levels[skill].xpCurrent, - calculated.levels[skill].xpForNext + calculated.skills.skills[skill].maxLevel, + calculated.skills.skills[skill].xp, + calculated.skills.skills[skill].xpCurrent, + calculated.skills.skills[skill].xpForNext ); } diff --git a/src/routes/api/wardrobe.js b/src/routes/api/wardrobe.js index 40db011dfd..c66adccb22 100644 --- a/src/routes/api/wardrobe.js +++ b/src/routes/api/wardrobe.js @@ -4,6 +4,7 @@ import express from "express"; import { tableify } from "../api.js"; import { db } from "../../mongo.js"; +import { getItems } from "../../stats.js"; const router = express.Router(); @@ -17,7 +18,7 @@ router.use(async (req, res, next) => { const { profile, uuid } = await lib.getProfile(db, req.player, req.profile, req.options); const userProfile = profile.members[uuid]; - const items = await lib.getItems(userProfile, false, undefined, req.options); + const items = await getItems(userProfile, false, undefined, req.options); const output = []; diff --git a/src/routes/api/weapons.js b/src/routes/api/weapons.js index d5105216f2..5d65039122 100644 --- a/src/routes/api/weapons.js +++ b/src/routes/api/weapons.js @@ -4,6 +4,7 @@ import express from "express"; import { tableify } from "../api.js"; import { db } from "../../mongo.js"; +import { getItems } from "../../stats.js"; const router = express.Router(); @@ -17,7 +18,7 @@ router.use(async (req, res, next) => { const { profile, uuid } = await lib.getProfile(db, req.player, req.profile, req.options); const userProfile = profile.members[uuid]; - const items = await lib.getItems(userProfile, false, undefined, req.options); + const items = await getItems(userProfile, false, undefined, req.options); const output = []; diff --git a/src/routes/apiv2/coins.js b/src/routes/apiv2/coins.js index 3ed10dbf95..81932c3a02 100644 --- a/src/routes/apiv2/coins.js +++ b/src/routes/apiv2/coins.js @@ -3,6 +3,7 @@ import * as lib from "../../lib.js"; import express from "express"; import { db } from "../../mongo.js"; +import { getItems } from "../../stats.js"; const router = express.Router(); @@ -26,7 +27,7 @@ router.get("/:player/:profile", async (req, res, next) => { continue; } - const items = await lib.getItems(singleProfile.members[profile.uuid], false, "", req.options); + const items = await getItems(singleProfile.members[profile.uuid], false, "", req.options); const data = await lib.getStats(db, singleProfile, allProfiles, items, req.options); output = { @@ -53,7 +54,7 @@ router.get("/:player", async (req, res, next) => { for (const singleProfile of allProfiles) { const cuteName = singleProfile.cute_name; - const items = await lib.getItems(singleProfile.members[profile.uuid], bingoProfile, false, "", req.options); + const items = await getItems(singleProfile.members[profile.uuid], bingoProfile, false, "", req.options); const data = await lib.getStats(db, singleProfile, bingoProfile, allProfiles, items, req.options); output.profiles[singleProfile.profile_id] = { diff --git a/src/routes/apiv2/dungeons.js b/src/routes/apiv2/dungeons.js index 199e9c4ded..21d19b062d 100644 --- a/src/routes/apiv2/dungeons.js +++ b/src/routes/apiv2/dungeons.js @@ -3,6 +3,7 @@ import * as lib from "../../lib.js"; import express from "express"; import { db } from "../../mongo.js"; +import { getDungeons } from "../../stats.js"; const router = express.Router(); @@ -29,7 +30,7 @@ router.get("/:player/:profile", async (req, res, next) => { const userProfile = singleProfile.members[profile.uuid]; const hypixelProfile = await helper.getRank(profile.uuid, db, req.cacheOnly); - const dungeonData = await lib.getDungeons(userProfile, hypixelProfile); + const dungeonData = await getDungeons(userProfile, hypixelProfile); output = { profile_id: singleProfile.profile_id, @@ -54,7 +55,7 @@ router.get("/:player", async (req, res, next) => { const userProfile = singleProfile.members[profile.uuid]; const hypixelProfile = await helper.getRank(profile.uuid, db, req.cacheOnly); - const dungeonData = await lib.getDungeons(userProfile, hypixelProfile); + const dungeonData = await getDungeons(userProfile, hypixelProfile); output.profiles[singleProfile.profile_id] = { profile_id: singleProfile.profile_id, diff --git a/src/routes/apiv2/profile.js b/src/routes/apiv2/profile.js index 6991144065..746051fbb9 100644 --- a/src/routes/apiv2/profile.js +++ b/src/routes/apiv2/profile.js @@ -3,6 +3,7 @@ import * as lib from "../../lib.js"; import express from "express"; import { db } from "../../mongo.js"; +import { getItems } from "../../stats.js"; const router = express.Router(); @@ -21,7 +22,7 @@ router.get("/:player", async (req, res, next) => { for (const singleProfile of allProfiles) { const userProfile = singleProfile.members[profile.uuid]; - const items = await lib.getItems(userProfile, bingoProfile, false, "", req.options); + const items = await getItems(userProfile, bingoProfile, false, "", req.options); const data = await lib.getStats(db, singleProfile, bingoProfile, allProfiles, items, req.options); output.profiles[singleProfile.profile_id] = { diff --git a/src/routes/apiv2/slayers.js b/src/routes/apiv2/slayers.js index 51ce0564c9..7042b50f3b 100644 --- a/src/routes/apiv2/slayers.js +++ b/src/routes/apiv2/slayers.js @@ -3,6 +3,7 @@ import * as lib from "../../lib.js"; import express from "express"; import { db } from "../../mongo.js"; +import { getItems } from "../../stats.js"; const router = express.Router(); @@ -26,7 +27,7 @@ router.get("/:player/:profile", async (req, res, next) => { continue; } - const items = await lib.getItems(singleProfile.members[profile.uuid], false, "", req.options); + const items = await getItems(singleProfile.members[profile.uuid], false, "", req.options); const data = await lib.getStats(db, singleProfile, allProfiles, items, req.options); output = { @@ -54,7 +55,7 @@ router.get("/:player", async (req, res, next) => { for (const singleProfile of allProfiles) { const userProfile = singleProfile.members[profile.uuid]; - const items = await lib.getItems(userProfile, bingoProfile, false, "", req.options); + const items = await getItems(userProfile, bingoProfile, false, "", req.options); const data = await lib.getStats(db, singleProfile, bingoProfile, allProfiles, items, req.options); output.profiles[singleProfile.profile_id] = { diff --git a/src/routes/apiv2/talismans.js b/src/routes/apiv2/talismans.js index f56fd7966d..9479260bdf 100644 --- a/src/routes/apiv2/talismans.js +++ b/src/routes/apiv2/talismans.js @@ -4,6 +4,7 @@ import express from "express"; import { db } from "../../mongo.js"; import { handleError } from "../apiv2.js"; +import { getItems } from "../../stats.js"; const router = express.Router(); @@ -24,7 +25,7 @@ router.get("/:player/:profile", async (req, res, next) => { continue; } - const items = await lib.getItems(singleProfile.members[profile.uuid], false, "", req.options); + const items = await getItems(singleProfile.members[profile.uuid], false, "", req.options); const accessories = items.accessories; output = { @@ -54,7 +55,7 @@ router.get("/:player", async (req, res, next) => { for (const singleProfile of allProfiles) { const userProfile = singleProfile.members[profile.uuid]; - const items = await lib.getItems(userProfile, false, "", req.options); + const items = await getItems(userProfile, false, "", req.options); const accessories = items.accessories; output.profiles[singleProfile.profile_id] = { diff --git a/src/scripts/update-collections.js b/src/scripts/update-collections.js new file mode 100644 index 0000000000..fb51221eb9 --- /dev/null +++ b/src/scripts/update-collections.js @@ -0,0 +1,42 @@ +import { db } from "../mongo.js"; +import axios from "axios"; + +import "axios-debug-log"; + +async function updateCollections() { + try { + const { data } = await axios("https://api.hypixel.net/resources/skyblock/collections"); + if (data.collections === undefined) { + throw new Error("Failed to fetch collections."); + } + + const output = {}; + for (const [category, collection] of Object.entries(data.collections)) { + output[category] = { + name: collection.name, + items: await Promise.all( + Object.keys(collection.items).map(async (id) => { + const itemData = await db.collection("items").findOne({ id: id }); + return { + id, + name: collection.items[id].name, + texture: itemData.texture !== undefined ? `/head/${itemData.texture}` : `/item/${id}`, + maxTier: collection.items[id].maxTiers, + tiers: collection.items[id].tiers, + }; + }) + ), + }; + } + + await db + .collection("collections") + .updateOne({ _id: "collections" }, { $set: { collections: output } }, { upsert: true }); + } catch (e) { + console.error(e); + } + + setTimeout(updateCollections, 1000 * 60 * 60 * 12); +} + +updateCollections(); diff --git a/src/stats.js b/src/stats.js index f8a1ef50b0..2305c77c54 100644 --- a/src/stats.js +++ b/src/stats.js @@ -1 +1,21 @@ export * from "./stats/bestiary.js"; +export * from "./stats/misc.js"; +export * from "./stats/skills.js"; +export * from "./stats/other.js"; +export * from "./stats/weight.js"; +export * from "./stats/slayer.js"; +export * from "./stats/dungeons.js"; +export * from "./stats/farming.js"; +export * from "./stats/enchanting.js"; +export * from "./stats/mining.js"; +export * from "./stats/crimson-isle.js"; +export * from "./stats/fishing.js"; +export * from "./stats/bingo.js"; +export * from "./stats/collections.js"; +export * from "./stats/temp-stats.js"; +export * from "./stats/pets.js"; +export * from "./stats/skyblock-level.js"; +export * from "./stats/minions.js"; +export * from "./stats/rift.js"; +export * from "./stats/items.js"; +export * from "./stats/missing.js"; diff --git a/src/stats/bestiary.js b/src/stats/bestiary.js index f28f3fdde7..1932e5762d 100644 --- a/src/stats/bestiary.js +++ b/src/stats/bestiary.js @@ -28,54 +28,49 @@ function formatBestiaryMobs(userProfile, mobs) { } export function getBestiary(userProfile) { - try { - if (userProfile.bestiary?.kills === undefined) { - return null; - } - - const output = {}; - let tiersUnlocked = 0, - totalTiers = 0; - for (const [category, data] of Object.entries(constants.BESTIARY)) { - const { name, texture, mobs } = data; - output[category] = { - name, - texture, - }; + if (userProfile.bestiary?.kills === undefined) { + return; + } - if (category === "fishing") { - for (const [key, value] of Object.entries(data)) { - output[category][key] = { - name: value.name, - texture: value.texture, - }; + const output = {}; + let tiersUnlocked = 0, + totalTiers = 0; + for (const [category, data] of Object.entries(constants.BESTIARY)) { + const { name, texture, mobs } = data; + output[category] = { + name, + texture, + }; - output[category][key].mobs = formatBestiaryMobs(userProfile, value.mobs); + if (category === "fishing") { + for (const [key, value] of Object.entries(data)) { + output[category][key] = { + name: value.name, + texture: value.texture, + }; - tiersUnlocked += output[category][key].mobs.reduce((acc, cur) => acc + cur.tier, 0); - totalTiers += output[category][key].mobs.reduce((acc, cur) => acc + cur.maxTier, 0); - output[category][key].mobsUnlocked = output[category][key].mobs.length; - output[category][key].mobsMaxed = output[category][key].mobs.filter((mob) => mob.tier === mob.maxTier).length; - } - } else { - output[category].mobs = formatBestiaryMobs(userProfile, mobs); - output[category].mobsUnlocked = output[category].mobs.length; - output[category].mobsMaxed = output[category].mobs.filter((mob) => mob.tier === mob.maxTier).length; + output[category][key].mobs = formatBestiaryMobs(userProfile, value.mobs); - tiersUnlocked += output[category].mobs.reduce((acc, cur) => acc + cur.tier, 0); - totalTiers += output[category].mobs.reduce((acc, cur) => acc + cur.maxTier, 0); + tiersUnlocked += output[category][key].mobs.reduce((acc, cur) => acc + cur.tier, 0); + totalTiers += output[category][key].mobs.reduce((acc, cur) => acc + cur.maxTier, 0); + output[category][key].mobsUnlocked = output[category][key].mobs.length; + output[category][key].mobsMaxed = output[category][key].mobs.filter((mob) => mob.tier === mob.maxTier).length; } - } + } else { + output[category].mobs = formatBestiaryMobs(userProfile, mobs); + output[category].mobsUnlocked = output[category].mobs.length; + output[category].mobsMaxed = output[category].mobs.filter((mob) => mob.tier === mob.maxTier).length; - return { - categories: output, - tiersUnlocked, - totalTiers, - milestone: tiersUnlocked / 10, - maxMilestone: totalTiers / 10, - }; - } catch (error) { - console.log(error); - return null; + tiersUnlocked += output[category].mobs.reduce((acc, cur) => acc + cur.tier, 0); + totalTiers += output[category].mobs.reduce((acc, cur) => acc + cur.maxTier, 0); + } } + + return { + categories: output, + tiersUnlocked, + totalTiers, + milestone: tiersUnlocked / 10, + maxMilestone: totalTiers / 10, + }; } diff --git a/src/stats/bingo.js b/src/stats/bingo.js new file mode 100644 index 0000000000..f073e56d6a --- /dev/null +++ b/src/stats/bingo.js @@ -0,0 +1,151 @@ +import * as constants from "../constants.js"; +import * as helper from "../helper.js"; + +function getBingoItemId(item, completed) { + if (item.tiers !== undefined) { + return item.progress >= item.tiers[0] ? 133 : 42; + } + + return completed ? 351 : 339; +} + +function getBingoItemDamage(completed) { + return completed ? 10 : 0; +} + +function formatBingOItemLore(item, bingoData, completed = false) { + const output = []; + + // Personal Goal + if (item.tiers === undefined) { + if (Array.isArray(item.lore)) { + output.push(...item.lore); + } else { + output.push("§8Personal Goal"); + output.push(""); + output.push(item.lore); + output.push(""); + output.push("§7Reward"); + output.push("§61 Bingo Point"); + + if (item.requiredAmount) { + output.push("", "§7Progress:"); + output.push( + `§a${completed ? item.requiredAmount.toLocaleString() : "???"} §7/ §6${item.requiredAmount.toLocaleString()}` + ); + } + + if (completed) { + output.push("", "§aGOAL REACHED"); + } else { + output.push("", "§cYou have not reached this goal!"); + } + } + } else { + // Community Goal + output.push("§8Community Goal"); + output.push(""); + + const total = item.progress; + const nextTierAmount = item.tiers.find((tier) => total < tier) || item.tiers[item.tiers.length - 1]; + const nextTier = item.tiers.indexOf(nextTierAmount) + 1; + + const percentage = (total / nextTierAmount) * 100; + output.push( + `§7Progress to ${item.name} ${helper.romanize(nextTier)}: §e${Math.min(percentage, 100).toFixed(2)}§6%` + ); + + output.push( + `${helper.formatProgressBar(total, 20)} §e${total.toLocaleString()} §6/ §e${helper.formatNumber(nextTierAmount)}` + ); + + const index = bingoData.filter((goal) => goal.tiers !== undefined).indexOf(item); + + output.push(""); + output.push("§7Contribution Rewards"); + output.push(...constants.BINGO_COMMUNITY_REWARDS[index].description); + output.push(""); + output.push( + "§7§oCommunity Goals are", + "§7§ocollaborative - anyone with a", + "§7§othe goal!", + "", + "§7§oBingo profile can help to reach", + "§7§oThe more you contribute", + "§7§otowards the goal, the more you", + "§7§owill be rewarded!" + ); + + if (percentage >= 100) { + output.push("", "§aGOAL REACHED"); + } + } + + return output; +} + +export function getBingoItems(completedGoals, bingoData) { + const output = []; + for (let i = 0; i < 6 * 9; i++) { + output[i] = helper.generateItem({ + id: undefined, + }); + } + + for (let slot = 0, index = 0; slot <= 6 * 9; slot++) { + if ([0, 1, 7, 8].includes(slot % 9) || !bingoData[index]) continue; + + const item = bingoData[index]; + const completed = item.tiers ? item.progress >= item.tiers[0] : completedGoals.includes(item.id); + + output[slot] = helper.generateItem({ + display_name: item.name, + id: getBingoItemId(item, completed), + Damage: getBingoItemDamage(completed), + rarity: "uncommon", + tag: { display: { Name: item.name, Lore: formatBingOItemLore(item, bingoData, completed) } }, + position: slot, + completed, + }); + + index++; + } + + for (const itemData of constants.BINGO_CARD_SLOTS) { + for (const slot of itemData.positions) { + if (Array.isArray(itemData.lore)) { + itemData.lore = helper.renderLore(itemData.lore.join("
")); + } + + itemData.display_name = itemData.display_name.split(/[0-9]/)[0]; + if (itemData.display_name.endsWith("#")) { + itemData.display_name += parseInt(itemData.positions.indexOf(slot)) + 1; + } + + output[slot] = helper.generateItem({ + ...itemData, + tag: { + display: { + Name: itemData.display_name, + Lore: itemData.lore, + }, + }, + position: slot, + }); + } + } + + return output; +} + +export function getBingoData(bingoProfile) { + if (bingoProfile?.events === undefined) { + return; + } + + return { + total: bingoProfile.events.length, + points: bingoProfile.events.reduce((a, b) => a + b.points, 0), + completed_goals: bingoProfile.events.reduce((a, b) => a + b.completed_goals.length, 0), + }; +} diff --git a/src/stats/collections.js b/src/stats/collections.js new file mode 100644 index 0000000000..3744b61c40 --- /dev/null +++ b/src/stats/collections.js @@ -0,0 +1,153 @@ +import * as constants from "../constants.js"; +import * as helper from "../helper.js"; +import { db } from "../mongo.js"; + +/** + * Retrieves the collection data for a given player UUID. + * + * @async + * @function + * @param {string} uuid - The UUID of the player to retrieve collection data for. + * @param {Object} profile - The player's profile object. + * @param {boolean} [cacheOnly=false] - Whether to only use cached data. + * @returns {Promise<{[key: string]: { + * name: string, + * id: string, + * texture: string, + * amount: number, + * totalAmount: number, + * tier: number, + * maxTier: number, + * amounts: {username: string, amount: number}[], + * }[]}>} An object containing the player's collection data, with each collection's data organized by its ID. + */ +export async function getCollections(uuid, profile, dungeons, kuudra, cacheOnly = false) { + const output = {}; + + const userProfile = profile.members[uuid]; + if (!("unlocked_coll_tiers" in userProfile.player_data) || !("collection" in userProfile)) { + return; + } + + const members = ( + await Promise.all(Object.keys(profile.members).map((a) => helper.resolveUsernameOrUuid(a, db, cacheOnly))) + ).reduce((acc, a) => ((acc[a.uuid] = a.display_name), acc), {}); + + const { collections: collectionData } = await db.collection("collections").findOne({ _id: "collections" }); + for (const [categoryId, categoryData] of Object.entries(collectionData)) { + const category = categoryId.toLowerCase(); + output[category] ??= { + name: categoryData.name, + collections: [], + }; + + for (const collection of categoryData.items) { + const { id, maxTier, name, texture } = collection; + + const amount = userProfile.collection[id] || 0; + + const amounts = Object.keys(profile.members).map((uuid) => { + return { + username: members[uuid], + amount: (profile.members[uuid].collection && profile.members[uuid].collection[id]) ?? 0, + }; + }); + + const totalAmount = amounts.reduce((a, b) => a + b.amount, 0); + + const tier = collection.tiers.findLast((a) => a.amountRequired <= totalAmount)?.tier ?? 0; + + output[category].collections.push({ + name, + id: id, + texture, + amount, + totalAmount, + tier, + maxTier, + amounts, + }); + } + + output[category].totalTiers = output[category].collections.length; + + output[category].maxTiers = output[category].collections.filter((a) => a.tier === a.maxTier).length; + } + + const bossCollections = getBossCollections(dungeons, kuudra); + output["BOSS"] = { + name: "Boss Collections", + collections: bossCollections, + totalTiers: bossCollections.length, + maxTiers: bossCollections.filter((a) => a.tier === a.maxTier).length, + }; + + output.totalCollections = Object.values(output).reduce((a, b) => a + b.collections.length, 0); + + output.maxedCollections = Object.values(output) + .map((a) => a.collections) + .flat() + .filter((a) => a && a.tier === a.maxTier).length; + + return output; +} + +function getBossCollections(dungeons, kuudra) { + const output = []; + if (dungeons === undefined) { + return output; + } + + const bossCompletions = {}; + for (const [floor, data] of Object.entries(dungeons.catacombs.floors)) { + bossCompletions[floor] ??= 0; + bossCompletions[floor] += data.stats.tier_completions ?? 0; + } + + if (dungeons.master_catacombs?.floors) { + for (const [floor, data] of Object.entries(dungeons.master_catacombs.floors)) { + bossCompletions[floor] ??= 0; + bossCompletions[floor] += (data.stats.tier_completions ?? 0) * 2; + } + } + + for (const collection of constants.BOSS_COLLECTIONS) { + const index = constants.BOSS_COLLECTIONS.indexOf(collection) + 1; + + const { name, texture } = collection; + + const amount = bossCompletions[index] ?? 0; + + const maxAmount = collection.rewards[collection.rewards.length - 1]?.required; + + const tier = collection.rewards.filter((a) => a.required <= amount).length ?? 0; + + const maxTier = collection.rewards.length; + + output.push({ + name, + texture, + amount, + maxAmount, + tier, + maxTier, + }); + } + + if (kuudra?.kuudra?.tiers) { + const completions = Object.values(kuudra.kuudra.tiers).reduce((a, b, index) => { + return a + (index + 1) * b.completions; + }, 0); + + output.push({ + name: "Kuudra", + texture: "/head/82ee25414aa7efb4a2b4901c6e33e5eaa705a6ab212ebebfd6a4de984125c7a0", + amount: completions, + maxAmount: 5000, + tier: Math.min(Math.floor(completions / 500), 5), // TODO: Fix this (it's not accurate) + maxTier: 5, + }); + } + + return output; +} diff --git a/src/stats/crimson-isle.js b/src/stats/crimson-isle.js new file mode 100644 index 0000000000..2f3bb7d3bd --- /dev/null +++ b/src/stats/crimson-isle.js @@ -0,0 +1,96 @@ +import * as constants from "../constants.js"; +import * as helper from "../helper.js"; + +function getTrophyFish(userProfile) { + const output = {}; + if (userProfile.trophy_fish === undefined) { + return; + } + + output.fish = []; + const tiers = ["bronze", "silver", "gold", "diamond"]; + for (const fish in constants.TROPHY_FISH) { + const trophyFish = { + total: userProfile.trophy_fish[fish.toLowerCase()] ?? 0, + }; + + for (const tier of tiers) { + trophyFish[tier] = userProfile.trophy_fish[`${fish.toLowerCase()}_${tier}`] ?? 0; + } + + trophyFish.highest_tier = [...tiers].reverse().find((tier) => trophyFish[tier] >= 1); + + trophyFish.texture = constants.TROPHY_FISH[fish].textures[trophyFish.highest_tier]; + + output.fish.push(Object.assign(trophyFish, constants.TROPHY_FISH[fish])); + } + + output.total_caught = userProfile.trophy_fish?.total_caught || 0; + + output.maxed = output.fish.filter((fish) => fish["diamond"] > 0).length === output.fish.length; + + const stage = userProfile.trophy_fish.rewards ? userProfile.trophy_fish.rewards.length - 1 : 0; + output.stage = constants.TROPHY_FISH_STAGES[stage]; + + return output; +} + +export function getCrimsonIsle(userProfile) { + const output = {}; + + const data = userProfile.nether_island_player_data; + if (data === undefined) { + return; + } + + output.factions = { + selected_faction: data.selected_faction ? helper.capitalizeFirstLetter(data.selected_faction) : "None", + mages_reputation: data.mages_reputation ?? 0, + barbarians_reputation: data.barbarians_reputation ?? 0, + }; + + if (data.kuudra_completed_tiers !== undefined) { + output.kuudra = { + tiers: {}, + total: 0, + }; + for (const tier in constants.KUUDRA_TIERS) { + output.kuudra.tiers[tier] = { + name: constants.KUUDRA_TIERS[tier].name, + head: constants.KUUDRA_TIERS[tier].head, + completions: data.kuudra_completed_tiers[tier] ?? 0, + }; + } + + output.kuudra.total = Object.values(output.kuudra.tiers).reduce((a, b) => a + b.completions, 0); + } + + if (data.dojo !== undefined) { + output.dojo = { + dojo: {}, + total_points: 0, + }; + + for (const key in constants.DOJO) { + const dojo = constants.DOJO[key]; + output.dojo.dojo[key.toUpperCase()] = { + name: dojo.name, + id: dojo.itemId, + damage: dojo.damage, + points: data.dojo?.[`dojo_points_${key}`] ?? 0, + time: data.dojo?.[`dojo_time_${key}`] ?? 0, + }; + } + + output.dojo.total_points = Object.values(output.dojo.dojo).reduce((a, b) => a + b.points, 0); + } + + output.trophy_fish = getTrophyFish(userProfile); + + output.abiphone = { + contacts: data.abiphone?.contact_data ?? {}, + active: data.abiphone?.active_contacts?.length || 0, + }; + + return output; +} diff --git a/src/stats/dungeons.js b/src/stats/dungeons.js new file mode 100644 index 0000000000..417be37084 --- /dev/null +++ b/src/stats/dungeons.js @@ -0,0 +1,170 @@ +import { getLeaderboardPosition } from "../helper/leaderboards.js"; +import { getLevelByXp } from "./skills/leveling.js"; +import * as constants from "../constants.js"; + +function getFloors(type, dungeon) { + const floors = {}; + for (const key of Object.keys(dungeon)) { + if (typeof dungeon[key] != "object") { + continue; + } + + for (const floor of Object.keys(dungeon[key])) { + if (!floors[floor]) { + floors[floor] = { + name: `floor_${floor}`, + icon_texture: "908fc34531f652f5be7f27e4b27429986256ac422a8fb59f6d405b5c85c76f7", + stats: {}, + }; + } + + const id = `${type}_${floor}`; + if (constants.DUNGEONS.floors[id]) { + floors[floor].name = constants.DUNGEONS.floors[id].name; + + floors[floor].icon_texture = constants.DUNGEONS.floors[id].texture; + } + + if (key.startsWith("most_damage")) { + if (!floors[floor].most_damage || dungeon[key][floor] > floors[floor].most_damage.value) { + floors[floor].most_damage = { + class: key.replace("most_damage_", ""), + value: dungeon[key][floor], + }; + } + } else if (key === "best_runs") { + floors[floor][key] = dungeon[key][floor]; + } else { + floors[floor].stats[key] = dungeon[key][floor]; + } + } + } + + return floors; +} + +function getEssence(userProfile) { + const output = {}; + if (userProfile.currencies === undefined || userProfile.currencies.essence == undefined) { + return; + } + + for (const essence in constants.ESSENCE) { + if (userProfile.currencies.essence[essence.toUpperCase()] == undefined) { + output[essence] = 0; + continue; + } + + output[essence] = userProfile.currencies.essence[essence.toUpperCase()]?.current ?? 0; + } + + return output; +} + +export async function getDungeons(userProfile, hypixelProfile) { + const dungeons = userProfile.dungeons; + if (dungeons == null || Object.keys(dungeons).length === 0 || dungeons.dungeon_types == undefined) { + return; + } + + const output = {}; + for (const type of Object.keys(dungeons.dungeon_types)) { + const dungeon = dungeons.dungeon_types[type]; + if (dungeon == null || Object.keys(dungeon).length === 0) { + output[type] = { visited: false }; + continue; + } + + const id = `dungeon_${type}`; + const floors = getFloors(type, dungeon); + output[type] = { + id: id, + visited: true, + level: getLevelByXp(dungeon.experience, { + type: "dungeoneering", + skill: "dungeoneering", + ignoreCap: true, + infinite: true, + }), + highest_floor: `floor_${dungeon.highest_tier_completed ?? 0}`, + floors: floors, + completions: Object.values(floors).reduce((a, b) => a + (b.stats?.tier_completions ?? 0), 0), + }; + + output[type].level.rank = await getLeaderboardPosition(`dungeons_${type}_xp`, dungeon.experience); + } + + output.floor_completions = (output.catacombs.completions ?? 0) + (output.master_catacombs?.completions ?? 0); + + // Classes + output.classes = { + selected_class: dungeons.selected_dungeon_class ?? "none", + classes: {}, + }; + for (const className of Object.keys(dungeons.player_classes)) { + const data = dungeons.player_classes[className]; + if (isNaN(data.experience) === true) { + data.experience = 0; + } + + output.classes.classes[className] = { + level: getLevelByXp(data.experience, { + type: "dungeoneering", + skill: "dungeoneering", + ignoreCap: true, + infinite: true, + }), + current: false, + }; + + output.classes.classes[className].level.rank = await getLeaderboardPosition( + `dungeons_class_${className}_xp`, + data.experience + ); + + output.classes.classes[className].current = className == output.classes.selected_class; + } + + const classValues = Object.values(output.classes.classes); + const classLength = classValues.length; + + output.classes.experience = classValues.reduce((a, b) => a + b.level.xp, 0); + + output.classes.average_level = classValues.reduce((a, b) => a + b.level.level, 0) / classLength; + + output.classes.average_level_with_progress = + classValues.reduce((a, b) => a + b.level.levelWithProgress, 0) / classLength; + + output.classes.maxed = classValues.filter((a) => a.level.level >= 50).length === classLength; + + output.secrets_found = hypixelProfile.achievements.skyblock_treasure_hunter || 0; + + // TODO: Fix this, constants are incorrect and hypixel has completely changed the system + /* + // Journal Entries + const JOURNAL_CONSTANTS = constants.DUNGEONS.journals; + const journals = { + pages_collected: 0, + journals_completed: 0, + total_pages: 0, + journal_entries: dungeons.dungeon_journal.unlocked_journals, + }; + + if (dungeons.dungeon_journal.unlocked_journals !== undefined) { + for (const entryID of dungeons.dungeon_journal.unlocked_journals) { + journals.journals_completed += 1; + journals.pages_collected += JOURNAL_CONSTANTS[entryID]?.pages || 0; + } + } + + for (const journal in JOURNAL_CONSTANTS) { + journals.total_pages += JOURNAL_CONSTANTS[journal].pages; + } + + output.journals = journals; + */ + + output.essence = getEssence(userProfile); + + return output; +} diff --git a/src/stats/enchanting.js b/src/stats/enchanting.js new file mode 100644 index 0000000000..41461025e7 --- /dev/null +++ b/src/stats/enchanting.js @@ -0,0 +1,69 @@ +import * as constants from "../constants.js"; +import moment from "moment"; +import _ from "lodash"; + +// simon = Chronomatron +// numbers = Ultrasequencer +// pairings = Superpairs + +export function getEnchanting(userProfile) { + const enchanting = { + unlocked: userProfile.experimentation !== undefined, + experiments: {}, + }; + + if (enchanting.unlocked) { + for (const game in constants.EXPERIMENTS.games) { + if (userProfile.experimentation[game] === undefined) { + continue; + } + + const gameConstants = constants.EXPERIMENTS.games[game]; + const gameData = userProfile.experimentation[game]; + + const gameOutput = { + name: gameConstants.name, + stats: {}, + tiers: {}, + }; + + for (const key in gameData) { + if (key.startsWith("attempts") || key.startsWith("claims") || key.startsWith("best_score")) { + let stat = key.split("_"); + const tierValue = parseInt(stat.pop()); + const tier = game === "numbers" ? tierValue + 2 : game === "simon" ? Math.min(tierValue + 1, 5) : tierValue; + + const tierInfo = _.cloneDeep(constants.EXPERIMENTS.tiers[tier]); + if (gameOutput.tiers[tier] === undefined) { + gameOutput.tiers[tier] = tierInfo; + } + + stat = stat.join("_"); + Object.assign(gameOutput.tiers[tier], { + [stat]: gameData[key], + }); + continue; + } + + if (key == "last_attempt" || key == "last_claimed") { + if (gameData[key] <= 0) { + continue; + } + + gameOutput.stats[key] = { + unix: gameData[key], + text: moment(gameData[key]).fromNow(), + }; + + continue; + } + + gameOutput.stats[key] = gameData[key]; + } + + enchanting.experiments[game] = gameOutput; + } + } + + return enchanting; +} diff --git a/src/stats/farming.js b/src/stats/farming.js new file mode 100644 index 0000000000..39459760e7 --- /dev/null +++ b/src/stats/farming.js @@ -0,0 +1,127 @@ +import * as constants from "../constants.js"; + +export function getFarming(userProfile) { + if (userProfile.jacobs_contest === undefined) { + return; + } + + const farming = { + talked: userProfile.jacobs_contest?.talked || false, + pelts: userProfile.trapper_quest?.pelt_count || 0, + }; + + if (farming.talked) { + // Your current badges + farming.current_badges = { + bronze: userProfile.jacobs_contest.medals_inv?.bronze || 0, + silver: userProfile.jacobs_contest.medals_inv?.silver || 0, + gold: userProfile.jacobs_contest.medals_inv?.gold || 0, + }; + + // Your total badges + farming.total_badges = { + bronze: 0, + silver: 0, + gold: 0, + }; + + // Your current perks + farming.perks = { + double_drops: userProfile.jacobs_contest.perks?.double_drops || 0, + farming_level_cap: userProfile.jacobs_contest.perks?.farming_level_cap || 0, + }; + + // Your amount of unique golds + farming.unique_golds = userProfile.jacobs_contest.unique_golds2?.length || 0; + + // Things about individual crops + farming.crops = {}; + + for (const crop in constants.FARMING_CROPS) { + farming.crops[crop] = constants.FARMING_CROPS[crop]; + + Object.assign(farming.crops[crop], { + attended: false, + unique_gold: userProfile.jacobs_contest.unique_golds2?.includes(crop) || false, + contests: 0, + personal_best: 0, + badges: { + gold: 0, + silver: 0, + bronze: 0, + }, + }); + } + + // Template for contests + const contests = { + attended_contests: 0, + all_contests: [], + }; + + for (const contestId in userProfile.jacobs_contest.contests) { + const data = userProfile.jacobs_contest.contests[contestId]; + + const contestName = contestId.split(":"); + const date = `${contestName[1]}_${contestName[0]}`; + const crop = contestName.slice(2).join(":"); + + if (data.collected < 100) { + continue; // Contests aren't counted in game with less than 100 collection + } + + farming.crops[crop].contests++; + farming.crops[crop].attended = true; + if (farming.crops[crop].personal_best < data.collected) { + farming.crops[crop].personal_best = data.collected; + } + + const contest = { + date: date, + crop: crop, + collected: data.collected, + claimed: data.claimed_rewards || false, + medal: null, + }; + + const placing = {}; + + if (contest.claimed) { + placing.position = data.claimed_position || 0; + placing.percentage = (data.claimed_position / data.claimed_participants) * 100; + const participants = data.claimed_participants; + + // Use the claimed medal if it exists and is valid + // This accounts for the farming mayor increased brackets perk + // Note: The medal brackets are the percentage + 1 extra person + if ( + contest.claimed_medal === "bronze" || + contest.claimed_medal === "silver" || + contest.claimed_medal === "gold" + ) { + contest.medal = contest.claimed_medal; + } else if (placing.position <= participants * 0.05 + 1) { + contest.medal = "gold"; + } else if (placing.position <= participants * 0.25 + 1) { + contest.medal = "silver"; + } else if (placing.position <= participants * 0.6 + 1) { + contest.medal = "bronze"; + } + + // Count the medal if it exists + if (contest.medal) { + farming.total_badges[contest.medal]++; + farming.crops[crop].badges[contest.medal]++; + } + } + + contest.placing = placing; + + contests.attended_contests++; + contests.all_contests.push(contest); + } + + farming.contests = contests; + } + return farming; +} diff --git a/src/stats/fishing.js b/src/stats/fishing.js new file mode 100644 index 0000000000..50518f1bce --- /dev/null +++ b/src/stats/fishing.js @@ -0,0 +1,14 @@ +export function getFishing(userProfile) { + if (userProfile.player_stats === undefined) { + return; + } + + return { + total: userProfile.player_stats.items_fished?.total ?? 0, + treasure: userProfile.player_stats.items_fished?.treasure ?? 0, + treasure_large: userProfile.player_stats.items_fished?.large_treasure ?? 0, + shredder_fished: userProfile.player_stats.shredder_rod?.fished ?? 0, + shredder_bait: userProfile.player_stats.shredder_rod?.bait ?? 0, + trophy_fish: userProfile.player_stats.items_fished?.trophy_fish ?? 0, + }; +} diff --git a/src/stats/items.js b/src/stats/items.js new file mode 100644 index 0000000000..370f137274 --- /dev/null +++ b/src/stats/items.js @@ -0,0 +1,208 @@ +import { processItems } from "./items/processing.js"; +import * as items from "./items/items.js"; +import * as helper from "../helper.js"; +import { v4 } from "uuid"; + +export async function getItems( + profile, + paramBingo, + customTextures = false, + packs, + options = { cacheOnly: false, debugId: `${helper.getClusterId()}/unknown@getItems` } +) { + const output = {}; + + console.debug(`${options.debugId}: getItems called.`); + const timeStarted = Date.now(); + + // Process inventories returned by API + const inventoryTypes = [ + { name: "armor", property: "inv_armor" }, + { name: "equipment", property: "equipment_contents" }, + { name: "inventory", property: "inv_contents" }, + { name: "wardrobe", property: "wardrobe_contents" }, + { name: "ender chest", property: "ender_chest_contents" }, + { name: "accessory bag", property: "talisman_bag", bagContents: true }, + { name: "fishing bag", property: "fishing_bag", bagContents: true }, + { name: "quiver", property: "quiver", bagContents: true }, + { name: "potion bag", property: "potion_bag", bagContents: true }, + { name: "candy bag", property: "candy_inventory_contents", shared: true }, + { name: "personal vault", property: "personal_vault_contents" }, + ]; + + const promises = inventoryTypes.map((type) => { + if (type.shared === true) { + if (profile.shared_inventory === undefined || profile.shared_inventory[type.property] === undefined) { + return []; + } + + return processItems( + profile.shared_inventory[type.property].data, + type.name, + customTextures, + packs, + options.cacheOnly + ); + } else if (type.bagContents === true) { + if ( + profile.inventory === undefined || + profile.inventory.bag_contents === undefined || + profile.inventory.bag_contents[type.property] === undefined + ) { + return []; + } + + return processItems( + profile.inventory.bag_contents[type.property].data, + type.name, + customTextures, + packs, + options.cacheOnly + ); + } else { + if (profile.inventory === undefined || profile.inventory[type.property] === undefined) { + return []; + } + + return processItems(profile.inventory[type.property].data, type.name, customTextures, packs, options.cacheOnly); + } + }); + + let [ + armor, + equipment, + inventory, + wardrobe_inventory, + enderchest, + accessory_bag, + fishing_bag, + quiver, + potion_bag, + candy_bag, + personal_vault, + ] = await Promise.all(promises); + + const storage = []; + if (profile.inventory && profile.inventory.backpack_contents) { + const storageSize = Math.max(18, Object.keys(profile.inventory.backpack_contents).length); + + const promises = []; + + for (let slot = 0; slot < storageSize; slot++) { + storage.push({}); + + if (profile.inventory.backpack_contents[slot] && profile.inventory.backpack_icons[slot]) { + const iconPromise = processItems( + profile.inventory.backpack_icons[slot].data, + "storage", + customTextures, + packs, + options.cacheOnly + ); + const itemsPromise = await processItems( + profile.inventory.backpack_contents[slot].data, + "storage", + customTextures, + packs, + options.cacheOnly + ); + + promises.push(iconPromise, itemsPromise); + + (async (slot, iconPromise, itemsPromise) => { + const [icon, items] = await Promise.all([iconPromise, itemsPromise]); + + for (const [index, item] of items.entries()) { + item.isInactive = true; + item.inBackpack = true; + item.item_index = index; + } + + const storageUnit = icon[0]; + storageUnit.containsItems = items; + storage[slot] = storageUnit; + })(slot, iconPromise, itemsPromise); + } + } + + await Promise.all(promises); + } + + const wardrobe = items.getWardrobe(wardrobe_inventory); + + const hotm = "mining_core" in profile ? await items.getHotmItems(profile, packs) : []; + + output.armor = items.getArmor(armor.filter((x) => x.rarity)); + output.equipment = items.getEquipment(equipment.filter((x) => x.rarity)); + output.wardrobe = wardrobe; + output.wardrobe_inventory = wardrobe_inventory; + output.inventory = inventory; + output.enderchest = enderchest; + output.accessory_bag = accessory_bag; + output.fishing_bag = fishing_bag; + output.quiver = quiver; + output.potion_bag = potion_bag; + output.personal_vault = personal_vault; + output.storage = storage; + output.hotm = hotm; + output.candy_bag = candy_bag; + + const museum = + "museum" in profile ? await items.getMuseumItems(profile, customTextures, packs, options.cacheOnly) : []; + + output.museumItems = museum?.museumItems ?? []; + output.museum = museum?.museum ?? []; + + output.bingo_card = await items.getBingoCard(paramBingo, options.cacheOnly); + + const allItems = armor.concat( + equipment, + inventory, + enderchest, + accessory_bag, + fishing_bag, + quiver, + potion_bag, + personal_vault, + wardrobe_inventory, + storage, + hotm, + candy_bag + ); + + for (const [index, item] of allItems.entries()) { + item.item_index = index; + item.itemId = v4("itemId"); + + if ("containsItems" in item && Array.isArray(item.containsItems)) { + item.containsItems.forEach((a, idx) => { + a.backpackIndex = item.item_index; + a.itemId = v4("itemId"); + }); + } + } + + output.accessories = items.getAccessories(profile, armor, accessory_bag, inventory, enderchest, storage); + + // Add candy bag contents as backpack contents to candy bag + for (const item of allItems) { + if (helper.getId(item) == "TRICK_OR_TREAT_BAG") { + item.containsItems = candy_bag; + } + } + + output.weapons = items.getWeapons(allItems); + output.farming_tools = items.getSkilllTools("farming", allItems); + output.mining_tools = items.getSkilllTools("mining", allItems); + output.fishing_tools = items.getSkilllTools("fishing", allItems); + output.pets = items.getPets(allItems); + + // Check if inventory access disabled by user + output.disabled = { + inventory: inventory.length === 0, + personal_vault: personal_vault.length === 0, + }; + + console.debug(`${options.debugId}: getItems returned. (${Date.now() - timeStarted}ms)`); + return output; +} diff --git a/src/stats/items/accessories.js b/src/stats/items/accessories.js new file mode 100644 index 0000000000..f974e045a6 --- /dev/null +++ b/src/stats/items/accessories.js @@ -0,0 +1,174 @@ +import * as constants from "../../constants.js"; +import * as helper from "../../helper.js"; +import { itemSorter } from "./processing.js"; + +export function getAccessories(userProfile, armor, accessoryBag, inventory, enderchest, storage) { + const output = {}; + const accessories = []; + const accessoryIds = []; + const accessoryRarities = { + common: 0, + uncommon: 0, + rare: 0, + epic: 0, + legendary: 0, + mythic: 0, + special: 0, + very_special: 0, + hegemony: null, + abicase: null, + rift_prism: userProfile.rift?.access?.consumed_prism ? true : null, + }; + + // Add accessories from armor + for (const accessory of armor.filter((a) => a.categories.includes("accessory"))) { + const id = helper.getId(accessory); + if (id === "") { + continue; + } + + const insertAccessory = Object.assign({ isUnique: true, isInactive: false }, accessory); + + accessories.push(insertAccessory); + accessoryIds.push({ + id: id, + rarity: insertAccessory.rarity, + }); + } + + // Add accessories from inventory and accessory bag + for (const accessory of accessoryBag.concat(inventory.filter((a) => a.categories.includes("accessory")))) { + const id = helper.getId(accessory); + if (id === "") { + continue; + } + + const insertAccessory = Object.assign({ isUnique: true, isInactive: false }, accessory); + + // mark lower tiers as inactive + if (constants.getUpgradeList(id) !== undefined) { + accessories.find((a) => { + if (constants.getUpgradeList(id).includes(helper.getId(a)) === false) { + return; + } + + insertAccessory.isInactive = true; + insertAccessory.isUnique = false; + }); + } + + // mark accessory inactive if player has two exactly same accessories + accessories.map((a) => { + if (a.isInactive == true) { + return; + } + + if (helper.getId(a) === helper.getId(insertAccessory)) { + insertAccessory.isInactive = true; + a.isInactive = true; + + // give accessories with higher rarity priority, mark lower rarity as inactive + if (constants.RARITIES.indexOf(a.rarity) > constants.RARITIES.indexOf(insertAccessory.rarity)) { + a.isInactive = false; + a.isUnique = true; + insertAccessory.isUnique = false; + } else if (constants.RARITIES.indexOf(insertAccessory.rarity) > constants.RARITIES.indexOf(a.rarity)) { + insertAccessory.isInactive = false; + insertAccessory.isUnique = true; + a.isUnique = false; + } else { + insertAccessory.isInactive = false; + insertAccessory.isUnique = false; + a.isInactive = true; + a.isUnique = true; + } + } + }); + + // mark accessory aliases as inactive + const ACCESSORY_ALIASES = constants.ACCESSORY_ALIASES; + if (id in ACCESSORY_ALIASES || Object.keys(ACCESSORY_ALIASES).find((a) => ACCESSORY_ALIASES[a].includes(id))) { + let accessoryDuplicates = constants.ACCESSORY_ALIASES[id]; + if (accessoryDuplicates === undefined) { + const aliases = Object.keys(ACCESSORY_ALIASES).filter((a) => ACCESSORY_ALIASES[a].includes(id)); + accessoryDuplicates = aliases.concat(constants.ACCESSORY_ALIASES[aliases]); + } + + for (const duplicate of accessoryDuplicates) { + accessoryBag.concat(inventory.filter((a) => a.categories.includes("accessory"))).map((a) => { + if (helper.getId(a) === duplicate) { + a.isInactive = true; + a.isUnique = false; + } + }); + } + } + + accessories.push(insertAccessory); + accessoryIds.push({ + id: id, + rarity: insertAccessory.rarity, + }); + + if (insertAccessory.isInactive === false) { + accessoryRarities[insertAccessory.rarity]++; + if (id == "HEGEMONY_ARTIFACT") { + accessoryRarities.hegemony = { rarity: insertAccessory.rarity }; + } + if (id === "ABICASE") { + accessoryRarities.abicase = { model: insertAccessory.extra?.model }; + } + } + } + + // Add accessories from enderchest and backpacks + for (const item of enderchest.concat(storage)) { + if ("categories" in item === false) { + continue; + } + + let items = [item]; + if (!item.categories.includes("accessory") && "containsItems" in item && Array.isArray(item.containsItems)) { + items = item.containsItems.slice(0); + } + + for (const accessory of items.filter((a) => a.categories.includes("accessory"))) { + const insertAccessory = Object.assign({ isUnique: false, isInactive: true }, accessory); + + accessories.push(insertAccessory); + } + } + + for (const accessory of accessories) { + accessory.base_name = accessory.display_name; + + if (accessory.tag?.ExtraAttributes?.modifier != undefined) { + accessory.base_name = accessory.display_name.split(" ").slice(1).join(" "); + accessory.reforge = accessory.tag.ExtraAttributes.modifier; + } + + if (accessory.tag?.ExtraAttributes?.talisman_enrichment != undefined) { + accessory.enrichment = accessory.tag.ExtraAttributes.talisman_enrichment.toLowerCase(); + } + + if (accessory.isUnique === false || accessory.isInactive === true) { + const source = accessory.extra?.source; + if (source !== undefined) { + accessory.tag.display.Lore.push("", `§7Location: §c${source}`); + } + } + } + + if (accessoryRarities.rift_prism === true) { + accessoryIds.push({ + id: "RIFT_PRISM", + rarity: "rare", + }); + } + + output.accessories = accessories.sort(itemSorter); + output.accessory_ids = accessoryIds; + output.accessory_rarities = accessoryRarities; + + return output; +} diff --git a/src/stats/items/armor.js b/src/stats/items/armor.js new file mode 100644 index 0000000000..f2e485cd74 --- /dev/null +++ b/src/stats/items/armor.js @@ -0,0 +1,88 @@ +import * as constants from "../../constants.js"; +import * as helper from "../../helper.js"; + +export function getArmor(armor) { + // One armor piece + if (armor.length === 1) { + const armorPiece = armor.find((x) => x.rarity); + + return { + armor, + set_name: armorPiece.display_name, + set_rarity: armorPiece.rarity, + }; + } + + // Full armor set (4 pieces) + if (armor.length === 4) { + let outputName; + let reforgeName; + + // Getting armor_name + armor.forEach((armorPiece) => { + let name = armorPiece.display_name; + + // Removing skin and stars / Whitelisting a-z and 0-9 + name = name.replace(/[^A-Za-z0-9 -']/g, "").trim(); + + // Removing modifier + if (armorPiece.tag?.ExtraAttributes?.modifier != undefined) { + name = name.split(" ").slice(1).join(" "); + } + + // Converting armor_name to generic name + // Ex: Superior Dragon Helmet -> Superior Dragon Armor + if (/^Armor .*? (Helmet|Chestplate|Leggings|Boots)$/g.test(name)) { + // name starts with Armor and ends with piece name, remove piece name + name = name.replaceAll(/(Helmet|Chestplate|Leggings|Boots)/g, "").trim(); + } else { + // removing old 'Armor' and replacing the piece name with 'Armor' + name = name.replace("Armor", "").replace(" ", " ").trim(); + name = name.replaceAll(/(Helmet|Chestplate|Leggings|Boots)/g, "Armor").trim(); + } + + armorPiece.armor_name = name; + }); + + // Getting full armor reforge (same reforge on all pieces) + if ( + armor.filter( + (a) => + a.tag?.ExtraAttributes?.modifier != undefined && + a.tag?.ExtraAttributes?.modifier == armor[0].tag.ExtraAttributes.modifier + ).length == 4 + ) { + reforgeName = armor[0].display_name + .replace(/[^A-Za-z0-9 -']/g, "") + .trim() + .split(" ")[0]; + } + + // Handling normal sets of armor + if (armor.filter((a) => a.armor_name == armor[0].armor_name).length == 4) { + outputName = armor[0].armor_name; + } + + // Handling special sets of armor (where pieces aren't named the same) + constants.SPECIAL_SETS.forEach((set) => { + if (armor.filter((a) => set.pieces.includes(helper.getId(a))).length == 4) { + outputName = set.name; + } + }); + + // Finalizing the output + if (reforgeName && outputName) { + outputName = reforgeName + " " + outputName; + } + + return { + armor, + set_name: outputName, + set_rarity: constants.RARITIES[Math.max(...armor.map((a) => helper.rarityNameToInt(a.rarity)))], + }; + } + + return { + armor, + }; +} diff --git a/src/stats/items/bingo.js b/src/stats/items/bingo.js new file mode 100644 index 0000000000..e8880722ce --- /dev/null +++ b/src/stats/items/bingo.js @@ -0,0 +1,21 @@ +import * as stats from "../../stats.js"; +import * as helper from "../../helper.js"; +import { db } from "../../mongo.js"; + +export async function getBingoCard(paramBingo, cacheOnly) { + if (paramBingo && paramBingo.events) { + const bingoData = await helper.getBingoGoals(db, cacheOnly); + if (bingoData === null || bingoData.goals === undefined) { + throw new Error("Failed to fetch Bingo data. Please try again later."); + } + + const bingoProfile = paramBingo.events.find((profile) => profile.key === bingoData.id); + + const completedBingoGoals = bingoProfile?.completed_goals ?? []; + const bingoGoals = bingoData.goals; + + return bingoProfile !== undefined ? stats.getBingoItems(completedBingoGoals, bingoGoals) : []; + } + + return []; +} diff --git a/src/stats/items/category.js b/src/stats/items/category.js new file mode 100644 index 0000000000..edf4828978 --- /dev/null +++ b/src/stats/items/category.js @@ -0,0 +1,99 @@ +import { itemSorter } from "./processing.js"; +import * as constants from "../../constants.js"; +import * as helper from "../../helper.js"; + +export function getCategory(allItems, category) { + const output = allItems.filter((a) => a.categories?.includes(category)); + + for (const item of allItems) { + if (!Array.isArray(item.containsItems)) { + continue; + } + + output.push(...getCategory(item.containsItems, category)); + } + + return output.sort(itemSorter); +} + +export function getWeapons(allItems) { + const weapons = getCategory(allItems, "weapon"); + + const countsOfId = {}; + for (const weapon of weapons) { + const id = helper.getId(weapon); + + countsOfId[id] = (countsOfId[id] || 0) + 1; + + if (countsOfId[id] > 2 && constants.RARITIES.indexOf(weapon.rarity) < constants.RARITIES.indexOf("legendary")) { + weapon.hidden = true; + } + } + + const highestPriorityWeapon = getCategory(allItems, "sword").filter((a) => a.backpackIndex === undefined)[0]; + + return { + weapons: weapons, + highest_priority_weapon: highestPriorityWeapon, + }; +} + +export function getFarmingTools(allItems) { + const tools = getCategory(allItems, "farming_tool"); + + const highestPriorityTool = getCategory(allItems, "farming_tool").filter((a) => a.backpackIndex === undefined)[0]; + + return { + tools: tools, + highest_priority_tool: highestPriorityTool, + }; +} + +export function getSkilllTools(skill, allItems) { + const tools = getCategory(allItems, `${skill}_tool`); + + const highestPriorityTool = getCategory(allItems, `${skill}_tool`).filter((a) => a.backpackIndex === undefined)[0]; + + return { + tools: tools, + highest_priority_tool: highestPriorityTool, + }; +} + +export function getPets(allItems) { + const output = allItems + .filter((a) => a.tag?.ExtraAttributes?.petInfo) + .map((a) => ({ + uuid: a.tag.ExtraAttributes.uuid, + type: a.tag.ExtraAttributes.petInfo.type, + exp: a.tag.ExtraAttributes.petInfo.exp, + active: a.tag.ExtraAttributes.petInfo.active, + tier: a.tag.ExtraAttributes.petInfo.tier, + heldItem: a.tag.ExtraAttributes.petInfo.heldItem || null, + candyUsed: a.tag.ExtraAttributes.petInfo.candyUsed, + skin: a.tag.ExtraAttributes.petInfo.skin || null, + })); + + for (const item of allItems) { + if (!Array.isArray(item.containsItems)) { + continue; + } + + output.push( + ...item.containsItems + .filter((a) => a.tag?.ExtraAttributes?.petInfo) + .map((a) => ({ + uuid: a.tag.ExtraAttributes.uuid, + type: a.tag.ExtraAttributes.petInfo.type, + exp: a.tag.ExtraAttributes.petInfo.exp, + active: a.tag.ExtraAttributes.petInfo.active, + tier: a.tag.ExtraAttributes.petInfo.tier, + heldItem: a.tag.ExtraAttributes.petInfo.heldItem || null, + candyUsed: a.tag.ExtraAttributes.petInfo.candyUsed, + skin: a.tag.ExtraAttributes.petInfo.skin || null, + })) + ); + } + + return output; +} diff --git a/src/stats/items/equipment.js b/src/stats/items/equipment.js new file mode 100644 index 0000000000..8994eb9c9d --- /dev/null +++ b/src/stats/items/equipment.js @@ -0,0 +1,5 @@ +export function getEquipment(equipment) { + return { + equipment, + }; +} diff --git a/src/stats/items/items.js b/src/stats/items/items.js new file mode 100644 index 0000000000..78c7edd110 --- /dev/null +++ b/src/stats/items/items.js @@ -0,0 +1,8 @@ +export * from "./wardrobe.js"; +export * from "./accessories.js"; +export * from "./mining.js"; +export * from "./bingo.js"; +export * from "./category.js"; +export * from "./armor.js"; +export * from "./equipment.js"; +export * from "./museum.js"; diff --git a/src/stats/items/mining.js b/src/stats/items/mining.js new file mode 100644 index 0000000000..55f1062f4c --- /dev/null +++ b/src/stats/items/mining.js @@ -0,0 +1,131 @@ +import { getTexture } from "../../custom-resources.js"; +import { getLevelByXp } from "../skills/leveling.js"; +import * as constants from "../../constants.js"; +import * as helper from "../../helper.js"; +import * as stats from "../../stats.js"; +import { fileURLToPath } from "url"; +import path from "path"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +export function getHotmItems(userProfile, packs) { + const data = userProfile.mining_core; + const output = []; + + // Filling the space with empty items + for (let index = 0; index < 7 * 9; index++) { + output.push(helper.generateItem()); + } + + if (!data) { + return output; + } + + const hotmLevelData = data.experience ? getLevelByXp(data.experience, { type: "hotm" }) : 0; + const nodes = data.nodes + ? Object.fromEntries(Object.entries(data.nodes).filter(([key, value]) => !key.startsWith("toggle_"))) + : {}; + const toggles = data.nodes + ? Object.fromEntries(Object.entries(data.nodes).filter(([key, value]) => key.startsWith("toggle_"))) + : {}; + const mcdata = stats.getMiningCoreData(userProfile); + + // Check for missing node classes + for (const nodeId in nodes) { + if (constants.HOTM.nodes[nodeId] == undefined) { + throw new Error(`Missing Heart of the Mountain node: ${nodeId}`); + } + } + + // Processing nodes + for (const nodeId in constants.HOTM.nodes) { + const enabled = toggles[`toggle_${nodeId}`] ?? true; + const level = nodes[nodeId] ?? 0; + const node = new constants.HOTM.nodes[nodeId]({ + level, + enabled, + nodes, + hotmLevelData, + selectedPickaxeAbility: data.selected_pickaxe_ability, + }); + + output[node.position7x9 - 1] = helper.generateItem({ + display_name: node.name, + id: node.itemData.id, + Damage: node.itemData.Damage, + glowing: node.itemData.glowing, + tag: { + display: { + Name: node.displayName, + Lore: node.lore, + }, + }, + position: node.position7x9, + }); + } + + // Processing HotM tiers + for (let tier = 1; tier <= constants.HOTM.tiers; tier++) { + const hotm = new constants.HOTM.hotm(tier, hotmLevelData); + + output[hotm.position7x9 - 1] = helper.generateItem({ + display_name: `Tier ${tier}`, + id: hotm.itemData.id, + Damage: hotm.itemData.Damage, + glowing: hotm.itemData.glowing, + tag: { + display: { + Name: hotm.displayName, + Lore: hotm.lore, + }, + }, + position: hotm.position7x9, + }); + } + + // Processing HotM items (stats, hc crystals, reset) + for (const itemClass of constants.HOTM.items) { + const item = new itemClass({ + resources: { + token_of_the_mountain: mcdata.tokens, + mithril_powder: mcdata.powder.mithril, + gemstone_powder: mcdata.powder.gemstone, + }, + crystals: mcdata.crystal_nucleus.crystals, + last_reset: mcdata.hotm_last_reset, + }); + + output[item.position7x9 - 1] = helper.generateItem({ + display_name: helper.removeFormatting(item.displayName), + id: item.itemData.id, + Damage: item.itemData.Damage, + glowing: item.itemData.glowing, + texture_path: item.itemData?.texture_path, + tag: { + display: { + Name: item.displayName, + Lore: item.lore, + }, + }, + position: item.position7x9, + }); + } + + // Processing textures + output.forEach(async (item) => { + const customTexture = await getTexture(item, { + ignore_id: true, + pack_ids: packs, + }); + + if (customTexture) { + item.animated = customTexture.animated; + item.texture_path = "/" + customTexture.path; + item.texture_pack = customTexture.pack.config; + item.texture_pack.base_path = + "/" + path.relative(path.resolve(__dirname, "..", "public"), customTexture.pack.base_path); + } + }); + + return output; +} diff --git a/src/stats/items/museum.js b/src/stats/items/museum.js new file mode 100644 index 0000000000..c588d78b52 --- /dev/null +++ b/src/stats/items/museum.js @@ -0,0 +1,238 @@ +import * as constants from "../../constants.js"; +import { processItems } from "./processing.js"; +import * as helper from "../../helper.js"; +import _ from "lodash"; + +async function processMuseumItems(items, museumData, customTextures, packs, options = { cacheOnly: false }) { + for (const [id, data] of Object.entries(items)) { + const { + donated_time: donatedTime, + borrowing, + items: { data: decodedData }, + } = data; + + const encodedData = await processItems(decodedData, "museum", customTextures, packs, options.cacheOnly); + + if (donatedTime) { + encodedData.map((i) => + helper.addToItemLore(i, ["", `§7Donated: §c`]) + ); + } + + if (borrowing) { + encodedData.map((i) => helper.addToItemLore(i, ["", `§7Status: §cBorrowing`])); + } + + museumData[id] = { + donated_time: donatedTime, + borrowing: borrowing ?? false, + data: encodedData.filter((i) => i.id), + }; + } +} + +async function getMuseumData(profile, customTextures, packs, options = { cacheOnly: false }) { + const museumData = { items: {}, special: [] }; + + await Promise.all([ + processMuseumItems(profile.museum.items, museumData.items, customTextures, packs, options), + processMuseumItems(profile.museum.special, museumData.special, customTextures, packs, options), + ]); + + return museumData; +} + +function markChildrenAsDonated(children, output) { + output[children] = { + donated_as_child: true, + }; + + const childOfChild = constants.MUSEUM.children[children]; + if (childOfChild !== undefined) { + markChildrenAsDonated(childOfChild, output); + } +} + +async function processMuseum(profile, customTextures, packs, options = { cacheOnly: false }) { + const member = profile.museum; + if (member.items === undefined || member.special === undefined) { + return null; + } + + const processedMuseumData = await getMuseumData(profile, customTextures, packs, options); + + const output = {}; + for (const item of constants.getMuseumItems()) { + const itemData = processedMuseumData.items[item]; + if (itemData === undefined) { + continue; + } + + output[item] = itemData; + + const children = constants.MUSEUM.children[item]; + if (children !== undefined) { + markChildrenAsDonated(children, output); + } + } + + return { + value: profile.museum.value ?? 0, + appraisal: profile.museum.appraisal, + total: { + amount: Object.keys(output).length, + total: constants.getMuseumItems().length, + }, + weapons: { + amount: Object.keys(output).filter((i) => constants.MUSEUM.weapons.includes(i)).length, + total: constants.MUSEUM.weapons.length, + }, + armor: { + amount: Object.keys(output).filter((i) => constants.MUSEUM.armor.includes(i)).length, + total: constants.MUSEUM.armor.length, + }, + rarities: { + amount: Object.keys(output).filter((i) => constants.MUSEUM.rarities.includes(i)).length, + total: constants.MUSEUM.rarities.length, + }, + special: { + amount: processedMuseumData.special.length, + }, + items: output, + specialItems: processedMuseumData.special, + }; +} + +export async function getMuseumItems(profile, customTextures, packs, options = { cacheOnly: false }) { + const museum = await processMuseum(profile, customTextures, packs, options.cacheOnly); + if (museum === null) { + return null; + } + + const output = []; + for (let i = 0; i < 6 * 9; i++) { + output[i] = helper.generateItem({ id: undefined }); + } + + for (const item of constants.MUSEUM.inventory) { + updateMuseumItemProgress(item, museum); + + output[item.position] = helper.generateItem(item); + + const inventoryType = item.inventoryType; + if (inventoryType === undefined) { + continue; + } + + const museumItems = museum[inventoryType].total ?? museum[inventoryType].amount; + const pages = Math.ceil(museumItems / constants.MUSEUM.item_slots.length); + + for (let page = 0; page < pages; page++) { + // FRAME + for (let i = 0; i < 6 * 9; i++) { + if (output[item.position].containsItems[i]) { + const presetItem = output[item.position].containsItems[i]; + + updateMuseumItemProgress(presetItem, museum); + + output[item.position].containsItems[presetItem.position + page * 54] = helper.generateItem(presetItem); + } + + output[item.position].containsItems[i + page * 54] ??= helper.generateItem({ id: undefined }); + } + + // CLEAR FIRST 4 ITEMS + for (let i = 0; i < 4; i++) { + output[item.position].containsItems[i + page * 54] = helper.generateItem({ id: undefined }); + } + + // CATEGORIES + for (const [index, slot] of Object.entries(constants.MUSEUM.item_slots)) { + const itemSlot = parseInt(index) + page * constants.MUSEUM.item_slots.length; + + // SPECIAL ITEMS CATEGORY + if (inventoryType === "special") { + const museumItem = museum.specialItems[itemSlot]; + if (museumItem === undefined) { + continue; + } + + const itemData = museumItem.data[0]; + + output[item.position].containsItems[slot + page * 54] = helper.generateItem(itemData); + continue; + } + + // WEAPONS, ARMOR & RARITIES + const itemId = constants.MUSEUM[inventoryType][itemSlot]; + if (itemId === undefined) { + continue; + } + + const museumItem = museum.items[itemId]; + + // MISSING ITEM + if (museumItem === undefined) { + const itemData = constants.MUSEUM.missing_item[inventoryType]; + itemData.display_name = _.startCase(constants.MUSEUM.armor_to_id[itemId] ?? itemId); + + output[item.position].containsItems[slot + page * 54] = helper.generateItem(itemData); + continue; + } + + // DONATED HIGHER TIER + if (museumItem.donated_as_child) { + const itemData = constants.MUSEUM.higher_tier_donated; + itemData.display_name = _.startCase(constants.MUSEUM.armor_to_id[itemId] ?? itemId); + + output[item.position].containsItems[slot + page * 54] = helper.generateItem(itemData); + continue; + } + + // NORMAL ITEM + const itemData = museumItem.data[0]; + if (museumItem.data.length > 1) { + itemData.containsItems = museumItem.data.map((i) => helper.generateItem(i)); + } + + output[item.position].containsItems[slot + page * 54] = helper.generateItem(itemData); + } + } + } + + return { + museumItems: museum, + museum: output, + }; +} + +function updateMuseumItemProgress(presetItem, museum) { + if (presetItem.progressType === undefined) { + return; + } + + if (presetItem.progressType === "appraisal") { + const { appraisal, value } = museum; + + return helper.addToItemLore(presetItem, [ + `§7Museum Appraisal Unlocked: ${appraisal ? "§aYes" : "§cNo"}`, + "", + `§7Museum Value: §6${Math.floor(value).toLocaleString()} Coins §7(§6${helper.formatNumber(value)}§7)`, + ]); + } + + if (presetItem.progressType === "special") { + const { amount } = museum[presetItem.inventoryType]; + + return helper.addToItemLore(presetItem, [`§7Items Donated: §b${amount}`, "", "§eClick to view!"]); + } + + const { amount, total } = museum[presetItem.progressType]; + + helper.addToItemLore(presetItem, [ + `§7Items Donated: §e${Math.floor((amount / total) * 100)}§6%`, + `§9§l${helper.formatProgressBar(amount, total, 9)} §b${amount} §9/ §b${total}`, + "", + "§eClick to view!", + ]); +} diff --git a/src/stats/items/processing.js b/src/stats/items/processing.js new file mode 100644 index 0000000000..243cba192d --- /dev/null +++ b/src/stats/items/processing.js @@ -0,0 +1,569 @@ +import { getTexture } from "../../custom-resources.js"; +import { getItemNetworth } from "skyhelper-networth"; +import * as constants from "../../constants.js"; +import minecraftData from "minecraft-data"; +import * as helper from "../../helper.js"; +const mcData = minecraftData("1.8.9"); +import { fileURLToPath } from "url"; +import { db } from "../../mongo.js"; +import path from "path"; +import _ from "lodash"; + +import util from "util"; +import nbt from "prismarine-nbt"; +const parseNbt = util.promisify(nbt.parse); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +export function itemSorter(a, b) { + if (a.rarity !== b.rarity) { + return constants.RARITIES.indexOf(b.rarity) - constants.RARITIES.indexOf(a.rarity); + } + + if (b.inBackpack && !a.inBackpack) { + return -1; + } + if (a.inBackpack && !b.inBackpack) { + return 1; + } + + return a.item_index - b.item_index; +} + +async function getBackpackContents(arraybuf) { + const buf = Buffer.from(arraybuf); + + let data = await parseNbt(buf); + data = nbt.simplify(data); + + const items = data.i; + + for (const [index, item] of items.entries()) { + item.isInactive = true; + item.inBackpack = true; + item.item_index = index; + } + + return items; +} + +// Process items returned by API +export async function processItems(base64, source, customTextures = false, packs, cacheOnly = false) { + // API stores data as base64 encoded gzipped Minecraft NBT data + const buf = Buffer.from(base64, "base64"); + + let data = await parseNbt(buf); + data = nbt.simplify(data); + + let items = data.i; + + // Check backpack contents and add them to the list of items + for (const [index, item] of items.entries()) { + if ( + item.tag?.display?.Name.includes("Backpack") || + ["NEW_YEAR_CAKE_BAG", "BUILDERS_WAND", "BASKET_OF_SEEDS"].includes(item.tag?.ExtraAttributes?.id) + ) { + let backpackData; + + for (const key of Object.keys(item.tag.ExtraAttributes)) { + if (key.endsWith("_data")) backpackData = item.tag.ExtraAttributes[key]; + } + + if (!Array.isArray(backpackData)) { + continue; + } + + const backpackContents = await getBackpackContents(backpackData); + + for (const backpackItem of backpackContents) { + backpackItem.backpackIndex = index; + } + + item.containsItems = []; + + items.push(...backpackContents); + } + + if ( + item.tag?.ExtraAttributes?.id?.includes("PERSONAL_COMPACTOR_") || + item.tag?.ExtraAttributes?.id?.includes("PERSONAL_DELETOR_") + ) { + item.containsItems = []; + for (const key in item.tag.ExtraAttributes) { + if (key.startsWith("personal_compact_") || key.startsWith("personal_deletor_")) { + const hypixelItem = await db.collection("items").findOne({ id: item.tag.ExtraAttributes[key] }); + + const itemData = { + Count: 1, + Damage: hypixelItem?.damage ?? 3, + id: hypixelItem?.item_id ?? 397, + itemIndex: item.containsItems.length, + glowing: hypixelItem?.glowing ?? false, + display_name: hypixelItem?.name ?? _.startCase(item.tag.ExtraAttributes[key].replace(/_/g, " ")), + rarity: hypixelItem?.tier ?? "common", + categories: [], + }; + + if (hypixelItem?.texture !== undefined) { + itemData.texture_path = `/head/${hypixelItem.texture}`; + } + + if (itemData.id >= 298 && itemData.id <= 301) { + const type = ["helmet", "chestplate", "leggings", "boots"][itemData.id - 298]; + + if (hypixelItem?.color !== undefined) { + const color = helper.rgbToHex(hypixelItem.color) ?? "955e3b"; + + itemData.texture_path = `/leather/${type}/${color}`; + } + } + + if (hypixelItem === null) { + itemData.texture_path = "/head/bc8ea1f51f253ff5142ca11ae45193a4ad8c3ab5e9c6eec8ba7a4fcb7bac40"; + } + + item.containsItems.push(itemData); + } + } + } + } + + for (const item of items) { + // Get extra info about certain things + if (item.tag?.ExtraAttributes != undefined) { + item.extra = { + hpbs: 0, + }; + } + + if (item.tag?.ExtraAttributes?.rarity_upgrades != undefined) { + const rarityUpgrades = item.tag.ExtraAttributes.rarity_upgrades; + + if (rarityUpgrades > 0) { + item.extra.recombobulated = true; + } + } + + if (item.tag?.ExtraAttributes?.model != undefined) { + item.extra.model = item.tag.ExtraAttributes.model; + } + + if (item.tag?.ExtraAttributes?.hot_potato_count != undefined) { + item.extra.hpbs = item.tag.ExtraAttributes.hot_potato_count; + } + + if (item.tag?.ExtraAttributes?.expertise_kills != undefined) { + const expertiseKills = item.tag.ExtraAttributes.expertise_kills; + + if (expertiseKills > 0) { + item.extra.expertise_kills = expertiseKills; + } + } + + if (item.tag?.ExtraAttributes?.compact_blocks !== undefined) { + const compactBlocks = item.tag.ExtraAttributes.compact_blocks; + + if (compactBlocks > 0) { + item.extra.compact_blocks = compactBlocks; + } + } + + if (item.tag?.ExtraAttributes?.hecatomb_s_runs != undefined) { + const hecatombSRuns = item.tag.ExtraAttributes.hecatomb_s_runs; + + if (hecatombSRuns > 0) { + item.extra.hecatomb_s_runs = hecatombSRuns; + } + } + + if (item.tag?.ExtraAttributes?.champion_combat_xp != undefined) { + const championCombatXp = item.tag.ExtraAttributes.champion_combat_xp; + + if (championCombatXp > 0) { + item.extra.champion_combat_xp = championCombatXp; + } + } + + if (item.tag?.ExtraAttributes?.farmed_cultivating != undefined) { + const farmedCultivating = item.tag.ExtraAttributes.farmed_cultivating; + + if (farmedCultivating > 0) { + item.extra.farmed_cultivating = farmedCultivating.toString(); + } + } + + if (item.tag?.ExtraAttributes?.blocks_walked != undefined) { + const blocksWalked = item.tag.ExtraAttributes.blocks_walked; + + if (blocksWalked > 0) { + item.extra.blocks_walked = blocksWalked; + } + } + + if (item.tag?.ExtraAttributes?.timestamp != undefined) { + const timestamp = item.tag.ExtraAttributes.timestamp; + + if (!isNaN(timestamp)) { + item.extra.timestamp = timestamp; + } else { + item.extra.timestamp = Date.parse(timestamp + " EDT"); + } + + item.extra.timestamp; + } + + if (item.tag?.ExtraAttributes?.spawnedFor != undefined) { + item.extra.spawned_for = item.tag.ExtraAttributes.spawnedFor.replaceAll("-", ""); + } + + if (item.tag?.ExtraAttributes?.baseStatBoostPercentage != undefined) { + item.extra.base_stat_boost = item.tag.ExtraAttributes.baseStatBoostPercentage; + } + + if (item.tag?.ExtraAttributes?.item_tier != undefined) { + item.extra.floor = item.tag.ExtraAttributes.item_tier; + } + + if (item.tag?.ExtraAttributes?.winning_bid != undefined) { + item.extra.price_paid = item.tag.ExtraAttributes.winning_bid; + } + + if (item.tag?.ExtraAttributes?.modifier != undefined) { + item.extra.reforge = item.tag.ExtraAttributes.modifier; + } + + if (item.tag?.ExtraAttributes?.ability_scroll != undefined) { + item.extra.ability_scroll = item.tag.ExtraAttributes.ability_scroll; + } + + if (item.tag?.ExtraAttributes?.mined_crops != undefined) { + item.extra.crop_counter = item.tag.ExtraAttributes.mined_crops; + } + + if (item.tag?.ExtraAttributes?.petInfo != undefined) { + item.tag.ExtraAttributes.petInfo = JSON.parse(item.tag.ExtraAttributes.petInfo); + } + + if (item.tag?.ExtraAttributes?.gems != undefined) { + item.extra.gems = item.tag.ExtraAttributes.gems; + } + + if (item.tag?.ExtraAttributes?.skin != undefined) { + item.extra.skin = item.tag.ExtraAttributes.skin; + } + + if (item.tag?.ExtraAttributes?.petInfo?.skin != undefined) { + item.extra.skin = `PET_SKIN_${item.tag.ExtraAttributes.petInfo.skin}`; + } + + // Set custom texture for colored leather armor + if (typeof item.id === "number" && item.id >= 298 && item.id <= 301) { + const color = item.tag?.display?.color?.toString(16).padStart(6, "0") ?? "955e3b"; + + const type = ["helmet", "chestplate", "leggings", "boots"][item.id - 298]; + + item.texture_path = `/leather/${type}/${color}`; + } + + // Set custom texture for colored potions + if (item.id == 373) { + const color = constants.POTION_COLORS[item.Damage % 16]; + + const type = item.Damage & 16384 ? "splash" : "normal"; + + item.texture_path = `/potion/${type}/${color}`; + } + + // Set raw display name without color and formatting codes + if (item.tag?.display?.Name != undefined) { + item.display_name = helper.getRawLore(item.tag.display.Name); + } + + // Resolve skull textures to their image path + if ( + Array.isArray(item.tag?.SkullOwner?.Properties?.textures) && + item.tag.SkullOwner.Properties.textures.length > 0 + ) { + try { + const json = JSON.parse(Buffer.from(item.tag.SkullOwner.Properties.textures[0].Value, "base64").toString()); + const url = json.textures.SKIN.url; + const uuid = url.split("/").pop(); + + item.texture_path = `/head/${uuid}?v6`; + } catch (e) { + console.error(e); + } + } + + const animatedTexture = helper.getAnimatedTexture(item); + + // Gives animated texture on certain items, will be overwritten by custom textures + if (animatedTexture) { + item.texture_path = animatedTexture.texture; + } + + // Uses animated skin texture + if (item?.extra?.skin != undefined && constants.ANIMATED_ITEMS?.[item.extra.skin]) { + item.texture_path = constants.ANIMATED_ITEMS[item.extra.skin].texture; + } + + if (item.tag?.ExtraAttributes?.skin == undefined && customTextures) { + const customTexture = await getTexture(item, { + ignore_id: false, + pack_ids: packs, + }); + + if (customTexture) { + item.animated = customTexture.animated; + item.texture_path = "/" + customTexture.path; + item.texture_pack = customTexture.pack.config; + item.texture_pack.base_path = + "/" + path.relative(path.resolve(__dirname, "..", "public"), customTexture.pack.base_path); + } + } + + if (source !== undefined) { + item.extra ??= {}; + item.extra.source = source + .split(" ") + .map((a) => a.charAt(0).toUpperCase() + a.slice(1)) + .join(" "); + } + + // Lore stuff + const itemLore = item?.tag?.display?.Lore ?? []; + const loreRaw = [...itemLore]; + + const lore = loreRaw != null ? loreRaw.map((a) => (a = helper.getRawLore(a))) : []; + + item.rarity = null; + item.categories = []; + + if (lore.length > 0) { + // item categories, rarity, recombobulated, dungeon, shiny + const itemType = helper.parseItemTypeFromLore(lore, item); + + for (const key in itemType) { + item[key] = itemType[key]; + } + + // fix custom maps texture + if (item.id == 358) { + item.id = 395; + item.Damage = 0; + } + } + + // Set HTML lore to be displayed on the website + if (itemLore.length > 0) { + if (item.extra?.recombobulated) { + itemLore.push("§8(Recombobulated)"); + } + + if (item.extra?.gems) { + itemLore.push( + "", + "§7Applied Gemstones:", + ...helper.parseItemGems(item.extra.gems, item.rarity).map((gem) => `§7 - ${gem.lore}`) + ); + } + + if (item.extra?.compact_blocks) { + const compactBlocks = item.extra.compact_blocks; + + if (loreRaw) { + itemLore.push("", `§7Ores Mined: §c${compactBlocks.toLocaleString()}`); + if (compactBlocks >= 15000) { + itemLore.push(`§8MAXED OUT!`); + } else { + let toNextLevel = 0; + for (const e of constants.ENCHANTMENT_LADDERS.compact_ores) { + if (compactBlocks < e) { + toNextLevel = e - compactBlocks; + break; + } + } + itemLore.push(`§8${toNextLevel.toLocaleString()} ores to tier up!`); + } + } + } + + if (item.extra?.expertise_kills) { + const expertiseKills = item.extra.expertise_kills; + + if (loreRaw) { + itemLore.push("", `§7Expertise Kills: §c${expertiseKills.toLocaleString()}`); + if (expertiseKills >= 15000) { + itemLore.push(`§8MAXED OUT!`); + } else { + let toNextLevel = 0; + for (const e of constants.ENCHANTMENT_LADDERS.expertise_kills) { + if (expertiseKills < e) { + toNextLevel = e - expertiseKills; + break; + } + } + itemLore.push(`§8${toNextLevel.toLocaleString()} kills to tier up!`); + } + } + } + + if (item.extra?.hecatomb_s_runs) { + const hecatombSRuns = item.extra.hecatomb_s_runs; + + if (loreRaw) { + itemLore.push("", `§7Hecatomb Runs: §c${hecatombSRuns.toLocaleString()}`); + if (hecatombSRuns >= 100) { + itemLore.push(`§8MAXED OUT!`); + } else { + let toNextLevel = 0; + for (const e of constants.ENCHANTMENT_LADDERS.hecatomb_s_runs) { + if (hecatombSRuns < e) { + toNextLevel = e - hecatombSRuns; + break; + } + } + itemLore.push(`§8${toNextLevel.toLocaleString()} runs to tier up!`); + } + } + } + + if (item.extra?.champion_combat_xp) { + const championCombatXp = Math.floor(item.extra.champion_combat_xp); + + if (loreRaw) { + itemLore.push("", `§7Champion XP: §c${championCombatXp.toLocaleString()}`); + if (championCombatXp >= 3000000) { + itemLore.push(`§8MAXED OUT!`); + } else { + let toNextLevel = 0; + for (const e of constants.ENCHANTMENT_LADDERS.champion_xp) { + if (championCombatXp < e) { + toNextLevel = Math.floor(e - championCombatXp); + break; + } + } + itemLore.push(`§8${toNextLevel.toLocaleString()} xp to tier up!`); + } + } + } + + if (item.extra?.farmed_cultivating) { + const farmedCultivating = Math.floor(item.extra.farmed_cultivating); + + if (loreRaw) { + itemLore.push("", `§7Cultivating Crops: §c${farmedCultivating.toLocaleString()}`); + if (farmedCultivating >= 100000000) { + itemLore.push(`§8MAXED OUT!`); + } else { + let toNextLevel = 0; + for (const e of constants.ENCHANTMENT_LADDERS.cultivating_crops) { + if (farmedCultivating < e) { + toNextLevel = Math.floor(e - farmedCultivating); + break; + } + } + itemLore.push(`§8${toNextLevel.toLocaleString()} crops to tier up!`); + } + } + } + + if (item.extra?.blocks_walked) { + const blocksWalked = item.extra.blocks_walked; + + if (loreRaw) { + itemLore.push("", `§7Blocks Walked: §c${blocksWalked.toLocaleString()}`); + if (blocksWalked >= 100000) { + itemLore.push(`§8MAXED OUT!`); + } else { + let toNextLevel = 0; + for (const e of constants.PREHISTORIC_EGG_BLOCKS_WALKED_LADDER) { + if (blocksWalked < e) { + toNextLevel = e - blocksWalked; + break; + } + } + itemLore.push(`§8Walk ${toNextLevel.toLocaleString()} blocks to tier up!`); + } + } + } + + if (item.tag?.display?.color) { + const hex = item.tag.display.color.toString(16).padStart(6, "0"); + itemLore.push("", `§7Color: #${hex.toUpperCase()}`); + } + + if (item.extra?.timestamp) { + itemLore.push("", `§7Obtained: §c`); + } + + if (item.extra?.spawned_for) { + if (!item.extra.timestamp) { + itemLore.push(""); + } + + const spawnedFor = item.extra.spawned_for; + const spawnedForUser = await helper.resolveUsernameOrUuid(spawnedFor, db, cacheOnly); + + itemLore.push(`§7By: §c${spawnedForUser.display_name}`); + } + + if (item.extra?.base_stat_boost) { + itemLore.push( + "", + `§7Dungeon Item Quality: ${item.extra.base_stat_boost == 50 ? "§6" : "§c"}${item.extra.base_stat_boost}/50%` + ); + } + + if (item.extra?.floor) { + itemLore.push(`§7Obtained From: §bFloor ${item.extra.floor}`); + } + + if (item.extra?.price_paid) { + itemLore.push("", `§7Price Paid at Dark Auction: §6${item.extra.price_paid.toLocaleString()} Coins`); + } + } + + if (item?.tag || item?.exp) { + if (item.tag?.ExtraAttributes?.id === "PET") { + item.tag.ExtraAttributes.petInfo = + JSON.stringify(item.tag.ExtraAttributes.petInfo) ?? item.tag.ExtraAttributes.petInfo; + } + + const ITEM_PRICE = await getItemNetworth(item, { cache: true }); + + if (ITEM_PRICE?.price > 0) { + itemLore.push( + "", + `§7Item Value: §6${Math.round(ITEM_PRICE.price).toLocaleString()} Coins §7(§6${helper.formatNumber( + ITEM_PRICE.price + )}§7)` + ); + } + + if (item.tag?.ExtraAttributes?.id === "PET") { + item.tag.ExtraAttributes.petInfo = + typeof item.tag.ExtraAttributes.petInfo === "string" + ? JSON.parse(item.tag.ExtraAttributes.petInfo) + : item.tag.ExtraAttributes.petInfo; + } + } + + if (!("display_name" in item) && "id" in item) { + const vanillaItem = mcData.items[item.id]; + + if ("displayName" in vanillaItem) { + item.display_name = vanillaItem.displayName; + } + } + } + + for (const item of items) { + if (item.inBackpack) { + items[item.backpackIndex].containsItems.push(Object.assign({}, item)); + } + } + + items = items.filter((a) => !a.inBackpack); + + return items; +} diff --git a/src/stats/items/wardrobe.js b/src/stats/items/wardrobe.js new file mode 100644 index 0000000000..f5d3d4e5d0 --- /dev/null +++ b/src/stats/items/wardrobe.js @@ -0,0 +1,28 @@ +import * as helper from "../../helper.js"; + +export function getWardrobe(wardrobeInventory) { + const wardrobeColumns = wardrobeInventory.length / 4; + + const wardrobe = []; + for (let i = 0; i < wardrobeColumns; i++) { + const page = Math.floor(i / 9); + + const wardrobeSlot = []; + + for (let j = 0; j < 4; j++) { + const index = 36 * page + (i % 9) + j * 9; + + if (helper.getId(wardrobeInventory[index]).length > 0) { + wardrobeSlot.push(wardrobeInventory[index]); + } else { + wardrobeSlot.push(null); + } + } + + if (wardrobeSlot.find((a) => a !== null) != undefined) { + wardrobe.push(wardrobeSlot); + } + } + + return wardrobe; +} diff --git a/src/stats/leaderboards.js b/src/stats/leaderboards.js new file mode 100644 index 0000000000..7b11885224 --- /dev/null +++ b/src/stats/leaderboards.js @@ -0,0 +1,229 @@ +// TODO: Full rewrite needed + +/* +import { redisClient } from "../redis.js"; +import * as helper from "../helper.js"; +import * as constants from "../constants.js"; +import * as stats from "../stats.js"; +import _ from "lodash"; + +function getMinMax(profiles, min, ...path) { + let output = null; + + const compareValues = profiles.map((a) => helper.getPath(a, ...path)).filter((a) => !isNaN(a)); + + if (compareValues.length == 0) { + return output; + } + + if (min) { + output = Math.min(...compareValues); + } else { + output = Math.max(...compareValues); + } + + if (isNaN(output)) { + return null; + } + + return output; +} + +function getMax(profiles, ...path) { + return getMinMax(profiles, false, ...path); +} + +function getAllKeys(profiles, ...path) { + return _.uniq([].concat(...profiles.map((a) => _.keys(helper.getPath(a, ...path))))); +} + +async function updateLeaderboardPositions(db, uuid, allProfiles) { + if (constants.BLOCKED_PLAYERS.includes(uuid)) { + return; + } + + const hypixelProfile = await helper.getRank(uuid, db, true); + + const memberProfiles = []; + + for (const singleProfile of allProfiles) { + const userProfile = singleProfile.members[uuid]; + + if (userProfile == null) { + continue; + } + + userProfile.levels = await stats.getSkills(userProfile, hypixelProfile); + + let totalSlayerXp = 0; + + userProfile.slayer_xp = 0; + + if (userProfile.slayer_bosses != undefined) { + for (const slayer in userProfile.slayer_bosses) { + totalSlayerXp += userProfile.slayer_bosses[slayer].xp || 0; + } + + userProfile.slayer_xp = totalSlayerXp; + + for (const mountMob in constants.MOB_MOUNTS) { + const mounts = constants.MOB_MOUNTS[mountMob]; + + userProfile.stats[`kills_${mountMob}`] = 0; + userProfile.stats[`deaths_${mountMob}`] = 0; + + for (const mount of mounts) { + userProfile.stats[`kills_${mountMob}`] += userProfile.stats[`kills_${mount}`] || 0; + userProfile.stats[`deaths_${mountMob}`] += userProfile.stats[`deaths_${mount}`] || 0; + + delete userProfile.stats[`kills_${mount}`]; + delete userProfile.stats[`deaths_${mount}`]; + } + } + } + + userProfile.skyblock_level = { + xp: userProfile.leveling?.experience || 0, + level: Math.floor(userProfile.leveling?.experience / 100 || 0), + }; + + userProfile.pet_score = 0; + + const maxPetRarity = {}; + if (Array.isArray(userProfile.pets)) { + for (const pet of userProfile.pets) { + if (!("tier" in pet)) { + continue; + } + + maxPetRarity[pet.type] = Math.max(maxPetRarity[pet.type] || 0, constants.PET_VALUE[pet.tier.toLowerCase()]); + } + + for (const key in maxPetRarity) { + userProfile.pet_score += maxPetRarity[key]; + } + } + + memberProfiles.push({ + profile_id: singleProfile.profile_id, + data: userProfile, + }); + } + + const values = {}; + + values["pet_score"] = getMax(memberProfiles, "data", "pet_score"); + + values["fairy_souls"] = getMax(memberProfiles, "data", "fairy_souls_collected"); + values["average_level"] = getMax(memberProfiles, "data", "levels", "average_level"); + values["total_skill_xp"] = getMax(memberProfiles, "data", "levels", "total_skill_xp"); + + for (const skill of getAllKeys(memberProfiles, "data", "levels", "levels")) { + values[`skill_${skill}_xp`] = getMax(memberProfiles, "data", "levels", "levels", skill, "xp"); + } + + values[`skyblock_level_xp`] = getMax(memberProfiles, "data", "skyblock_level", "xp"); + values["slayer_xp"] = getMax(memberProfiles, "data", "slayer_xp"); + + for (const slayer of getAllKeys(memberProfiles, "data", "slayer_bosses")) { + for (const key of getAllKeys(memberProfiles, "data", "slayer_bosses", slayer)) { + if (!key.startsWith("boss_kills_tier")) { + continue; + } + + const tier = key.split("_").pop(); + + values[`${slayer}_slayer_boss_kills_tier_${tier}`] = getMax(memberProfiles, "data", "slayer_bosses", slayer, key); + } + + values[`${slayer}_slayer_xp`] = getMax(memberProfiles, "data", "slayer_bosses", slayer, "xp"); + } + + for (const item of getAllKeys(memberProfiles, "data", "collection")) { + values[`collection_${item.toLowerCase()}`] = getMax(memberProfiles, "data", "collection", item); + } + + for (const stat of getAllKeys(memberProfiles, "data", "stats")) { + values[stat] = getMax(memberProfiles, "data", "stats", stat); + } + + // Dungeons (Mainly Catacombs now.) + for (const stat of getAllKeys(memberProfiles, "data", "dungeons", "dungeon_types", "catacombs")) { + switch (stat) { + case "best_runs": + case "highest_tier_completed": + break; + case "experience": + values[`dungeons_catacombs_xp`] = getMax( + memberProfiles, + "data", + "dungeons", + "dungeon_types", + "catacombs", + "experience" + ); + break; + default: + for (const floor of getAllKeys(memberProfiles, "data", "dungeons", "dungeon_types", "catacombs", stat)) { + const floorId = `catacombs_${floor}`; + if (!constants.DUNGEONS.floors[floorId] || !constants.DUNGEONS.floors[floorId].name) continue; + + const floorName = constants.DUNGEONS.floors[floorId].name; + values[`dungeons_catacombs_${floorName}_${stat}`] = getMax( + memberProfiles, + "data", + "dungeons", + "dungeon_types", + "catacombs", + stat, + floor + ); + } + } + } + + for (const dungeonClass of getAllKeys(memberProfiles, "data", "dungeons", "player_classes")) { + values[`dungeons_class_${dungeonClass}_xp`] = getMax( + memberProfiles, + "data", + "dungeons", + "player_classes", + dungeonClass, + "experience" + ); + } + + values[`dungeons_secrets_found`] = hypixelProfile.achievements.skyblock_treasure_hunter || 0; + + const multi = redisClient.pipeline(); + + for (const key in values) { + if (values[key] == null) { + continue; + } + + multi.zadd(`lb_${key}`, values[key], uuid); + } + for (const singleProfile of allProfiles) { + if (singleProfile.banking?.balance != undefined) { + multi.zadd(`lb_bank`, singleProfile.banking.balance, singleProfile.profile_id); + } + + const minionCrafts = []; + + for (const member in singleProfile.members) { + if (Array.isArray(singleProfile.members[member].crafted_generators)) { + minionCrafts.push(...singleProfile.members[member].crafted_generators); + } + } + + multi.zadd(`lb_unique_minions`, _.uniq(minionCrafts).length, singleProfile.profile_id); + } + + try { + await multi.exec(); + } catch (e) { + console.error(e); + } +} +*/ diff --git a/src/stats/mining.js b/src/stats/mining.js new file mode 100644 index 0000000000..b9e1c9dc22 --- /dev/null +++ b/src/stats/mining.js @@ -0,0 +1,146 @@ +import { getLevelByXp } from "./skills/leveling.js"; +import * as constants from "../constants.js"; +import * as helper from "../helper.js"; +import { db } from "../mongo.js"; +import moment from "moment"; + +export function getMiningCoreData(userProfile) { + const output = {}; + + const data = userProfile.mining_core; + if (data === undefined) { + return; + } + + output.level = getLevelByXp(data.experience, { type: "hotm" }); + + const totalTokens = helper.calcHotmTokens(output.level.level, data.nodes?.special_0 ?? 0); + output.tokens = { + total: totalTokens, + spent: data.tokens_spent ?? 0, + available: totalTokens - (data.tokens_spent ?? 0), + }; + + output.selected_pickaxe_ability = constants.HOTM.names[data.selected_pickaxe_ability] ?? null; + + output.powder = { + mithril: { + total: (data.powder_mithril ?? 0) + (data.powder_spent_mithril ?? 0), + spent: data.powder_spent_mithril ?? 0, + available: data.powder_mithril ?? 0, + }, + gemstone: { + total: (data.powder_gemstone ?? 0) + (data.powder_spent_gemstone ?? 0), + spent: data.powder_spent_gemstone || 0, + available: data.powder_gemstone ?? 0, + }, + }; + + const crystalsCompleted = data.crystals + ? Object.values(data.crystals) + .filter((x) => x.total_placed) + .map((x) => x.total_placed) + : []; + output.crystal_nucleus = { + times_completed: crystalsCompleted.length > 0 ? Math.min(...crystalsCompleted) : 0, + crystals: data.crystals ?? {}, + precursor: data.biomes?.precursor ?? null, + }; + + output.daily_ores = { + mined: data.daily_ores_mined, + day: data.daily_ores_mined_day, + ores: { + mithril: { + day: data.daily_ores_mined_day_mithril_ore, + count: data.daily_ores_mined_mithril_ore, + }, + gemstone: { + day: data.daily_ores_mined_day_gemstone, + count: data.daily_ores_mined_gemstone, + }, + }, + }; + + output.hotm_last_reset = data.last_reset ?? 0; + + output.crystal_hollows_last_access = data.greater_mines_last_access ?? 0; + + output.daily_effect = { + effect: data.current_daily_effect ?? null, + last_changed: data.current_daily_effect_last_changed ?? null, + }; + + output.nodes = data.nodes ?? {}; + + return output; +} + +async function getForge(userProfile) { + const output = {}; + + if (userProfile.forge?.forge_processes?.forge_1) { + const forge = Object.values(userProfile.forge.forge_processes.forge_1); + + const processes = []; + for (const item of forge) { + const forgeItem = { + id: item.id, + slot: item.slot, + timeFinished: 0, + timeFinishedText: "", + }; + + if (item.id in constants.FORGE_TIMES) { + let forgeTime = constants.FORGE_TIMES[item.id] * 60 * 1000; + const quickForge = userProfile.mining_core?.nodes?.forge_time; + if (quickForge != null) { + forgeTime *= constants.QUICK_FORGE_MULTIPLIER[quickForge]; + } + + const dbObject = await db.collection("items").findOne({ id: item.id }); + forgeItem.name = item.id == "PET" ? "[Lvl 1] Ammonite" : dbObject ? dbObject.name : item.id; + + const timeFinished = item.startTime + forgeTime; + forgeItem.timeFinished = timeFinished; + forgeItem.timeFinishedText = moment(timeFinished).fromNow(); + } else { + forgeItem.id = `UNKNOWN-${item.id}`; + } + + processes.push(forgeItem); + } + + output.processes = processes; + } + + return output; +} + +export async function getMining(userProfile, hypixelProfile) { + const mining = { + commissions: { + milestone: 0, + completions: hypixelProfile.achievements.skyblock_hard_working_miner || 0, + }, + forge: {}, + core: {}, + }; + + if (userProfile.objectives?.tutorial !== undefined) { + for (const key of userProfile.objectives.tutorial) { + if (key.startsWith("commission_milestone_reward_mining_xp_tier_") === false) { + continue; + } + + const tier = parseInt(key.slice(43)); + mining.commissions.milestone = Math.max(mining.commissions.milestone, tier); + } + } + + mining.core = getMiningCoreData(userProfile); + + mining.forge = await getForge(userProfile); + + return mining; +} diff --git a/src/stats/minions.js b/src/stats/minions.js new file mode 100644 index 0000000000..30eadaaaa0 --- /dev/null +++ b/src/stats/minions.js @@ -0,0 +1,95 @@ +import * as constants from "../constants.js"; +import * as helper from "../helper.js"; +import _ from "lodash"; + +function getProfileMinions(coopMembers) { + const minions = []; + + const craftedGenerators = []; + for (const member in coopMembers) { + if ( + coopMembers[member]?.player_data === undefined || + "crafted_generators" in coopMembers[member].player_data === false + ) { + continue; + } + + craftedGenerators.push(...coopMembers[member].player_data.crafted_generators); + } + + for (const generator of craftedGenerators) { + const split = generator.split("_"); + + const minionLevel = parseInt(split.pop()); + const minionName = split.join("_"); + + const minion = minions.find((a) => a.id == minionName); + + if (minion == undefined) { + minions.push({ id: minionName, tiers: [minionLevel] }); + } else { + minion.tiers.push(minionLevel); + } + } + + const output = {}; + for (const category in constants.MINIONS) { + output[category] ??= { + minions: [], + }; + + for (const [id, data] of Object.entries(constants.MINIONS[category])) { + const minion = minions.find((m) => m.id === id); + + output[category].minions.push({ + id: id, + name: data.name ?? helper.titleCase(id.replace("_", " ")), + texture: data.texture, + tiers: minion?.tiers?.length > 0 ? _.uniq(minion.tiers.sort((a, b) => a - b)) : [], + tier: minion?.tiers?.length > 0 ? Math.max(...minion.tiers) : 0, + maxTier: data.maxTier ?? 11, + }); + } + + output[category].totalMinions = output[category].minions.length; + output[category].maxedMinions = output[category].minions.filter((m) => m.tier === m.maxTier).length; + + output[category].totalTiers = output[category].minions.reduce((a, b) => a + b.tiers.length, 0); + output[category].maxedTiers = output[category].minions.filter((m) => m.tier === m.maxTier).length; + } + + return output; +} + +function getMinionSlots(minions) { + const output = { current: 5 }; + + const uniquesRequired = Object.keys(constants.MINION_SLOTS).sort((a, b) => parseInt(a) - parseInt(b)); + for (const [index, uniques] of uniquesRequired.entries()) { + if (parseInt(uniques) <= minions.maxedTiers) { + continue; + } + + output.current = constants.MINION_SLOTS[uniquesRequired[index - 1]]; + output.next = uniquesRequired[index] - minions.maxedTiers; + break; + } + + return output; +} + +export function getMinions(profile) { + const output = {}; + + output.minions = getProfileMinions(profile.members); + + output.totalMinions = Object.values(output.minions).reduce((a, b) => a + b.totalMinions, 0); + output.maxedMinions = Object.values(output.minions).reduce((a, b) => a + b.maxedMinions, 0); + + output.totalTiers = Object.values(output.minions).reduce((a, b) => a + b.totalTiers, 0); + output.maxedTiers = Object.values(output.minions).reduce((a, b) => a + b.maxedTiers, 0); + + output.minion_slots = getMinionSlots(output); + + return output; +} diff --git a/src/stats/misc.js b/src/stats/misc.js new file mode 100644 index 0000000000..8a4b624d27 --- /dev/null +++ b/src/stats/misc.js @@ -0,0 +1,284 @@ +import moment from "moment"; +import * as constants from "../constants.js"; +import * as helper from "../helper.js"; + +/** + * Returns an object containing the number of fairy souls collected by the user, the total number of fairy souls available to collect, the progress made towards collecting all fairy souls, and the number of fairy exchanges made by the user. + * @param {object} userProfile - The user's profile object. + * @param {object} profile - The user's game profile object. + * @returns {{collected: number, total: number, progress: number, fairy_exchanges: number}} - An object containing the number of fairy souls collected by the user, the total number of fairy souls available to collect, the progress made towards collecting all fairy souls, and the number of fairy exchanges made by the user. + * */ + +export function getFairySouls(userProfile, profile) { + try { + if (userProfile.fairy_soul === undefined) { + return; + } + + if (isNaN(userProfile.fairy_soul.total_collected)) { + return { + collected: 0, + total: 0, + progress: 0, + fairy_exchanges: 0, + }; + } + + const totalSouls = + profile.game_mode === "island" ? constants.FAIRY_SOULS.max.stranded : constants.FAIRY_SOULS.max.normal; + + return { + collected: userProfile.fairy_soul.total_collected, + total: totalSouls, + progress: ((userProfile.fairy_soul.total_collected / totalSouls) * 100).toFixed(2), + fairy_exchanges: userProfile.fairy_soul.fairy_exchanges, + }; + } catch (error) { + console.error(error); + } +} + +function getProfileUpgrades(profile) { + const output = {}; + for (const upgrade in constants.PROFILE_UPGRADES) { + output[upgrade] = 0; + } + + if (profile.community_upgrades?.upgrade_states != undefined) { + for (const u of profile.community_upgrades.upgrade_states) { + output[u.upgrade] = Math.max(output[u.upgrade] || 0, u.tier); + } + } + + return output; +} + +function getMiscUncategorized(userProfile) { + const output = {}; + + if ("soulflow" in userProfile.item_data) { + const soulflow = userProfile.item_data.soulflow; + + output.soulflow = { + raw: soulflow, + formatted: helper.formatNumber(soulflow), + }; + } + + if ("fastest_target_practice" in userProfile) { + output.fastest_target_practice = { + raw: userProfile.fastest_target_practice, + formatted: `${helper.formatNumber(userProfile.fastest_target_practice)}s`, + }; + } + + if ("favorite_arrow" in userProfile.item_data) { + const favoriteArrow = userProfile.item_data.favorite_arrow; + + output.favorite_arrow = { + raw: favoriteArrow, + formatted: `${helper.titleCase(favoriteArrow.replace("_", " "))}`, + }; + } + + if ("teleporter_pill_consumed" in userProfile.item_data) { + const teleporterPill = userProfile.item_data.teleporter_pill_consumed; + + output.teleporter_pill_consumed = { + raw: teleporterPill, + formatted: teleporterPill ? "Yes" : "No", + }; + } + + if ("reaper_peppers_eaten" in userProfile.player_data) { + const reaperPeppersEaten = userProfile.player_data.reaper_peppers_eaten; + + output.reaper_peppers_eaten = { + raw: reaperPeppersEaten, + formatted: reaperPeppersEaten, + maxed: reaperPeppersEaten === constants.MAX_REAPER_PEPPERS_EATEN, + }; + } + + if ("personal_bank_upgrade" in userProfile.profile) { + const personalBankUpgrade = userProfile.profile.personal_bank_upgrade; + + output.bank_cooldown = { + raw: personalBankUpgrade, + formatted: constants.BANK_COOLDOWN[personalBankUpgrade] ?? "Unknown", + maxed: personalBankUpgrade === Object.keys(constants.BANK_COOLDOWN).length, + }; + } + + return output; +} + +function getPetMilestone(type, amount) { + return { + amount: amount ?? 0, + rarity: constants.MILESTONE_RARITIES[constants.PET_MILESTONES[type].findLastIndex((x) => amount >= x)] ?? "common", + total: constants.PET_MILESTONES[type].at(-1), + progress: Math.min((amount / constants.PET_MILESTONES[type].at(-1)) * 100, 100).toFixed(2), + }; +} + +export function getMisc(profile, userProfile, hypixelProfile) { + if (userProfile.player_stats === undefined) { + return; + } + + const misc = {}; + if ("races" in userProfile.player_stats) { + misc.races = {}; + const races = userProfile.player_stats.races; + for (const key in userProfile.objectives) { + if (key.startsWith("complete_the_") === false) { + continue; + } + + const raceTimeID = `${key.replace("complete_the_", "").split("_").slice(0, -1).join("_")}_best_time`; + const customRaceID = constants.CUSTOM_RACE_IDS[raceTimeID]; + const tier = parseInt(key.split("_").at(-1)); + + const raceTime = races[raceTimeID] ?? races[customRaceID]; + let actualRaceId = (customRaceID ?? raceTimeID).split("_").slice(0, 2).join("_"); + if (raceTime) { + misc.races.other ??= { name: "Other", races: {} }; + misc.races.other.races[actualRaceId] = { + name: constants.RACE_NAMES[actualRaceId], + time: moment.duration(raceTime, "milliseconds").format("m:ss.SSS"), + tier: tier, + }; + } else { + // Thank you Hypxiel + actualRaceId = actualRaceId.replace("_race", ""); + const categoryRaceID = raceTimeID.replace(`${actualRaceId}_`, "").replace("_best_time", ""); + + misc.races[actualRaceId] ??= { + name: constants.RACE_NAMES[actualRaceId], + races: { + no_return: {}, + with_return: {}, + }, + }; + + const raceId = raceTimeID.replace("_race", ""); + if (categoryRaceID.endsWith("no_return_race")) { + const subcategoryRaceId = categoryRaceID.replace("_no_return_race", ""); + const raceName = helper.titleCase(subcategoryRaceId.replace("_", " ")); + + misc.races[actualRaceId].races.with_return[subcategoryRaceId] = { + name: raceName, + time: moment.duration(races.dungeon_hub[raceId], "milliseconds").format("m:ss.SSS"), + tier: tier, + }; + } else { + const subcategoryRaceId = categoryRaceID.replace("_with_return_race", ""); + const raceName = helper.titleCase(subcategoryRaceId.replace("_", " ")); + + misc.races[actualRaceId].races.no_return[subcategoryRaceId] = { + name: raceName, + time: moment.duration(races.dungeon_hub[raceId], "milliseconds").format("m:ss.SSS"), + tier: tier, + }; + } + } + } + } + + if ("gifts" in userProfile.player_stats) { + misc.gifts = { + given: userProfile.player_stats.gifts.total_given ?? 0, + received: userProfile.player_stats.gifts.total_received ?? 0, + }; + } + + if ("winter" in userProfile.player_stats) { + misc.winter = { + most_snowballs_hit: userProfile.player_stats.winter.most_snowballs_hit ?? 0, + most_damage_dealt: userProfile.player_stats.winter.most_damage_dealt ?? 0, + most_magma_damage_dealt: userProfile.player_stats.winter.most_magma_damage_dealt ?? 0, + most_cannonballs_hit: userProfile.player_stats.winter.most_cannonballs_hit ?? 0, + }; + } + + if (userProfile.player_stats.end_island?.dragon_fight !== undefined) { + const dragonKills = Object.keys(userProfile.player_stats.kills) + .filter((key) => key.endsWith("_dragon") && !key.startsWith("master_wither_king")) + .reduce((obj, key) => ({ ...obj, [key.replace("_dragon", "")]: userProfile.player_stats.kills[key] }), {}); + + dragonKills.total = Object.values(dragonKills).reduce((a, b) => a + b, 0); + + const dragonDeaths = Object.keys(userProfile.player_stats.deaths) + .filter((key) => key.endsWith("_dragon") && !key.startsWith("master_wither_king")) + .reduce((obj, key) => ({ ...obj, [key.replace("_dragon", "")]: userProfile.player_stats.deaths[key] }), {}); + + dragonDeaths.total = Object.values(dragonDeaths).reduce((a, b) => a + b, 0); + + misc.dragons = { + ender_crystals_destroyed: userProfile.player_stats.end_island.dragon_fight.ender_crystals_destroyed, + most_damage: userProfile.player_stats.end_island.dragon_fight.most_damage, + fastest_kill: userProfile.player_stats.end_island.dragon_fight.fastest_kill, + kills: dragonKills, + deaths: dragonDeaths, + }; + } + + if ( + userProfile.player_stats.kills?.corrupted_protector !== undefined || + userProfile.player_stats.deaths?.corrupted_protector !== undefined + ) { + misc.endstone_protector = { + kills: userProfile.player_stats.kills.corrupted_protector ?? 0, + deaths: userProfile.player_stats.deaths.corrupted_protector ?? 0, + }; + } + + if ("highest_critical_damage" in userProfile.player_stats) { + misc.damage = { + highest_critical_damage: userProfile.player_stats.highest_critical_damage, + }; + } + + if (userProfile.player_stats.pets?.milestone !== undefined) { + misc.pet_milestones = { + sea_creatures_killed: getPetMilestone( + "sea_creatures_killed", + userProfile.player_stats.pets.milestone.sea_creatures_killed + ), + ores_mined: getPetMilestone("ores_mined", userProfile.player_stats.pets.milestone.ores_mined), + }; + } + + if ("mythos" in userProfile.player_stats) { + misc.mythological_event = userProfile.player_stats.mythos; + } + + if ( + userProfile.player_data.active_effects !== undefined || + userProfile.player_data.paused_effects !== undefined || + userProfile.player_data.disabled_potion_effects !== undefined + ) { + misc.effects = { + active: userProfile.player_data.active_effects || [], + paused: userProfile.player_data.paused_effects || [], + disabled: userProfile.player_data.disabled_potion_effects || [], + }; + } + + misc.profile_upgrades = getProfileUpgrades(profile); + + if ("auctions" in userProfile.player_stats) { + misc.auctions = userProfile.player_stats.auctions; + } + + if (hypixelProfile.claimed_items) { + misc.claimed_items = hypixelProfile.claimed_items; + } + + if ("item_data" in userProfile) { + misc.uncategorized = getMiscUncategorized(userProfile); + } + + return misc; +} diff --git a/src/stats/missing.js b/src/stats/missing.js new file mode 100644 index 0000000000..23b05444b6 --- /dev/null +++ b/src/stats/missing.js @@ -0,0 +1,169 @@ +import * as constants from "../constants.js"; +import * as helper from "../helper.js"; + +/** + * Checks if an accessory is present in an array of accessories. + * + * @param {Object[]} accessories - The array of accessories to search. + * @param {Object|string} accessory - The accessory object or ID to find. + * @param {Object} [options] - The options object. + * @param {boolean} [options.ignoreRarity=false] - Whether to ignore the rarity of the accessory when searching. + * @returns {boolean} True if the accessory is found, false otherwise. + */ +function hasAccessory(accessories, accessory, options = { ignoreRarity: false }) { + const id = typeof accessory === "object" ? accessory.id : accessory; + + if (options.ignoreRarity === false) { + return accessories.some( + (a) => a.id === id && constants.RARITIES.indexOf(a.rarity) >= constants.RARITIES.indexOf(accessory.rarity) + ); + } else { + return accessories.some((a) => a.id === id); + } +} + +/** + * Finds an accessory in an array of accessories by its ID. + * + * @param {Object[]} accessories - The array of accessories to search. + * @param {string} accessory - The ID of the accessory to find. + * @returns {Object|undefined} The accessory object if found, or undefined if not found. + */ +function getAccessory(accessories, accessory) { + return accessories.find((a) => a.id === accessory); +} + +function getMissing(accessories) { + const ACCESSORIES = constants.getAllAccessories(); + const unique = ACCESSORIES.map(({ id, tier: rarity }) => ({ id, rarity })); + + for (const { id } of unique) { + if (id in constants.ACCESSORY_ALIASES === false) continue; + + for (const duplicate of constants.ACCESSORY_ALIASES[id]) { + if (hasAccessory(accessories, duplicate, { ignoreRarity: true }) === true) { + getAccessory(accessories, duplicate).id = id; + } + } + } + + let missing = unique.filter((accessory) => hasAccessory(accessories, accessory) === false); + for (const { id } of missing) { + const upgrades = constants.getUpgradeList(id); + if (upgrades === undefined) { + continue; + } + + for (const upgrade of upgrades.filter((item) => upgrades.indexOf(item) > upgrades.indexOf(id))) { + if (hasAccessory(accessories, upgrade) === true) { + missing = missing.filter((item) => item.id !== id); + } + } + } + + const upgrades = []; + const other = []; + for (const { id, rarity } of missing) { + const ACCESSORY = ACCESSORIES.find((a) => a.id === id && a.tier === rarity); + + const object = { + ...ACCESSORY, + display_name: ACCESSORY.name ?? null, + rarity: rarity, + }; + + if ((constants.getUpgradeList(id) && constants.getUpgradeList(id)[0] !== id) || ACCESSORY.rarities) { + upgrades.push(object); + } else { + other.push(object); + } + } + + return { + missing: other, + upgrades: upgrades, + }; +} + +export async function getMissingAccessories(calculated, items, packs) { + const accessoryIds = items.accessories.accessory_ids; + if (!accessoryIds || accessoryIds?.length === 0) { + return; + } + + const output = getMissing(accessoryIds); + for (const key in output) { + for (const item of output[key]) { + let price = 0; + + if (item.customPrice === true) { + if (item.upgrade) { + price = (await helper.getItemPrice(item.upgrade.item)) * item.upgrade.cost[item.rarity]; + } + + if (item.id === "POWER_ARTIFACT") { + for (const { slot_type: slot } of item.gemstone_slots) { + price += await helper.getItemPrice(`PERFECT_${slot}_GEM`); + } + } + } else { + price = await helper.getItemPrice(item.id); + } + + item.extra = { price }; + if (price > 0) { + helper.addToItemLore( + item, + `§7Price: §6${Math.round(price).toLocaleString()} Coins §7(§6${helper.formatNumber( + Math.floor(price / helper.getMagicalPower(item.rarity, item.id)) + )} §7per MP)` + ); + } + + item.tag ??= {}; + item.tag.ExtraAttributes ??= {}; + item.tag.ExtraAttributes.id ??= item.id; + item.Damage ??= item.damage; + item.id = item.item_id; + + helper.applyResourcePack(item, packs); + } + + output[key].sort((a, b) => { + const aPrice = a.extra?.price || 0; + const bPrice = b.extra?.price || 0; + + if (aPrice === 0) return 1; + if (bPrice === 0) return -1; + + return aPrice - bPrice; + }); + } + + const accessories = items.accessories.accessories; + + output.unique = accessories.filter((a) => a.isUnique === true).length; + output.total = constants.UNIQUE_ACCESSORIES_COUNT; + + output.recombobulated = accessories.filter((a) => a?.extra?.recombobulated === true).length; + output.total_recombobulated = constants.RECOMBABLE_ACCESSORIES_COUNT; + + const activeAccessories = accessories.filter((a) => a.isUnique === true && a.isInactive === false); + + output.magical_power = { + accessories: activeAccessories.reduce((a, b) => a + helper.getMagicalPower(b.rarity, helper.getId(b)), 0), + abiphone: calculated.crimson_isle?.abiphone?.active ? Math.floor(calculated.crimson_isle.abiphone.active / 2) : 0, + rift_prism: accessoryIds.find((a) => a.id === "RIFT_PRISM") ? 11 : 0, + }; + + output.magical_power.total = Object.values(output.magical_power).reduce((a, b) => a + b, 0); + + output.magical_power.rarities = {}; + for (const rarity in constants.MAGICAL_POWER) { + output.magical_power.rarities[rarity] = activeAccessories + .filter((a) => a.rarity === rarity) + .reduce((a, b) => a + helper.getMagicalPower(rarity, helper.getId(b)), 0); + } + + return output; +} diff --git a/src/stats/other.js b/src/stats/other.js new file mode 100644 index 0000000000..159ef75941 --- /dev/null +++ b/src/stats/other.js @@ -0,0 +1,135 @@ +import * as constants from "../constants.js"; +import * as helper from "../helper.js"; +import moment from "moment"; + +/** + * Returns an object containing the user's first join date and current area information. + * @param {Object} userProfile - The user's profile object. + * @returns {{ + * first_join: { + * unix: number, + * text: string + * }, + * current_area: { + * current_area: string, + * current_area_updated: number + * } + * }} - An object containing the user's first join date and current area information. + */ +export function getUserData(userProfile) { + if (userProfile.profile === undefined) { + return; + } + + return { + first_join: { + unix: userProfile.profile.first_join, + text: moment(userProfile.profile.first_join).fromNow(), + }, + current_area: { + current_area: userProfile.current_area, + current_area_updated: userProfile.current_area_updated, + }, + }; +} + +/** + * Returns an object containing the bank and purse currencies data of a user. + * + * @param {Object} userProfile - The user profile object. + * @param {Object} profile - The profile object. + * @returns {{ + * bank: number, + * purse: number + * }} An object containing the bank and purse currencies data of a user. + */ +export function getCurrenciesData(userProfile, profile) { + return { + bank: profile.banking?.balance ?? 0, + purse: userProfile.currencies?.coin_purse || 0, + }; +} + +/** + * Returns an array of objects containing kill statistics for a given user profile. + * @param {Object} userProfile - The user profile to retrieve kill statistics for. + * @returns {{ + * type: string, + * entity_id: string, + * amount: number, + * entity_name: string + * }[]} An array of objects containing kill statistics. + */ +export function getKills(userProfile) { + if (userProfile.player_stats?.kills === undefined) { + return; + } + + const output = { kills: [], total: 0 }; + for (const entityId in userProfile.player_stats.kills) { + if (userProfile.player_stats.kills[entityId] === 0 || entityId === "total") { + continue; + } + + const entityName = + constants.MOB_NAMES[entityId] ?? + entityId + .split("_") + .map((s) => helper.capitalizeFirstLetter(s)) + .join(" "); + + output.kills.push({ + type: "kills", + entity_id: entityId, + amount: userProfile.player_stats.kills[entityId], + entity_name: entityName, + }); + } + + output.kills = output.kills.sort((a, b) => b.amount - a.amount); + output.total = userProfile.player_stats.kills.total; + + return output; +} + +/** + * Returns an array of objects containing death statistics for a given user profile. + * @param {Object} userProfile - The user profile to retrieve death statistics for. + * @returns {{ + * type: string, + * entity_id: string, + * amount: number, + * entity_name: string + * }[]} An array of objects containing death statistics. + */ +export function getDeaths(userProfile) { + if (userProfile.player_stats?.deaths === undefined) { + return; + } + + const output = { deaths: [], total: 0 }; + for (const entityId in userProfile.player_stats.deaths) { + if (userProfile.player_stats.deaths[entityId] === 0 || entityId === "total") { + continue; + } + + const entityName = + constants.MOB_NAMES[entityId] ?? + entityId + .split("_") + .map((s) => helper.capitalizeFirstLetter(s)) + .join(" "); + + output.deaths.push({ + type: "deaths", + entity_id: entityId, + amount: userProfile.player_stats.deaths[entityId], + entity_name: entityName, + }); + } + + output.deaths = output.deaths.sort((a, b) => b.amount - a.amount); + output.total = userProfile.player_stats.deaths.total; + + return output; +} diff --git a/src/stats/pets.js b/src/stats/pets.js new file mode 100644 index 0000000000..7aba58479f --- /dev/null +++ b/src/stats/pets.js @@ -0,0 +1,472 @@ +import { getItemNetworth } from "skyhelper-networth"; +import * as constants from "../constants.js"; +import * as helper from "../helper.js"; +import _ from "lodash"; + +function getPetLevel(petExp, offsetRarity, maxLevel) { + const rarityOffset = constants.PET_RARITY_OFFSET[offsetRarity]; + const levels = constants.PET_LEVELS.slice(rarityOffset, rarityOffset + maxLevel - 1); + + const xpMaxLevel = levels.reduce((a, b) => a + b, 0); + let xpTotal = 0; + let level = 1; + + let xpForNext = Infinity; + + for (let i = 0; i < maxLevel; i++) { + xpTotal += levels[i]; + + if (xpTotal > petExp) { + xpTotal -= levels[i]; + break; + } else { + level++; + } + } + + let xpCurrent = Math.floor(petExp - xpTotal); + let progress; + + if (level < maxLevel) { + xpForNext = Math.ceil(levels[level - 1]); + progress = Math.max(0, Math.min(xpCurrent / xpForNext, 1)); + } else { + level = maxLevel; + xpCurrent = petExp - levels[maxLevel - 1]; + xpForNext = 0; + progress = 1; + } + + return { + level, + xpCurrent, + xpForNext, + progress, + xpMaxLevel, + }; +} + +function getPetRarity(pet, petData) { + if (!(pet.heldItem == "PET_ITEM_TIER_BOOST" && !pet.ignoresTierBoost)) { + return pet.rarity; + } + + return constants.RARITIES[ + Math.min(constants.RARITIES.indexOf(petData.maxTier), constants.RARITIES.indexOf(pet.rarity) + 1) + ]; +} + +function getPetName(pet, petData) { + if (petData.hatching?.level > pet.level.level) { + // Golden Dragon Pet hatching name + return petData.hatching.name; + } else if (petData.name) { + // Normal pet name + return petData.name[pet.rarity] ?? petData.name.default; + } else { + // Unknown pet name + return helper.titleCase(pet.type.replaceAll("_", " ")); + } +} + +function getProfilePets(pets, calculated) { + let output = []; + + if (pets === undefined) { + return output; + } + + // debug pets + // pets = helper.generateDebugPets("SLUG"); + + for (const pet of pets) { + if ("tier" in pet === false) { + continue; + } + + const petData = constants.PET_DATA[pet.type] ?? { + head: "/head/bc8ea1f51f253ff5142ca11ae45193a4ad8c3ab5e9c6eec8ba7a4fcb7bac40", + type: "???", + maxTier: "legendary", + maxLevel: 100, + emoji: "❓", + }; + + petData.typeGroup = petData.typeGroup ?? pet.type; + + pet.rarity = pet.tier.toLowerCase(); + pet.stats = {}; + pet.ignoresTierBoost = petData.ignoresTierBoost; + /** @type {string[]} */ + const lore = []; + + pet.rarity = getPetRarity(pet, petData); + + pet.level = getPetLevel(pet.exp, petData.customLevelExpRarityOffset ?? pet.rarity, petData.maxLevel); + + // Get texture + if (typeof petData.head === "object") { + pet.texture_path = petData.head[pet.rarity] ?? petData.head.default; + } else { + pet.texture_path = petData.head; + } + + // Golden Dragon Pet hatching texture + if (petData.hatching?.level > pet.level.level) { + pet.texture_path = petData.hatching.head; + } + + if (pet.rarity in (petData.upgrades ?? {})) { + pet.texture_path = petData.upgrades[pet.rarity]?.head || pet.texture_path; + } + + let petSkin = null; + if (pet.skin && constants.PET_SKINS[`PET_SKIN_${pet.skin}`] !== undefined) { + pet.texture_path = constants.PET_SKINS[`PET_SKIN_${pet.skin}`].texture; + petSkin = constants.PET_SKINS[`PET_SKIN_${pet.skin}`].name; + } + + // Get first row of lore + const loreFirstRow = ["§8"]; + if (petData.type === "all") { + loreFirstRow.push("All Skills"); + } else { + loreFirstRow.push(helper.capitalizeFirstLetter(petData.type), " ", petData.category ?? "Pet"); + + if (petData.obtainsExp === "feed") { + loreFirstRow.push(", feed to gain XP"); + } + + if (petSkin) { + loreFirstRow.push(`, ${petSkin} Skin`); + } + } + + lore.push(loreFirstRow.join(""), ""); + + // Get name + const petName = getPetName(pet, petData); + + const rarity = constants.RARITIES.indexOf(pet.rarity); + + const searchName = pet.type in constants.PET_STATS ? pet.type : "???"; + + const petInstance = new constants.PET_STATS[searchName](rarity, pet.level.level, pet.extra, calculated); + + pet.stats = Object.assign({}, petInstance.stats); + pet.ref = petInstance; + + if (pet.heldItem) { + const { heldItem } = pet; + + if (heldItem in constants.PET_ITEMS) { + for (const stat in constants.PET_ITEMS[heldItem]?.stats) { + pet.stats[stat] ??= 0; + + pet.stats[stat] += constants.PET_ITEMS[heldItem].stats[stat]; + } + + for (const stat in constants.PET_ITEMS[heldItem]?.statsPerLevel) { + pet.stats[stat] ??= 0; + + pet.stats[stat] += constants.PET_ITEMS[heldItem].statsPerLevel[stat] * pet.level.level; + } + + for (const stat in constants.PET_ITEMS[heldItem]?.multStats) { + pet.stats[stat] ??= 0; + pet.stats[stat] *= constants.PET_ITEMS[heldItem].multStats[stat]; + } + + if ("multAllStats" in constants.PET_ITEMS[heldItem]) { + for (const stat in pet.stats) { + pet.stats[stat] *= constants.PET_ITEMS[heldItem].multAllStats; + } + } + } + + // push specific pet lore before stats added (mostly cosmetic) + if (constants.PET_DATA[pet.type]?.subLore !== undefined) { + lore.push(constants.PET_DATA[pet.type].subLore, " "); + } + + // push pet lore after held item stats added + const stats = pet.ref.lore(pet.stats); + for (const line of stats) { + lore.push(line); + } + + // then the ability lore + const abilities = pet.ref.abilities; + for (const ability of abilities) { + lore.push(" ", ability.name); + + for (const description of ability.desc) { + lore.push(description); + } + } + + // now we push the lore of the held items + const heldItemObj = constants.PET_ITEMS[heldItem] ?? constants.PET_ITEMS["???"]; + if (heldItem in constants.PET_ITEMS) { + lore.push("", `§6Held Item: §${constants.RARITY_COLORS[heldItemObj.tier.toLowerCase()]}${heldItemObj.name}`); + lore.push(constants.PET_ITEMS[heldItem].description); + } else { + lore.push("", `§6Held Item: §c${helper.titleCase(heldItem.replaceAll("_", " "))}`); + lore.push("§cThis item is not in our database yet. Please report it on our Discord server."); + } + + // extra line + lore.push(" "); + } else { + // no held items so push the new stats + const stats = pet.ref.lore(); + for (const line of stats) { + lore.push(line); + } + + const abilities = pet.ref.abilities; + for (const ability of abilities) { + lore.push(" ", ability.name); + + for (const description of ability.desc) { + lore.push(description); + } + } + + // extra line + lore.push(" "); + } + + // passive perks text + if (petData.passivePerks) { + lore.push("§8This pet's perks are active even when the pet is not summoned!", ""); + } + + // always gains exp text + if (petData.alwaysGainsExp) { + lore.push("§8This pet gains XP even when not summoned!", ""); + + if (typeof petData.alwaysGainsExp === "string") { + lore.push(`§8This pet only gains XP on the ${petData.alwaysGainsExp}§8!`, ""); + } + } + + if (pet.level.level < petData.maxLevel) { + lore.push(`§7Progress to Level ${pet.level.level + 1}: §e${(pet.level.progress * 100).toFixed(1)}%`); + + const progress = Math.ceil(pet.level.progress * 20); + const numerator = pet.level.xpCurrent.toLocaleString(); + const denominator = helper.formatNumber(pet.level.xpForNext, false); + + lore.push(`§2${"-".repeat(progress)}§f${"-".repeat(20 - progress)} §e${numerator} §6/ §e${denominator}`); + } else { + lore.push("§bMAX LEVEL"); + } + + let progress = Math.floor((pet.exp / pet.level.xpMaxLevel) * 100); + if (isNaN(progress)) { + progress = 0; + } + + lore.push( + "", + `§7Total XP: §e${helper.formatNumber(pet.exp, true, 1)} §6/ §e${helper.formatNumber( + pet.level.xpMaxLevel, + true, + 1 + )} §6(${progress.toLocaleString()}%)` + ); + + if (petData.obtainsExp !== "feed") { + lore.push(`§7Candy Used: §e${pet.candyUsed || 0} §6/ §e10`); + } + + if (pet.price > 0) { + lore.push( + "", + `§7Item Value: §6${Math.round(pet.price).toLocaleString()} Coins §7(§6${helper.formatNumber(pet.price)}§7)` + ); + } + + pet.lore = ""; + for (const line of lore) { + pet.lore += '' + helper.renderLore(line) + ""; + } + + pet.display_name = `${petName}${petSkin ? " ✦" : ""}`; + pet.emoji = petData.emoji; + pet.ref.profile = null; + + output.push(pet); + } + + // I have no idea what's going on here so I'm just going to leave it as is + output = output.sort((a, b) => { + if (a.active === b.active) { + if (a.rarity == b.rarity) { + if (a.type == b.type) { + return a.level.level > b.level.level ? -1 : 1; + } else { + let maxPetA = output + .filter((x) => x.type == a.type && x.rarity == a.rarity) + .sort((x, y) => y.level.level - x.level.level); + + maxPetA = maxPetA.length > 0 ? maxPetA[0].level.level : null; + + let maxPetB = output + .filter((x) => x.type == b.type && x.rarity == b.rarity) + .sort((x, y) => y.level.level - x.level.level); + + maxPetB = maxPetB.length > 0 ? maxPetB[0].level.level : null; + + if (maxPetA && maxPetB && maxPetA == maxPetB) { + return a.type < b.type ? -1 : 1; + } else { + return maxPetA > maxPetB ? -1 : 1; + } + } + } else { + return constants.RARITIES.indexOf(a.rarity) < constants.RARITIES.indexOf(b.rarity) ? 1 : -1; + } + } + + return a.active ? -1 : 1; + }); + + return output; +} + +function getMissingPets(pets, gameMode, userProfile) { + const profile = { + pets: [], + }; + + const missingPets = []; + + const ownedPetTypes = pets.map((pet) => constants.PET_DATA[pet.type]?.typeGroup || pet.type); + + for (const [petType, petData] of Object.entries(constants.PET_DATA)) { + if ( + ownedPetTypes.includes(petData.typeGroup ?? petType) || + (petData.bingoExclusive === true && gameMode !== "bingo") + ) { + continue; + } + + const key = petData.typeGroup ?? petType; + + missingPets[key] ??= []; + missingPets[key].push({ + type: petType, + active: false, + exp: helper.getPetExp(constants.PET_DATA[petType].maxTier, constants.PET_DATA[petType].maxLevel), + tier: constants.PET_DATA[petType].maxTier, + candyUsed: 0, + heldItem: null, + skin: null, + uuid: helper.generateUUID(), + }); + } + + for (const pets of Object.values(missingPets)) { + if (pets.length > 1) { + // using exp to find the highest tier + profile.pets.push(pets.sort((a, b) => b.exp - a.exp)[0]); + continue; + } + + profile.pets.push(pets[0]); + } + + return getProfilePets(profile.pets, userProfile); +} + +function getPetScore(pets) { + const highestRarity = {}; + const highestLevel = {}; + + for (const pet of pets) { + if (constants.PET_DATA[pet.type]?.ignoredInPetScoreCalculation === true) { + continue; + } + + if (!(pet.type in highestRarity) || constants.PET_VALUE[pet.rarity] > highestRarity[pet.type]) { + highestRarity[pet.type] = constants.PET_VALUE[pet.rarity]; + } + + if (!(pet.type in highestLevel) || pet.level.level > highestLevel[pet.type]) { + if (constants.PET_DATA[pet.type] && pet.level.level < constants.PET_DATA[pet.type].maxLevel) { + continue; + } + + highestLevel[pet.type] = 1; + } + } + + const total = + Object.values(highestRarity).reduce((a, b) => a + b, 0) + Object.values(highestLevel).reduce((a, b) => a + b, 0); + + let bonus = {}; + for (const score of Object.keys(constants.PET_REWARDS).reverse()) { + if (parseInt(score) > total) { + continue; + } + + bonus = Object.assign({}, constants.PET_REWARDS[score]); + + break; + } + + return { + total: total, + amount: bonus.magic_find, + bonus: bonus, + }; +} + +export async function getPets(userProfile, calculated, items, profile) { + const output = {}; + + // Get pets from profile + const pets = userProfile.pets_data?.pets ?? []; + + // Adds pets from inventories + pets.push(...items.pets); + + // Add Montezume pet from the Rift + if (userProfile.rift?.dead_cats?.montezuma !== undefined) { + pets.push(userProfile.rift.dead_cats.montezuma); + pets.at(-1).active = false; + } + + for (const pet of pets) { + await getItemNetworth(pet, { cache: true, returnItemData: false }); + } + + output.pets = getProfilePets(pets, calculated); + output.missing = getMissingPets(output.pets, profile.game_mode, output); + output.pet_score = getPetScore(output.pets); + Object.assign(output, getMiscPetData(calculated, output.pets)); + + return output; +} + +function getMiscPetData(calculated, pets) { + const output = { + amount_pets: _.uniqBy(pets, "type").length, + total_pets: _.uniqBy( + Object.keys(constants.PET_DATA) + .filter((pet) => + calculated.profile.game_mode === "bingo" ? constants.PET_DATA[pet] : !constants.PET_DATA[pet].bingoExclusive + ) + .map((pet) => constants.PET_DATA[pet].typeGroup) + ).length, + total_pet_skins: Object.keys(constants.PET_SKINS).length, + amount_pet_skins: _.uniqBy(pets, "skin").length, + + total_candy_used: pets.reduce((a, b) => a + b.candyUsed, 0), + total_pet_xp: pets.reduce((a, b) => a + b.exp, 0), + }; + + return output; +} diff --git a/src/stats/rift.js b/src/stats/rift.js new file mode 100644 index 0000000000..edd7a03b5a --- /dev/null +++ b/src/stats/rift.js @@ -0,0 +1,51 @@ +import * as constants from "../constants.js"; + +export function getRift(userProfile) { + if (!("rift" in userProfile) || (userProfile.visited_zones && userProfile.visited_zones.includes("rift") === false)) { + return; + } + + const rift = userProfile.rift; + + const killedEyes = []; + for (const [key, data] of constants.RIFT_EYES.entries()) { + data.unlocked = rift.wither_cage?.killed_eyes && rift.wither_cage.killed_eyes[key] !== undefined; + + killedEyes.push(data); + } + + const timecharms = []; + for (const [key, data] of constants.RIFT_TIMECHARMS.entries()) { + data.unlocked = rift.gallery?.secured_trophies && rift.gallery.secured_trophies[key]?.type !== undefined; + data.unlocked_at = rift.gallery?.secured_trophies && rift.gallery.secured_trophies[key]?.timestamp; + + timecharms.push(data); + } + + return { + motes: { + purse: userProfile.currencies?.motes_purse ?? 0, + lifetime: userProfile.player_stats.rift?.lifetime_motes_earned ?? 0, + orbs: userProfile.player_stats.rift?.motes_orb_pickup ?? 0, + }, + enigma: { + souls: rift.enigma.found_souls?.length ?? 0, + total_souls: constants.RIFT_ENIGMA_SOULS, + }, + wither_cage: { + killed_eyes: killedEyes, + }, + timecharms: { + timecharms: timecharms, + obtained_timecharms: timecharms.filter((a) => a.unlocked).length, + }, + dead_cats: { + montezuma: rift?.dead_cats?.montezuma ?? {}, + found_cats: rift?.dead_cats?.found_cats ?? [], + }, + castle: { + grubber_stacks: rift.castle?.grubber_stacks ?? 0, + max_burgers: constants.MAX_GRUBBER_STACKS, + }, + }; +} diff --git a/src/stats/skills.js b/src/stats/skills.js new file mode 100644 index 0000000000..8359389212 --- /dev/null +++ b/src/stats/skills.js @@ -0,0 +1,82 @@ +import { getLeaderboardPosition } from ".././helper/leaderboards.js"; +import { getXpByLevel, getLevelByXp } from "./skills/leveling.js"; +import * as constants from "../constants.js"; + +async function getLevels(userProfile, profileMembers, hypixelProfile, levelCaps) { + const skillLevels = {}; + if ("experience" in userProfile.player_data) { + const SKILL = userProfile.player_data.experience; + + const socialExperience = Object.keys(profileMembers).reduce((a, b) => { + return a + profileMembers[b].player_data?.experience?.SKILL_SOCIAL || 0; + }, 0); + + Object.assign(skillLevels, { + taming: getLevelByXp(SKILL.SKILL_TAMING, { skill: "taming" }), + farming: getLevelByXp(SKILL.SKILL_FARMING, { skill: "farming", cap: levelCaps.farming }), + mining: getLevelByXp(SKILL.SKILL_MINING, { skill: "mining" }), + combat: getLevelByXp(SKILL.SKILL_COMBAT, { skill: "combat" }), + foraging: getLevelByXp(SKILL.SKILL_FORAGING, { skill: "foraging" }), + fishing: getLevelByXp(SKILL.SKILL_FISHING, { skill: "fishing" }), + enchanting: getLevelByXp(SKILL.SKILL_ENCHANTING, { skill: "enchanting" }), + alchemy: getLevelByXp(SKILL.SKILL_ALCHEMY, { skill: "alchemy" }), + carpentry: getLevelByXp(SKILL.SKILL_CARPENTRY, { skill: "carpentry" }), + runecrafting: getLevelByXp(SKILL.SKILL_RUNECRAFTING, { type: "runecrafting", cap: levelCaps.runecrafting }), + social: getLevelByXp(socialExperience, { type: "social" }), + }); + } else { + const achievementSkills = { + farming: hypixelProfile.achievements.skyblock_harvester || 0, + mining: hypixelProfile.achievements.skyblock_excavator || 0, + combat: hypixelProfile.achievements.skyblock_combat || 0, + foraging: hypixelProfile.achievements.skyblock_gatherer || 0, + fishing: hypixelProfile.achievements.skyblock_angler || 0, + enchanting: hypixelProfile.achievements.skyblock_augmentation || 0, + alchemy: hypixelProfile.achievements.skyblock_concoctor || 0, + taming: hypixelProfile.achievements.skyblock_domesticator || 0, + carpentry: 0, + }; + + for (const skill in achievementSkills) { + skillLevels[skill] = getXpByLevel(achievementSkills[skill], { skill: skill }); + } + } + + const totalSkillXp = Object.keys(skillLevels) + .filter((skill) => constants.COSMETIC_SKILLS.includes(skill) === false) + .reduce((total, skill) => total + skillLevels[skill].xp, 0); + + const nonCosmeticSkills = Math.max(Object.keys(skillLevels).length - constants.COSMETIC_SKILLS.length, 9); + + const averageSkillLevel = + Object.keys(skillLevels) + .filter((skill) => constants.COSMETIC_SKILLS.includes(skill) === false) + .reduce((total, skill) => total + skillLevels[skill].level + skillLevels[skill].progress, 0) / nonCosmeticSkills; + + const averageSkillLevelWithoutProgress = + Object.keys(skillLevels) + .filter((skill) => constants.COSMETIC_SKILLS.includes(skill) === false) + .reduce((total, skill) => total + skillLevels[skill].level, 0) / nonCosmeticSkills; + + for (const skill in skillLevels) { + skillLevels[skill].rank = await getLeaderboardPosition(`skill_${skill}_xp`, skillLevels[skill].xp); + } + + return { + skills: skillLevels, + averageSkillLevel, + averageSkillLevelWithoutProgress, + totalSkillXp, + }; +} + +export async function getSkills(userProfile, hypixelProfile, profileMembers) { + const levelCaps = { + farming: constants.DEFAULT_SKILL_CAPS.farming + (userProfile.jacobs_contest?.perks?.farming_level_cap ?? 0), + carpentry: hypixelProfile.rankText + ? constants.DEFAULT_SKILL_CAPS.runecrafting + : constants.NON_RUNECRAFTING_LEVEL_CAP, + }; + + return await getLevels(userProfile, profileMembers, hypixelProfile, levelCaps); +} diff --git a/src/stats/skills/leveling.js b/src/stats/skills/leveling.js new file mode 100644 index 0000000000..b628b4983f --- /dev/null +++ b/src/stats/skills/leveling.js @@ -0,0 +1,171 @@ +import * as constants from "../../constants.js"; + +/** + * gets the xp table for the given type + * @param {string} type + * @returns {{[key: number]: number}} + */ +function getXpTable(type) { + switch (type) { + case "runecrafting": + return constants.RUNECRAFTING_XP; + case "social": + return constants.SOCIAL_XP; + case "dungeoneering": + return constants.DUNGEONEERING_XP; + case "hotm": + return constants.HOTM_XP; + case "skyblock_level": + return constants.SKYBLOCK_XP; + default: + return constants.LEVELING_XP; + } +} + +/** + * estimates the xp based on the level + * @param {number} uncappedLevel + * @param {{type?: string, cap?: number, skill?: string}} extra + * @param type the type of levels (used to determine which xp table to use) + * @param cap override the cap highest level the player can reach + * @param skill the key of default_skill_caps + */ +export function getXpByLevel(uncappedLevel, extra = {}) { + const xpTable = getXpTable(extra.type); + + if (typeof uncappedLevel !== "number" || isNaN(uncappedLevel)) { + uncappedLevel = 0; + } + + /** the level that this player is caped at */ + const levelCap = + extra.cap ?? + Math.max(uncappedLevel, constants.DEFAULT_SKILL_CAPS[extra.skill]) ?? + Math.max(...Object.keys(xpTable).map((a) => Number(a))); + + /** the maximum level that any player can achieve (used for gold progress bars) */ + const maxLevel = constants.MAXED_SKILL_CAPS[extra.skill] ?? levelCap; + + /** the amount of xp over the amount required for the level (used for calculation progress to next level) */ + const xpCurrent = 0; + + /** the sum of all levels including level */ + let xp = 0; + + for (let x = 1; x <= uncappedLevel; x++) { + xp += xpTable[x]; + } + + /** the level as displayed by in game UI */ + const level = Math.min(levelCap, uncappedLevel); + + /** the amount amount of xp needed to reach the next level (used for calculation progress to next level) */ + const xpForNext = level < maxLevel ? Math.ceil(xpTable[level + 1]) : Infinity; + + /** the fraction of the way toward the next level */ + const progress = level < maxLevel ? 0.05 : 0; + + /** a floating point value representing the current level for example if you are half way to level 5 it would be 4.5 */ + const levelWithProgress = level + progress; + + return { + xp, + level, + maxLevel, + xpCurrent, + xpForNext, + progress, + levelCap, + uncappedLevel, + levelWithProgress, + }; +} + +/** + * gets the level and some other information from an xp amount + * @param {number} xp + * @param {{type?: string, cap?: number, skill?: string, ignoreCap?: boolean, infinite?: boolean }} extra + * @param type the type of levels (used to determine which xp table to use) + * @param cap override the cap highest level the player can reach + * @param skill the id of the skill (used to determine the default cap) + * @param ignoreCap whether to ignore the in-game cap or not + * @param infinite repeats the last level's experience requirement infinitely + * @param skill the key of default_skill_caps + */ +export function getLevelByXp(xp, extra = {}) { + const xpTable = getXpTable(extra.type); + + if (typeof xp !== "number" || isNaN(xp)) { + xp = 0; + } + + /** the level that this player is caped at */ + const levelCap = + extra.cap ?? constants.DEFAULT_SKILL_CAPS[extra.skill] ?? Math.max(...Object.keys(xpTable).map(Number)); + + /** the level ignoring the cap and using only the table */ + let uncappedLevel = 0; + + /** the amount of xp over the amount required for the level (used for calculation progress to next level) */ + let xpCurrent = xp; + + /** like xpCurrent but ignores cap */ + let xpRemaining = xp; + + while (xpTable[uncappedLevel + 1] <= xpRemaining) { + uncappedLevel++; + xpRemaining -= xpTable[uncappedLevel]; + if (uncappedLevel <= levelCap) { + xpCurrent = xpRemaining; + } + } + + /** adds support for infinite leveling (dungeoneering and skyblock level) */ + if (extra.infinite) { + const maxExperience = Object.values(xpTable).at(-1); + + uncappedLevel += Math.floor(xpRemaining / maxExperience); + xpRemaining %= maxExperience; + xpCurrent = xpRemaining; + } + + /** the maximum level that any player can achieve (used for gold progress bars) */ + const maxLevel = + extra.ignoreCap && uncappedLevel >= levelCap ? uncappedLevel : constants.MAXED_SKILL_CAPS[extra.skill] ?? levelCap; + + /** the maximum amount of experience that any player can acheive (used for skyblock level gold progress bar) */ + const maxExperience = constants.MAXED_SKILL_XP[extra.skill]; + + // not sure why this is floored but I'm leaving it in for now + xpCurrent = Math.floor(xpCurrent); + + /** the level as displayed by in game UI */ + const level = extra.ignoreCap ? uncappedLevel : Math.min(levelCap, uncappedLevel); + + /** the amount amount of xp needed to reach the next level (used for calculation progress to next level) */ + const xpForNext = + level < maxLevel ? Math.ceil(xpTable[level + 1] ?? Object.values(xpTable).at(-1)) : maxExperience ?? Infinity; + + /** the fraction of the way toward the next level */ + const progress = level >= maxLevel ? (extra.ignoreCap ? 1 : 0) : Math.max(0, Math.min(xpCurrent / xpForNext, 1)); + + /** a floating point value representing the current level for example if you are half way to level 5 it would be 4.5 */ + const levelWithProgress = level + progress; + + /** a floating point value representing the current level ignoring the in-game unlockable caps for example if you are half way to level 5 it would be 4.5 */ + const unlockableLevelWithProgress = extra.cap ? Math.min(uncappedLevel + progress, maxLevel) : levelWithProgress; + + return { + xp, + level, + maxLevel, + xpCurrent, + maxExperience, + xpForNext, + progress, + levelCap, + uncappedLevel, + levelWithProgress, + unlockableLevelWithProgress, + }; +} diff --git a/src/stats/skyblock-level.js b/src/stats/skyblock-level.js new file mode 100644 index 0000000000..9eff784471 --- /dev/null +++ b/src/stats/skyblock-level.js @@ -0,0 +1,17 @@ +import { getLeaderboardPosition } from "../helper/leaderboards.js"; +import { getLevelByXp } from "./skills/leveling.js"; + +export async function getSkyBlockLevel(userProfile) { + const skyblockExperience = userProfile.leveling?.experience ?? 0; + + const output = getLevelByXp(skyblockExperience, { + skill: "skyblock_level", + type: "skyblock_level", + infinite: true, + ignoreCap: true, + }); + + output.rank = await getLeaderboardPosition("skyblock_level_xp", skyblockExperience); + + return output; +} diff --git a/src/stats/slayer.js b/src/stats/slayer.js new file mode 100644 index 0000000000..0bb320fa02 --- /dev/null +++ b/src/stats/slayer.js @@ -0,0 +1,93 @@ +import * as constants from "../constants.js"; + +function getSlayerLevel(slayer, slayerName) { + // eslint-disable-next-line + const { xp = 0, claimed_levels } = slayer; + + if (constants.SLAYER_XP[slayerName] === undefined) { + return { + currentLevel: 0, + xp: 0, + maxLevel: 0, + progress: 0, + xpForNext: 0, + unclaimed: false, + }; + } + + let currentLevel = 0; + let progress = 0; + let xpForNext = 0; + let unclaimed = false; + + const maxLevel = Object.keys(constants.SLAYER_XP[slayerName]).length; + for (const levelName in claimed_levels) { + // Ignoring legacy levels for zombie + if (slayerName === "zombie" && ["level_7", "level_8", "level_9"].includes(levelName)) { + continue; + } + + const level = parseInt(levelName.split("_")[1]); + + if (level > currentLevel) { + currentLevel = level; + } + } + + if (currentLevel < maxLevel) { + const nextLevel = constants.SLAYER_XP[slayerName][currentLevel + 1]; + + progress = xp / nextLevel; + xpForNext = nextLevel; + } else { + progress = 1; + } + + if (progress >= 1 && currentLevel < maxLevel) { + unclaimed = true; + } + + return { currentLevel, xp, maxLevel, progress, xpForNext, unclaimed }; +} + +export function getSlayer(userProfile) { + if (userProfile.slayer === undefined || "slayer_bosses" in userProfile.slayer === false) { + return; + } + + const output = { slayers: {} }; + for (const slayerName in userProfile.slayer.slayer_bosses) { + const slayer = userProfile.slayer.slayer_bosses[slayerName]; + if ("claimed_levels" in slayer === false) { + continue; + } + + output.slayers[slayerName] = { + level: getSlayerLevel(slayer, slayerName), + coins_spent: 0, + kills: {}, + }; + + for (const property in slayer) { + if (property.startsWith("boss_kills_tier_")) { + const tier = parseInt(property.split("_").at(-1)) + 1; + + output.slayers[slayerName].kills[tier] = slayer[property]; + + output.slayers[slayerName].coins_spent += slayer[property] * constants.SLAYER_COST[tier]; + } + } + + output.slayers[slayerName].kills.total ??= Object.values(output.slayers[slayerName].kills).reduce( + (a, b) => a + b, + 0 + ); + + Object.assign(output.slayers[slayerName], constants.SLAYER_INFO[slayerName]); + } + + output.total_slayer_xp = Object.values(output.slayers).reduce((a, b) => a + b.level.xp, 0); + output.total_coins_spent = Object.values(output.slayers).reduce((a, b) => a + b.coins_spent, 0); + + return output; +} diff --git a/src/stats/temp-stats.js b/src/stats/temp-stats.js new file mode 100644 index 0000000000..c065f26905 --- /dev/null +++ b/src/stats/temp-stats.js @@ -0,0 +1,28 @@ +import * as constants from "../constants.js"; + +export function getTempStats(userProfile) { + const output = {}; + if (userProfile.player_data === undefined) { + return; + } + + output.century_cakes = []; + if (userProfile.player_data.temp_stat_buffs) { + for (const cake of userProfile.player_data.temp_stat_buffs) { + if (cake.key.startsWith("cake_") === false) { + continue; + } + + const id = cake.key.replace("cake_", ""); + + const stat = constants.CENTURY_CAKE_STATS[id] || id; + + output.century_cakes.push({ + stat: stat, + amount: cake.amount, + }); + } + } + + return output; +} diff --git a/src/stats/weight.js b/src/stats/weight.js new file mode 100644 index 0000000000..2128cf5e78 --- /dev/null +++ b/src/stats/weight.js @@ -0,0 +1,11 @@ +import { calculateSenitherWeight } from "../constants/weight/senither-weight.js"; +import { calculateFarmingWeight } from "../constants/weight/farming-weight.js"; +import { calculateLilyWeight } from "../constants/weight/lily-weight.js"; + +export function getWeight(userProfile) { + return { + senither: calculateSenitherWeight(userProfile), + lily: calculateLilyWeight(userProfile), + farming: calculateFarmingWeight(userProfile), + }; +} diff --git a/src/weight/lily-weight.js b/src/weight/lily-weight.js deleted file mode 100644 index e471bbb75d..0000000000 --- a/src/weight/lily-weight.js +++ /dev/null @@ -1,26 +0,0 @@ -import LilyWeight from "lilyweight"; - -const SKILL_ORDER = ["enchanting", "taming", "alchemy", "mining", "farming", "foraging", "combat", "fishing"]; -const SLAYER_ORDER = ["zombie", "spider", "wolf", "enderman", "blaze"]; - -/** - * converts a dungeon floor into a completion map - * @param {{[key:string]:{stats:{tier_completions:number}}}} floors - * @returns {{[key:string]:number}} - */ -function getTierCompletions(floors = {}) { - return Object.fromEntries(Object.entries(floors).map(([key, value]) => [key, value.stats.tier_completions ?? 0])); -} - -export function calculateLilyWeight(profile) { - const skillLevels = SKILL_ORDER.map((key) => profile.levels[key].uncappedLevel); - const skillXP = SKILL_ORDER.map((key) => profile.levels[key].xp); - - const cataCompletions = getTierCompletions(profile.dungeons?.catacombs?.floors ?? {}); - const masterCataCompletions = getTierCompletions(profile.dungeons?.master_catacombs?.floors ?? {}); - const cataXP = profile.dungeons?.catacombs?.level?.xp ?? 0; - - const slayerXP = SLAYER_ORDER.map((key) => profile.slayers?.[key]?.level?.xp ?? 0); - - return LilyWeight.getWeightRaw(skillLevels, skillXP, cataCompletions, masterCataCompletions, cataXP, slayerXP); -} diff --git a/views/index.ejs b/views/index.ejs index 38a2709ac3..2529a9415a 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -21,11 +21,11 @@

Show SkyBlock stats for

- value="<%= player %>" <% }%> type="search" enterkeyhint="go" placeholder="Enter username" aria-label="username" autofocus required> + value="<%= player %>" <% }%> type="search" enterkeyhint="go" placeholder="Enter username" aria-label="username" autofocus required> <%#

or take me to a random profile.

%>
- <% if(extra.cacheOnly) { %> + <% if (extra.cacheOnly) { %>
- -
- <% - const notAvailable = []; - - if(items.no_inventory) - notAvailable.push('Weapons', 'Accessories', 'Inventory', 'Storage'); - if(items.no_personal_vault) - notAvailable.push('Personal Vault'); - - if(Object.keys(calculated.collections).length == 0) - notAvailable.push('Collections'); - %> - <% if (notAvailable.length > 0 || ['ironman', 'bingo', 'island'].includes(calculated.profile.game_mode)) { %> -
-
-
Notice
- <% if(notAvailable.length > 0){ %> - <%= notAvailable.join(', ') %> not available for <%= calculated.display_name %> due to limited API access.
See here how to enable full API access. - <% } %> - <% if(calculated.profile.game_mode == 'ironman'){ %> - <% if(notAvailable.length > 0){ %>

<% } %> - This is an Ironman profile. The player cannot use the auction house, bazaar, trade, or pick up drops from other players. - <% } %> - <% if(calculated.profile.game_mode == 'bingo'){ %> - <% if(notAvailable.length > 0){ %>

<% } %> - This is a Bingo profile. The player cannot spend gems, use the auction house, bazaar, trade, or pick up drops from other players. - <% } %> - <% if(calculated.profile.game_mode == 'island'){ %> - <% if(notAvailable.length > 0){ %>

<% } %> - This is a Stranded profile. The player cannot leave their skyblock island or trade with other players. + + Plancke + +
+ + + + + <% if ('DISCORD' in calculated.social) { %> + <% } %> -
-
- <% } %> -
- -

Armor

-
- <% if(items.armor.length == 0){ %> -
<%= calculated.display_name %> doesn't have any armor equipped.
- <% }else{ %> - <% if(items.armor_set) { %> -

- Set: <%= items.armor_set %> -

+ + <% if ('TWITTER' in calculated.social) { %> + <% } %> -
- <% for(const item of items.armor.slice().reverse()){ %> -
- <% if(rarityOrder.indexOf(item.rarity) <= 4){ %> -
- <% } %> - <% itemIcon(item, ['piece-icon']); %> -
- <% } %> -
-
- <% } %> -

Equipment

- <% if(Object.keys(items.equipment).length == 0){ %> -
<%= calculated.display_name %> doesn't have any equipment equipped.
- <% }else{ %> -
- <% for(const item of items.equipment.slice().reverse()){ %> -
- <% if(rarityOrder.indexOf(item.rarity) <= 4){ %> -
- <% } %> - <% itemIcon(item, ['piece-icon']); %> -
- <% } %> -
-
- <% } %> -
-
- <% if(items.wardrobe.length > 0){ %> -

Wardrobe

-
- <% for(const set of items.wardrobe){ %> -
- <% for(const [index, item] of set.entries()) { %> - <% if (item) { %> -
- <% if(rarityOrder.indexOf(item.rarity) <= 4){ %> -
- <% } %> - <% itemIcon(item, ['piece-icon']); %> -
- <% } else { %> -
-
-
- <% } %> - <% } %> -
- <% } %> - <% } %> -
- <% if(!items.no_inventory){ %> -
- -

Weapons

-
- <% if (items.weapons.length == 0) { %> -
<%= calculated.display_name %> doesn't have any weapons.
- <% } else { %> - <% if (items.highest_rarity_sword) { %> -

- Active Weapon: - <%- helper.renderLore(items.highest_rarity_sword.tag.display.Name) %> -

- <% } else if (items.weapons.length > 0) { %> -

- Active Weapon: None -

- <% } %> -
- <% - items.weapons.filter(a => !a.hidden).forEach(item => { - %> -
- <% if (rarityOrder.indexOf(item.rarity) <= 4) { %> -
- <% } %> - <% itemIcon(item, ['piece-icon']); %> -
- <% - }); - %> -
- <% } %> -
-
-
- -

Accessories

-
- <% if(items.no_inventory){ %> -
<%= calculated.display_name %> doesn't have inventory access via API enabled. See here how to enable full API access.
- <% }else if(items.accessories.length == 0){ %> -
<%= calculated.display_name %> doesn't have any accessories.
- <% }else{ %> -

- <% - const talis = items.accessories.filter(a => a.isUnique).length + (items.accessory_rarities.rift_prism?.consumed ? 1 : 0) - const maxTalis = talis >= constants.UNIQUE_ACCESSORIES_COUNT ? 'golden-text': '' - const maxRecombTalis = items.accessories.filter(a => a.isUnique && a.extra?.recombobulated).length >= constants.RECOMBABLE_ACCESSORIES_COUNT ? 'golden-text': '' - %> - - Unique Accessories: - <%= talis %> / <%= constants.UNIQUE_ACCESSORIES_COUNT %> -
- Completion: - <%= Math.round(talis / constants.UNIQUE_ACCESSORIES_COUNT * 100) %> -
- 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] * 2 : 0 - const mp_abiphone = rarities.abicase ? Math.floor(calculated.abiphone.active / 2) : 0 - const mp_rift_prism = rarities.rift_prism?.consumed ? 11 : 0; - const mp_total = Object.values(player_magical_power).reduce((a, b) => a + b) + mp_hegemony + mp_abiphone + mp_rift_prism - 22; - %> - );' class='grey-text'>Abicase = +<%= mp_abiphone %> MP
- <% } %> - <% if (mp_rift_prism) { %> - Rift Prism: = +<%= mp_rift_prism %> MP
- <% } %> -
- Total: <%= mp_total.toLocaleString() %> Magical Power - "> - Magical Power: <%= mp_total.toLocaleString() %> - -

- <% if(items.accessories.find(a => !a.isInactive) != undefined){ %> -
-

Active Accessories

- <% items.accessories.filter(a => !a.isInactive).forEach(item => { - %> -
- <% if(rarityOrder.indexOf(item.rarity) <= 4){ %> -
- <% } %> - <% itemIcon(item, ['piece-icon']); %> -
- <% }) %> -
-
<%- getEnrichments(items.accessories.filter(a => a.isUnique && !a.isInactive)) %>
-
-
-
- <% } %> - <% const inactiveAccessories = items.accessories.filter(a => a.isInactive === true); %> - <% if (inactiveAccessories.length > 0) { %> -
-

Inactive Accessories

- <% for (const item of inactiveAccessories) { %> -
- <% if(rarityOrder.indexOf(item.rarity) <= 4){ %> -
- <% } %> - <% itemIcon(item, ['piece-icon']); %> -
- <% } %> -
+ + <% if ('YOUTUBE' in calculated.social) { %> + <% } %> - <% if(calculated.missingAccessories.missing.length > 0 || calculated.missingAccessories.upgrades.length > 0) { - - if(items.accessories.length == 1) %>
- - -
-
- - <% if(calculated.missingAccessories.missing.length > 0) { %> -

Missing Accessories

- <% for(const [index, accessory] of calculated.missingAccessories.missing.entries()){ %> -
-
-
- <% } %> - <% } %> - - <% if (calculated.missingAccessories.upgrades.length > 0) { %> -

Missing Accessory Upgrades

- <% for(const [index, accessory] of calculated.missingAccessories.upgrades.entries()){ %> -
-
-
- <% } %> - <% } %> -
- <% } %> + + <% if ('INSTAGRAM' in calculated.social) { %> + <% } %> -
-
- <% } %> - <% if(calculated.pets.length > 0){ %> -
- -

Pets

-
- <% - const uniquePets = _.uniq( - calculated.pets - .filter(pet => !constants.PET_DATA[pet.type]?.bingoExclusive === true || calculated.profile.game_mode === 'bingo') - .map((pet) => constants.PET_DATA[pet.type]?.typeGroup ?? pet.type) - ) - const totalPets = _.uniq( - Object.entries( - Object.fromEntries( - Object.entries(constants.PET_DATA) - .filter(pet => !pet[1]?.bingoExclusive === true || calculated.profile.game_mode === 'bingo') - ) - ) - .map(arr => arr[1].typeGroup || arr[0]) - ).length - - let totalPetXp = 0 - for (const pet of calculated.pets) { - totalPetXp += pet.exp - } - - let totalSkins = {} - let badSkins = {} - for (const [skin, skinData] of Object.entries(constants.PET_SKINS)) { - if (skinData.release < Date.now()) { - totalSkins[skin] = calculated.pets.find((pet) => `PET_SKIN_${pet.skin}` === skin) != undefined; - } - } - let userUniqueSkins = Object.values(totalSkins).filter(skin => skin).length - let totalUniqueSkins = Object.keys(totalSkins).length - - let totalCandiesUsed = calculated.pets.reduce((total, pet) => total + pet.candyUsed, 0) - %> -

- <% max = uniquePets.length >= totalPets ? 'golden-text': '' %> - Unique Pets: <%= uniquePets.length %> / <%= totalPets %>
- - <% max = userUniqueSkins >= totalUniqueSkins ? 'golden-text': '' %> - Unique Pet Skins: <%= userUniqueSkins %> / <%= totalUniqueSkins %>
- <% - const petRewards = constants.PET_REWARDS - const petScore = calculated.petScore - - max = petScore >= Math.max(...Object.keys(petRewards)) ? 'golden-text' : '' - %> - Pet Score: <%= petScore %><% if(calculated.pet_score_bonus.magic_find > 0){ %> (+<%= calculated.pet_score_bonus.magic_find %> MF)<% } %>
- - <% max = totalCandiesUsed === 0 ? 'golden-text': '' %> - Total Candies Used: <%= totalCandiesUsed %>
- - Total Pet XP: <%= helper.formatNumber(totalPetXp, true) %> -

- <% - const petsToShow = 100 - const petsToShowLimit = 1000 - const activePet = calculated.pets.find(pet => pet.active) - const otherPets = calculated.pets.filter(pet => !pet.active).slice(0, petsToShowLimit) - %> - <% if (activePet) { %> -

Active Pet

-
-
- <% if (rarityOrder.indexOf(activePet.rarity) <= 4) { %> -
- <% } %> -
-
-
-
<%= activePet.rarity %> <%- activePet.display_name %>
-
Level <%= activePet.level.level %>
-
-
-
+ + <% if ('TWITCH' in calculated.social) { %> + <% } %> - <% if (otherPets) { %> -

<%= activePet ? 'Other Pets' : '' %>

-
- <% for(const [index, pet] of otherPets.entries()) { %> - - <% if ( - (activePet && index === petsToShow - 1) || - (!activePet && index === petsToShow) - ) { %> -
- -
- <% } %> -
- <% if(rarityOrder.indexOf(pet.rarity) <= 4) { %> -
- <% } %> -
-
Lvl <%= pet.level.level %>
-
- <% } %> -
- <% } %> - <% if(calculated.missingPets.length > 0) { %> - <% if(calculated.pets.length == 1) { %> -
- <% } %> - -
- <% for(const [index, pet] of calculated.missingPets.entries()) { %> -
-
-
- <% } %> -
+ + <% if ('HYPIXEL' in calculated.social) { %> + <% } %> + +
- <% } %> - <% if(!items.no_inventory){ %> -
- -

Inventory

-
- <% if(items.no_inventory){ %> -
<%= calculated.display_name %> doesn't have inventory access via API enabled. See here how to enable full API access.
- <% }else{ %> -
-
-
-
- <% - const inventoryIconUrl = `https://crafatar.com/renders/head/${extra.isFoolsDay ? 'bd482739767c45dca1f8c33c40530952' : calculated.uuid }?size=32&overlay` - %> - - - <% if(items.storage.length > 0){ %> - - <% } %> +
- <% if(items.enderchest.length > 0){ %> - - <% } %> + + <% if (calculated.uuid == "b876ec32e396476ba1158438d83c67d4") { %> +
+ Thank you for everything, Technoblade. +
+

+ If you can, donate to the Sarcoma Foundation of America and buy his merch. +

+
+ <% } %> - <% if(items.personal_vault.length > 0){ %> - - <% } %> + + <%- include('./sections/stats/basic_stats.ejs', { skillItems }); %> - <% if(items.accessory_bag.length > 0){ %> - - <% } %> + + - <% if(items.potion_bag.length > 0){ %> - - <% } %> + +
+ <% const notAvailable = []; + if (items.disabled?.inventory === true) { + notAvailable.push('Weapons', 'Accessories', 'Inventory', 'Storage'); + } - <% if(items.fishing_bag.length > 0){ %> - - <% } %> + if (items.disabled?.personal_vault === true) { + notAvailable.push('Personal Vault'); + } - <% if(items.quiver.length > 0){ %> - - <% } %> -
-
- -
- <% } %> -
-
- <% } %> -
- -

Skills

-
- -
-
-
-
-
- mining -
- - <% const mining = calculated.mining; %> - - <% if(items.mining_tools.length > 0){ %> -

Mining Tools

- <% if(items.highest_rarity_mining_tool){ %> -

- Active Tool: - <%- helper.renderLore(items.highest_rarity_mining_tool.tag.display.Name) %> -

- <% }else{ %> -

- Active Tool: None -

- <% } %> -
- <% items.mining_tools.filter(a => !a.hidden).forEach(item => { %> -
- <% if(rarityOrder.indexOf(item.rarity) <= 4){ %> -
- <% } %> - <% itemIcon(item, ['piece-icon']); %> -
- <% }); %> -
- <% } %> + if (calculated?.collections === undefined) { + notAvailable.push('Collections'); + } - <% if (mining.core === undefined || calculated.visited_modes.includes("crystal_hollows") === false || calculated.visited_modes.includes("mining_3") === false) { %> -

<%= calculated.display_name %> hasn't visited Dwarven Mines or Crystal Hollows yet.

- <% } else { %> -

Dwarven Mines & Crystal Hollows

-

- - <% max = mining.commissions.milestone == constants.COMMISSIONS_MILESTONE ? 'golden-text' : '' %> - Commissions Milestone: - <%= mining.commissions.milestone.toLocaleString() %> -
- - Commissions: - <%= mining.commissions.completions.toLocaleString() %> -
- - <% const chpass = mining.core.crystal_hollows_last_access > Date.now() - 5*60*60*1000 %> - <% max = chpass ? 'golden-text' : '' %> - Crystal Hollows Pass: - - - "><%= chpass ? 'Purchased' : 'Expired' %> -
- - <% - const delivered_parts = mining.core.crystal_nucleus?.precursor?.parts_delivered || [] - const precursor_parts = {} - - for (const [partId, partName] of Object.entries(constants.PRECURSOR_PARTS)) { - precursor_parts[partName] = delivered_parts.indexOf(partId) > -1 - } - %> - Crystal Nucleus: - Completed <%= mining.core.crystal_nucleus.times_completed.toLocaleString() %> times -
-

- -

Heart of the Mountain

-

- - <% max = mining.core.tier.level === constants.HOTM.tiers ? 'golden-text' : '' %> - Tier: - <%= mining.core.tier.level %> -
- - <% mining.core.tokens.spent > mining.core.tokens.total ? mining.core.tokens.total = mining.core.tokens.spent : null %> - <% max = mining.core.tokens.spent === mining.core.tokens.total ? 'golden-text' : '' %> - Token of the Mountain: - <%= mining.core.tokens.spent %>/<%= mining.core.tokens.total %> -
- - <% max = (mining.core.nodes["special_0"] || 0) === constants.MAX_PEAK_OF_THE_MOUNTAIN_LEVEL ? 'golden-text' : '' %> - Peak of the Mountain: - <%= mining.core.nodes["special_0"] || 0 %>/<%= constants.MAX_PEAK_OF_THE_MOUNTAIN_LEVEL %> -
- - <% max = mining.core.powder.mithril.total >= constants.HOTM.powder_for_max_nodes.mithril_powder ? 'golden-text' : '' %> - Mithril Powder: - <%= mining.core.powder.mithril.total.toLocaleString() %> + if (notAvailable.length > 0 || calculated.profile.game_mode !== "normal") { %> +

+
+
Notice
+ <% if (notAvailable.length > 0) { %> + <%= notAvailable.join(', ') %> not available for <%= calculated.display_name %> due to limited API access.
See here how to enable full API access.
- - <% max = mining.core.powder.gemstone.total >= constants.HOTM.powder_for_max_nodes.gemstone_powder ? 'golden-text' : '' %> - Gemstone Powder: - <%= mining.core.powder.gemstone.total.toLocaleString() %> -
- - Pickaxe Ability: - <%= mining.core.selected_pickaxe_ability || 'None' %> -

- -
- -
- -

Forge

- <% - if (mining.forge?.processes?.length > 0) { - mining.forge.processes.forEach(process => { - %> -
-

Slot <%= process.slot %>:

- <% if (!process.id.startsWith("UNKNOWN-")) { %> - <%= process.name %> - <%= process.timeFinished < Date.now() ? "ended" : `ending ${process.timeFinishedText}`%> - <% } else { %> - Unknown item - - <% } %> -
- <% - }); - } else { - %> -

No items currently forging!

- <% } %> -
<% } %> -
-
-
-
-
- farming -
- -

- - Farming Weight: - <%= calculated.weight.farming.weight.toFixed(2).toLocaleString() %>
- Pelts: <%= calculated.farming.pelts.toLocaleString() %>
- <% const farming = calculated.farming; - if (farming.talked && farming.contests.attended_contests > 0) {%> - Contests attended: <%= farming.contests.attended_contests.toLocaleString() %>
- <% max = farming.unique_golds == 10 ? 'golden-text' : '' %>Unique Golds: <%= farming.unique_golds.toLocaleString() %>
- <% } %> -

- - <% if (farming.talked && farming.contests.attended_contests > 0) {%> -

- <% for(let badge of badgeOrder){ %> - - <%= badge %> medals: - <%= farming.total_badges[badge].toLocaleString() %>
+ <% if (calculated.profile.game_mode == 'ironman') { %> + <% if (notAvailable.length > 0) { %> +
<% } %> -

- <% }else{ %> -

- <%= calculated.display_name %> hasn't attended any contests yet. -

- <% } %> - <% if(items.farming_tools.length > 0){ %> -

Farming Tools

- <% if(items.highest_rarity_farming_tool){ %> -

- Active Tool: - <%- helper.renderLore(items.highest_rarity_farming_tool.tag.display.Name) %> -

- <% }else{ %> -

- Active Tool: None -

+ This is an Ironman profile. The player cannot use the auction house, bazaar, trade, or pick up drops from other players. <% } %> -
- <% items.farming_tools.filter(a => !a.hidden).forEach(item => { %> -
- <% if(rarityOrder.indexOf(item.rarity) <= 4){ %> -
- <% } %> - <% itemIcon(item, ['piece-icon']); %> -
- <% }); %> -
- <% } %> - - <% if(farming.talked) { %> - <% if(Object.keys(calculated.farming.crops).length > 0){ %> - -
- <% - const crops = Object.values(calculated.farming.crops).sort((a, b) => { - return b.contests - a.contests; - }); - - for(const crop of crops){ - if(!crop.attended) continue; - - let amountsTooltip = ''; - - for(let badge of badgeOrder) - amountsTooltip += ` - ${helper.titleCase(badge)} Medals: - ${crop.badges[badge].toLocaleString()}
- `; - %> -
-
-
-
-
-
<%= crop.name %>
-
- Personal Best: <%= helper.formatNumber(crop.personal_best, true) %>
- Contests: <%= crop.contests.toLocaleString() %>
-
-
-
- <% } %> -
- <% } %> - <% } %> -
-
-
-
-
-
- fishing -
- - <% - let totalSeaCreatureKills = 0; - - for(const creature of calculated.kills){ - if(seaCreatures.includes(creature.entityId)) { - totalSeaCreatureKills += creature.amount; - } - } - %> - -

- Items fished: <%= calculated.fishing.total.toLocaleString() %>
- Treasures fished: <%= calculated.fishing.treasure.toLocaleString() %>
- Large treasures fished: <%= calculated.fishing.treasure_large.toLocaleString() %>
- Sea Creatures killed: <%= totalSeaCreatureKills.toLocaleString() %>
- <% - if(calculated.fishing.shredder_fished > 0 && calculated.fishing.shredder_bait > 0){ - - %> - Fished with Shredder: <%= calculated.fishing.shredder_fished.toLocaleString() %>
- <% } %> -

- - <% if(items.fishing_tools.length > 0){ %> -

Fishing Rods

- <% if(items.highest_rarity_fishing_tool){ %> -

- Active Rod: - <%- helper.renderLore(items.highest_rarity_fishing_tool.tag.display.Name) %> -

- <% }else if(items.fishing_tools.length > 0){ %> -

- Active Rod: None -

- <% } %> -
- <% - items.fishing_tools.filter(a => !a.hidden).forEach(item => { - %> -
- <% if(rarityOrder.indexOf(item.rarity) <= 4){ %> -
- <% } %> - <% itemIcon(item, ['piece-icon']); %> -
- <% - }); - %> -
- <% } %> - - <% if(totalSeaCreatureKills > 0){ %> - -
- <% for(const creatureId of seaCreatures) { - const creature = calculated.kills.find(creature => creature.entityId == creatureId); - - if(creature?.amount > 0 ) { - %> -
-
<%= creature.entityName %>
-
-
<%= creature.amount.toLocaleString() %> Kill<%= creature.amount != 1 ? 's' : '' %>
-
- <% - } - } %> -
- <% } %> - -
-

- Total Caught: - <%= calculated.trophy_fish.total_caught.toLocaleString() %> -
- <% max = calculated.trophy_fish.stage.type === "diamond" ? 'golden-text' : '' %> - Current Stage: - <%= calculated.trophy_fish.stage.name %> - <% if (calculated.trophy_fish.stage.progress !== null) { %> - (<%= calculated.trophy_fish.stage.progress %>) - <% } %> -
-

- - -
- -
- <% for (const fish of calculated.trophy_fish.fish) { %> -
-
-
-
- -
-
-
- <%= fish.name %> - x<%= fish.caught.total.toLocaleString() %> -
- -
- <% for (const fishTier of trophyFishOrder) { - let count = fish.caught[fishTier] ?? 0; %> -
- <% if (count > 0) { %> - - <%= fish.caught[fishTier].toLocaleString() %> - - <% } else { %> - - - - - - <% } %> - <% } %> -
-
-
-
- <% } %> -
-
-
- <% if(calculated.enchanting.experimented){ %> -
-
-
-
-
- enchanting -
- - <% const enchanting = calculated.enchanting; %> - -
- <% for(let game in enchanting.experiments){ - const game_data = enchanting.experiments[game]; %> -
-
- <%= game_data.name %> -
- -

- <% - const game_stats = helper.sortObject(game_data.stats); - for(let stat in game_stats){ - %> - <%= helper.titleCase(stat.replace('_', ' ')) %>: - <%= (stat == 'last_attempt' || stat == 'last_claimed') ? game_stats[stat].text : game_stats[stat] %>
- <% } %> -

- <% - for(let tier in game_data.tiers) { - const tier_data = game_data.tiers[tier]; - %> -
-
-
-
-
-
-
<%= tier_data.name %>
-
- <% - for(let info in tier_data) { - if(info == 'name' || info == 'icon') continue; - %> - <%= helper.titleCase(info.replace('_', ' ')) %>: <%= tier_data[info] %>
- <% } %> -
-
-
- <% } %> -
-
+ <% if (calculated.profile.game_mode == 'bingo') { %> + <% if (notAvailable.length > 0) { %> +
<% } %> -
-
- <% } %> -
-
-
- -

Dungeons

-
- <% if ( - Object.keys(calculated.dungeons).length === 0 || - !calculated.dungeons.catacombs?.visited || - Object.keys(calculated.dungeons.catacombs.floors).length === 0 - ) { %> -

- <%= calculated.display_name %> hasn't entered any dungeon yet. -

- <% } else { %> - - <% if (calculated.dungeons.used_classes) { %> -
- - - - - - - -
- <% } %> - -

- Selected Class: - <%= helper.titleCase(calculated.dungeons.selected_class) %> -
- <% max = calculated.dungeons.class_average.max ? "golden-text" : "" %> - - Class Average: <%= calculated.dungeons.class_average.avrg_level.toFixed(2) %> -
- <% max = calculated.dungeons.catacombs.bonuses.item_boost === 465 ? "golden-text" : "" %> - Dungeon Item Boost: - <%= calculated.dungeons.catacombs.bonuses.item_boost.toLocaleString() %> -
- <% max = calculated.dungeons.catacombs.highest_floor === "floor_7" ? "golden-text" : "" %> - Highest Floor Beaten (normal): - <%= helper.titleCase(calculated.dungeons.catacombs.highest_floor.replace("_", " ")) %> -
- <% if (calculated.dungeons.master_catacombs?.visited && Object.keys(calculated.dungeons.master_catacombs.floors).length > 0) { %> - <% max = calculated.dungeons.master_catacombs.highest_floor === "floor_7" ? "golden-text" : "" %> - Highest Floor Beaten (master): - <%= helper.titleCase(calculated.dungeons.master_catacombs.highest_floor.replace("_", " ")) %> -
- <% } %> - <% max = calculated.dungeons.journals.pages_collected === calculated.dungeons.journals.total_pages ? "golden-text" : "" %> - Journals Completed: - <%= calculated.dungeons.journals.journals_completed %> - (<%= calculated.dungeons.journals.pages_collected %>/<%= calculated.dungeons.journals.total_pages %>) -
- <% const completions = calculated.dungeons.catacombs.completions + calculated.dungeons.master_catacombs.completions; %> - <% const secretsFound = calculated.dungeons.secrets_found; %> - <% const secretsPerCompletion = completions > 0 ? (secretsFound / completions).toFixed(2) : 0 %> - - Secrets Found: - <%= secretsFound.toLocaleString() %> - (<%= secretsPerCompletion %> S/R) -
- <% const bloodMobKills = calculated.bestiary ? calculated.bestiary.categories["catacombs"].mobs.find((mob) => mob.name === "Undead").kills : 0%> - Blood Mob Kills: - <%= bloodMobKills.toLocaleString() %> -

- -

Catacombs

-
- <% for (let [id, floor] of Object.entries(calculated.dungeons.catacombs.floors)) { %> -
-
-
- <%= floor.name.replace("_", " ") %> -
- - -
- <% for (let [stat, value] of Object.entries(floor.stats)) { %> - <%= helper.capitalizeFirstLetter(stat.split("_").join(" ")) %>: - - <% if(stat.startsWith("fastest_time")) { %> - <%= moment.duration(value, "milliseconds").format("m:ss.SSS") %> - <% } else { %> - <%= helper.formatNumber(value) %> - <% } %> -
- <% } %> - <% if(floor.most_damage) { %> - Most Damage: - <%= helper.formatNumber(floor.most_damage.value) %> - (<%= helper.titleCase(floor.most_damage.class) %>) - <% } %> -
- <% if(floor.best_runs) { %> - -
- Grade: - <%= helper.calcDungeonGrade(floor.best_runs[floor.best_runs.length - 1]) %> -
- <% for (let [stat, value] of Object.entries(floor.best_runs[floor.best_runs.length - 1])) { - if(stat == "teammates") continue; %> - <%= helper.capitalizeFirstLetter(stat.split("_").join(" ")) %>: - ><%= - (() => { - switch (stat) { - case "timestamp": - return moment(value).fromNow(); - case "elapsed_time": - return moment.duration(value, "milliseconds").format("m:ss.SSS"); - case "dungeon_class": - return helper.titleCase(value); - default: - return helper.formatNumber(value); - } - })() - %>
- <% } %> -
- <% } %> -
-
- <% } %> -
- - <% if (calculated.dungeons.master_catacombs?.visited) { %> -

Master Catacombs

- - <% if (Object.keys(calculated.dungeons.master_catacombs.floors).length === 0) { %> -

<%= calculated.display_name %> hasn't completed any Master Catacombs floor yet.

- <% } else { %> -
- <% for (let [id, floor] of Object.entries(calculated.dungeons.master_catacombs.floors)) { %> -
-
-
- <%= floor.name.replace("_", " ") %> -
- - -
- <% for (let [stat, value] of Object.entries(floor.stats)) { %> - <%= helper.capitalizeFirstLetter(stat.split("_").join(" ")) %>: - - <% if(stat.startsWith("fastest_time")) { %> - <%= moment.duration(value, "milliseconds").format("m:ss.SSS") %> - <% } else { %> - <%= helper.formatNumber(value) %> - <% } %> -
- <% } %> - <% if(floor.most_damage) { %> - Most Damage: - <%= helper.formatNumber(floor.most_damage.value) %> - (<%= helper.titleCase(floor.most_damage.class) %>) - <% } %> -
- <% if(floor.best_runs) { %> - -
- Grade: - <%= helper.calcDungeonGrade(floor.best_runs[floor.best_runs.length - 1]) %> -
- <% for (let [stat, value] of Object.entries(floor.best_runs[floor.best_runs.length - 1])) { - if(stat == "teammates") continue; %> - <%= helper.capitalizeFirstLetter(stat.split("_").join(" ")) %>: - ><%= - (() => { - switch (stat) { - case "timestamp": - return moment(value).fromNow(); - case "elapsed_time": - return moment.duration(value, "milliseconds").format("m:ss.SSS"); - case "dungeon_class": - return helper.titleCase(value); - default: - return helper.formatNumber(value); - } - })() - %>
- <% } %> -
- <% } %> -
-
+ + This is a Bingo profile. The player cannot spend gems, use the auction house, bazaar, trade, or pick up drops from other players. <% } %> -
- <% } %> - <% } %> - <% if (calculated.dungeons.unlocked_collections) { %> -

Boss Collections

-
- <% - for (let boss in calculated.dungeons.boss_collections) { - let collection = calculated.dungeons.boss_collections[boss]; - let claimedTooltip = ""; - - if (collection.claimed.length > 0) { - claimedTooltip += 'Claimed items:'; - for (let item in collection.claimed) { - claimedTooltip += `
- ${collection.claimed[item]}`; - } - claimedTooltip += '
'; - } - - if (collection.unclaimed > 0) { - claimedTooltip += `Unclaimed items: ${collection.unclaimed}`; - } - %> -
data-tippy-content="<%= claimedTooltip %>" <% } %>> -
-
-
-
-
<%= collection.name %> <% if (collection.tier > 0) { %><%= collection.tier %><% } %>
-
Bosses killed: <%= collection.killed.toLocaleString() %>
-
-
- <% } %> -
- <% } %> - <% } %> -
-
-
- -

Slayer

-
- <% if (calculated.slayer_coins_spent.total == 0 || calculated.slayer_coins_spent.total === undefined) { %> -

- <%= calculated.display_name %> hasn't played any Slayer yet. -

- <% } else { %> -

- - Total Slayer XP: <%= calculated.slayer_xp.toLocaleString() %> -

-
- <% - - for(const slayerName in calculated.slayers){ - const slayer = calculated.slayers[slayerName]; - - if(slayer.level.progress >= 1 && slayer.level.currentLevel < slayer.level.maxLevel){ - slayer.level.unclaimed = true; - } - } - - const slayerNames = Object.keys(calculated.slayers).sort((a, b) => slayerOrder.indexOf(a) - slayerOrder.indexOf(b)); - for (const slayerName of slayerNames){ - const slayer = calculated.slayers[slayerName] - - if (slayer.xp === undefined || slayer.xp == 0) { - continue; - } - - if (Object.keys(slayer.kills).length == 0) { - slayer.kills['1'] = 0; - } - - const maxSlayerLevel = slayer.level.currentLevel; - const totalKills = Object.values(slayer.kills).reduce((a, b) => a + b, 0); - - if (slayerInfo[slayerName] === undefined) { - continue; - } - %> - -
-
-
- <%= slayerInfo[slayerName].boss %> -
- -
- <% for(const [index, tier] of Object.keys(slayer.kills).entries()){ %> -
-
Tier <%= romanize(tier) %>
-
<%= slayer.kills[tier].toLocaleString() %>
-
- <% } %> -
-
Total
-
<%= totalKills.toLocaleString() %>
-
-
- - <% if (slayer.level.unclaimed){ %> -
unclaimed slayer rewards!
- <% } %> - - <% max = slayer.level.currentLevel == slayer.level.maxLevel ? 'golden-text' : '' %> - - <%= slayerName %> level <%= slayer.level.currentLevel %> - - -
-
-
- <%= slayer.level.xp.toLocaleString() %><% if(slayer.level.xpForNext != 0) { %> / <%= slayer.level.xpForNext.toLocaleString() %> <% } %> XP -
-
-
- <% } %> -
- <% - const maxSlayerLevel = Math.max(...Object.values(calculated.slayers).map(slayer => slayer.level?.currentLevel ?? 0)); - if (maxSlayerLevel > 0){ %> -
- <% } %> - <% } %> -
-
-
- -

Minions

-
- <% - let uniqueMinions = 0; - let maxedMinions = 0; - let skippedMinions = 0; - let uniqueMinionsType = {}; - let minionsType = {}; - - for(const minion of calculated.minions){ - if (!uniqueMinionsType[minion.type]) uniqueMinionsType[minion.type] = 0 - if (!minionsType[minion.type]) minionsType[minion.type] = 0 - - minionsType[minion.type] += 1 - if (minion.tiers == minion.maxLevel) uniqueMinionsType[minion.type] += 1 - - uniqueMinions += minion.levels.length; - skippedMinions += minion.maxLevel - minion.levels.length; - - if(minion.maxLevel == minion.tiers) - maxedMinions++; - } - %> - -
-
Minions Sheet by TBlazeWarriorT
-
Check the next cheapest or fastest Minion upgrades and find out which Minions will earn you the most from Bazaar, for free.
-
-

- <% max = uniqueMinions == constants.MINIONS_MAX_UNIQUES ? 'golden-text' : '' %>Unique Minions: <%= uniqueMinions %> / <%= constants.MINIONS_MAX_UNIQUES %> (<%= Math.floor(uniqueMinions / constants.MINIONS_MAX_UNIQUES * 100) %>%)
- <% max = calculated.minion_slots.currentSlots == constants.MINIONS_MAX_SLOTS ? 'golden-text' : '' %>Minion Slots: <%= calculated.minion_slots.currentSlots %> (<%= calculated.minion_slots.toNextSlot %> to next slot)
- <% max = calculated.misc.profile_upgrades.minion_slots == 5 ? 'golden-text' : '' %>Bonus Minion Slots: <%= calculated.misc.profile_upgrades.minion_slots %> / <%= constants.PROFILE_UPGRADES['minion_slots'] %>
- <% max = maxedMinions == _.size(constants.MINIONS) ? 'golden-text' : '' %>Maxed Minions: <%= maxedMinions %> / <%= _.size(constants.MINIONS) %>
- <% if(skippedMinions > 0){ %> - Skipped Minion Tiers: <%= skippedMinions %>
- <% } %> -

- <% - for(const type of constants.MINION_TYPES){ - const minions = calculated.minions.filter(a => a.type == type) - if(minions.length == 0) continue; - %> -
-
-
-
- <%= type %> - - <% if(uniqueMinionsType[type] >= minionsType[type]){ %> - max! - <% }else{ %> - (<%= uniqueMinionsType[type] %> / <%= minionsType[type] %> max) + This is a Stranded profile. The player cannot leave their skyblock island or trade with other players. <% } %>
-
- <% for(const minion of minions){ %> -
-
-
-
-
- <%= minion.name %> <%= minion.maxLevel %> -
-
- <% } %> -
- <% - } %>
-
+ <% } %> - <% if (calculated.bestiary !== null) {%> - <%- include('./sections/stats/bestiary.ejs', {}); %> + + <% if (items.armor !== undefined) { %> + <%- include('./sections/stats/items/armor.ejs', { getRarityUpgradeClass, rarityOrder, isEnchanted }); %> <% } %> - <% if(Object.keys(calculated.collections).length > 0){ %> -
- -

Collections

-
-

- <% - let maxCollections = 0; - for(const collection of constants.COLLECTION_DATA) - if(collection.skyblockId in calculated.collections - && calculated.collections[collection.skyblockId].tier >= collection.maxTier) - maxCollections++; - %> - <% max = maxCollections == constants.COLLECTION_DATA.length ? 'golden-text' : '' %>Maxed Collections: <%= maxCollections %> / <%= constants.COLLECTION_DATA.length %> -

- <% for(const type of constants.COLLECTION_TYPES){ - const collections = []; - - const totalOfType = constants.COLLECTION_DATA.filter(a => a.type == type).length; - let maxOfType = 0; - - for(const collection of constants.COLLECTION_DATA.filter(a => a.type == type)) - if(collection.skyblockId in calculated.collections) - collections.push(Object.assign(collection, calculated.collections[collection.skyblockId])); - - for(const collection of collections) - if(collection.tier >= collection.maxTier) - maxOfType++; - - if(collections.length == 0) - continue; - - %> -
-
-
-
- <%= type %> - <% if(maxOfType >= totalOfType){ %> - max! - <% }else{ %> - (<%= maxOfType %> / <%= totalOfType %> max) - <% } %> -
-
- <% - - for(const collection of collections){ - let amountsTooltip = ''; - - for(const [index, amount] of collection.amounts.entries()){ - amountsTooltip += `${amount.username}: ${amount.amount.toLocaleString()}`; - - if(index < collection.amounts.length) - amountsTooltip += '
'; - } - - amountsTooltip += `
Total: ${collection.totalAmount.toLocaleString()}`; - %> -
-
- <% if ("texture" in collection) { %> -
- <% } else { %> -
- <% } %> -
-
-
<%= collection.name %> <%= collection.tier %>
-
Amount: <%= collection.amount.toLocaleString() %>
-
-
- <% } %> -
- <% } %> -
-
+ + <% if (items.wardrobe !== undefined) { %> + <%- include('./sections/stats/items/wardrobe.ejs', { getRarityUpgradeClass, rarityOrder, isEnchanted }); %> <% } %> -
- -

Crimson Isle

- <% if (calculated.visited_zones.includes("crimson_isle") === false) { %> -

<%= calculated.display_name %> hasn't visited the Crimson Isle yet.

- <% } else { %> - <% if (Object.keys(calculated.crimsonIsles.factions).length > 0) { %> -

- Selected Faction: <%= calculated.crimsonIsles.factions.selected_faction.replaceAll('mages', 'Mage').replaceAll('barbarians', 'Barbarian') %>
- <% max = calculated.crimsonIsles.factions.mages_reputation >= 12000 ? 'golden-text' : '' %> - Mage Reputation: <%= calculated.crimsonIsles.factions.mages_reputation.toLocaleString() %>
- <% max = calculated.crimsonIsles.factions.barbarians_reputation >= 12000 ? 'golden-text' : '' %> - Barbarian Reputation: <%= calculated.crimsonIsles.factions.barbarians_reputation.toLocaleString() %>
- <% } %> - <% if (Object.keys(calculated.crimsonIsles.kuudra_completed_tiers).length > 0) { %> -

Kuudra Completions

-
- <%for (const tier in calculated.crimsonIsles.kuudra_completed_tiers) { - const collection = calculated.crimsonIsles.kuudra_completed_tiers[tier];%> -
-
-
-
-
-
<%= collection.name %>
-
Kills: <%= collection.completions.toLocaleString() %>
-
-
- <% } %> -
+ <% if (items.disabled?.inventory === false) { %> + + <% if (items.weapons !== undefined) { %> + <%- include('./sections/stats/items/weapons.ejs', { getRarityUpgradeClass, rarityOrder, isEnchanted }); %> <% } %> - <% function getDojoRank(points) { - if (points >= 1000) return 'S' - if (points >= 800) return 'A' - if (points >= 600) return 'B' - if (points >= 400) return 'C' - if (points >= 200) return 'D' - return 'F' - } - if (Object.keys(calculated.crimsonIsles.dojo).length > 0) { %> -

Dojo Completions

- <% max = calculated.crimsonIsles.total_dojo_points >= 7000 ? 'golden-text' : '' %>Total Points: <%= calculated.crimsonIsles.total_dojo_points.toLocaleString() %>

-
- <%for (const type of Object.keys(calculated.crimsonIsles.dojo)) { - const dojo = calculated.crimsonIsles.dojo[type]%> -
-
-
-
-
-
<%= dojo.name %>
-
- Points: <%= dojo.points.toLocaleString() %>
- Rank: <%= getDojoRank(dojo.points) %>
- Time: <%= (dojo.time / 1000).toLocaleString() %> Seconds -
-
-
- <% } %> -
+ + <% if (calculated.accessories !== undefined) { %> + <%- include('./sections/stats/items/accessories.ejs', { getRarityUpgradeClass, rarityOrder, isEnchanted }); %> <% } %> -
<% } %> - - <% if (calculated.rift !== null) { %> -
- -

Rift

- - Motes: <%= Math.floor(calculated.rift.motes.purse).toLocaleString() %> -
- - - - <% max = calculated.rift.enigma.souls === calculated.rift.enigma.total_souls ? 'golden-text' : '' %> - Enigma Souls: <%= calculated.rift.enigma.souls %> / <%= calculated.rift.enigma.total_souls %> -
- - - <% max = calculated.rift.castle.grubber_stacks === calculated.rift.castle.max_burgers ? 'golden-text' : '' %> - McGrubber's Burgers: <%= calculated.rift.castle.grubber_stacks %> / <%= calculated.rift.castle.max_burgers %> - -

Porhtal

- <% const porhtalsUnlocked = calculated.rift.wither_cage.killed_eyes.filter((a) => a.unlocked === true).length %> - <% max = porhtalsUnlocked === calculated.rift.wither_cage.killed_eyes.length ? 'golden-text' : '' %> - Eyes Unlocked: <%= porhtalsUnlocked %>

-
- <% for (const portal of calculated.rift.wither_cage.killed_eyes) { %> -
-
-
-
- -
-
<%= portal.name %>
-
-
- <% } %> -
- - -

Timecharms

- <% max = calculated.rift.timecharms.obtained_timecharms === calculated.rift.timecharms.timecharms.length ? 'golden-text' : '' %> - Timecharms obtained: <%= calculated.rift.timecharms.obtained_timecharms %>

-
- <% for (const timecharm of calculated.rift.timecharms.timecharms) { %> -
-
-
-
- -
- <% max = timecharm.unlocked === true ? 'golden-text' : '' %> -
<%= timecharm.name %>
-
- <% if (timecharm.unlocked === true) { %> - Obtained: <%= moment(timecharm.unlocked_at).fromNow() %> - <% } else { %> - Not Obtained! - <% } %> -
-
-
- <% } %> -
-
+ + <% if (calculated.pets !== undefined) { %> + <%- include('./sections/stats/pets.ejs', { getRarityUpgradeClass, rarityOrder }); %> <% } %> - - <% if (calculated.profile.game_mode === "bingo") { %> -
- -

Bingo

-
-

- Bingo Profiles: - <%= calculated.bingo.total.toLocaleString() %> -
- Completed Goals: - <%= calculated.bingo.completed_goals.toLocaleString() %> -
- Points: - <%= calculated.bingo.points.toLocaleString() %> -
-

-
-
-
-
-
- Bingo Card -
- <% if (Object.keys(items.bingo_card).length !== 0) { %> -
- -
- <% } else { %> -

This player hasn't participated in the latest bingo event.

- <% } %> -
-
-
+ <% if (items.disabled?.inventory === false) { %> + + <%- include('./sections/stats/items/inventory.ejs', { getRarityUpgradeClass, rarityOrder, isEnchanted }); %> <% } %> - <% if(Object.keys(calculated.misc).length > 0){ %> -
- -

Miscellaneous

-
- -
-
-
-
- Essence -
-
- <% for (const [key, value] of Object.entries(calculated.essence)) { %> -
-
-
-
-
-
<%= constants.ESSENCE[key].name %>
-
Amount: <%= value.toLocaleString() %>
-
-
- <% } %> -
- -
-
- <% for (const essence of Object.keys(constants.ESSENCE_SHOP)) { %> -
-
-
- <%= helper.capitalizeFirstLetter(essence) %> Shop
-
- - <% for (const perk of Object.keys(constants.ESSENCE_SHOP[essence])) { - const essenceShop = constants.ESSENCE_SHOP[essence][perk]; - const playerPerk = Object.keys(calculated.perks).find(p => p === perk); - %> - -
- - <% max = calculated.perks[playerPerk] >= essenceShop.maxLevel ? 'golden-text' : '' %> - <%= essenceShop.name %>: <%= calculated.perks[playerPerk] || 0 %> -
-
- <% } %> -
- <% } %> -
-
- <% if(calculated.kills.length > 0 || calculated.deaths.length > 0){ - let totalKills = calculated.kills.length; - let totalDeaths = calculated.deaths.length; + + <%- include('./sections/stats/skills.ejs', { getRarityUpgradeClass, rarityOrder, isEnchanted }); %> - let rows = Math.min(Math.max(totalKills, totalDeaths), 10); + + <% if (calculated.dungeons !== undefined) { %> + <%- include('./sections/stats/dungeons.ejs', { skillItems }); %> + <% } %> - %> -
-
-
-
kills -
-

- Total Kills: <%= calculated.kills.map(a => a.amount).reduce((a, b) => a + b, 0).toLocaleString() %>
- Total Deaths: <%= calculated.deaths.map(a => a.amount).reduce((a, b) => a + b, 0).toLocaleString() %> -

- -
-
-
Kills
-
- <% for(let i = 0; i < rows; i++){ - const kill = calculated.kills[i]; - - if(typeof calculated.kills[i] === 'undefined'){ - %> -
-
-
- <% }else{ %> -
-
#<%= i + 1 %> 
-
<%= kill.entityName %>
-
-
<%= kill.amount.toLocaleString() %>
-
- <% } - } %> - <% if(calculated.kills.length > 10 || calculated.deaths.length > 10){ %> - - <% } %> -
-
-
-
Deaths
-
- <% for(let i = 0; i < rows; i++){ - const death = calculated.deaths[i]; - - if(typeof death === 'undefined'){ - %> -
-
-
- <% }else{ %> -
-
#<%= i + 1 %> 
-
<%= death.entityName %>
-
-
<%= death.amount.toLocaleString() %>
-
- <% } - } %> - <% if(calculated.kills.length > 10 || calculated.deaths.length > 10){ %> - - <% } %> -
-
-
- <% } %> + + <% if (calculated.slayer !== undefined) { %> + <%- include('./sections/stats/slayer.ejs', {}); %> + <% } %> - <% if('races' in calculated.misc){ %> -
-
-
-
races -
+ + <% if (calculated.minions.totalMinions > 0) { // Show only if player has unlocked a minion %> + <%- include('./sections/stats/minions.ejs', { skillItems }); %> + <% } %> -
- <% - const races = [ - { id: "dungeon_hub_crystal_core", name: "Crystal Core", icon: '399_0' }, - { id: "dungeon_hub_giant_mushroom", name: "Giant Mushroom", icon: '100_0' }, - { id: "dungeon_hub_precursor_ruins", name: "Precursor Ruins", icon: '98_1' } - ]; - - const types = ["anything", "no_pearls", "no_abilities", "nothing"]; - - for(const race of races){ - const times = Object.keys(calculated.misc.races).filter(a => a.startsWith(race.id)); - - if(times.length > 0){ - %> -
-
<%= race.name %>
-
- <% - const races_no_return = times.filter(a => a.includes("no_return")); - const races_with_return = times.filter(a => a.includes("with_return")); - - if(races_no_return.length > 0){ %> -
No Return:
- <% } - - for(const type of types){ - const key = `${race.id}_${type}_no_return_best_time`; - const duration = calculated.misc.races[key] ?? 0; - const raceTier = calculated.misc.objectives.completedRaces[key] ?? 0; - - if(duration == 0) - continue; - - let raceDuration = moment.duration(duration, "milliseconds").format("m:ss.SSS"); - - if(duration < 1000) - raceDuration = '0.' + raceDuration; - %> -
-
- <%= helper.titleCase(type.split("_").join(" ")) %>: - <%= raceDuration %> -
-
- <%= helper.renderRaceTier(raceTier) %> -
-
- <% } - - if(races_with_return.length > 0){ %> -
With Return:
- <% } - - for(const type of types){ - const key = `${race.id}_${type}_with_return_best_time`; - const duration = calculated.misc.races[key] ?? 0; - const raceTier = calculated.misc.objectives.completedRaces[key] ?? 0; - - if(duration == 0) - continue; - - let raceDuration = moment.duration(duration, "milliseconds").format("m:ss.SSS"); - - if(duration < 1000) - raceDuration = '0.' + raceDuration; - %> -
-
- <%= helper.titleCase(type.split("_").join(" ")) %>: - <%= raceDuration %> -
-
- <%= helper.renderRaceTier(raceTier) %> -
-
- <% } %> -
-
- <% - } - } - %> -
-
Other Races
-
- <% for(const key in calculated.misc.races){ - if(key.startsWith('dungeon_hub')) - continue; - - const raceName = helper.capitalizeFirstLetter(key.replace(/_race_best_time.*/, "").split("_").join(" ")); - let raceDuration = moment.duration(calculated.misc.races[key], "milliseconds").format("m:ss.SSS"); - const raceTier = calculated.misc.objectives.completedRaces[key] ?? 0; - - if(calculated.misc.races[key] < 1000) - raceDuration = '0.' + raceDuration; - %> -
-
- <%= raceName %>: - <%= raceDuration %> -
-
- <%= helper.renderRaceTier(raceTier) %> -
-
- <% } %> -
-
-
- <% } %> - <% if('gifts' in calculated.misc){ %> -
-
-
-
Gifts -
-

- <% for(const key in calculated.misc.gifts){ %> - <%= helper.capitalizeFirstLetter(key.split("_").join(" ")); %>: <%= calculated.misc.gifts[key].toLocaleString() %>
- <% } %> -

- <% } %> - <% if('winter' in calculated.misc){ %> -
-
-
-
Season of jerry -
-

- <% for(const key in calculated.misc.winter){ %> - <%= helper.capitalizeFirstLetter(key.split("_").join(" ")); %>: <%= calculated.misc.winter[key].toLocaleString() %>
- <% } %> -

- <% } %> - <% if('dragons' in calculated.misc){ %> -
-
-
-
Dragons -
-

- <% for(const key in calculated.misc.dragons){ - let tooltip = ""; - - if(key == 'last_hits') - for(const kill of calculated.kills.filter(a => a.entityId.endsWith('_dragon') && a.entityId != 'master_wither_king_dragon')) - tooltip += `${ kill.entityName }: ${ kill.amount } (${ Math.round(kill.amount / calculated.misc.dragons[key] * 100) }%)
`; - - if(key == 'deaths') - for(const death of calculated.deaths.filter(a => a.entityId.endsWith('_dragon') && a.entityId != 'master_wither_king_dragon')) - tooltip += `${ death.entityName }: ${ death.amount }
`; - %> - ><%= helper.capitalizeFirstLetter(key.split("_").join(" ")); %>: <%= calculated.misc.dragons[key].toLocaleString() %>
- <% } %> -

- <% } %> - <% if('protector' in calculated.misc){ %> -
-
-
-
Endstone protectors -
-

- <% for(const key in calculated.misc.protector){ %> - <%= helper.capitalizeFirstLetter(key.split("_").join(" ")); %>: <%= calculated.misc.protector[key].toLocaleString() %>
- <% } %> -

- <% } %> - <% if('damage' in calculated.misc){ %> -
-
-
-
Damage -
-

- <% for(const key in calculated.misc.damage){ %> - <% const damage = calculated.misc.damage[key] > 100_000_000_000 ? helper.formatNumber(Math.floor(calculated.misc.damage[key]).toFixed(0), true) : Math.floor(calculated.misc.damage[key]).toLocaleString(); %> - <%= helper.capitalizeFirstLetter(key.split("_").join(" ")); %>: <%= damage %>
- <% } %> -

- <% } %> - <% if('milestones' in calculated.misc){ %> -
-
-
-
Pet milestones -
-

- <% for(const key in calculated.misc.milestones){ - let progress = { - rarity: milestone_rarities[pet_milestones[key].length-1], - maxed: true - }; - let tooltip = ""; - - for(let i = 0; i < pet_milestones[key].length; i++){ - if(calculated.misc.milestones[key] < pet_milestones[key][i]){ - progress = { - percentage: Math.round(calculated.misc.milestones[key]/pet_milestones[key][i]*100), - rarity: milestone_rarities[i], - maxed: false - }; - break; - } - } - - tooltip += `Rarity: ${ helper.capitalizeFirstLetter(progress.rarity) }
Progress: `; - if(progress.maxed) - tooltip += `Maxed!`; - else - tooltip += `${ progress.percentage.toLocaleString() }`; - %> - > - <%= helper.capitalizeFirstLetter(key.split("_").join(" ")); %>: <%= calculated.misc.milestones[key].toLocaleString() %>
- <% } %> -

- <% } %> - <% if('burrows' in calculated.misc){ %> -
-
-
-
Griffin burrows -
-

- <% let burrow_naming = {"dug_next": "dug_arrows", "dug_combat": "dug_monsters"} - for(const key in calculated.misc.burrows){ - let name = burrow_naming[key] || key; - let tooltip = ""; - - for(const rarity in calculated.misc.burrows[key]) - if(rarity != "total" && rarity != "null") - tooltip += `${ helper.capitalizeFirstLetter(rarity) }: ${ calculated.misc.burrows[key][rarity] }
`; - %> - ><%= helper.capitalizeFirstLetter(name.split("_").join(" ")); %>: <%= calculated.misc.burrows[key].total.toLocaleString() %>
- <% } %> -

- <% } %> + <% if (calculated.bestiary !== undefined) {%> + <%- include('./sections/stats/bestiary.ejs', {}); %> + <% } %> - <% if('effects' in calculated.misc){ - if (Object.keys(calculated.misc.effects).some((key) => Object.keys(calculated.misc.effects[key]).length > 0)) { %> -
-
-
-
- Potions -
- <% - let effects_tooltips = []; - for (const key of Object.keys(calculated.misc.effects).sort()) { - effects_tooltips[key] ??= []; - for (let potion of calculated.misc.effects[key]) { - if (key === "active") { - if (potion?.effect === undefined || potion?.level === null) continue; - effects_tooltips[key].push(`${constants.POTION_EFFECTS[potion.effect]?.[potion.level].name.replace('Potion', '') || potion.effect.split('_').map((effect) => helper.capitalizeFirstLetter(effect.toLowerCase())).join(' ')}
`) - - } else { - potion = potion?.effect || potion - - effects_tooltips[key].push(`${potion.split('_').map((effect) => helper.capitalizeFirstLetter(effect.toLowerCase())).join(' ')}
`); - } - } - } - %> -

- > - Active Potion Effects: <%= calculated.misc.effects.active.length %> -
- - > - Paused Potion Effects: <%= calculated.misc.effects.paused.length %> -
- - > - Disabled Potion Effects: <%= calculated.misc.effects.disabled.length %> -
-

- <% } %> - <% } %> + + <% if (calculated.collections !== undefined) { %> + <%- include('./sections/stats/collections.ejs', { skillItems }); %> + <% } %> - <% if('profile_upgrades' in calculated.misc){ %> -
-
-
-
Profile upgrades -
-

- <% for(const upgrade in constants.PROFILE_UPGRADES){ %> - <% max = calculated.misc.profile_upgrades[upgrade] == constants.PROFILE_UPGRADES[upgrade] ? 'golden-text' : '' %><%= helper.capitalizeFirstLetter(upgrade.split("_").join(" ")); %>: <%= calculated.misc.profile_upgrades[upgrade] %> / <%= constants.PROFILE_UPGRADES[upgrade] %>
- <% } %> -

- <% } %> - <% if('auctions_sell' in calculated.misc){ %> -
-
-
-
Auctions sold -
-

- <% for(const key in calculated.misc.auctions_sell){ - let tooltip = ""; - - if(key == 'items_sold') - for(const key of Object.keys(calculated.auctions_sold).sort((a, b) => rarityOrder.indexOf(a) - rarityOrder.indexOf(b))) - tooltip += `${ helper.capitalizeFirstLetter(key) }: ${ calculated.auctions_sold[key] }
`; - %> - ><%= helper.capitalizeFirstLetter(key.split("_").join(" ")); %>: <%= calculated.misc.auctions_sell[key].toLocaleString() %>
- <% } %> -

- <% } %> - <% if('auctions_buy' in calculated.misc){ %> -
-
-
-
Auctions bought -
-

- <% for(const key in calculated.misc.auctions_buy){ - let tooltip = ""; - - if(key == 'items_bought') - for(const key of Object.keys(calculated.auctions_bought).sort((a, b) => rarityOrder.indexOf(a) - rarityOrder.indexOf(b))) - tooltip += `${ helper.capitalizeFirstLetter(key) }: ${ calculated.auctions_bought[key] }
`; - %> - ><%= helper.capitalizeFirstLetter(key.split("_").join(" ")); %>: <%= calculated.misc.auctions_buy[key].toLocaleString() %>
- <% } %> -

- <% } %> + + <% if (calculated.crimson_isle !== undefined) { %> + <%- include('./sections/stats/crimson_isle.ejs', {}); %> + <% } %> - <% if('claimed_items' in calculated.misc){ %> -
-
-
-
Claimed items -
-

- <% for(const key in calculated.misc.claimed_items){ - let timestamp = calculated.misc.claimed_items[key]; - %> - <%= helper.capitalizeFirstLetter(key.split("_").join(" ")); %>: - <%= moment(timestamp).fromNow() %>
- <% } %> -

- <% } %> + + <% if (calculated.rift !== undefined) { %> + <%- include('./sections/stats/rift.ejs', {}); %> + <% } %> - <% if ('uncategorized' in calculated.misc && Object.keys(calculated.misc.uncategorized).length > 0) { %> -
-
-
-
Uncategorized -
-

- <% for (const [key, value] of Object.entries(calculated.misc.uncategorized)) { %> - <% max = value.maxed === true ? 'golden-text': '' %> - <%= helper.capitalizeFirstLetter(key.split("_").join(" ")); %>: <%= value.formatted %> -
- <% } %> -

- <% } %> -
-
+ + <% if (calculated.profile.game_mode === "bingo" && calculated.bingo !== undefined) { %> + <%- include('./sections/stats/bingo.ejs', {}); %> + <% } %> + + + <% if (calculated.misc !== undefined) { %> + <%- include('./sections/stats/misc.ejs', { rarityOrder }); %> <% } %>
+ <%- include('../includes/footer'); %> +