From 0c7bd11abc36fffbac6de27832378a13ce7fa2f8 Mon Sep 17 00:00:00 2001 From: xiashtra <91220277+xiashtra@users.noreply.github.com> Date: Fri, 18 Oct 2024 23:20:40 -0500 Subject: [PATCH] first pass --- packages/core/src/sims/cycle_sim.ts | 36 +++++++++++++++---- .../core/src/sims/processors/count_sim.ts | 5 +++ packages/core/src/sims/sim_types.ts | 26 ++++++++++++-- packages/core/src/sims/sim_utils.ts | 9 ++++- .../src/test/sims/cycle_processor_tests.ts | 7 ++++ .../sims/components/ability_used_table.ts | 7 +++- 6 files changed, 80 insertions(+), 10 deletions(-) diff --git a/packages/core/src/sims/cycle_sim.ts b/packages/core/src/sims/cycle_sim.ts index 72aba640..89917f52 100644 --- a/packages/core/src/sims/cycle_sim.ts +++ b/packages/core/src/sims/cycle_sim.ts @@ -757,6 +757,10 @@ export class CycleProcessor { ...record.dot, damagePerTick: dmgInfo.dot.damagePerTick, } : null, + channel: record.channel ? { + ...record.channel, + damagePerTick: dmgInfo.channel.damagePerTick, + } : null, } } }) @@ -792,8 +796,12 @@ export class CycleProcessor { const directDamage = multiplyFixed(record.directDamage, partialRate); const dot = record.dot; const dotDmg = dot ? multiplyIndependent(dot.damagePerTick, dot.actualTickCount) : fixedValue(0); - const totalDamage = addValues(directDamage, dotDmg); - const totalPotency = record.ability.potency + ('dot' in record.ability ? record.ability.dot.tickPotency * record.dot.actualTickCount : 0); + const channel = record.channel; + const channelDmg = channel ? multiplyIndependent(channel.damagePerTick, channel.actualTickCount) : fixedValue(0); + const totalDamage = addValues(directDamage, dotDmg, channelDmg); + const totalPotency = record.ability.potency + + ('dot' in record.ability ? record.ability.dot.tickPotency * record.dot.actualTickCount : 0) + + ('channel' in record.ability ? record.ability.channel.tickPotency * record.channel.actualTickCount : 0); return { usedAt: record.usedAt, original: record, @@ -801,6 +809,7 @@ export class CycleProcessor { directDamage: directDamage.expected, directDamageFull: directDamage, dotInfo: dot, + channelInfo: channel, totalDamage: totalDamage.expected, totalDamageFull: totalDamage, totalPotency: totalPotency, @@ -947,6 +956,7 @@ export class CycleProcessor { const animLock = animationLock(ability); const effectiveAnimLock = effectiveCastTime ? Math.max(effectiveCastTime + CASTER_TAX, animLock) : animLock; const animLockFinishedAt = this.currentTime + effectiveAnimLock; + const channelFinishedAt = 'channel' in ability ? this.currentTime + ability.channel.duration : undefined; this.advanceTo(snapshotsAt, true); const { buffs, @@ -979,6 +989,7 @@ export class CycleProcessor { buffs: finalBuffs, usedAt: gcdStartsAt, dot: dmgInfo.dot, + channel: dmgInfo.channel, appDelay: appDelayFromSnapshot, appDelayFromStart: appDelayFromStart, totalTimeTaken: Math.max(effectiveAnimLock, abilityGcd), @@ -998,15 +1009,22 @@ export class CycleProcessor { if (ability.activatesBuffs) { ability.activatesBuffs.forEach(buff => this.activateBuffWithDelay(buff, buffDelay)); } - // Anim lock OR cast time, both effectively block use of skills. - // If cast time > GCD recast, then we use that instead. Also factor in caster tax. - this.advanceTo(animLockFinishedAt); + // if this is a channeled ability, then we need to advance to the end of the channel time without auto-attacks + if ('channel' in ability) { + this.advanceTo(channelFinishedAt, true); + } else { + // Anim lock OR cast time, both effectively block use of skills. + // If cast time > GCD recast, then we use that instead. Also factor in caster tax. + this.advanceTo(animLockFinishedAt); + } // If we're casting a long-cast, then the GCD is blocked for more than a GCD. if (isGcd) { this.nextGcdTime = Math.max(gcdFinishedAt, animLockFinishedAt); } // Account for potential GCD clipping - else { + else if ('channel' in ability) { + this.nextGcdTime = Math.max(this.nextGcdTime, channelFinishedAt); + } else { this.nextGcdTime = Math.max(this.nextGcdTime, animLockFinishedAt); } // Workaround for auto-attacks after first ability @@ -1261,6 +1279,12 @@ export class CycleProcessor { this.dotMap.set(dotId, usedAbility); } } + + if (usedAbility.channel) { + // TODO: handle cutting off a channel early due to end of sim time + // TODO: allow cutting off a channel early by other action use? + usedAbility.channel.actualTickCount = usedAbility.channel.fullDurationTicks; + } } /** diff --git a/packages/core/src/sims/processors/count_sim.ts b/packages/core/src/sims/processors/count_sim.ts index bd8889d1..0a723487 100644 --- a/packages/core/src/sims/processors/count_sim.ts +++ b/packages/core/src/sims/processors/count_sim.ts @@ -158,6 +158,11 @@ export abstract class BaseUsageCountSim; +/** + * Represents a channeled action. + */ +export type ChannelInfo = Readonly<{ + duration: number, + tickPotency: number, +}>; + /** * Represents combo-related data. * @@ -242,6 +250,7 @@ export type DamagingAbility = Readonly<{ autoCrit?: boolean, autoDh?: boolean, dot?: DotInfo, + channel?: ChannelInfo }>; /** @@ -387,6 +396,12 @@ export type DotDamageUnf = { actualTickCount?: number }; +export type ChannelDamageUnf = { + fullDurationTicks: number, + damagePerTick: ComputedDamage, + actualTickCount?: number +}; + export type ComputedDamage = ValueWithDev; /** @@ -421,6 +436,10 @@ export type PreDmgUsedAbility = { * If a DoT, the DoT damage */ dot?: DotDamageUnf, + /** + * If a channeled action, the channeled damage + */ + channel?: ChannelDamageUnf, /** * The total cast time from usedAt */ @@ -455,7 +474,8 @@ export type PreDmgUsedAbility = { export type PostDmgUsedAbility = PreDmgUsedAbility & { directDamage: ComputedDamage, - dot?: DotDamageUnf + dot?: DotDamageUnf, + channel?: ChannelDamageUnf } /** * Represents a pseudo-ability used to round out a cycle to exactly 120s. @@ -478,6 +498,7 @@ export type FinalizedAbility = { directDamage: number, directDamageFull: ComputedDamage, dotInfo: DotDamageUnf, + channelInfo: ChannelDamageUnf, combinedEffects: CombinedBuffEffect, ability: Ability, buffs: Buff[] @@ -645,7 +666,8 @@ export type Buff = PersonalBuff | PartyBuff; export type DamageResult = { readonly directDamage: ComputedDamage | null, - readonly dot: DotDamageUnf | null + readonly dot: DotDamageUnf | null, + readonly channel: ChannelDamageUnf | null } /** diff --git a/packages/core/src/sims/sim_utils.ts b/packages/core/src/sims/sim_utils.ts index 7b92fd1c..b8f470fd 100644 --- a/packages/core/src/sims/sim_utils.ts +++ b/packages/core/src/sims/sim_utils.ts @@ -50,7 +50,8 @@ export function abilityToDamageNew(stats: ComputedSetStats, ability: Ability, co if (!('potency' in ability)) { return { directDamage: null, - dot: null + dot: null, + channel: null } } // noinspection AssignmentToFunctionParameterJS @@ -62,6 +63,12 @@ export function abilityToDamageNew(stats: ComputedSetStats, ability: Ability, co fullDurationTicks: ability.dot.duration === 'indefinite' ? 'indefinite' : (ability.dot.duration / 3), damagePerTick: dotPotencyToDamage(stats, ability.dot.tickPotency, ability, combinedBuffEffects), } : null, + channel: 'channel' in ability ? { + // channeled actions tick once on use, and once per second afterwards for the full duration + fullDurationTicks: ability.channel.duration + 1, + // channeled actions use the same damage formula as DoTs currently + damagePerTick: dotPotencyToDamage(stats, ability.channel.tickPotency, ability, combinedBuffEffects), + } : null, } } diff --git a/packages/core/src/test/sims/cycle_processor_tests.ts b/packages/core/src/test/sims/cycle_processor_tests.ts index d303371e..9f3eb398 100644 --- a/packages/core/src/test/sims/cycle_processor_tests.ts +++ b/packages/core/src/test/sims/cycle_processor_tests.ts @@ -664,6 +664,13 @@ function multiplyDamage(damageResult: DamageResult, multiplier: number, multiply stdDev: 0 } }, + channel: (damageResult.channel === null || !multiplyDot) ? damageResult.channel : { + ...damageResult.channel, + damagePerTick: { + expected: damageResult.directDamage.expected * multiplier, + stdDev: 0 + } + }, } } diff --git a/packages/frontend/src/scripts/sims/components/ability_used_table.ts b/packages/frontend/src/scripts/sims/components/ability_used_table.ts index 4a6bae41..93663d88 100644 --- a/packages/frontend/src/scripts/sims/components/ability_used_table.ts +++ b/packages/frontend/src/scripts/sims/components/ability_used_table.ts @@ -127,7 +127,9 @@ export class AbilitiesUsedTable extends CustomTable { return document.createTextNode('--'); } let text = used.totalDamage.toFixed(2); - if (used.partialRate !== null || (used.dotInfo && used.dotInfo.fullDurationTicks !== "indefinite" && used.dotInfo.actualTickCount < used.dotInfo.fullDurationTicks)) { + if (used.partialRate !== null + || (used.dotInfo && used.dotInfo.fullDurationTicks !== "indefinite" && used.dotInfo.actualTickCount < used.dotInfo.fullDurationTicks) + || (used.channelInfo && used.channelInfo.actualTickCount < used.channelInfo.fullDurationTicks)) { text += '*'; } return document.createTextNode(text); @@ -150,6 +152,9 @@ export class AbilitiesUsedTable extends CustomTable { title.push(`This ability is a DoT. It dealt ${value.dotInfo.actualTickCount}/${value.dotInfo.fullDurationTicks} ticks of ${value.dotInfo.damagePerTick.expected.toFixed(3)} each.\n`); } } + if (value.channelInfo) { + title.push(`This ability is a channel. It dealt ${value.channelInfo.actualTickCount}/${value.channelInfo.fullDurationTicks} ticks of ${value.channelInfo.damagePerTick.expected.toFixed(3)} each.\n`); + } if (title.length > 0) { colElement.title = title.join('\n'); }