diff --git a/spec/System/TestSkills_spec.lua b/spec/System/TestSkills_spec.lua index bbd0e280d7..b3c7963caf 100644 --- a/spec/System/TestSkills_spec.lua +++ b/spec/System/TestSkills_spec.lua @@ -1,4 +1,4 @@ -describe("TestAttacks", function() +describe("TestSkills", function() before_each(function() newBuild() end) @@ -365,4 +365,90 @@ describe("TestAttacks", function() assert.are.equals(0, nonDurationSkill.skillModList:Sum("BASE", nonDurationSkill.skillCfg, "BaseFlagTest")) assert.are.equals(1, nonDurationSkill.skillModList:Sum("BASE", nonDurationSkill.skillCfg, "NegatedBaseFlagTest")) end) + + it("applies Pact of Beidat coverage based on uptime", function() + for _, test in ipairs({ + { skill = "Fireball", output = "ProjectileCount", bonus = "BeidatAdditionalProjectiles", amount = 4 }, + { skill = "Arc", output = "ChainMax", bonus = "BeidatAdditionalBeamChains", amount = 4 }, + { skill = "Firestorm", bonus = "BeidatAdditionalCascades", amount = 5 }, + }) do + newBuild() + build.skillsTab:PasteSocketGroup(test.skill.." 20/0 1") + runCallback("OnFrame") + local baseOutput = test.output and build.calcsTab.mainOutput[test.output] + local baseDPS = build.calcsTab.mainOutput.TotalDPS + + build.skillsTab:PasteSocketGroup("Pact of Beidat 1/0 1") + runCallback("OnFrame") + local output = build.calcsTab.mainOutput + local expectedBonus = test.amount * output.BeidatUpTimeRatio / 100 + assert.are.near(expectedBonus, output[test.bonus], 10 ^ -9) + if test.output then + assert.are.near(baseOutput + expectedBonus, output[test.output], 10 ^ -9) + end + assert.is_true(output.TotalDPS > baseDPS) + end + + newBuild() + build.skillsTab:PasteSocketGroup("Fireball 20/0 1\nPact of Beidat 1/0 1") + build.configTab.input.pactMode = "MAX" + build.configTab:BuildModList() + runCallback("OnFrame") + assert.are.equals(4, build.calcsTab.mainOutput.BeidatAdditionalProjectiles) + end) + + it("applies Pact damage to eligible spells", function() + for _, test in ipairs({ + { skill = "Creeping Frost", pact = "Pact of Ghorr", uptime = "GhorrUpTimeRatio", dps = "TotalDotDPS" }, + { skill = "Lightning Tendrils", pact = "Pact of Lycia", uptime = "LyciaUpTimeRatio", dps = "TotalDPS" }, + }) do + newBuild() + build.skillsTab:PasteSocketGroup(test.skill.." 20/0 1") + runCallback("OnFrame") + local baseDPS = build.calcsTab.mainOutput[test.dps] + build.skillsTab:PasteSocketGroup(test.pact.." 1/0 1") + runCallback("OnFrame") + local output = build.calcsTab.mainOutput + assert.is_true(output[test.uptime] > 0) + assert.is_true(output[test.dps] > baseDPS) + end + + newBuild() + build.skillsTab:PasteSocketGroup("Lightning Tendrils 20/0 1\nSpell Totem 20/0 1") + runCallback("OnFrame") + local baseDPS = build.calcsTab.mainOutput.TotalDPS + build.skillsTab:PasteSocketGroup("Pact of Lycia 1/0 1") + runCallback("OnFrame") + assert.is_nil(build.calcsTab.mainOutput.LyciaUpTimeRatio) + assert.are.equals(baseDPS, build.calcsTab.mainOutput.TotalDPS) + end) + + it("applies Pact of K'Tash to instant damaging Vaal spells", function() + build.skillsTab:PasteSocketGroup("Vaal Righteous Fire 20/0 1") + runCallback("OnFrame") + local baseDPS = build.calcsTab.mainOutput.TotalDotDPS + local basePrevention = build.calcsTab.mainOutput.SoulGainPreventionDuration + + build.skillsTab:PasteSocketGroup("Pact of K'Tash 1/0 1") + runCallback("OnFrame") + local output = build.calcsTab.mainOutput + assert.are.equals(100, output.KtashUpTimeRatio) + assert.are.near(baseDPS * 1.1, output.TotalDotDPS, 10 ^ -6) + assert.are.equals(100, output.KtashSoulRefundChance) + assert.are.equals(math.max(math.ceil(basePrevention * 0.5 * data.misc.ServerTickRate), 1) / data.misc.ServerTickRate, output.SoulGainPreventionDuration) + end) + + it("does not apply Pacts to ineligible spells", function() + for _, test in ipairs({ + { "Vaal Arc", "Pact of Beidat", "BeidatUpTimeRatio" }, + { "Vaal Arc", "Pact of Ghorr", "GhorrUpTimeRatio" }, + { "Fireball", "Pact of K'Tash", "KtashUpTimeRatio" }, + { "Fireball", "Pact of Lycia", "LyciaUpTimeRatio" }, + }) do + newBuild() + build.skillsTab:PasteSocketGroup(test[1].." 20/0 1\n"..test[2].." 1/0 1") + runCallback("OnFrame") + assert.is_nil(build.calcsTab.mainOutput[test[3]]) + end + end) end) diff --git a/src/Data/SkillStatMap.lua b/src/Data/SkillStatMap.lua index d65129f608..83cc8886c6 100644 --- a/src/Data/SkillStatMap.lua +++ b/src/Data/SkillStatMap.lua @@ -2368,4 +2368,7 @@ return { ["is_totem"] = { -- Display only }, +["pact_empower_limitation_specifier_for_stat_description"] = { + -- Display only +}, } diff --git a/src/Data/Skills/other.lua b/src/Data/Skills/other.lua index 33c503b2af..2e4d7b41bc 100644 --- a/src/Data/Skills/other.lua +++ b/src/Data/Skills/other.lua @@ -2778,7 +2778,26 @@ skills["PactOfBeidat"] = { skillTypes = { [SkillType.Spell] = true, [SkillType.Pact] = true, [SkillType.Buff] = true, [SkillType.Cooldown] = true, [SkillType.InstantNoRepeatWhenHeld] = true, [SkillType.InstantShiftAttackForLeftMouse] = true, [SkillType.Triggerable] = true, }, statDescriptionScope = "skill_stat_descriptions", castTime = 0.8, + statMap = { + ["skill_empowers_next_x_spells_cast"] = { + mod("BeidatEmpoweredSpells", "BASE", nil), + }, + ["pact_skill_additional_beam_only_chains"] = { + mod("BeidatAdditionalBeamChains", "BASE", nil), + }, + ["pact_skill_grant_x_additional_projectiles_fired_in_nova"] = { + mod("BeidatAdditionalProjectiles", "BASE", nil), + }, + ["pact_skill_grant_x_cascades_to_do_in_spiral"] = { + mod("BeidatAdditionalCascades", "BASE", nil), + }, + ["pact_skill_damage_+%_final_with_hits_and_ailments_to_grant"] = { + mod("BeidatPactDamage", "LIST", { mod = mod("Damage", "MORE", nil, ModFlag.Spell, bit.bor(KeywordFlag.Hit, KeywordFlag.Ailment)) }), + }, + }, baseFlags = { + spell = true, + pact = true, }, qualityStats = { { "skill_empowers_next_x_spells_cast", 0.1 }, @@ -2849,7 +2868,18 @@ skills["PactOfGhorr"] = { skillTypes = { [SkillType.Spell] = true, [SkillType.Pact] = true, [SkillType.Buff] = true, [SkillType.Cooldown] = true, [SkillType.InstantNoRepeatWhenHeld] = true, [SkillType.InstantShiftAttackForLeftMouse] = true, [SkillType.Triggerable] = true, }, statDescriptionScope = "skill_stat_descriptions", castTime = 0.8, + statMap = { + ["skill_empowers_next_x_spells_cast"] = { + mod("GhorrEmpoweredSpells", "BASE", nil), + }, + ["pact_skill_grant_damage_over_time_+%_final_to_spells"] = { + mod("GhorrPactDamage", "LIST", { mod = mod("Damage", "MORE", nil, ModFlag.Dot) }), + }, + }, baseFlags = { + spell = true, + pact = true, + duration = true, }, constantStats = { { "skill_empowers_next_x_spells_cast", 3 }, @@ -2920,6 +2950,9 @@ skills["TriggeredBloodrend"] = { statDescriptionScope = "debuff_skill_stat_descriptions", castTime = 0, baseFlags = { + spell = true, + pact = true, + projectile = true, }, qualityStats = { { "skill_effect_duration_+%", 1 }, @@ -2990,7 +3023,23 @@ skills["PactOfKtash"] = { skillTypes = { [SkillType.Spell] = true, [SkillType.Pact] = true, [SkillType.Buff] = true, [SkillType.Cooldown] = true, [SkillType.InstantNoRepeatWhenHeld] = true, [SkillType.InstantShiftAttackForLeftMouse] = true, [SkillType.Triggerable] = true, }, statDescriptionScope = "skill_stat_descriptions", castTime = 0.8, + statMap = { + ["skill_empowers_next_x_spells_cast"] = { + mod("KtashEmpoweredSpells", "BASE", nil), + }, + ["pact_skill_grant_damage_+%_final_to_exerted_skills"] = { + mod("KtashPactDamage", "LIST", { mod = mod("Damage", "MORE", nil) }), + }, + ["active_skill_osm_vaal_skill_soul_gain_prevention_+%_final_to_grant"] = { + mod("KtashPactSoulGainPrevention", "BASE", nil), + }, + ["active_skill_osm_vaal_skill_soul_refund_chance_%_to_grant"] = { + mod("KtashPactSoulRefundChance", "BASE", nil), + }, + }, baseFlags = { + spell = true, + pact = true, }, qualityStats = { { "base_cooldown_speed_+%", 0.5 }, @@ -3060,7 +3109,17 @@ skills["PactOfLycia"] = { skillTypes = { [SkillType.Spell] = true, [SkillType.Pact] = true, [SkillType.Buff] = true, [SkillType.Cooldown] = true, [SkillType.InstantNoRepeatWhenHeld] = true, [SkillType.InstantShiftAttackForLeftMouse] = true, [SkillType.Triggerable] = true, }, statDescriptionScope = "skill_stat_descriptions", castTime = 0.8, + statMap = { + ["skill_empowers_next_x_spells_cast"] = { + mod("LyciaEmpoweredSpells", "BASE", nil), + }, + ["pact_skill_grant_damage_+%_final_to_exerted_skills"] = { + mod("LyciaPactDamage", "LIST", { mod = mod("Damage", "MORE", nil) }), + }, + }, baseFlags = { + spell = true, + pact = true, }, constantStats = { { "skill_empowers_next_x_spells_cast", 3 }, @@ -3128,6 +3187,9 @@ skills["TriggeredHeavensScourge"] = { statDescriptionScope = "skill_stat_descriptions", castTime = 1, baseFlags = { + spell = true, + pact = true, + area = true, }, qualityStats = { { "active_skill_base_area_of_effect_radius", 0.1 }, diff --git a/src/Export/Skills/other.txt b/src/Export/Skills/other.txt index ab103e85e7..be8b414c1a 100644 --- a/src/Export/Skills/other.txt +++ b/src/Export/Skills/other.txt @@ -741,21 +741,74 @@ local skills, mod, flag, skill = ... #mods #skill PactOfBeidat +#flags spell pact + statMap = { + ["skill_empowers_next_x_spells_cast"] = { + mod("BeidatEmpoweredSpells", "BASE", nil), + }, + ["pact_skill_additional_beam_only_chains"] = { + mod("BeidatAdditionalBeamChains", "BASE", nil), + }, + ["pact_skill_grant_x_additional_projectiles_fired_in_nova"] = { + mod("BeidatAdditionalProjectiles", "BASE", nil), + }, + ["pact_skill_grant_x_cascades_to_do_in_spiral"] = { + mod("BeidatAdditionalCascades", "BASE", nil), + }, + ["pact_skill_damage_+%_final_with_hits_and_ailments_to_grant"] = { + mod("BeidatPactDamage", "LIST", { mod = mod("Damage", "MORE", nil, ModFlag.Spell, bit.bor(KeywordFlag.Hit, KeywordFlag.Ailment)) }), + }, + }, #mods #skill PactOfGhorr +#flags spell pact duration + statMap = { + ["skill_empowers_next_x_spells_cast"] = { + mod("GhorrEmpoweredSpells", "BASE", nil), + }, + ["pact_skill_grant_damage_over_time_+%_final_to_spells"] = { + mod("GhorrPactDamage", "LIST", { mod = mod("Damage", "MORE", nil, ModFlag.Dot) }), + }, + }, #mods #skill TriggeredBloodrend +#flags spell pact projectile #mods #skill PactOfKtash +#flags spell pact + statMap = { + ["skill_empowers_next_x_spells_cast"] = { + mod("KtashEmpoweredSpells", "BASE", nil), + }, + ["pact_skill_grant_damage_+%_final_to_exerted_skills"] = { + mod("KtashPactDamage", "LIST", { mod = mod("Damage", "MORE", nil) }), + }, + ["active_skill_osm_vaal_skill_soul_gain_prevention_+%_final_to_grant"] = { + mod("KtashPactSoulGainPrevention", "BASE", nil), + }, + ["active_skill_osm_vaal_skill_soul_refund_chance_%_to_grant"] = { + mod("KtashPactSoulRefundChance", "BASE", nil), + }, + }, #mods #skill PactOfLycia +#flags spell pact + statMap = { + ["skill_empowers_next_x_spells_cast"] = { + mod("LyciaEmpoweredSpells", "BASE", nil), + }, + ["pact_skill_grant_damage_+%_final_to_exerted_skills"] = { + mod("LyciaPactDamage", "LIST", { mod = mod("Damage", "MORE", nil) }), + }, + }, #mods #skill TriggeredHeavensScourge +#flags spell pact area #mods #skill Portal diff --git a/src/Modules/CalcOffence.lua b/src/Modules/CalcOffence.lua index c01dca2588..164ff71666 100644 --- a/src/Modules/CalcOffence.lua +++ b/src/Modules/CalcOffence.lua @@ -2800,6 +2800,90 @@ function calcs.offence(env, actor, activeSkill) globalOutput.ExertedAttackUptimeRatioCalculated = true end end + + -- Each Pact has different eligible spell types, but shares the same uptime calculation. + if activeSkill.skillTypes[SkillType.Spell] and not activeSkill.skillTypes[SkillType.Brand] then + for _, value in ipairs(actor.activeSkillList) do + local pactName = value.activeEffect.grantedEffect.name + local pactKey = pactName:match("^Pact of (.+)$") + if pactKey then + pactKey = pactKey == "K'Tash" and "Ktash" or pactKey + local isVaal = activeSkill.skillTypes[SkillType.Vaal] + local pactApplies = pactKey == "Beidat" and not isVaal and not activeSkill.skillTypes[SkillType.Channel] + and (skillFlags.projectile or activeSkill.skillTypes[SkillType.Cascadable] or skillFlags.chaining and not skillFlags.projectile) + or pactKey == "Ghorr" and not isVaal and activeSkill.skillTypes[SkillType.DamageOverTime] + or pactKey == "Ktash" and isVaal and (activeSkill.skillTypes[SkillType.Damage] or activeSkill.skillTypes[SkillType.DamageOverTime]) + or pactKey == "Lycia" and not isVaal and activeSkill.skillTypes[SkillType.Channel] and skillFlags.selfCast + local calculated = "PactOf"..pactKey.."Calculated" + if pactApplies and not globalOutput[calculated] and not value.skillFlags.disable then + local cooldown = calcSkillCooldown(value.skillModList, value.skillCfg, value.skillData) + local castRate = 1 / value.activeEffect.grantedEffect.castTime * calcLib.mod(value.skillModList, value.skillCfg, "Speed") * calcs.actionSpeedMod(actor) + local castTime = 1 / m_min(castRate, data.misc.ServerTickRate) + local count = value.skillModList:Sum("BASE", value.skillCfg, pactKey.."EmpoweredSpells") + local storedUses = value.skillData.storedUses + value.skillModList:Sum("BASE", value.skillCfg, "AdditionalCooldownUses") + local uptime = (globalOutput.Speed or 0) == 0 and 100 or m_min((count / globalOutput.Speed) / (cooldown + castTime), 1) * 100 + uptime = m_min(100, uptime * storedUses) + local effect = skillModList:Flag(nil, "Condition:PactMaxHit") and 100 or uptime + local effectMult = effect / 100 + + globalOutput.CreatePactOffensiveCalcSection = true + globalOutput["PactOf"..pactKey.."Cooldown"] = cooldown + globalOutput["PactOf"..pactKey.."CastTime"] = castTime + globalOutput[pactKey.."EmpoweredCount"] = count + globalOutput[pactKey.."UpTimeRatio"] = uptime + if globalBreakdown then + globalBreakdown[pactKey.."UpTimeRatio"] = { + s_format("(%.2f ^8(number of Empowered)", count), + s_format("/ %.2f) ^8(casts per second)", globalOutput.Speed or 0), + s_format("/ (%.2f ^8(pact cooldown)", cooldown), + s_format("+ %.2f) ^8(pact cast time)", castTime), + s_format("* %.2f ^8(stored uses)", storedUses), + s_format("= %.2f%%", uptime), + } + end + + local damage = value.skillModList:List(value.skillCfg, pactKey.."PactDamage")[1] + if damage then + local mod = damage.mod + skillModList:NewMod(mod.name, mod.type, mod.value * effectMult, "Uptime Scaled "..pactName, mod.flags, mod.keywordFlags, unpack(mod)) + end + + if pactKey == "Beidat" then + -- Coverage bonuses are averaged with the same uptime as the damage bonus. + if skillFlags.projectile and not skillModList:Flag(skillCfg, "NoAdditionalProjectiles") and not skillModList:Flag(skillCfg, "SingleProjectile") then + globalOutput.BeidatAdditionalProjectiles = value.skillModList:Sum("BASE", value.skillCfg, "BeidatAdditionalProjectiles") * effectMult + globalOutput.ProjectileCount = globalOutput.ProjectileCount + globalOutput.BeidatAdditionalProjectiles + end + if skillFlags.chaining and not skillFlags.projectile and not skillModList:Flag(skillCfg, "CannotChain") and not skillModList:Flag(skillCfg, "NoAdditionalChains") then + globalOutput.BeidatAdditionalBeamChains = value.skillModList:Sum("BASE", value.skillCfg, "BeidatAdditionalBeamChains") * effectMult + globalOutput.ChainMax = globalOutput.ChainMax + globalOutput.BeidatAdditionalBeamChains + globalOutput.ChainMaxString = globalOutput.ChainMax + globalOutput.ChainRemaining = m_max(0, globalOutput.ChainMax - globalOutput.Chain) + end + if activeSkill.skillTypes[SkillType.Cascadable] then + globalOutput.BeidatAdditionalCascades = value.skillModList:Sum("BASE", value.skillCfg, "BeidatAdditionalCascades") * effectMult + end + elseif pactKey == "Ktash" then + globalOutput.KtashSoulRefundChance = value.skillModList:Sum("BASE", value.skillCfg, "KtashPactSoulRefundChance") * effectMult + if globalOutput.SoulGainPreventionDuration then + local duration = globalOutput.SoulGainPreventionDuration + local durationMod = 1 + value.skillModList:Sum("BASE", value.skillCfg, "KtashPactSoulGainPrevention") * effectMult / 100 + globalOutput.SoulGainPreventionDuration = m_max(m_ceil(duration * durationMod * data.misc.ServerTickRate), 1) / data.misc.ServerTickRate + if globalBreakdown then + globalBreakdown.SoulGainPreventionDuration = { + s_format("%.3fs ^8(before Pact of K'Tash)", duration), + s_format("x %.4f ^8(average Pact modifier)", durationMod), + "rounded up to nearest server tick", + s_format("= %.3fs", globalOutput.SoulGainPreventionDuration), + } + end + end + end + globalOutput[calculated] = true + end + end + end + end end output.RuthlessBlowHitEffect = 1 diff --git a/src/Modules/CalcSections.lua b/src/Modules/CalcSections.lua index 54f10fba55..599b5974c7 100644 --- a/src/Modules/CalcSections.lua +++ b/src/Modules/CalcSections.lua @@ -428,6 +428,40 @@ return { }, } } } }, +{ 3, "Pacts", 1, colorCodes.OFFENCE, { { defaultCollapsed = false, label = "Empowered Spells", data = { + colWidth = 114, + { + haveOutput = "CreatePactOffensiveCalcSection", + { format = "Uptime" }, + { format = "# of Empowered" }, + { format = "Projectiles" }, + { format = "Beam Chains" }, + { format = "Cascades" }, + }, + { label = "Pact of Beidat", haveOutput = "BeidatUpTimeRatio", + { format = "{2:output:BeidatUpTimeRatio}%", { breakdown = "BeidatUpTimeRatio" }, }, + { format = "{2:output:BeidatEmpoweredCount}" }, + { format = "+{2:output:BeidatAdditionalProjectiles}" }, + { format = "+{2:output:BeidatAdditionalBeamChains}" }, + { format = "+{2:output:BeidatAdditionalCascades}" }, + }, + { label = "Pact of Ghorr", haveOutput = "GhorrUpTimeRatio", + { format = "{2:output:GhorrUpTimeRatio}%", { breakdown = "GhorrUpTimeRatio" }, }, + { format = "{2:output:GhorrEmpoweredCount}" }, + { format = "" }, { format = "" }, { format = "" }, + }, + { label = "Pact of K'Tash", haveOutput = "KtashUpTimeRatio", + { format = "{2:output:KtashUpTimeRatio}%", { breakdown = "KtashUpTimeRatio" }, }, + { format = "{2:output:KtashEmpoweredCount}" }, + { format = "" }, { format = "" }, { format = "" }, + }, + { label = "Pact of Lycia", haveOutput = "LyciaUpTimeRatio", + { format = "{2:output:LyciaUpTimeRatio}%", { breakdown = "LyciaUpTimeRatio" }, }, + { format = "{2:output:LyciaEmpoweredCount}" }, + { format = "" }, { format = "" }, { format = "" }, + }, +} } +} }, { 3, "Dot", 1, colorCodes.OFFENCE, {{ defaultCollapsed = false, label = "Skill Damage over Time", data = { extra = "{1:output:TotalDotCalcSection} total DoT", flag = "dot", @@ -670,6 +704,7 @@ return { { label = "Aura Duration", haveOutput = "AuraDuration", { format = "{3:output:AuraDuration}s", { breakdown = "AuraDuration" }, }, }, { label = "Reserve Duration", haveOutput = "ReserveDuration", { format = "{3:output:ReserveDuration}s", { breakdown = "ReserveDuration" }, }, }, { label = "Soul Gain Prevent.", haveOutput = "SoulGainPreventionDuration", { format = "{3:output:SoulGainPreventionDuration}s", { breakdown = "SoulGainPreventionDuration" }, }, }, + { label = "Soul Refund Chance", haveOutput = "KtashSoulRefundChance", { format = "{2:output:KtashSoulRefundChance}%" }, }, { label = "Uptime", haveOutput = "DurationUptime", { format = "{2:output:DurationUptime}%", { breakdown = "DurationUptime" }, }, }, { label = "Secondary Uptime", haveOutput = "DurationSecondaryUptime", { format = "{2:output:DurationSecondaryUptime}%", { breakdown = "DurationSecondaryUptime" }, }, }, { label = "Tertiary Uptime", haveOutput = "DurationTertiaryUptime", { format = "{2:output:DurationTertiaryUptime}%", { breakdown = "DurationTertiaryUptime" }, }, }, diff --git a/src/Modules/ConfigOptions.lua b/src/Modules/ConfigOptions.lua index 36025fcc53..2a4b9df8ee 100644 --- a/src/Modules/ConfigOptions.lua +++ b/src/Modules/ConfigOptions.lua @@ -237,6 +237,11 @@ return { modList:NewMod("Condition:WarcryMaxHit", "FLAG", true, "Config") end end }, + { var = "pactMode", type = "list", label = "Pact calc mode:", ifSkill = { "Pact of Beidat", "Pact of Ghorr", "Pact of K'Tash", "Pact of Lycia" }, tooltip = "Controls how Empowered spells from Pacts are calculated:\nAverage: Averages the effect based on Pact uptime.\nMax Hit: Applies the full effect to one Empowered spell.", list = {{val="AVERAGE",label="Average"},{val="MAX",label="Max Hit"}}, apply = function(val, modList, enemyModList) + if val == "MAX" then + modList:NewMod("Condition:PactMaxHit", "FLAG", true, "Config") + end + end }, { var = "EVBypass", type = "check", label = "Disable Emperor's Vigilance Bypass", ifCond = "EVBypass", apply = function(val, modList, enemyModList) modList:NewMod("Condition:EVBypass", "FLAG", true, "Config") end },