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: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,8 @@ SimC's built-in tank dummy uses level-70 damage values and never threatens a Mid
- Tank-busters: a heavy melee hit every 30 seconds. SimC does not time defensive cooldowns to incoming busters, so a fixed buster size either kills a spec every time or never. The buster is therefore resized until the imported gear dies in the chosen share of fights (25% by default). Better gear can then survive more often, worse gear less often.
- Healing: healers top the tank up to full health on a fixed rhythm (every 5 seconds by default). Tanks can die (`infinite_health=0`).

- Adds: every target beyond the first is a mob that attacks the tank (Targets 3 or 5, or the "compare 3 and 5 targets" box), each with an auto attack of 40% of the boss's swing (adjustable under Boss details, "Each add hits for"). The boss is still calibrated alone, so calibration is the same for one target and for five, and the adds then add their damage on top: more targets means more damage taken and a higher chance to die, and armor, avoidance and self-healing are worth what they are against a pack, not against one hit. The adds stand and swing for the whole fight, and the tank's own damage cleaves them. SimC's own adds (Hectic Add Cleave, Dungeon Slice) are pets that never attack, and Dungeon Slice, Mythic+ route and the Silvermoon dummies have no incoming damage at all, so those fight styles are off for a tank; use Patchwerk, Casting Patchwerk or the movement styles with more targets. Weapon Lab and Trinket Lab keep the single-boss fight they always had.

Presets are Mythic+ dungeon (3% health per second, 45% starting buster), Heroic raid (4%, 50%) and Mythic raid (5%, 55%), all adjustable under Boss details. The job result states the calibrated boss and how often the current gear died during calibration.

Survival is the sum of two percentages: the reduction in net damage taken (damage taken minus the tank's own healing, relative to the boss's sustained damage), and the change in the share of the fight survived. Absorbs are already removed from damage taken. Score = weight × survival + (1 − weight) × DPS change, with the weight set by a slider (50% by default). Upgrade Finder screens and selects finalists by score.
Expand Down
4 changes: 2 additions & 2 deletions desktop/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 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.40.0",
"version": "1.41.0",
"description": "Local SimulationCraft workbench with upgrade finder and tank simulation.",
"author": "Roburmaster",
"private": true,
Expand Down
11 changes: 7 additions & 4 deletions lib/engine.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {isArmoryProfile} from './armory.mjs';
import {isDummyStyle,dummyLines,dummyCount,dummyScenario,normalizeDummyHealth,needsCalibration,bands as dummyBands,calibrate as calibrateDummies} from './dummies.mjs';
import {isRouteStyle,loadRoutes,resolveRoute,routeLines,trialRoute,finishRoute,pullLengths,routeResult} from './mplus.mjs';
import {locate} from './wowaddon.mjs';
import {normalizeTank,bossLines,survivalLines,actorTank,calibrate,tankComparison,tankMetricList} from './tank.mjs';
import {normalizeTank,addsOf,bossLines,survivalLines,actorTank,calibrate,tankComparison,tankMetricList} from './tank.mjs';
import {appRoot,home,runsDir,upstreamDir,readEngineMetadata,enginePaths} from './paths.mjs';
export {runsDir};
export const root = appRoot;
Expand Down Expand Up @@ -125,9 +125,11 @@ export async function prepare(request,catalog,talentData,season) {
}
// Dungeon Slice builds its own fight and nothing in it attacks the tank boss's target: every tank metric comes back
// zero (no damage taken, no deaths, no healing), so the survival half of the score would be noise. Raidbots says the same.
if(s.style==='DungeonSlice'&&settings.tank)throw new Error('Dungeon Slice does not work for tank specializations: nothing in that fight attacks the tank, so there is no survival to measure. Choose another fight style, such as Patchwerk or Hectic Add Cleave.');
if(s.style==='DungeonSlice'&&settings.tank)throw new Error('Dungeon Slice does not work for tank specializations: nothing in that fight attacks the tank, so there is no survival to measure. Choose another fight style, such as Patchwerk with more targets (each extra target is a mob that attacks you) or Hectic Add Cleave.');
// SimC's Dungeon Slice sets its own 6 minutes and one boss, whatever max_time and desired_targets say, so say so here too.
if(s.style==='DungeonSlice')Object.assign(s,{targets:1,duration:360}),delete s.bleedTail;
// SimC's own adds are pets that never attack, so with attacking adds on they would only be targets for the tank's damage.
if(s.style==='HecticAddCleave'&&settings.tank?.adds)throw new Error('Hectic Add Cleave does not work for tank specializations: SimC\u2019s adds in that fight never attack. Use Patchwerk with 3 or 5 targets instead: every extra target is a mob that attacks you.');
s.targets=integer(s.targets,1,1,20,'Targets');
// Stop attacking at the end and let damage over time run out, as a meter on a dummy counts it.
if(s.bleedTail===undefined||s.bleedTail===null||s.bleedTail===false)delete s.bleedTail;else s.bleedTail=integer(s.bleedTail,15,3,60,'Seconds for bleeds to run out');
Expand Down Expand Up @@ -191,7 +193,8 @@ export const scenarioLines=scenario=>[...(scenario.bloodlust===false?['override.
// so the stop lands where it is set. DPS is then counted over the whole time, as a meter does.
export const bleedTailLines=(scenario,duration)=>scenario.bleedTail?`vary_combat_length=0\nraid_events+=/stun,first=${duration},duration=${scenario.bleedTail+30},cooldown=9999,players_only=1\n`:'';
// Enemies declared before the player make it a later actor, which profilesets must be told.
export const playerActorIndex=(settings,scenario)=>settings.tank?.boss?1:dummyCount(scenario.style);
// The boss and its adds are defined before the player, so the player is the actor after them.
export const playerActorIndex=(settings,scenario)=>settings.tank?.boss?1+addsOf(settings.tank,scenario):dummyCount(scenario.style);
export function inputFor(variant,settings,scenario,paths) {
const boss=settings.tank?.boss,dummies=isDummyStyle(scenario.style),route=isRouteStyle(scenario.style)?scenario.route:null;
// fight_style=Patchwerk clears every raid event, including the tank healer; the unset style is the same fight.
Expand All @@ -202,7 +205,7 @@ export function inputFor(variant,settings,scenario,paths) {
const style=route?'fight_style=DungeonRoute\n':dummies||(boss||scenario.bleedTail)&&scenario.style==='Patchwerk'?'':`fight_style=${scenario.style}\n`;
const pulls=route?`${/^warrior=/m.test(variant.text)?'warrior_fixed_time=0\n':''}${routeLines(route).join('\n')}\n`:'';
const duration=scenario.duration||settings.duration;
return `${boss?bossLines(boss).join('\n')+'\n':''}${dummies?dummyLines(scenario.style,scenario.dummyHealth).join('\n')+'\n':''}${variant.text}\n\n# Controlled by SimC Lab\nptr=0\nitem_db_source=local\niterations=${settings.iterations}\ntarget_error=${settings.targetError}\nmax_time=${duration+(scenario.bleedTail||0)}\nvary_combat_length=0.2\nthreads=${settings.threads}\n${style}desired_targets=${scenario.targets}\ncalculate_scale_factors=0\n${environmentLines(settings.environment,variant.text).join('\n')}\n${scenarioLines(scenario)}${bleedTailLines(scenario,duration)}${pulls}${boss?survivalLines(boss).join('\n')+'\n':''}${paths.apl?`save_actions="${paths.apl.replaceAll('\\','/')}"\n`:''}json2="${paths.json.replaceAll('\\','/')}"\nhtml="${paths.html.replaceAll('\\','/')}"\n`;
return `${boss?bossLines(boss,addsOf(settings.tank,scenario),settings.tank.addDamage).join('\n')+'\n':''}${dummies?dummyLines(scenario.style,scenario.dummyHealth).join('\n')+'\n':''}${variant.text}\n\n# Controlled by SimC Lab\nptr=0\nitem_db_source=local\niterations=${settings.iterations}\ntarget_error=${settings.targetError}\nmax_time=${duration+(scenario.bleedTail||0)}\nvary_combat_length=0.2\nthreads=${settings.threads}\n${style}desired_targets=${scenario.targets}\ncalculate_scale_factors=0\n${environmentLines(settings.environment,variant.text).join('\n')}\n${scenarioLines(scenario)}${bleedTailLines(scenario,duration)}${pulls}${boss?survivalLines(boss).join('\n')+'\n':''}${paths.apl?`save_actions="${paths.apl.replaceAll('\\','/')}"\n`:''}json2="${paths.json.replaceAll('\\','/')}"\nhtml="${paths.html.replaceAll('\\','/')}"\n`;
}
export function resultFrom(report) {
const p=report.sim?.players?.[0]; const dps=p?.collected_data?.dps;
Expand Down
15 changes: 12 additions & 3 deletions lib/tank.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,18 @@ export const presets={
heroic:{name:'Heroic raid',pressure:4,buster:50,magic:25,healGap:5,deathTarget:25},
mythic:{name:'Mythic raid',pressure:5,buster:55,magic:25,healGap:5,deathTarget:25}
};
const limits={pressure:[0.5,20],buster:[0,150],magic:[0,100],healGap:[1,30],deathTarget:[5,60],weight:[0,100]};
const limits={pressure:[0.5,20],buster:[0,150],magic:[0,100],healGap:[1,30],deathTarget:[5,60],weight:[0,100],addDamage:[0,100]};

// With `adds` on, every target beyond the first is a mob that attacks the tank, for `addDamage` percent of a boss swing
// each. Off, extra targets are only things for the tank's own damage to hit (what every job did before), which is what
// Weapon Lab and Trinket Lab still use.
export const addsOf=(tank,scenario)=>tank?.adds?Math.max(0,(Number(scenario?.targets)||1)-1):0;

export function normalizeTank(input,info){
if(!isTank(info))return null;
const options=input&&typeof input==='object'?input:{};
const preset=options.preset??'mythic';if(!presets[preset])throw new Error('Choose a tank boss preset.');
const result={preset,...presets[preset],weight:50};
const result={preset,...presets[preset],weight:50,adds:options.adds===true,addDamage:40};
for(const key of Object.keys(limits)){
if(options[key]===undefined||options[key]===null||options[key]==='')continue;
const value=Number(options[key]),[min,max]=limits[key];
Expand All @@ -31,11 +36,15 @@ export function normalizeTank(input,info){

// Enemy lines go before the character so later gear lines apply to the player; profilesets then need
// profileset_main_actor_index=1 because the boss is actor 0.
export function bossLines(boss){
// `adds` mobs follow the boss, each its own enemy that swings at the tank with an auto attack of a share of the boss's.
// They are defined here, before the player, so the player is actor 1 + adds (see playerActorIndex).
export function bossLines(boss,adds=0,addDamage=0){
const r=n=>Math.max(1,Math.round(n));
const lines=['enemy=Tank_Boss','actions=/auto_attack,damage='+r(boss.auto)+',range='+r(boss.auto*0.1)+',attack_speed=2,aoe_tanks=1'];
if(boss.buster>0)lines.push('actions+=/melee_nuke,damage='+r(boss.buster)+',range='+r(boss.buster*0.05)+',attack_speed=1,cooldown=30,aoe_tanks=1');
if(boss.dot>0)lines.push('actions+=/spell_dot,damage='+r(boss.dot)+',range='+r(boss.dot*0.1)+',tick_time=2,dot_duration=30,cooldown=30,aoe_tanks=1');
const swing=boss.auto*addDamage/100;
for(let k=1;k<=adds;k++)lines.push(`enemy=Tank_Add_${k}`,'actions=/auto_attack,damage='+r(swing)+',range='+r(swing*0.1)+',attack_speed=2,aoe_tanks=1');
return lines;
}
export function survivalLines(boss){
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.40.0",
"version": "1.41.0",
"private": true,
"type": "module",
"scripts": {
Expand Down
10 changes: 7 additions & 3 deletions public/app.js

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

10 changes: 5 additions & 5 deletions public/tank.js

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

Loading
Loading