HASH 4a4be5dae958
DATE 2025-07-29
SUBJECT sim: update all
FILES 7 CHANGED
HASH 4a4be5dae958
DATE 2025-07-29
SUBJECT sim: update all
FILES 7 CHANGED
┌─ flake.lock ───────────────────────────────────────────────────────────────┐
│ diff --git a/flake.lock b/flake.lock │
│ index d15d620..aacffa5 100644 │
│ --- a/flake.lock │
│ +++ b/flake.lock │
│ @@ -86,11 +86,11 @@ │
│ ] │
│ }, │
│ "locked": { │
│ - "lastModified": 1753685245, │
│ - "narHash": "sha256-nFSuXwbWYevQlZc/P9qRGWT1JfiYbbuOIxx08PtS1Ug=", │
│ + "lastModified": 1753762429, │
│ + "narHash": "sha256-vAp1f/m7JFEstwf2ve0HsfuvTkiPD2mJWeCZ+5rDQ1E=", │
│ "owner": "ooks-io", │
│ "repo": "mop", │
│ - "rev": "6156bf245b8a1663badb5c1663372ada3f8757c2", │
│ + "rev": "3d85fa980d1fe8cde01384a790b8b5b093f44906", │
│ "type": "github" │
│ }, │
│ "original": { │
│ @@ -103,11 +103,11 @@ │
│ "wowsims-upstream": { │
│ "flake": false, │
│ "locked": { │
│ - "lastModified": 1753683048, │
│ - "narHash": "sha256-nIeQRXq++k0nvTo11s948s5C5Ur/Hrh7aR8ENK+P6d8=", │
│ + "lastModified": 1753730192, │
│ + "narHash": "sha256-5PP87yeofSm/WIoxuLniJMvQfxLrb97E+h/Z2kQRDs0=", │
│ "owner": "wowsims", │
│ "repo": "mop", │
│ - "rev": "06153aa432c961a62ca8255eed5eb1ee1e7114ca", │
│ + "rev": "99ee7a6a9db6acb96e684459e68abaad90c0b608", │
│ "type": "github" │
│ }, │
│ "original": { │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ flake.lock ─────────────────────────┐
│ diff --git a/flake.lock b/flake.lock │
│ index d15d620..aacffa5 100644 │
│ --- a/flake.lock │
│ +++ b/flake.lock │
│ @@ -86,11 +86,11 @@ │
│ ] │
│ }, │
│ "locked": { │
│ - "lastModified": 1753685245, │
│ - "narHash": "sha256-nFSuXwbWYevQlZc/ │
│ P9qRGWT1JfiYbbuOIxx08PtS1Ug=", │
│ + "lastModified": 1753762429, │
│ + "narHash": "sha256-vAp1f/m7JFEstwf2 │
│ ve0HsfuvTkiPD2mJWeCZ+5rDQ1E=", │
│ "owner": "ooks-io", │
│ "repo": "mop", │
│ - "rev": "6156bf245b8a1663badb5c16633 │
│ 72ada3f8757c2", │
│ + "rev": "3d85fa980d1fe8cde01384a790b │
│ 8b5b093f44906", │
│ "type": "github" │
│ }, │
│ "original": { │
│ @@ -103,11 +103,11 @@ │
│ "wowsims-upstream": { │
│ "flake": false, │
│ "locked": { │
│ - "lastModified": 1753683048, │
│ - "narHash": "sha256-nIeQRXq++k0nvTo1 │
│ 1s948s5C5Ur/Hrh7aR8ENK+P6d8=", │
│ + "lastModified": 1753730192, │
│ + "narHash": "sha256-5PP87yeofSm/WIox │
│ uLniJMvQfxLrb97E+h/Z2kQRDs0=", │
│ "owner": "wowsims", │
│ "repo": "mop", │
│ - "rev": "06153aa432c961a62ca8255eed5 │
│ eb1ee1e7114ca", │
│ + "rev": "99ee7a6a9db6acb96e684459e68 │
│ abaad90c0b608", │
│ "type": "github" │
│ }, │
│ "original": { │
└──────────────────────────────────────────────┘
┌─ web/.astro/settings.json ─────────────────────────────────────────────────┐
│ diff --git a/web/.astro/settings.json b/web/.astro/settings.json │
│ index 214ce60..0e40bfc 100644 │
│ --- a/web/.astro/settings.json │
│ +++ b/web/.astro/settings.json │
│ @@ -1,5 +1,5 @@ │
│ { │
│ - "_variables": { │
│ - "lastUpdateCheck": 1753092924601 │
│ - } │
│ -} │
│ \ No newline at end of file │
│ + "_variables": { │
│ + "lastUpdateCheck": 1753092924601 │
│ + } │
│ +} │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ web/.astro/settings.json ───────────┐
│ diff --git a/web/.astro/settings.json b/web/ │
│ .astro/settings.json │
│ index 214ce60..0e40bfc 100644 │
│ --- a/web/.astro/settings.json │
│ +++ b/web/.astro/settings.json │
│ @@ -1,5 +1,5 @@ │
│ { │
│ - "_variables": { │
│ - "lastUpdateCheck": 1753092924601 │
│ - } │
│ -} │
│ \ No newline at end of file │
│ + "_variables": { │
│ + "lastUpdateCheck": 1753092924601 │
│ + } │
│ +} │
└──────────────────────────────────────────────┘
┌─ web/.astro/types.d.ts ────────────────────────────────────────────────────┐
│ diff --git a/web/.astro/types.d.ts b/web/.astro/types.d.ts │
│ index f964fe0..03d7cc4 100644 │
│ --- a/web/.astro/types.d.ts │
│ +++ b/web/.astro/types.d.ts │
│ @@ -1 +1,2 @@ │
│ /// <reference types="astro/client" /> │
│ +/// <reference path="content.d.ts" /> │
│ \ No newline at end of file │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ web/.astro/types.d.ts ──────────────┐
│ diff --git a/web/.astro/types.d.ts b/web/.as │
│ tro/types.d.ts │
│ index f964fe0..03d7cc4 100644 │
│ --- a/web/.astro/types.d.ts │
│ +++ b/web/.astro/types.d.ts │
│ @@ -1 +1,2 @@ │
│ /// <reference types="astro/client" /> │
│ +/// <reference path="content.d.ts" /> │
│ \ No newline at end of file │
└──────────────────────────────────────────────┘
┌─ web/astro.config.mjs ─────────────────────────────────────────────────────┐
│ diff --git a/web/astro.config.mjs b/web/astro.config.mjs │
│ index 8dae6ea..7914983 100644 │
│ --- a/web/astro.config.mjs │
│ +++ b/web/astro.config.mjs │
│ @@ -1,7 +1,7 @@ │
│ // @ts-check │
│ -import { defineConfig } from 'astro/config'; │
│ +import { defineConfig } from "astro/config"; │
│ │
│ // https://astro.build/config │
│ export default defineConfig({ │
│ - site: 'https://wowsimstats.com', │
│ + site: "https://wowsimstats.com", │
│ }); │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ web/astro.config.mjs ───────────────┐
│ diff --git a/web/astro.config.mjs b/web/astr │
│ o.config.mjs │
│ index 8dae6ea..7914983 100644 │
│ --- a/web/astro.config.mjs │
│ +++ b/web/astro.config.mjs │
│ @@ -1,7 +1,7 @@ │
│ // @ts-check │
│ -import { defineConfig } from 'astro/config' │
│ ; │
│ +import { defineConfig } from "astro/config" │
│ ; │
│ │
│ // https://astro.build/config │
│ export default defineConfig({ │
│ - site: 'https://wowsimstats.com', │
│ + site: "https://wowsimstats.com", │
│ }); │
└──────────────────────────────────────────────┘
┌─ web/src/components/Callout.astro ─────────────────────────────────────────┐
│ diff --git a/web/src/components/Callout.astro b/web/src/components/Callout.astro │
│ index cd6e375..7de7613 100644 │
│ --- a/web/src/components/Callout.astro │
│ +++ b/web/src/components/Callout.astro │
│ @@ -104,4 +104,4 @@ const config = typeConfig[type]; │
│ .callout-content strong { │
│ color: var(--typeColor); │
│ } │
│ -</style> │
│ \ No newline at end of file │
│ +</style> │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ web/src/components/Callout.astro ───┐
│ diff --git a/web/src/components/Callout.astr │
│ o b/web/src/components/Callout.astro │
│ index cd6e375..7de7613 100644 │
│ --- a/web/src/components/Callout.astro │
│ +++ b/web/src/components/Callout.astro │
│ @@ -104,4 +104,4 @@ const config = typeConfi │
│ g[type]; │
│ .callout-content strong { │
│ color: var(--typeColor); │
│ } │
│ -</style> │
│ \ No newline at end of file │
│ +</style> │
└──────────────────────────────────────────────┘
┌─ web/src/components/ItemDatabase.astro ────────────────────────────────────┐
│ diff --git a/web/src/components/ItemDatabase.astro b/web/src/components/ItemDataba │
│ se.astro │
│ index 79364d2..7e40bd8 100644 │
│ --- a/web/src/components/ItemDatabase.astro │
│ +++ b/web/src/components/ItemDatabase.astro │
│ @@ -76,30 +76,30 @@ const statNames = { │
│ }; │
│ │
│ // Global functions for item database functionality │
│ -window.ItemDatabase = { │
│ +(window as any).ItemDatabase = { │
│ // Helper function to format reforging │
│ - formatReforge: (reforgeId) => { │
│ + formatReforge: (reforgeId: any) => { │
│ if (!reforgeId || reforgeId === 0) return ''; │
│ - const reforge = reforgeData[reforgeId.toString()]; │
│ + const reforge = reforgeData[reforgeId.toString() as keyof typeof reforgeData] │
│ ; │
│ if (!reforge) return ` [R:${reforgeId}]`; │
│ │
│ - const fromStat = statNames[reforge.s1] || reforge.s1; │
│ - const toStat = statNames[reforge.s2] || reforge.s2; │
│ + const fromStat = statNames[reforge.s1 as keyof typeof statNames] || reforge.s │
│ 1; │
│ + const toStat = statNames[reforge.s2 as keyof typeof statNames] || reforge.s2; │
│ return ` [${fromStat} ? ${toStat}]`; │
│ }, │
│ │
│ // Generate Wowhead URL for items │
│ - getWowheadUrl: (itemId) => { │
│ + getWowheadUrl: (itemId: any) => { │
│ return `https://www.wowhead.com/mop-classic/item=${itemId}`; │
│ }, │
│ │
│ // Format equipment item with all details - uses enriched data from backend │
│ - formatEquipmentItem: (item) => { │
│ + formatEquipmentItem: (item: any) => { │
│ if (!item || !item.id) return null; │
│ │
│ // Use enriched name if available, otherwise fallback to ID │
│ const itemName = item.name || `Item ${item.id}`; │
│ - const wowheadUrl = window.ItemDatabase.getWowheadUrl(item.id); │
│ + const wowheadUrl = (window as any).ItemDatabase.getWowheadUrl(item.id); │
│ const iconUrl = item.icon ? `https://wow.zamimg.com/images/wow/icons/large/${ │
│ item.icon}.jpg` : null; │
│ │
│ // Build details array with specific layout structure │
│ @@ -118,17 +118,17 @@ window.ItemDatabase = { │
│ details.push(`<div class="item-reforge">Reforged ${item.reforging.descripti │
│ on}</div>`); │
│ } else if (item.reforging && typeof item.reforging === 'number') { │
│ // Fallback to manual reforge lookup if not enriched │
│ - const reforge = reforgeData[item.reforging.toString()]; │
│ + const reforge = reforgeData[item.reforging.toString() as keyof typeof refor │
│ geData]; │
│ if (reforge) { │
│ - const fromStat = statNames[reforge.s1] || reforge.s1; │
│ - const toStat = statNames[reforge.s2] || reforge.s2; │
│ + const fromStat = statNames[reforge.s1 as keyof typeof statNames] || refor │
│ ge.s1; │
│ + const toStat = statNames[reforge.s2 as keyof typeof statNames] || reforge │
│ .s2; │
│ details.push(`<div class="item-reforge">Reforged ${fromStat} ? ${toStat}< │
│ /div>`); │
│ } │
│ } │
│ │
│ // Gems - each gem on separate line with icon and white stats │
│ if (item.gems && item.gems.length > 0) { │
│ - item.gems.forEach(gem => { │
│ + item.gems.forEach((gem: any) => { │
│ if (gem && gem.name) { │
│ const gemIcon = gem.icon ? `<img src="https://wow.zamimg.com/images/wow │
│ /icons/small/${gem.icon}.jpg" alt="${gem.name}" class="gem-icon-inline" />` : ''; │
│ const gemStats = gem.stats && gem.stats.length > 0 ? gem.stats.join(', │
│ ') : gem.name; │
│ @@ -163,12 +163,12 @@ window.ItemDatabase = { │
│ }, │
│ │
│ // Format gem information │
│ - formatGems: (gems) => { │
│ + formatGems: (gems: any) => { │
│ if (!gems || gems.length === 0) return ''; │
│ - const activeGems = gems.filter(g => g && g !== 0); │
│ + const activeGems = gems.filter((g: any) => g && g !== 0); │
│ if (activeGems.length === 0) return ''; │
│ │
│ - return activeGems.map(gemId => `Gem ${gemId}`).join(', '); │
│ + return activeGems.map((gemId: any) => `Gem ${gemId}`).join(', '); │
│ } │
│ }; │
│ </script> │
│ \ No newline at end of file │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...components/ItemDatabase.astro ───┐
│ diff --git a/web/src/components/ItemDatabase │
│ .astro b/web/src/components/ItemDatabase.ast │
│ ro │
│ index 79364d2..7e40bd8 100644 │
│ --- a/web/src/components/ItemDatabase.astro │
│ +++ b/web/src/components/ItemDatabase.astro │
│ @@ -76,30 +76,30 @@ const statNames = { │
│ }; │
│ │
│ // Global functions for item database funct │
│ ionality │
│ -window.ItemDatabase = { │
│ +(window as any).ItemDatabase = { │
│ // Helper function to format reforging │
│ - formatReforge: (reforgeId) => { │
│ + formatReforge: (reforgeId: any) => { │
│ if (!reforgeId || reforgeId === 0) retu │
│ rn ''; │
│ - const reforge = reforgeData[reforgeId.t │
│ oString()]; │
│ + const reforge = reforgeData[reforgeId.t │
│ oString() as keyof typeof reforgeData]; │
│ if (!reforge) return ` [R:${reforgeId}] │
│ `; │
│ │
│ - const fromStat = statNames[reforge.s1] │
│ || reforge.s1; │
│ - const toStat = statNames[reforge.s2] || │
│ reforge.s2; │
│ + const fromStat = statNames[reforge.s1 a │
│ s keyof typeof statNames] || reforge.s1; │
│ + const toStat = statNames[reforge.s2 as │
│ keyof typeof statNames] || reforge.s2; │
│ return ` [${fromStat} ? ${toStat}]`; │
│ }, │
│ │
│ // Generate Wowhead URL for items │
│ - getWowheadUrl: (itemId) => { │
│ + getWowheadUrl: (itemId: any) => { │
│ return `https://www.wowhead.com/mop-cla │
│ ssic/item=${itemId}`; │
│ }, │
│ │
│ // Format equipment item with all details │
│ - uses enriched data from backend │
│ - formatEquipmentItem: (item) => { │
│ + formatEquipmentItem: (item: any) => { │
│ if (!item || !item.id) return null; │
│ │
│ // Use enriched name if available, othe │
│ rwise fallback to ID │
│ const itemName = item.name || `Item ${i │
│ tem.id}`; │
│ - const wowheadUrl = window.ItemDatabase. │
│ getWowheadUrl(item.id); │
│ + const wowheadUrl = (window as any).Item │
│ Database.getWowheadUrl(item.id); │
│ const iconUrl = item.icon ? `https://wo │
│ w.zamimg.com/images/wow/icons/large/${item.i │
│ con}.jpg` : null; │
│ │
│ // Build details array with specific la │
│ yout structure │
│ @@ -118,17 +118,17 @@ window.ItemDatabase = │
│ { │
│ details.push(`<div class="item-reforg │
│ e">Reforged ${item.reforging.description}</d │
│ iv>`); │
│ } else if (item.reforging && typeof ite │
│ m.reforging === 'number') { │
│ // Fallback to manual reforge lookup │
│ if not enriched │
│ - const reforge = reforgeData[item.refo │
│ rging.toString()]; │
│ + const reforge = reforgeData[item.refo │
│ rging.toString() as keyof typeof reforgeData │
│ ]; │
│ if (reforge) { │
│ - const fromStat = statNames[reforge. │
│ s1] || reforge.s1; │
│ - const toStat = statNames[reforge.s2 │
│ ] || reforge.s2; │
│ + const fromStat = statNames[reforge. │
│ s1 as keyof typeof statNames] || reforge.s1; │
│ + const toStat = statNames[reforge.s2 │
│ as keyof typeof statNames] || reforge.s2; │
│ details.push(`<div class="item-refo │
│ rge">Reforged ${fromStat} ? ${toStat}</div>` │
│ ); │
│ } │
│ } │
│ │
│ // Gems - each gem on separate line wit │
│ h icon and white stats │
│ if (item.gems && item.gems.length > 0) │
│ { │
│ - item.gems.forEach(gem => { │
│ + item.gems.forEach((gem: any) => { │
│ if (gem && gem.name) { │
│ const gemIcon = gem.icon ? `<img │
│ src="https://wow.zamimg.com/images/wow/icons │
│ /small/${gem.icon}.jpg" alt="${gem.name}" cl │
│ ass="gem-icon-inline" />` : ''; │
│ const gemStats = gem.stats && gem │
│ .stats.length > 0 ? gem.stats.join(', ') : g │
│ em.name; │
│ @@ -163,12 +163,12 @@ window.ItemDatabase = │
│ { │
│ }, │
│ │
│ // Format gem information │
│ - formatGems: (gems) => { │
│ + formatGems: (gems: any) => { │
│ if (!gems || gems.length === 0) return │
│ ''; │
│ - const activeGems = gems.filter(g => g & │
│ & g !== 0); │
│ + const activeGems = gems.filter((g: any) │
│ => g && g !== 0); │
│ if (activeGems.length === 0) return ''; │
│ │
│ - return activeGems.map(gemId => `Gem ${g │
│ emId}`).join(', '); │
│ + return activeGems.map((gemId: any) => ` │
│ Gem ${gemId}`).join(', '); │
│ } │
│ }; │
│ </script> │
│ \ No newline at end of file │
└──────────────────────────────────────────────┘
┌─ web/src/layouts/ChartLayout.astro ────────────────────────────────────────┐
│ diff --git a/web/src/layouts/ChartLayout.astro b/web/src/layouts/ChartLayout.astro │
│ index c376218..c2c6292 100644 │
│ --- a/web/src/layouts/ChartLayout.astro │
│ +++ b/web/src/layouts/ChartLayout.astro │
│ @@ -269,6 +269,21 @@ const isFixedClassSpec = fixedClass && fixedSpec; │
│ flex: 1; │
│ } │
│ │
│ + .trinket-icon { │
│ + width: 20px; │
│ + height: 20px; │
│ + border-radius: 3px; │
│ + margin-right: 8px; │
│ + vertical-align: middle; │
│ + border: 1px solid rgba(255,255,255,0.2); │
│ + } │
│ + │
│ + .trinket-ilvl { │
│ + font-weight: 600; │
│ + color: var(--text-primary); │
│ + vertical-align: middle; │
│ + } │
│ + │
│ .chart-bar-container { │
│ display: flex; │
│ align-items: center; │
│ @@ -771,6 +786,34 @@ const isFixedClassSpec = fixedClass && fixedSpec; │
│ │
│ <div id="metadata-container"></div> │
│ │
│ + <!-- Trinket comparison callout --> │
│ + <div id="trinket-callout" class="hidden"> │
│ + <Callout type="note"> │
│ + <p> │
│ + <em>Trinket rankings are still a work-in-progress.</em> │
│ + </p> │
│ + <p><strong>Current Implementation</strong>:</p> │
│ + <p> │
│ + We take the selected phase's bis profile gearset, remove the trinkets │
│ , and simulate to get the <strong>baseline</strong> result. We then generate a gea │
│ rset for each trinket and compare the results against the baseline. │
│ + </p> │
│ + <p> │
│ + This implementation comes with some caveats that can scew the results │
│ : │
│ + </p> │
│ + <ul> │
│ + <li>We <strong>don't</strong> re-reforge the gearset for every trinke │
│ t</li> │
│ + <li>Ideally every trinket would come with it's own optimized gearset< │
│ /li> │
│ + <li>This is simulating the trinkets <strong>individually</strong>, no │
│ t in pairs</li> │
│ + <li>Like all other simulations, keep in mind WoWSims is still in <str │
│ ong>alpha</strong> so not all spec/trinket implementations are not complete</li> │
│ + </ul> │
│ + <p> │
│ + With all that in mind, you should only consider this list as a <em>ro │
│ ugh approximation</em> of the value of each individual trinket. │
│ + </p> │
│ + <p> │
│ + In the future we will reforge the gearset for each trinket to get a m │
│ ore accurate value. │
│ + </p> │
│ + </Callout> │
│ + </div> │
│ + │
│ <div class="controls"> │
│ <h3 class="controls-title"> │
│ {isRankingsMode ? "Simulation Options" : (isFixedClassSpec ? "Simulatio │
│ n Options" : "Comparison Options")} │
│ @@ -781,7 +824,7 @@ const isFixedClassSpec = fixedClass && fixedSpec; │
│ <label for="comparisonType">Compare:</label> │
│ <select id="comparisonType"> │
│ <option value="race">Races</option> │
│ - <option value="trinket" disabled>Trinkets (Coming Soon)</option> │
│ + <option value="trinket">Trinkets</option> │
│ </select> │
│ </div> │
│ )} │
│ @@ -852,6 +895,7 @@ const isFixedClassSpec = fixedClass && fixedSpec; │
│ <div class="control-group"> │
│ <label for="sortBy">Sort By:</label> │
│ <select id="sortBy"> │
│ + <option value="percent">Average Percentage Increase</option> │
│ <option value="dps">Average DPS</option> │
│ <option value="max">Max DPS</option> │
│ <option value="min">Min DPS</option> │
│ @@ -955,9 +999,9 @@ const formatRace = (race) => { │
│ return race.replace('Race', '').replace(/([A-Z])/g, ' $1').trim(); │
│ }; │
│ │
│ -const formatClass = (className) => { │
│ - return className.replace('Class', '').replace(/([A-Z])/g, ' $1').trim(); │
│ -}; │
│ +// const formatClass = (className) => { │
│ +// return className.replace('Class', '').replace(/([A-Z])/g, ' $1').trim(); │
│ +// }; │
│ │
│ const formatTalents = (talents) => { │
│ if (!talents || !talents.talents || !Array.isArray(talents.talents)) { │
│ @@ -1132,7 +1176,7 @@ const formatEquipmentSummary = (items) => { │
│ │
│ │
│ // Generate loadout dropdown HTML │
│ -const generateLoadoutDropdown = (loadout, chartData) => { │
│ +const generateLoadoutDropdown = (loadout, _chartData) => { │
│ const sections = formatLoadout(loadout); │
│ if (!sections || sections.length === 0) return ''; │
│ │
│ @@ -1349,6 +1393,12 @@ class UnifiedChart { │
│ comparisonSelect.value = comparisonType; │
│ } │
│ } │
│ + │
│ + // Initialize phase options for comparison modes │
│ + if (!this.isRankingsMode) { │
│ + this.updatePhaseOptions(); │
│ + this.updateTrinketCallout(); │
│ + } │
│ } │
│ │
│ bindEvents() { │
│ @@ -1365,7 +1415,12 @@ class UnifiedChart { │
│ │
│ const dataSelects = document.querySelectorAll('#spec, #comparisonType'); │
│ dataSelects.forEach(select => { │
│ - select.addEventListener('change', () => this.loadData()); │
│ + select.addEventListener('change', () => { │
│ + if (select.id === 'comparisonType') { │
│ + this.updateSortOptions(); │
│ + } │
│ + this.loadData(); │
│ + }); │
│ }); │
│ } │
│ │
│ @@ -1428,6 +1483,71 @@ class UnifiedChart { │
│ return comparisonSelect ? comparisonSelect.value : 'race'; │
│ } │
│ │
│ + updateSortOptions() { │
│ + const sortBySelect = document.getElementById('sortBy'); │
│ + if (!sortBySelect) return; │
│ + │
│ + const currentComparisonType = this.getCurrentComparisonType(); │
│ + const percentOption = sortBySelect.querySelector('option[value="percent"]'); │
│ + │
│ + if (percentOption) { │
│ + // Show percentage option only for trinket comparisons │
│ + percentOption.style.display = currentComparisonType === 'trinket' ? '' : 'n │
│ one'; │
│ + │
│ + if (currentComparisonType === 'trinket') { │
│ + // Default to percentage for trinket comparisons │
│ + if (sortBySelect.value === 'dps' || !sortBySelect.value) { │
│ + sortBySelect.value = 'percent'; │
│ + } │
│ + } else { │
│ + // If percentage is selected but not available, switch to DPS │
│ + if (sortBySelect.value === 'percent') { │
│ + sortBySelect.value = 'dps'; │
│ + } │
│ + } │
│ + } │
│ + │
│ + // Update phase options for trinket comparisons │
│ + this.updatePhaseOptions(); │
│ + │
│ + // Update trinket callout visibility │
│ + this.updateTrinketCallout(); │
│ + } │
│ + │
│ + updateTrinketCallout() { │
│ + const trinketCallout = document.getElementById('trinket-callout'); │
│ + if (!trinketCallout) return; │
│ + │
│ + const currentComparisonType = this.getCurrentComparisonType(); │
│ + │
│ + if (currentComparisonType === 'trinket') { │
│ + trinketCallout.classList.remove('hidden'); │
│ + } else { │
│ + trinketCallout.classList.add('hidden'); │
│ + } │
│ + } │
│ + │
│ + updatePhaseOptions() { │
│ + const phaseSelect = document.getElementById('phase'); │
│ + if (!phaseSelect) return; │
│ + │
│ + // const currentComparisonType = this.getCurrentComparisonType(); │
│ + const preRaidOption = phaseSelect.querySelector('option[value="preRaid"]'); │
│ + │
│ + if (preRaidOption) { │
│ + if (!this.isRankingsMode) { │
│ + // Hide preRaid option for all comparison modes and force p1 │
│ + preRaidOption.style.display = 'none'; │
│ + if (phaseSelect.value === 'preRaid') { │
│ + phaseSelect.value = 'p1'; │
│ + } │
│ + } else { │
│ + // Show preRaid option for rankings mode │
│ + preRaidOption.style.display = ''; │
│ + } │
│ + } │
│ + } │
│ + │
│ getFileName() { │
│ const targetCount = document.getElementById('targetCount').value; │
│ const encounterType = document.getElementById('encounterType').value; │
│ @@ -1450,7 +1570,14 @@ class UnifiedChart { │
│ } else { │
│ const currentClass = this.getCurrentClass(); │
│ const currentSpec = this.getCurrentSpec(); │
│ - return `/data/comparison/${currentClass}/${currentSpec}/`; │
│ + const currentComparisonType = this.getCurrentComparisonType(); │
│ + │
│ + // Handle different comparison type paths │
│ + if (currentComparisonType === 'trinket') { │
│ + return `/data/comparison/trinkets/${currentClass}/${currentSpec}/`; │
│ + } else { │
│ + return `/data/comparison/race/${currentClass}/${currentSpec}/`; │
│ + } │
│ } │
│ } │
│ │
│ @@ -1471,6 +1598,9 @@ class UnifiedChart { │
│ return; │
│ } │
│ } │
│ + │
│ + // Update sort options based on comparison type │
│ + this.updateSortOptions(); │
│ │
│ const loadingEl = document.getElementById('loading'); │
│ const errorEl = document.getElementById('error'); │
│ @@ -1570,6 +1700,8 @@ class UnifiedChart { │
│ // Sort based on selected metric │
│ if (sortBy === 'stdev') { │
│ chartItems.sort((a, b) => a.value - b.value); // Lower stdev is better │
│ + } else if (sortBy === 'percent') { │
│ + chartItems.sort((a, b) => (b.percentIncrease || 0) - (a.percentIncrease || │
│ 0)); // Higher percentage is better │
│ } else { │
│ chartItems.sort((a, b) => b.value - a.value); // Higher values are better │
│ } │
│ @@ -1758,6 +1890,7 @@ class UnifiedChart { │
│ const displayValue = Math.round(item.value).toLocaleString(); │
│ const avgDps = Math.round(item.dps).toLocaleString(); │
│ const tooltip = sortBy === 'dps' ? displayValue : `${displayValue} (A │
│ vg: ${avgDps})`; │
│ + const chartDisplayValue = displayValue; // Rankings always show raw v │
│ alues │
│ │
│ const barColor = classColors[item.className] || '#666'; │
│ const dropdownContent = item.loadout ? generateLoadoutDropdown(item.l │
│ oadout, this.currentData) : ''; │
│ @@ -1774,7 +1907,7 @@ class UnifiedChart { │
│ <div class="chart-bar-track"> │
│ <div class="chart-bar" style="width: ${barWidth}%; background │
│ -color: ${barColor};"></div> │
│ </div> │
│ - <span class="chart-value" title="${tooltip}">${displayValue}</s │
│ pan> │
│ + <span class="chart-value" title="${tooltip}">${chartDisplayValu │
│ e}</span> │
│ </div> │
│ </div> │
│ ${dropdownContent} │
│ @@ -1793,8 +1926,30 @@ class UnifiedChart { │
│ const chartItems = []; │
│ │
│ for (const [itemName, itemData] of Object.entries(this.currentData.results)) │
│ { │
│ + let label = itemName.replace(/_/g, ' '); │
│ + let iconData = null; │
│ + │
│ + // For trinket comparisons, extract icon and ilvl from equipment │
│ + if (currentComparisonType === 'trinket' && itemData.loadout && itemData.loa │
│ dout.equipment) { │
│ + const trinket1 = itemData.loadout.equipment.items[12]; // Trinket slot 1 │
│ + const trinket2 = itemData.loadout.equipment.items[13]; // Trinket slot 2 │
│ + │
│ + // Find the trinket that's not empty (baseline has no trinkets) │
│ + const trinket = trinket1 && trinket1.id ? trinket1 : (trinket2 && trinket │
│ 2.id ? trinket2 : null); │
│ + │
│ + if (trinket) { │
│ + const ilvl = trinket.stats && trinket.stats.ilvl ? trinket.stats.ilvl : │
│ ''; │
│ + iconData = { │
│ + icon: trinket.icon, │
│ + ilvl: ilvl, │
│ + name: trinket.name │
│ + }; │
│ + label = ilvl ? `${ilvl}` : trinket.name; // Show just ilvl if available │
│ + } │
│ + } │
│ + │
│ chartItems.push({ │
│ - label: itemName.replace(/_/g, ' '), │
│ + label: label, │
│ dps: itemData.dps, │
│ max: itemData.max, │
│ min: itemData.min, │
│ @@ -1802,7 +1957,29 @@ class UnifiedChart { │
│ value: itemData[sortBy] || itemData.dps, │
│ category: itemName, │
│ rawName: itemName, │
│ - loadout: itemData.loadout || null │
│ + loadout: itemData.loadout || null, │
│ + iconData: iconData │
│ + }); │
│ + } │
│ + │
│ + // Find baseline for percentage calculations (only for trinket comparisons) │
│ + const baseline = this.currentData.results.baseline; │
│ + const baselineDps = baseline ? baseline.dps : null; │
│ + │
│ + // Add percentage and DPS increase calculations to chart items │
│ + if (baselineDps && currentComparisonType === 'trinket') { │
│ + chartItems.forEach(item => { │
│ + if (item.rawName === 'baseline') { │
│ + item.percentIncrease = 0; │
│ + item.dpsIncrease = 0; │
│ + } else { │
│ + // Calculate increases based on sort type (compare like-to-like) │
│ + const baselineValue = baseline[sortBy] || baseline.dps; │
│ + const itemValue = item[sortBy] || item.dps; │
│ + │
│ + item.percentIncrease = ((itemValue - baselineValue) / baselineValue * 1 │
│ 00); │
│ + item.dpsIncrease = itemValue - baselineValue; │
│ + } │
│ }); │
│ } │
│ │
│ @@ -1815,7 +1992,8 @@ class UnifiedChart { │
│ dps: `${comparisonLabel} DPS Rankings (Average)`, │
│ max: `${comparisonLabel} DPS Rankings (Maximum)`, │
│ min: `${comparisonLabel} DPS Rankings (Minimum)`, │
│ - stdev: `${comparisonLabel} DPS Consistency Rankings (Low StdDev = More Cons │
│ istent)` │
│ + stdev: `${comparisonLabel} DPS Consistency Rankings (Low StdDev = More Cons │
│ istent)`, │
│ + percent: `${comparisonLabel} Performance Rankings (% Increase)` │
│ }; │
│ │
│ // TODO: abstract │
│ @@ -1828,25 +2006,52 @@ class UnifiedChart { │
│ │
│ const displayValue = Math.round(item.value).toLocaleString(); │
│ const avgDps = Math.round(item.dps).toLocaleString(); │
│ - const tooltip = sortBy === 'dps' ? displayValue : `${displayValue} (A │
│ vg: ${avgDps})`; │
│ │
│ - const barColor = currentComparisonType === 'race' ? │
│ - (classColors[this.getCurrentClass()] || '#666') : '#666'; │
│ + // Format tooltip and display value based on sort type and comparison │
│ type │
│ + let tooltip, chartDisplayValue; │
│ + if (currentComparisonType === 'trinket' && item.percentIncrease !== u │
│ ndefined) { │
│ + const percentDisplay = item.percentIncrease === 0 ? 'Baseline' : `+ │
│ ${item.percentIncrease.toFixed(1)}%`; │
│ + const dpsIncreaseDisplay = item.dpsIncrease === 0 ? 'Baseline' : `+ │
│ ${Math.round(item.dpsIncrease).toLocaleString()}`; │
│ + │
│ + if (sortBy === 'percent') { │
│ + // When sorting by percentage, show percentage as main value │
│ + chartDisplayValue = percentDisplay; │
│ + tooltip = `${percentDisplay} (${avgDps} DPS)`; │
│ + } else if (sortBy === 'stdev') { │
│ + // For consistency sort, show actual standard deviation │
│ + chartDisplayValue = Math.round(item.stdev).toLocaleString(); │
│ + tooltip = `${chartDisplayValue} StdDev (${avgDps} DPS avg, lower │
│ is more consistent)`; │
│ + } else { │
│ + // For DPS-based sorts, show DPS increase from baseline │
│ + chartDisplayValue = dpsIncreaseDisplay; │
│ + tooltip = sortBy === 'dps' ? `${displayValue} DPS (${dpsIncreaseD │
│ isplay} vs baseline)` : `${displayValue} (Avg: ${avgDps} DPS, ${dpsIncreaseDisplay │
│ } vs baseline)`; │
│ + } │
│ + } else { │
│ + chartDisplayValue = displayValue; │
│ + tooltip = sortBy === 'dps' ? displayValue : `${displayValue} (Avg: │
│ ${avgDps})`; │
│ + } │
│ + │
│ + const barColor = classColors[this.getCurrentClass()] || '#666'; │
│ const dropdownContent = item.loadout ? generateLoadoutDropdown(item.l │
│ oadout, this.currentData) : ''; │
│ │
│ + // Create the label content with icon if trinket │
│ + const labelContent = item.iconData ? │
│ + `<img src="https://wow.zamimg.com/images/wow/icons/small/${item.ico │
│ nData.icon}.jpg" alt="${item.iconData.name}" class="trinket-icon" title="${item.ic │
│ onData.name}" /><span class="trinket-ilvl">${item.label}</span>` : │
│ + `<span class="chart-label">${item.label}</span>`; │
│ + │
│ return ` │
│ <div class="chart-item-wrapper"> │
│ <div class="chart-item" onclick="toggleChartItem(this.parentElement │
│ )"> │
│ <div class="chart-labels"> │
│ <span class="chart-rank">#${index + 1}</span> │
│ - <span class="chart-label">${item.label}</span> │
│ + ${labelContent} │
│ ${item.loadout ? '<span class="chart-expand-icon">?</span>' : ' │
│ '} │
│ </div> │
│ <div class="chart-bar-container"> │
│ <div class="chart-bar-track"> │
│ <div class="chart-bar" style="width: ${barWidth}%; background │
│ -color: ${barColor};"></div> │
│ </div> │
│ - <span class="chart-value" title="${tooltip}">${displayValue}</s │
│ pan> │
│ + <span class="chart-value" title="${tooltip}">${chartDisplayValu │
│ e}</span> │
│ </div> │
│ </div> │
│ ${dropdownContent} │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...src/layouts/ChartLayout.astro ───┐
│ diff --git a/web/src/layouts/ChartLayout.ast │
│ ro b/web/src/layouts/ChartLayout.astro │
│ index c376218..c2c6292 100644 │
│ --- a/web/src/layouts/ChartLayout.astro │
│ +++ b/web/src/layouts/ChartLayout.astro │
│ @@ -269,6 +269,21 @@ const isFixedClassSpec │
│ = fixedClass && fixedSpec; │
│ flex: 1; │
│ } │
│ │
│ + .trinket-icon { │
│ + width: 20px; │
│ + height: 20px; │
│ + border-radius: 3px; │
│ + margin-right: 8px; │
│ + vertical-align: middle; │
│ + border: 1px solid rgba(255,255,255, │
│ 0.2); │
│ + } │
│ + │
│ + .trinket-ilvl { │
│ + font-weight: 600; │
│ + color: var(--text-primary); │
│ + vertical-align: middle; │
│ + } │
│ + │
│ .chart-bar-container { │
│ display: flex; │
│ align-items: center; │
│ @@ -771,6 +786,34 @@ const isFixedClassSpec │
│ = fixedClass && fixedSpec; │
│ │
│ <div id="metadata-container"></div> │
│ │
│ + <!-- Trinket comparison callout --> │
│ + <div id="trinket-callout" class="hidd │
│ en"> │
│ + <Callout type="note"> │
│ + <p> │
│ + <em>Trinket rankings are still │
│ a work-in-progress.</em> │
│ + </p> │
│ + <p><strong>Current Implementation │
│ </strong>:</p> │
│ + <p> │
│ + We take the selected phase's bi │
│ s profile gearset, remove the trinkets, and │
│ simulate to get the <strong>baseline</strong │
│ > result. We then generate a gearset for eac │
│ h trinket and compare the results against th │
│ e baseline. │
│ + </p> │
│ + <p> │
│ + This implementation comes with │
│ some caveats that can scew the results: │
│ + </p> │
│ + <ul> │
│ + <li>We <strong>don't</strong> r │
│ e-reforge the gearset for every trinket</li> │
│ + <li>Ideally every trinket would │
│ come with it's own optimized gearset</li> │
│ + <li>This is simulating the trin │
│ kets <strong>individually</strong>, not in p │
│ airs</li> │
│ + <li>Like all other simulations, │
│ keep in mind WoWSims is still in <strong>al │
│ pha</strong> so not all spec/trinket impleme │
│ ntations are not complete</li> │
│ + </ul> │
│ + <p> │
│ + With all that in mind, you shou │
│ ld only consider this list as a <em>rough ap │
│ proximation</em> of the value of each indivi │
│ dual trinket. │
│ + </p> │
│ + <p> │
│ + In the future we will reforge t │
│ he gearset for each trinket to get a more ac │
│ curate value. │
│ + </p> │
│ + </Callout> │
│ + </div> │
│ + │
│ <div class="controls"> │
│ <h3 class="controls-title"> │
│ {isRankingsMode ? "Simulation Opt │
│ ions" : (isFixedClassSpec ? "Simulation Opti │
│ ons" : "Comparison Options")} │
│ @@ -781,7 +824,7 @@ const isFixedClassSpec = │
│ fixedClass && fixedSpec; │
│ <label for="comparisonType">C │
│ ompare:</label> │
│ <select id="comparisonType"> │
│ <option value="race">Races< │
│ /option> │
│ - <option value="trinket" dis │
│ abled>Trinkets (Coming Soon)</option> │
│ + <option value="trinket">Tri │
│ nkets</option> │
│ </select> │
│ </div> │
│ )} │
│ @@ -852,6 +895,7 @@ const isFixedClassSpec = │
│ fixedClass && fixedSpec; │
│ <div class="control-group"> │
│ <label for="sortBy">Sort By:</l │
│ abel> │
│ <select id="sortBy"> │
│ + <option value="percent">Avera │
│ ge Percentage Increase</option> │
│ <option value="dps">Average D │
│ PS</option> │
│ <option value="max">Max DPS</ │
│ option> │
│ <option value="min">Min DPS</ │
│ option> │
│ @@ -955,9 +999,9 @@ const formatRace = (race │
│ ) => { │
│ return race.replace('Race', '').replace(/ │
│ ([A-Z])/g, ' $1').trim(); │
│ }; │
│ │
│ -const formatClass = (className) => { │
│ - return className.replace('Class', '').rep │
│ lace(/([A-Z])/g, ' $1').trim(); │
│ -}; │
│ +// const formatClass = (className) => { │
│ +// return className.replace('Class', ''). │
│ replace(/([A-Z])/g, ' $1').trim(); │
│ +// }; │
│ │
│ const formatTalents = (talents) => { │
│ if (!talents || !talents.talents || !Arra │
│ y.isArray(talents.talents)) { │
│ @@ -1132,7 +1176,7 @@ const formatEquipmentS │
│ ummary = (items) => { │
│ │
│ │
│ // Generate loadout dropdown HTML │
│ -const generateLoadoutDropdown = (loadout, c │
│ hartData) => { │
│ +const generateLoadoutDropdown = (loadout, _ │
│ chartData) => { │
│ const sections = formatLoadout(loadout); │
│ if (!sections || sections.length === 0) r │
│ eturn ''; │
│ │
│ @@ -1349,6 +1393,12 @@ class UnifiedChart { │
│ comparisonSelect.value = comparison │
│ Type; │
│ } │
│ } │
│ + │
│ + // Initialize phase options for compari │
│ son modes │
│ + if (!this.isRankingsMode) { │
│ + this.updatePhaseOptions(); │
│ + this.updateTrinketCallout(); │
│ + } │
│ } │
│ │
│ bindEvents() { │
│ @@ -1365,7 +1415,12 @@ class UnifiedChart { │
│ │
│ const dataSelects = document.querySel │
│ ectorAll('#spec, #comparisonType'); │
│ dataSelects.forEach(select => { │
│ - select.addEventListener('change', ( │
│ ) => this.loadData()); │
│ + select.addEventListener('change', ( │
│ ) => { │
│ + if (select.id === 'comparisonType │
│ ') { │
│ + this.updateSortOptions(); │
│ + } │
│ + this.loadData(); │
│ + }); │
│ }); │
│ } │
│ │
│ @@ -1428,6 +1483,71 @@ class UnifiedChart { │
│ return comparisonSelect ? comparisonSel │
│ ect.value : 'race'; │
│ } │
│ │
│ + updateSortOptions() { │
│ + const sortBySelect = document.getElemen │
│ tById('sortBy'); │
│ + if (!sortBySelect) return; │
│ + │
│ + const currentComparisonType = this.getC │
│ urrentComparisonType(); │
│ + const percentOption = sortBySelect.quer │
│ ySelector('option[value="percent"]'); │
│ + │
│ + if (percentOption) { │
│ + // Show percentage option only for tr │
│ inket comparisons │
│ + percentOption.style.display = current │
│ ComparisonType === 'trinket' ? '' : 'none'; │
│ + │
│ + if (currentComparisonType === 'trinke │
│ t') { │
│ + // Default to percentage for trinke │
│ t comparisons │
│ + if (sortBySelect.value === 'dps' || │
│ !sortBySelect.value) { │
│ + sortBySelect.value = 'percent'; │
│ + } │
│ + } else { │
│ + // If percentage is selected but no │
│ t available, switch to DPS │
│ + if (sortBySelect.value === 'percent │
│ ') { │
│ + sortBySelect.value = 'dps'; │
│ + } │
│ + } │
│ + } │
│ + │
│ + // Update phase options for trinket com │
│ parisons │
│ + this.updatePhaseOptions(); │
│ + │
│ + // Update trinket callout visibility │
│ + this.updateTrinketCallout(); │
│ + } │
│ + │
│ + updateTrinketCallout() { │
│ + const trinketCallout = document.getElem │
│ entById('trinket-callout'); │
│ + if (!trinketCallout) return; │
│ + │
│ + const currentComparisonType = this.getC │
│ urrentComparisonType(); │
│ + │
│ + if (currentComparisonType === 'trinket' │
│ ) { │
│ + trinketCallout.classList.remove('hidd │
│ en'); │
│ + } else { │
│ + trinketCallout.classList.add('hidden' │
│ ); │
│ + } │
│ + } │
│ + │
│ + updatePhaseOptions() { │
│ + const phaseSelect = document.getElement │
│ ById('phase'); │
│ + if (!phaseSelect) return; │
│ + │
│ + // const currentComparisonType = this.g │
│ etCurrentComparisonType(); │
│ + const preRaidOption = phaseSelect.query │
│ Selector('option[value="preRaid"]'); │
│ + │
│ + if (preRaidOption) { │
│ + if (!this.isRankingsMode) { │
│ + // Hide preRaid option for all comp │
│ arison modes and force p1 │
│ + preRaidOption.style.display = 'none │
│ '; │
│ + if (phaseSelect.value === 'preRaid' │
│ ) { │
│ + phaseSelect.value = 'p1'; │
│ + } │
│ + } else { │
│ + // Show preRaid option for rankings │
│ mode │
│ + preRaidOption.style.display = ''; │
│ + } │
│ + } │
│ + } │
│ + │
│ getFileName() { │
│ const targetCount = document.getElement │
│ ById('targetCount').value; │
│ const encounterType = document.getEleme │
│ ntById('encounterType').value; │
│ @@ -1450,7 +1570,14 @@ class UnifiedChart { │
│ } else { │
│ const currentClass = this.getCurrentC │
│ lass(); │
│ const currentSpec = this.getCurrentSp │
│ ec(); │
│ - return `/data/comparison/${currentCla │
│ ss}/${currentSpec}/`; │
│ + const currentComparisonType = this.ge │
│ tCurrentComparisonType(); │
│ + │
│ + // Handle different comparison type p │
│ aths │
│ + if (currentComparisonType === 'trinke │
│ t') { │
│ + return `/data/comparison/trinkets/$ │
│ {currentClass}/${currentSpec}/`; │
│ + } else { │
│ + return `/data/comparison/race/${cur │
│ rentClass}/${currentSpec}/`; │
│ + } │
│ } │
│ } │
│ │
│ @@ -1471,6 +1598,9 @@ class UnifiedChart { │
│ return; │
│ } │
│ } │
│ + │
│ + // Update sort options based on compari │
│ son type │
│ + this.updateSortOptions(); │
│ │
│ const loadingEl = document.getElementBy │
│ Id('loading'); │
│ const errorEl = document.getElementById │
│ ('error'); │
│ @@ -1570,6 +1700,8 @@ class UnifiedChart { │
│ // Sort based on selected metric │
│ if (sortBy === 'stdev') { │
│ chartItems.sort((a, b) => a.value - b │
│ .value); // Lower stdev is better │
│ + } else if (sortBy === 'percent') { │
│ + chartItems.sort((a, b) => (b.percentI │
│ ncrease || 0) - (a.percentIncrease || 0)); / │
│ / Higher percentage is better │
│ } else { │
│ chartItems.sort((a, b) => b.value - a │
│ .value); // Higher values are better │
│ } │
│ @@ -1758,6 +1890,7 @@ class UnifiedChart { │
│ const displayValue = Math.round │
│ (item.value).toLocaleString(); │
│ const avgDps = Math.round(item. │
│ dps).toLocaleString(); │
│ const tooltip = sortBy === 'dps │
│ ' ? displayValue : `${displayValue} (Avg: ${ │
│ avgDps})`; │
│ + const chartDisplayValue = displ │
│ ayValue; // Rankings always show raw values │
│ │
│ const barColor = classColors[it │
│ em.className] || '#666'; │
│ const dropdownContent = item.lo │
│ adout ? generateLoadoutDropdown(item.loadout │
│ , this.currentData) : ''; │
│ @@ -1774,7 +1907,7 @@ class UnifiedChart { │
│ <div class="chart-bar-tra │
│ ck"> │
│ <div class="chart-bar" │
│ style="width: ${barWidth}%; background-color │
│ : ${barColor};"></div> │
│ </div> │
│ - <span class="chart-value" │
│ title="${tooltip}">${displayValue}</span> │
│ + <span class="chart-value" │
│ title="${tooltip}">${chartDisplayValue}</sp │
│ an> │
│ </div> │
│ </div> │
│ ${dropdownContent} │
│ @@ -1793,8 +1926,30 @@ class UnifiedChart { │
│ const chartItems = []; │
│ │
│ for (const [itemName, itemData] of Obje │
│ ct.entries(this.currentData.results)) { │
│ + let label = itemName.replace(/_/g, ' │
│ '); │
│ + let iconData = null; │
│ + │
│ + // For trinket comparisons, extract i │
│ con and ilvl from equipment │
│ + if (currentComparisonType === 'trinke │
│ t' && itemData.loadout && itemData.loadout.e │
│ quipment) { │
│ + const trinket1 = itemData.loadout.e │
│ quipment.items[12]; // Trinket slot 1 │
│ + const trinket2 = itemData.loadout.e │
│ quipment.items[13]; // Trinket slot 2 │
│ + │
│ + // Find the trinket that's not empt │
│ y (baseline has no trinkets) │
│ + const trinket = trinket1 && trinket │
│ 1.id ? trinket1 : (trinket2 && trinket2.id ? │
│ trinket2 : null); │
│ + │
│ + if (trinket) { │
│ + const ilvl = trinket.stats && tri │
│ nket.stats.ilvl ? trinket.stats.ilvl : ''; │
│ + iconData = { │
│ + icon: trinket.icon, │
│ + ilvl: ilvl, │
│ + name: trinket.name │
│ + }; │
│ + label = ilvl ? `${ilvl}` : trinke │
│ t.name; // Show just ilvl if available │
│ + } │
│ + } │
│ + │
│ chartItems.push({ │
│ - label: itemName.replace(/_/g, ' '), │
│ + label: label, │
│ dps: itemData.dps, │
│ max: itemData.max, │
│ min: itemData.min, │
│ @@ -1802,7 +1957,29 @@ class UnifiedChart { │
│ value: itemData[sortBy] || itemData │
│ .dps, │
│ category: itemName, │
│ rawName: itemName, │
│ - loadout: itemData.loadout || null │
│ + loadout: itemData.loadout || null, │
│ + iconData: iconData │
│ + }); │
│ + } │
│ + │
│ + // Find baseline for percentage calcula │
│ tions (only for trinket comparisons) │
│ + const baseline = this.currentData.resul │
│ ts.baseline; │
│ + const baselineDps = baseline ? baseline │
│ .dps : null; │
│ + │
│ + // Add percentage and DPS increase calc │
│ ulations to chart items │
│ + if (baselineDps && currentComparisonTyp │
│ e === 'trinket') { │
│ + chartItems.forEach(item => { │
│ + if (item.rawName === 'baseline') { │
│ + item.percentIncrease = 0; │
│ + item.dpsIncrease = 0; │
│ + } else { │
│ + // Calculate increases based on s │
│ ort type (compare like-to-like) │
│ + const baselineValue = baseline[so │
│ rtBy] || baseline.dps; │
│ + const itemValue = item[sortBy] || │
│ item.dps; │
│ + │
│ + item.percentIncrease = ((itemValu │
│ e - baselineValue) / baselineValue * 100); │
│ + item.dpsIncrease = itemValue - ba │
│ selineValue; │
│ + } │
│ }); │
│ } │
│ │
│ @@ -1815,7 +1992,8 @@ class UnifiedChart { │
│ dps: `${comparisonLabel} DPS Rankings │
│ (Average)`, │
│ max: `${comparisonLabel} DPS Rankings │
│ (Maximum)`, │
│ min: `${comparisonLabel} DPS Rankings │
│ (Minimum)`, │
│ - stdev: `${comparisonLabel} DPS Consis │
│ tency Rankings (Low StdDev = More Consistent │
│ )` │
│ + stdev: `${comparisonLabel} DPS Consis │
│ tency Rankings (Low StdDev = More Consistent │
│ )`, │
│ + percent: `${comparisonLabel} Performa │
│ nce Rankings (% Increase)` │
│ }; │
│ │
│ // TODO: abstract │
│ @@ -1828,25 +2006,52 @@ class UnifiedChart { │
│ │
│ const displayValue = Math.round │
│ (item.value).toLocaleString(); │
│ const avgDps = Math.round(item. │
│ dps).toLocaleString(); │
│ - const tooltip = sortBy === 'dps │
│ ' ? displayValue : `${displayValue} (Avg: ${ │
│ avgDps})`; │
│ │
│ - const barColor = currentCompari │
│ sonType === 'race' ? │
│ - (classColors[this.getCurrentC │
│ lass()] || '#666') : '#666'; │
│ + // Format tooltip and display v │
│ alue based on sort type and comparison type │
│ + let tooltip, chartDisplayValue; │
│ + if (currentComparisonType === ' │
│ trinket' && item.percentIncrease !== undefin │
│ ed) { │
│ + const percentDisplay = item.p │
│ ercentIncrease === 0 ? 'Baseline' : `+${item │
│ .percentIncrease.toFixed(1)}%`; │
│ + const dpsIncreaseDisplay = it │
│ em.dpsIncrease === 0 ? 'Baseline' : `+${Math │
│ .round(item.dpsIncrease).toLocaleString()}`; │
│ + │
│ + if (sortBy === 'percent') { │
│ + // When sorting by percenta │
│ ge, show percentage as main value │
│ + chartDisplayValue = percent │
│ Display; │
│ + tooltip = `${percentDisplay │
│ } (${avgDps} DPS)`; │
│ + } else if (sortBy === 'stdev' │
│ ) { │
│ + // For consistency sort, sh │
│ ow actual standard deviation │
│ + chartDisplayValue = Math.ro │
│ und(item.stdev).toLocaleString(); │
│ + tooltip = `${chartDisplayVa │
│ lue} StdDev (${avgDps} DPS avg, lower is mor │
│ e consistent)`; │
│ + } else { │
│ + // For DPS-based sorts, sho │
│ w DPS increase from baseline │
│ + chartDisplayValue = dpsIncr │
│ easeDisplay; │
│ + tooltip = sortBy === 'dps' │
│ ? `${displayValue} DPS (${dpsIncreaseDisplay │
│ } vs baseline)` : `${displayValue} (Avg: ${a │
│ vgDps} DPS, ${dpsIncreaseDisplay} vs baselin │
│ e)`; │
│ + } │
│ + } else { │
│ + chartDisplayValue = displayVa │
│ lue; │
│ + tooltip = sortBy === 'dps' ? │
│ displayValue : `${displayValue} (Avg: ${avgD │
│ ps})`; │
│ + } │
│ + │
│ + const barColor = classColors[th │
│ is.getCurrentClass()] || '#666'; │
│ const dropdownContent = item.lo │
│ adout ? generateLoadoutDropdown(item.loadout │
│ , this.currentData) : ''; │
│ │
│ + // Create the label content wit │
│ h icon if trinket │
│ + const labelContent = item.iconD │
│ ata ? │
│ + `<img src="https://wow.zamimg │
│ .com/images/wow/icons/small/${item.iconData. │
│ icon}.jpg" alt="${item.iconData.name}" class │
│ ="trinket-icon" title="${item.iconData.name} │
│ " /><span class="trinket-ilvl">${item.label} │
│ </span>` : │
│ + `<span class="chart-label">${ │
│ item.label}</span>`; │
│ + │
│ return ` │
│ <div class="chart-item-wrapper" │
│ > │
│ <div class="chart-item" oncli │
│ ck="toggleChartItem(this.parentElement)"> │
│ <div class="chart-labels"> │
│ <span class="chart-rank"> │
│ #${index + 1}</span> │
│ - <span class="chart-label" │
│ >${item.label}</span> │
│ + ${labelContent} │
│ ${item.loadout ? '<span c │
│ lass="chart-expand-icon">?</span>' : ''} │
│ </div> │
│ <div class="chart-bar-conta │
│ iner"> │
│ <div class="chart-bar-tra │
│ ck"> │
│ <div class="chart-bar" │
│ style="width: ${barWidth}%; background-color │
│ : ${barColor};"></div> │
│ </div> │
│ - <span class="chart-value" │
│ title="${tooltip}">${displayValue}</span> │
│ + <span class="chart-value" │
│ title="${tooltip}">${chartDisplayValue}</sp │
│ an> │
│ </div> │
│ </div> │
│ ${dropdownContent} │
└──────────────────────────────────────────────┘
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET