diff --git a/README.md b/README.md index 1cbeac2..b83b7a2 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ A score is the share of that whole character the weapon carries, so the tiers re Trinket Lab is one trinket tier list per specialization, charted the way Bloodmallet charts them. It runs on the same reference characters as Weapon Lab — SimulationCraft's own profiles, SimC Lab's own where SimulationCraft has none — with both trinkets taken off. That character, wearing no trinket at all, is the baseline, and each trinket is worn alone in the first slot with the second left empty, so one trinket's worth never depends on which second trinket a profile's authors happened to pick. Healing specializations are left out: SimulationCraft cannot simulate healing, and a trinket is mostly its effect, which no stat score can value. -The candidates are every trinket in the active season's loot tables — raid, Mythic+ (reissued older dungeon trinkets included, through the season's own tables), delves and crafting — filtered for the specialization as Upgrade Finder filters them. Each is simulated at every chosen item level, the top of an upgrade track (Champion, Hero and Myth by default), below the level its own source can give it, and then at that level: a delve or dungeon trinket stops at the top of the Hero track, a raid trinket reaches the Myth track and a last boss's its own higher drop level, and a crafted trinket its crafting cap. Source limits are set as in Weapon Lab, and equal footing shows every trinket at every level instead. The bar for a trinket is what it adds over no trinket, split at each level; its tier is read at its top level, with the same thresholds as Weapon Lab. +The candidates are every trinket in the active season's loot tables — raid, Mythic+ (reissued older dungeon trinkets included, through the season's own tables), delves and crafting — filtered for the specialization as Upgrade Finder filters them. Each is simulated at every chosen item level, the top of an upgrade track (Champion, Hero and Myth by default), below the level its own source can give it, and then at that level: a delve or dungeon trinket stops at the top of the Hero track, a raid trinket reaches the Myth track and a last boss's its own higher drop level, and a crafted trinket its crafting cap. Source limits are set as in Weapon Lab. The Great Vault is on by default: it gives Mythic+ loot on the Myth track, above the dungeon chest's Hero track, so a dungeon trinket runs up to the vault's level (Myth 6/6 unless you choose another) and the chest's level stays one of its steps. Crafted trinkets have no track; their level is chosen from the item levels the season uses, up to the crafting cap SimulationCraft's own profiles craft to. Equal footing shows every trinket at every level instead. The bar for a trinket is what it adds over no trinket, split at each level; its tier is read at its top level, with the same thresholds as Weapon Lab. A specialization with more trinkets than the final round size (16, 32 or 64; 64 covers every list in Midnight Season 2) is screened first, one run per trinket at its top level, and the best go on to be simulated at every level; the rest keep their screened number and no curve. Tanks are ranked on the weighted DPS and survival score with a calibrated boss each, and Augmentation on the raid's damage against its measured share, as in Weapon Lab. A trinket that completes an item set with the reference gear — Zul'jin's Guillotine Technique beside Maze'roa — is marked, and the rest are measured from the best trinket without one. A job is limited to 8,000 trinket and item level runs, counting every scenario; a full season list is about 2,400. diff --git a/desktop/package.json b/desktop/package.json index 7a814b6..acfae11 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -2,7 +2,7 @@ "name": "simc-lab", "productName": "SimC Lab", "desktopName": "simc-lab.desktop", - "version": "1.17.0", + "version": "1.18.0", "description": "Local SimulationCraft workbench with upgrade finder and tank simulation.", "author": "Roburmaster", "private": true, diff --git a/lib/trinketpage.mjs b/lib/trinketpage.mjs index 92f33d3..41834c4 100644 --- a/lib/trinketpage.mjs +++ b/lib/trinketpage.mjs @@ -195,7 +195,7 @@ footer a{color:var(--amber)}
${src.equal ? `Every trinket at every chosen item level (${escape((src.steps||[]).join(', '))}), equal footing: this answers what a trinket is worth, not what you can reach with it.` - : `Each trinket up to the level its own source can give it: ${escape([src.raid&&`raid ${src.raid.label}`,src.mplus&&`Mythic+ ${src.mplus.label}`,src.delves&&`delves ${src.delves.label}`,src.crafted&&`crafted ${src.crafted.label}`].filter(Boolean).join(', '))}. A trinket's tier is read at the highest of those levels.`} + : `Each trinket up to the level its own source can give it: ${escape([src.raid&&`raid ${src.raid.label}`,src.mplus&&`Mythic+ ${src.mplus.label}`,src.delves&&`delves ${src.delves.label}`,src.vault&&`Mythic+ through the Great Vault ${src.vault.label.replace(/^Great Vault · /,'')}`,src.crafted&&`crafted ${src.crafted.label}`].filter(Boolean).join(', '))}. A trinket's tier is read at the highest of those levels.`} Each trinket is worn alone, the second slot empty, and the bar is what it adds over the same character with no trinket at all. Simulated locally with SimulationCraft ${escape(job.engine.version)} on WoW ${escape(job.engine.wowVersion)}.
A specialization with more trinkets than this is screened first, one run per trinket at its top level; only the best go on to be simulated at every level. The rest keep their screened number and no curve.
How far each source can take a trinket. Only the raid reaches the Myth track, and its last bosses drop above it. Crafted trinkets sit at their own cap${data.craftedCap?` — item level ${data.craftedCap} this season`:''}.
- + ${['mplus','delves'].map(kind=>``).join('')} + + +The vault gives Mythic+ loot on the Myth track, above what the dungeon's own chest can. With it on, a dungeon trinket runs up to the vault's level, and the chest's level stays one of its steps.
Equal footing answers what a trinket is worth rather than what you can reach with it, so it shows trinkets at levels their source cannot give.
The reference character keeps its gear, gems and enchants and loses both trinkets; one trinket goes in the first slot. Tank specializations are ranked on the same weighted DPS and survival score as the rest of the app, with a boss calibrated for each of them.
`; } + // Crafted trinkets have no upgrade track: the choice is an item level, every one the season's tracks use up to the + // crafting cap, and the cap itself, which is the level SimulationCraft's own season profiles craft to. + function craftedOptions(){ + const cap=data.craftedCap||data.tracks.at(-2)?.levels.at(-1).itemLevel; + const levels=[...new Set([...data.tracks.flatMap(t=>t.levels.map(l=>l.itemLevel)).filter(l=>l${src.equal?`Equal footing: every trinket at ${esc((src.steps||[]).join(', '))}, including levels its source cannot give.`:`Each trinket up to the level its source can give: ${esc([src.raid&&`raid ${src.raid.label}`,src.mplus&&`Mythic+ ${src.mplus.label}`,src.delves&&`delves ${src.delves.label}`,src.crafted&&`crafted ${src.crafted.label}`].filter(Boolean).join(' · '))}.`} One trinket worn, the other slot empty; the gain is over no trinket at all.
Screening uses up to ${number(lab.screen.iterations)} iterations at ${lab.screen.targetError}% target error; the final round uses ${number(job.settings.iterations)} iterations at ${job.settings.targetError}%.
${levels.map(l=>`${l}`).join('')}
${src.equal?`Equal footing: every trinket at ${esc((src.steps||[]).join(', '))}, including levels its source cannot give.`:`Each trinket up to the level its source can give: ${esc([src.raid&&`raid ${src.raid.label}`,src.mplus&&`Mythic+ ${src.mplus.label}`,src.delves&&`delves ${src.delves.label}`,src.vault&&`Mythic+ through the Great Vault ${src.vault.label.replace(/^Great Vault · /,'')}`,src.crafted&&`crafted ${src.crafted.label}`].filter(Boolean).join(' · '))}.`} One trinket worn, the other slot empty; the gain is over no trinket at all.
Screening uses up to ${number(lab.screen.iterations)} iterations at ${lab.screen.targetError}% target error; the final round uses ${number(job.settings.iterations)} iterations at ${job.settings.targetError}%.
${levels.map(l=>`${l}`).join('')}
Left out: ${esc(lab.skipped.map(s=>s.label).join(', '))}.
`; if(job.results.some(r=>Number.isFinite(r.rank)))html+=`Open the tier list page ↗Download itOne page with every class and specialization and its chart, ready to read or send on.
`; for(const stage of (job.stages||[]).filter(st=>st.status==='failed'&&st.stage===0))html+=`${esc(specs.find(s=>s.key===stage.spec)?.label||stage.spec)}: tank boss calibration failed — ${esc(stage.error)}
`; diff --git a/tests/trinkets.test.mjs b/tests/trinkets.test.mjs index 91cd570..e68807d 100644 --- a/tests/trinkets.test.mjs +++ b/tests/trinkets.test.mjs @@ -1,6 +1,6 @@ import test from 'node:test'; import assert from 'node:assert/strict'; -import {withoutTrinkets,levelSteps,trinketCandidates,selectTopTrinkets,gainOf,specSteps,trinketCount,topRows} from '../lib/trinkets.mjs'; +import {vaultLevel,withVault,withoutTrinkets,levelSteps,trinketCandidates,selectTopTrinkets,gainOf,specSteps,trinketCount,topRows} from '../lib/trinkets.mjs'; import {dropLevels,rankWeaponRows} from '../lib/weapons.mjs'; const track=(id,name,base,bonus)=>({id,name,levels:[1,2,3].map(level=>({level,max:3,bonusId:bonus+level,itemLevel:base+3*level}))}); @@ -69,6 +69,22 @@ test('a trinket that completes a set with the reference gear is marked',()=>{ assert.equal(list.find(c=>c.name==='Idol').set,undefined); }); +test('the Great Vault takes a dungeon trinket to the Myth track, and the chest level stays a step',()=>{ + const vault=vaultLevel(season); + assert.equal(vault.label,'Great Vault · Myth 3/3');assert.equal(vault.itemLevel,327); + const vaulted=withVault(dropLevels(season,{craftedCap:331}),vault); + const list=trinketCandidates(arms,season,catalog,{drops:vaulted.of,steps}); + const flask=list.filter(c=>c.name==='Flask'); + assert.deepEqual(flask.map(c=>c.itemLevel),[299,314,327]); + assert.equal(flask.at(-1).levelLabel,'Great Vault · Myth 3/3'); + assert.match(flask.at(-1).line,/bonus_id=12803$/); + assert.deepEqual(list.filter(c=>c.name==='Idol').map(c=>c.itemLevel),[299,314,327],'raid is unchanged'); + assert.equal(vaulted.public.vault.itemLevel,327); + assert.equal(vaultLevel(season,false),null); + assert.equal(withVault(dropLevels(season,{}),null).public.vault,undefined); + assert.equal(vaultLevel(season,{track:617,level:2}).itemLevel,311,'a lower vault choice'); +}); + test('equal footing shows every trinket at every step',()=>{ const list=trinketCandidates(arms,season,catalog,{drops,steps,equal:true}); for(const id of [50,51,52,53,54])assert.deepEqual(list.filter(c=>c.itemId===id).map(c=>c.itemLevel),[299,314,327]);