diff --git a/README.md b/README.md index 2b8aacf..bf1e087 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ Known limits: boss mechanics, movement, deaths and crowd control are not simulat Protection Warrior, Protection Paladin, Blood Death Knight, Guardian Druid, Brewmaster Monk and Vengeance Demon Hunter are detected on import. Every mode (Quick Sim, Enchant Lab, Gear Compare, Upgrade Finder, Crest Planner, Great Vault and Talent Search) then ranks results on DPS and survival. -**Tank Sim** has its own place in the menu: the imported tank against a boss and its adds, one simulation per fight you tick (the boss alone, the boss with 2 adds, the boss with 4 adds), with the numbers a tank cares about: damage taken per second and as a share of your health, healing and absorbs, how much of the fight you survive, how often you die, and your DPS. It needs a tank specialization. +**Tank Sim** has its own place in the menu: the imported tank against a boss and its adds, one simulation per fight you tick (the boss alone, the boss with 2 adds, the boss with 4 adds), with the numbers a tank cares about: damage taken per second and as a share of your health, healing and absorbs, how much of the fight you survive, how often you die, and your DPS. It needs a tank specialization. Tank Sim can also **find gear upgrades** (on by default): Upgrade Finder's sources appear in the same mode, and every item from them is simulated in its slot in each fight you ticked and the best ones again at full precision, ranked on the survival and damage score. Each fight in the result shows what your current gear takes per second, how much of the fight it survives and how often it dies, then the upgrades for that fight. The same upgrades can be run from Upgrade Finder itself with the same number of targets. SimC's built-in tank dummy uses level-70 damage values and never threatens a Midnight tank, and SimC gives players infinite health by default. SimC Lab replaces it with a calibrated boss, tuned once per job against the imported gear and then shared by every variant and scenario: diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 2dc17bc..6c248fc 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "simc-lab", - "version": "1.42.0", + "version": "1.43.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "simc-lab", - "version": "1.42.0", + "version": "1.43.0", "license": "GPL-3.0-or-later", "dependencies": { "electron-updater": "^6.8.9" diff --git a/desktop/package.json b/desktop/package.json index 5d3b116..ace1f02 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.42.0", + "version": "1.43.0", "description": "Local SimulationCraft workbench with upgrade finder and tank simulation.", "author": "Roburmaster", "private": true, diff --git a/package-lock.json b/package-lock.json index 131404d..26fabda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "simc-lab", - "version": "1.42.0", + "version": "1.43.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "simc-lab", - "version": "1.42.0", + "version": "1.43.0", "license": "GPL-3.0-or-later", "devDependencies": { "luaparse": "^0.3.1", diff --git a/package.json b/package.json index 22bec17..2a6cb51 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "simc-lab", - "version": "1.42.0", + "version": "1.43.0", "private": true, "type": "module", "scripts": { diff --git a/public/app.js b/public/app.js index f88d096..12629e8 100644 --- a/public/app.js +++ b/public/app.js @@ -30,7 +30,7 @@ let variants=[{name:'Alternative 1',text:''}]; async function api(url,data){const res=await fetch(url,data===undefined?{}:{method:'POST',headers:{'Content-Type':'application/json','X-SimC-Token':token},body:JSON.stringify(data)});const result=await res.json();if(!res.ok)throw new Error(result.error||'The request failed.');return result;} function notice(message){$('#notice').hidden=!message;$('#notice').textContent=message||'';} function safeStore(key,value){try{localStorage.setItem(key,value);}catch{}} -async function setMode(value){mode=value;$$('.nav').forEach(b=>b.classList.toggle('active',b.dataset.mode===value));$('#page-title').innerHTML=`${esc(modes[value][0])}.`;$('#breadcrumb').textContent=modes[value][0];$('#page-description').textContent=modes[value][1];$('#workspace').hidden=['history','wow','logs'].includes(value);$('#history').hidden=value!=='history';$('#logs-panel').hidden=value!=='logs';if(value==='logs')logs.show();$('#wow-panel').hidden=value!=='wow';$('#enchant-panel').hidden=value!=='enchants';$('#compare-panel').hidden=value!=='compare';$('#quick-info').hidden=value!=='quick';$('#talent-panel').hidden=value!=='talents';$('#upgrade-panel').hidden=value!=='upgrades';$('#tanksim-panel').hidden=value!=='tank';$('#crest-panel').hidden=value!=='crests';$('#vault-panel').hidden=value!=='vault';$('#weapon-panel').hidden=value!=='weapons';$('#trinket-panel').hidden=value!=='trinkets'; +async function setMode(value){mode=value;$$('.nav').forEach(b=>b.classList.toggle('active',b.dataset.mode===value));$('#page-title').innerHTML=`${esc(modes[value][0])}.`;$('#breadcrumb').textContent=modes[value][0];$('#page-description').textContent=modes[value][1];$('#workspace').hidden=['history','wow','logs'].includes(value);$('#history').hidden=value!=='history';$('#logs-panel').hidden=value!=='logs';if(value==='logs')logs.show();$('#wow-panel').hidden=value!=='wow';$('#enchant-panel').hidden=value!=='enchants';$('#compare-panel').hidden=value!=='compare';$('#quick-info').hidden=value!=='quick';$('#talent-panel').hidden=value!=='talents';$('#tanksim-panel').hidden=value!=='tank';syncUpgradePanel(value);$('#crest-panel').hidden=value!=='crests';$('#vault-panel').hidden=value!=='vault';$('#weapon-panel').hidden=value!=='weapons';$('#trinket-panel').hidden=value!=='trinkets'; // Weapon Lab and Trinket Lab run on SimC's reference profiles, so they hide the character panel and always offer tank settings. const lab=['weapons','trinkets'].includes(value); // Trinket Lab carries every setting it uses in its own panel, so the rest of the app's settings are out of sight. @@ -61,8 +61,11 @@ function renderVariants(){$('#variants').innerHTML=variants.map((v,i)=>`
{variants.push({name:`Alternative ${variants.length+1}`,text:''});renderVariants();updateCount();}); $('#variants').addEventListener('input',event=>{const el=event.target;if(el.dataset.field){variants[Number(el.closest('.variant').dataset.index)][el.dataset.field]=el.value;updateCount();}}); $('#variants').addEventListener('click',event=>{if(event.target.dataset.remove!==undefined){variants.splice(Number(event.target.dataset.remove),1);renderVariants();updateCount();}}); -function request(){if(mode==='trinkets')return trinkets.request();const targets=Number($('#targets').value);const scenarioTargets=dummyStyles[$('#fight-style').value]?[5]:$('#matrix').checked?[...new Set([targets,3,5])]:[targets];const route=$('#fight-style').value===routeStyle;return {profile:$('#profile').value,mode,enchants:selections,combine:$('input[name=combine]:checked').value==='true',talentSearch:features.settings(),upgrades:upgrades.settings(),crests:crests.settings(),vault:vault.settings(),weapons:weapons.settings(),trinkets:trinkets.settings(),tank:tank.settings(),environment:environment.settings(),variants,iterations:Number($('#iterations').value),targetError:Number($('#target-error').value),duration:Number($('#duration').value),threads:Number($('#threads').value),scenarios:mode==='tank'?tankSim.scenarios($('#fight-style').value):route?mplus.scenarios():scenarioTargets.map(n=>({style:$('#fight-style').value,targets:n,...(dummyStyles[$('#fight-style').value]?{dummyHealth:{mode:$('#dummy-health').value}}:{}),...($('#bleed-tail-on').checked?{bleedTail:Number($('#bleed-tail').value)}:{})}))};} +function request(){if(mode==='trinkets')return trinkets.request();const targets=Number($('#targets').value);const scenarioTargets=dummyStyles[$('#fight-style').value]?[5]:$('#matrix').checked?[...new Set([targets,3,5])]:[targets];const route=$('#fight-style').value===routeStyle;return {profile:$('#profile').value,mode:mode==='tank'&&tankSim.upgrades()?'upgrades':mode,enchants:selections,combine:$('input[name=combine]:checked').value==='true',talentSearch:features.settings(),upgrades:upgrades.settings(),crests:crests.settings(),vault:vault.settings(),weapons:weapons.settings(),trinkets:trinkets.settings(),tank:tank.settings(),environment:environment.settings(),variants,iterations:Number($('#iterations').value),targetError:Number($('#target-error').value),duration:Number($('#duration').value),threads:Number($('#threads').value),scenarios:mode==='tank'?tankSim.scenarios($('#fight-style').value):route?mplus.scenarios():scenarioTargets.map(n=>({style:$('#fight-style').value,targets:n,...(dummyStyles[$('#fight-style').value]?{dummyHealth:{mode:$('#dummy-health').value}}:{}),...($('#bleed-tail-on').checked?{bleedTail:Number($('#bleed-tail').value)}:{})}))};} +// Upgrade Finder's sources show in its own mode, and in Tank Sim while it is asked to find gear upgrades. +function syncUpgradePanel(value=mode){$('#upgrade-panel').hidden=!(value==='upgrades'||value==='tank'&&tankSim.upgrades());} function updateCount(){ + syncUpgradePanel(); syncGearSelection(); const groups=Object.values(selections).filter(v=>v.length);const count=groups.reduce((n,v)=>n+v.length,0);$('#selection-count').textContent=`${count} selected`; const combined=$('input[name=combine]:checked').value==='true';$('#combine-help').textContent=combined?'Combine every selected alternative across slots. Equipped gear runs separately as the baseline.':'Compare each enchant against your equipped gear. Other slots stay unchanged.'; @@ -71,7 +74,7 @@ function updateCount(){ clearTimeout(previewTimer); if(mode==='crests'){$('#run-summary').textContent=profile?'Every upgrade level in one run per scenario':'Waiting for character import';crests.count(request,!!profile);} if(mode==='vault'){$('#run-summary').textContent=profile?'Every vault choice in one run per scenario':'Waiting for character import';vault.count(request,!!profile);} - if(mode==='tank'){const n=tankSim.chosen().length;$('#run-summary').textContent=!profile?'Waiting for character import':!profile.isTank?'Import a tank specialization':n?'One simulation per fight':'Choose at least one fight';$('#run-count').textContent=profile?`${n} run${n===1?'':'s'}`:'';} + if(mode==='tank'){const n=tankSim.chosen().length,find=tankSim.upgrades();$('#run-summary').textContent=!profile?'Waiting for character import':!profile.isTank?'Import a tank specialization':!n?'Choose at least one fight':find?'Screening + final round per fight':'One simulation per fight';$('#run-count').textContent=profile?`${find?2*n:n} run${(find?2*n:n)===1?'':'s'}`:'';if(find)upgrades.count(request,!!profile&&!!profile.isTank&&n>0);} if(mode==='upgrades'){$('#run-summary').textContent=profile?'Screening + final round per scenario':'Waiting for character import';upgrades.count(request,!!profile);} // Weapon Lab needs no import: the reference profiles are always there, so the run summary never waits for one. if(mode==='weapons'){$('#run-summary').textContent='One tier list per specialization';$('#run-count').textContent='';weapons.count(request);} diff --git a/public/tanksim.js b/public/tanksim.js index a7d3848..0b62783 100644 --- a/public/tanksim.js +++ b/public/tanksim.js @@ -12,12 +12,13 @@ export const fights=[ // Tank Sim: one imported tank, run against a boss and its adds, with the numbers a tank cares about. export function tankSimUI({updateCount}){ - $('#quick-info').insertAdjacentHTML('beforebegin',``); + $('#quick-info').insertAdjacentHTML('beforebegin',``); $('#tanksim-panel').addEventListener('change',()=>updateCount()); const chosen=()=>fights.filter(f=>$(`[data-tanksim="${f.id}"]`)?.checked); const row=(job,s)=>job.results.find(r=>r.scenario===s&&r.baseline&&r.status==='complete'); + const upgrades=()=>!!$('[data-tanksim-upgrades]')?.checked; return { - chosen, + chosen,upgrades, // One scenario per fight, in the fight style chosen in the settings. scenarios(style,extra={}){return chosen().map(f=>({style,targets:f.targets,...extra}));}, results(job){ diff --git a/public/upgrades.js b/public/upgrades.js index 4859f79..939ef58 100644 --- a/public/upgrades.js +++ b/public/upgrades.js @@ -107,8 +107,8 @@ export function upgradeUI({api,notice,updateCount}){ const rows=chosen; const upgrades=rows.filter(r=>r.stage===2&&r.rank>0).sort((a,b)=>b.rank-a.rank); const scenario=job.scenarios[s]; - html+=`

${esc(scenario.style)} / ${scenario.targets} targets / ${job.settings.duration} sec

`; - const base=baselines[2]||baselines[1];if(base)html+=`

Current gear: ${number(base.dps)} DPS${base.error95!==null?` ± ${number(base.error95)}`:''}

`; + html+=`

${esc(scenario.style)} / ${job.settings.tank?.adds?`boss + ${scenario.targets-1} add${scenario.targets===2?'':'s'}`:`${scenario.targets} targets`} / ${job.settings.duration} sec

`; + const base=baselines[2]||baselines[1];if(base)html+=`

Current gear: ${number(base.dps)} DPS${base.error95!==null?` ± ${number(base.error95)}`:''}${base.tank?` · takes ${number(base.tank.dtps)} damage per second${base.tank.health?` (${(100*base.tank.dtps/base.tank.health).toFixed(1)}% of your health)`:''}, survives ${(100*base.tank.alive).toFixed(0)}% of the fight${base.tank.deaths!==undefined?`, dies in ${(100*base.tank.deaths).toFixed(0)}% of fights`:''}`:''}

`; html+=slotFilterBar(measured); if(slotFilter!=='all')html+=`

${esc(familyNames[slotFilter]||slotFilter)}: every item measured for this slot, best first.${rows.length?'':' Nothing was measured for it in this job.'}

`; const stageName=n=>n===1?'Screening':n===3?'Embellishment pairs':'Final round';