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 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.32.0",
"version": "1.33.0",
"description": "Local SimulationCraft workbench with upgrade finder and tank simulation.",
"author": "Roburmaster",
"private": true,
Expand Down
8 changes: 7 additions & 1 deletion lib/analysis/fury.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,17 @@ export function analyse(data,{sim=null,potions=[]}={}){
}
for(const [key,cpm] of Object.entries(sim))if(cpm>=1&&!abilities.some(a=>simKey(a.name)===key))add('tip',`${title(key)} not used`,`SimC casts ${title(key)} ${round(cpm,2)} times a minute; you did not cast it.`,null);
}
// The end on a dummy: after the last cast, bleeds and other damage over time tick on. A meter counts that time
// too, so its DPS is lower than while attacking; the sim's "Stop at the end, let bleeds run out" matches it.
const lastCast=casts.length?casts.at(-1).time:end,tailSeconds=Math.max(0,end-lastCast);
const attacking=Math.max(1,lastCast-start+1.5),attackingDamage=data.hits.filter(([t])=>t<=lastCast+1.5).reduce((n,[,a])=>n+a,0);
const attackingDps=Math.round(attackingDamage/attacking);
if(tailSeconds>=5)add('tip','Bleeds after you stopped',`Your last cast was ${round(tailSeconds)} s before the last tick. That time counts in the ${Math.round(damage/length).toLocaleString('en-US')} DPS; while attacking it was ${attackingDps.toLocaleString('en-US')}. To compare with a sim, turn on “Stop at the end, let bleeds run out” for ${Math.round(tailSeconds)} s.`,round(tailSeconds));
const order={warning:0,tip:1,good:2};findings.sort((a,b)=>order[a.severity]-order[b.severity]);
const spells=[...data.damage.values()].sort((a,b)=>b.amount-a.amount).map(d=>({name:d.name,id:d.id,amount:d.amount,share:pct(d.amount,damage),hits:d.hits,crit:pct(d.crits,d.hits)}));
return {spec:spec.label,name:data.name,length:round(length),dps:Math.round(damage/length),damage,targets:data.targets.size,
uptime:{enrage:enrageUp,recklessness:pct(total(reckUp),length),whirlwind:pct(total(ww),length)},rage:{gained:Math.round(gained),wasted:Math.round(wasted)},
findings,abilities,spells,cooldowns,potions:potionUses,casts:casts.map(c=>({t:round(c.time-start,2),name:c.name,id:c.id}))};
tail:{seconds:round(tailSeconds),attackingDps},findings,abilities,spells,cooldowns,potions:potionUses,casts:casts.map(c=>({t:round(c.time-start,2),name:c.name,id:c.id}))};
}
// SimC names actions after the spell: "Odyn's Fury" is odyns_fury.
export const simKey=name=>String(name).toLowerCase().replace(/['’]/g,'').replace(/[^a-z0-9]+/g,'_').replace(/^_|_$/g,'');
Expand Down
4 changes: 2 additions & 2 deletions lib/combatlog.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export async function scanLog(file,progress=()=>{}){

// One player's part of one fight: casts, damage per spell, buffs on them, resource gains, and what they hit.
export async function readPlayer(file,fight,guid){
const out={guid,name:null,start:null,end:null,casts:[],damage:new Map(),auras:[],energize:[],targets:new Map(),spec:null};
const out={guid,name:null,start:null,end:null,casts:[],damage:new Map(),auras:[],energize:[],targets:new Map(),spec:null,hits:[]};
for await(const {line} of lines(file,fight.startOffset,fight.endOffset)){
const p=parseLine(line);if(!p)continue;const f=p.fields;
if(p.event==='COMBATANT_INFO'&&f[1]===guid){const bracket=f.findIndex(x=>x.startsWith('['));if(bracket>1)out.spec=Number(f[bracket-1]);continue;}
Expand All @@ -117,7 +117,7 @@ export async function readPlayer(file,fight,guid){
const swing=p.event==='SWING_DAMAGE';const id=swing?6603:Number(f[9]);const name=swing?'Melee':f[10];
const amount=damageOf(p);const at=advancedStart(p.event);
const d=out.damage.get(id)||{id,name,amount:0,hits:0,crits:0};d.amount+=amount;d.hits++;if(f[at+26]==='1')d.crits++;out.damage.set(id,d);
out.start??=p.time;out.end=p.time;
out.start??=p.time;out.end=p.time;out.hits.push([p.time,amount]);
const t=out.targets.get(f[5])||{guid:f[5],name:f[6],hits:0,times:[]};t.hits++;if(t.times.length<4000)t.times.push(p.time);out.targets.set(f[5],t);
}
}
Expand Down
14 changes: 11 additions & 3 deletions lib/engine.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ export async function prepare(request,catalog,talentData,season) {
if(s.dummyHealth.mode==='falling'&&weapons)throw new Error('Falling dummy health is measured on your own character. Use held health in Weapon Lab and Trinket Lab.');
}
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');
if(s.duration!==undefined)s.duration=integer(s.duration,300,10,1200,'Scenario duration');
const key=`${s.style}-${s.targets}-${s.duration||''}-${s.dummyHealth?.mode||''}`; if(unique.has(key)) throw new Error('The same scenario was selected more than once.'); unique.add(key);
}
Expand Down Expand Up @@ -160,14 +162,20 @@ export function reproduction(plan){
}
// What a scenario turns off after the environment: a Mythic+ pull that gets no Bloodlust or no potion.
export const scenarioLines=scenario=>[...(scenario.bloodlust===false?['override.bloodlust=0']:[]),...(scenario.potion===false?['potion=disabled','override.allow_potions=0']:[])].map(l=>l+'\n').join('');
// The end of a fight where the player stops and their damage over time keeps ticking: a stun from the set duration on
// (players only, so nothing is cast and no auto attack swings), the fight longer by the tail, and no length variation
// 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);
export function inputFor(variant,settings,scenario,paths) {
const boss=settings.tank?.boss,dummies=isDummyStyle(scenario.style);
// fight_style=Patchwerk clears every raid event, including the tank healer; the unset style is the same fight.
// The dummies are their own enemies in that unset (Patchwerk) fight.
const style=dummies||boss&&scenario.style==='Patchwerk'?'':`fight_style=${scenario.style}\n`;
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=${scenario.duration||settings.duration}\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)}${boss?survivalLines(boss).join('\n')+'\n':''}${paths.apl?`save_actions="${paths.apl.replaceAll('\\','/')}"\n`:''}json2="${paths.json.replaceAll('\\','/')}"\nhtml="${paths.html.replaceAll('\\','/')}"\n`;
// A bleed tail is a raid event, which fight_style=Patchwerk would clear too.
const style=dummies||(boss||scenario.bleedTail)&&scenario.style==='Patchwerk'?'':`fight_style=${scenario.style}\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)}${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 Expand Up @@ -586,7 +594,7 @@ export class Jobs {
// Falling dummy health: one short run per health band with the dummies held there, measuring how much damage each
// dummy takes per second; every dummy then gets its own timeline from 100% to 1% (see dummies.mjs).
async calibrateDummyHealth(job,plan,s){
const dir=path.join(runsDir,job.id),scenario=plan.scenarios[s],duration=scenario.duration||plan.settings.duration;
const dir=path.join(runsDir,job.id),{bleedTail,...scenario}=plan.scenarios[s],duration=(scenario.duration||plan.settings.duration)+(bleedTail||0);
const settings={...plan.settings,iterations:Math.min(plan.settings.iterations,1000),targetError:Math.max(plan.settings.targetError,0.5)};
const reports=[];
for(const band of dummyBands){
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.32.0",
"version": "1.33.0",
"private": true,
"type": "module",
"scripts": {
Expand Down
5 changes: 3 additions & 2 deletions public/app.js

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

Loading
Loading