Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion lib/trinketpage.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ footer a{color:var(--amber)}
<h1>Trinket <em>tier list</em></h1>
<p class="lede">${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)}.</p>
<div class="stats">
${stat(String(specs.length),'specializations')}
Expand Down
18 changes: 17 additions & 1 deletion lib/trinkets.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,22 @@ export function trinketCandidates(spec,season,catalog,{drops,steps,equal=false})
return list;
}

// The Great Vault gives Mythic+ loot on a higher track than the dungeon's own chest: in Midnight Season 2 the vault
// reaches the Myth track, the chest stops at Hero. With the vault on, a Mythic+ trinket runs up to the vault's level
// and keeps the chest's level as one of its steps. `choice` is {track, level}; the default is the top track's top.
export function vaultLevel(season,choice){
if(choice===false||choice?.enabled===false)return null;
const track=season.tracks.find(t=>t.id===Number(choice?.track))||season.tracks.at(-1);
const level=track.levels.find(l=>l.level===Number(choice?.level))||track.levels.at(-1);
return {bonusId:level.bonusId,itemLevel:level.itemLevel,label:`Great Vault · ${track.name} ${level.level}/${level.max}`,track:track.id,level:level.level};
}
export function withVault(drops,vault){
if(!vault||!drops.of.mplus)return drops;
const chest=drops.of.mplus;
const mplus={of:source=>{const own=chest.of(source);return vault.itemLevel>own.itemLevel?vault:own;}};
return {...drops,of:{...drops.of,mplus},public:{...drops.public,vault:{track:vault.track,level:vault.level,label:vault.label,itemLevel:vault.itemLevel}}};
}

export const trinketCount=spec=>new Set(spec.candidates.map(c=>c.itemId)).size;

export async function prepareTrinkets(request,catalog,season,talentData,source,scenarios=1){
Expand All @@ -76,7 +92,7 @@ export async function prepareTrinkets(request,catalog,season,talentData,source,s
if(!limits.finalists.includes(finalists))throw new Error('Choose a supported final round size.');
const equal=!!options.equal;
const steps=levelSteps(season,options.steps);
const drops=dropLevels(season,{sources:options.sources,craftedCap:await craftedItemLevel(source)});
const drops=withVault(dropLevels(season,{sources:options.sources,craftedCap:await craftedItemLevel(source)}),equal?null:vaultLevel(season,options.vault));
const keys=options.specs===undefined?available.map(s=>s.key):options.specs;
if(!Array.isArray(keys)||!keys.length)throw new Error('Choose at least one specialization.');
if(keys.length>limits.specs)throw new Error(`Choose at most ${limits.specs} specializations.`);
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "simc-lab",
"version": "1.17.0",
"version": "1.18.0",
"private": true,
"type": "module",
"scripts": {
Expand Down
19 changes: 15 additions & 4 deletions public/trinkets.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 17 additions & 1 deletion tests/trinkets.test.mjs
Original file line number Diff line number Diff line change
@@ -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}))});
Expand Down Expand Up @@ -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]);
Expand Down
Loading