-- ToME - Tales of Maj'Eyal -- Copyright (C) 2009 - 2019 Nicolas Casalini -- -- This program is free software: you can redistribute it and/or modify -- it under the terms of the GNU General Public License as published by -- the Free Software Foundation, either version 3 of the License, or -- (at your option) any later version. -- -- This program is distributed in the hope that it will be useful, -- but WITHOUT ANY WARRANTY; without even the implied warranty of -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -- GNU General Public License for more details. -- -- You should have received a copy of the GNU General Public License -- along with this program. If not, see <http://www.gnu.org/licenses/>. -- -- Nicolas Casalini "DarkGod" -- darkgod@te4.org --- general tactical function accounting for Hymn Acolyte and Hymn Incantor -- uses t.base_tactical and t.adept_deac_tactical local hymn_tactical = function(self, t, aitarget) local bt_type, tacs = type(t.base_tactical) if bt_type == "function" then tacs = t.base_tactical(self, t, aitarget) elseif bt_type == "table" then tacs = table.clone(t.base_tactical) end tacs = tacs or {} local lvl = self:getTalentLevel(self.T_HYMN_INCANTOR) if lvl > 0 then tacs.buff = (tacs.buff or 0) + 2*lvl/(lvl + 5) end lvl = self:getTalentLevel(self.T_HYMN_ADEPT) if lvl > 0 and self:isTalentActive(t.id) then if t.adept_deac_tactical then table.mergeAdd(tacs, t.adept_deac_tactical) end tacs.special = (tacs.special or 0) + 0.01 -- forces some "inertia" in toggling hymns end return tacs end newTalent{ name = "Hymn of Shadows", type = {"celestial/hymns-hymns", 1}, mode = "sustained", hide = true, require = divi_req1, points = 5, cooldown = 12, sustain_negative = 20, no_energy = true, dont_provide_pool = true, tactical = hymn_tactical, base_tactical = { buff=1, defend=0.5, escape=1 , closein=1}, adept_deac_tactical = {escape = -0.5, closein = -0.5}, -- adept tactic adjustments when deactivating (negated) range = 0, moveSpeed = function(self, t) return self:combatTalentSpellDamage(t, 20, 50) end, castSpeed = function(self, t) return self:combatTalentSpellDamage(t, 7, 20) end, callbackOnActBase = function(self, t) if self:isTalentActive(self.T_HYMN_NOCTURNALIST) then local t2 = self:getTalentFromId(self.T_HYMN_NOCTURNALIST) t2.do_beams(self, t2) end end, sustain_slots = 'celestial_hymn', activate = function(self, t) game:playSoundNear(self, "talents/spell_generic2") local ret = {} self:talentTemporaryValue(ret, "movement_speed", t.moveSpeed(self, t)/100) self:talentTemporaryValue(ret, "combat_spellspeed", t.castSpeed(self, t)/100) ret.particle = self:addParticles(Particles.new("darkness_shield", 1)) if self:knowTalent(self.T_HYMN_INCANTOR) then local t2 = self:getTalentFromId(self.T_HYMN_INCANTOR) self:talentTemporaryValue(ret, "on_melee_hit", {[DamageType.DARKNESS]=t2.getDamageOnMeleeHit(self, t2)}) self:talentTemporaryValue(ret, "inc_damage", {[DamageType.DARKNESS] = t2.getDarkDamageIncrease(self, t2)}) end if self:knowTalent(self.T_HYMN_ADEPT) then local t2 = self:getTalentFromId(self.T_HYMN_ADEPT) self:talentTemporaryValue(ret, "infravision", t2.getBonusInfravision(self, t2)) end if self:isTalentActive(self.T_HYMN_NOCTURNALIST) then local t2 = self:getTalentFromId(self.T_HYMN_NOCTURNALIST) end return ret end, deactivate = function(self, t, p) self:removeParticles(p.particle) if self.turn_procs.resetting_talents then return true end if self:knowTalent(self.T_HYMN_ADEPT) then local t2 = self:getTalentFromId(self.T_HYMN_ADEPT) game:onTickEnd(function() self:setEffect(self.EFF_WILD_SPEED, 1, {power=t2.getSpeed(self, t2), no_talents=0}) end) end return true end, info = function(self, t) return ([[Chant the glory of the Moons, gaining the agility of shadows. This increases your movement speed by %d%% and your spell speed by %d%%. You may only have one Hymn active at once. The effects will increase with your Spellpower.]]): tformat(t.moveSpeed(self, t), t.castSpeed(self, t)) end, } newTalent{ name = "Hymn of Detection", type = {"celestial/hymns-hymns", 1}, mode = "sustained", hide = true, require = divi_req1, points = 5, cooldown = 12, sustain_negative = 20, no_energy = true, dont_provide_pool = true, tactical = hymn_tactical, adept_deac_tactical = {escape = -0.5}, -- adept tactic adjustments when deactivating (negated) base_tactical = function(self, t, aitarget) -- buff tactic up to +3 depending on how much the talent helps to see a stealthed/invisible target or a target that is out of sense range local buff, max_buff = 0, 4 if not aitarget or not self.aiSeeTargetPos then -- out of combat, stay vigilant for enemies buff = max_buff else if self:attr("blind") or self:attr("esp_all") then return end -- target automatically seen or unseen local ax, ay = self:aiSeeTargetPos(aitarget) local dist = core.fov.distance(self.x, self.y, ax, ay) if dist <= self.sight then -- target within sight range local is_active = self:isTalentActive(t.id) local see, see_chance = self:canSeeNoCache(aitarget) if see_chance and see_chance < 100 then -- detecting hidden is useful (rules out esp, etc.) local tgt_inv = aitarget:attr("invisible") if tgt_inv then -- buff value increases depending on relative invisibility detection local base_det = self:combatSeeInvisible() - (is_active and self.compute_vals[is_active.invis] or 0) if base_det <= 0 then return {buff = max_buff} end -- cannot see at all with no detection ability local need_det = tgt_inv - base_det buff = buff + math.max(0, max_buff*(0.5 + need_det/(math.abs(need_det) + 20))) -- adds 0 (need_det = -20) to max_buff (need_det = +20) end local tgt_stealth = aitarget:attr("stealth") if tgt_stealth then -- buff value increases depending on relative stealth detection tgt_stealth = tgt_stealth + (aitarget:attr("inc_stealth") or 0) local base_det = is_active and is_active.base_see_stealth or self:combatSeeStealth() local need_det = tgt_stealth - base_det buff = buff + math.max(0, max_buff*(0.5 + need_det/(math.abs(need_det) + 20))) -- adds 0 (need_det = -20) to max_buff (need_det = +20) end end end end buff=math.min(buff, max_buff) return {buff=buff + 0.25, special = buff/2} end, range = 0, sustain_slots = 'celestial_hymn', getSeeInvisible = function(self, t) return self:combatTalentSpellDamage(t, 2, 25) end, getSeeStealth = function(self, t) return self:combatTalentSpellDamage(t, 2, 25) end, critPower = function(self, t) return self:combatTalentSpellDamage(t, 10, 50) end, callbackOnActBase = function(self, t) if self:isTalentActive(self.T_HYMN_NOCTURNALIST) then local t2 = self:getTalentFromId(self.T_HYMN_NOCTURNALIST) t2.do_beams(self, t2) end end, activate = function(self, t) game:playSoundNear(self, "talents/spell_generic2") -- base_see_stealth = self:combatSeeStealth(), local ret = {} self:talentTemporaryValue(ret, "see_invisible", t.getSeeInvisible(self, t)) self:talentTemporaryValue(ret, "see_stealth", t.getSeeStealth(self, t)) self:talentTemporaryValue(ret, "blindfight", 1) self:talentTemporaryValue(ret, "combat_critical_power", t.critPower(self, t)) ret.particle = self:addParticles(Particles.new("darkness_shield", 1)) if self:knowTalent(self.T_HYMN_INCANTOR) then local t2 = self:getTalentFromId(self.T_HYMN_INCANTOR) self:talentTemporaryValue(ret, "on_melee_hit", {[DamageType.DARKNESS]=t2.getDamageOnMeleeHit(self, t2)}) self:talentTemporaryValue(ret, "inc_damage", {[DamageType.DARKNESS] = t2.getDarkDamageIncrease(self, t2)}) end if self:knowTalent(self.T_HYMN_ADEPT) then local t2 = self:getTalentFromId(self.T_HYMN_ADEPT) self:talentTemporaryValue(ret, "infravision", t2.getBonusInfravision(self, t2)) end if self:isTalentActive(self.T_HYMN_NOCTURNALIST) then local t2 = self:getTalentFromId(self.T_HYMN_NOCTURNALIST) end return ret end, deactivate = function(self, t, p) self:removeParticles(p.particle) if self.turn_procs.resetting_talents then return true end if self:knowTalent(self.T_HYMN_ADEPT) then local t2 = self:getTalentFromId(self.T_HYMN_ADEPT) self:setEffect(self.EFF_INVISIBILITY, t2.invisDur(self, t2), {power=t2.invisPower(self, t2), penalty=0.4}) end return true end, info = function(self, t) local invis = t.getSeeInvisible(self, t) local stealth = t.getSeeStealth(self, t) return ([[Chant the glory of the Moons, granting you stealth detection (+%d power), and invisibility detection (+%d power). You may also attack creatures you cannot see without penalty and your critical hits do %d%% more damage. You may only have one Hymn active at once. The stealth and invisibility detection will increase with your Spellpower.]]): tformat(stealth, invis, t.critPower(self, t)) end, } newTalent{ name = "Hymn of Perseverance", type = {"celestial/hymns-hymns",1}, mode = "sustained", hide = true, require = divi_req1, points = 5, cooldown = 12, sustain_negative = 20, no_energy = true, dont_provide_pool = true, tactical = hymn_tactical, base_tactical = {defend=1}, -- could check base immunities here adept_deac_tactical = {defend = -0.5}, -- adept tactic adjustments when deactivating (negated) range = 10, getImmunities = function(self, t) return self:combatTalentLimit(t, 1, 0.2, 0.45) end, -- Limit < 100% callbackOnActBase = function(self, t) if self:isTalentActive(self.T_HYMN_NOCTURNALIST) then local t2 = self:getTalentFromId(self.T_HYMN_NOCTURNALIST) t2.do_beams(self, t2) end end, sustain_slots = 'celestial_hymn', activate = function(self, t) game:playSoundNear(self, "talents/spell_generic2") local ret = {} self:talentTemporaryValue(ret, "stun_immune", t.getImmunities(self, t)) self:talentTemporaryValue(ret, "confusion_immune", t.getImmunities(self, t)) self:talentTemporaryValue(ret, "blind_immune", t.getImmunities(self, t)) ret.particle = self:addParticles(Particles.new("darkness_shield", 1)) if self:knowTalent(self.T_HYMN_INCANTOR) then local t2 = self:getTalentFromId(self.T_HYMN_INCANTOR) self:talentTemporaryValue(ret, "on_melee_hit", {[DamageType.DARKNESS]=t2.getDamageOnMeleeHit(self, t2)}) self:talentTemporaryValue(ret, "inc_damage", {[DamageType.DARKNESS] = t2.getDarkDamageIncrease(self, t2)}) end if self:knowTalent(self.T_HYMN_ADEPT) then local t2 = self:getTalentFromId(self.T_HYMN_ADEPT) self:talentTemporaryValue(ret, "infravision", t2.getBonusInfravision(self, t2)) end if self:isTalentActive(self.T_HYMN_NOCTURNALIST) then local t2 = self:getTalentFromId(self.T_HYMN_NOCTURNALIST) end return ret end, deactivate = function(self, t, p) self:removeParticles(p.particle) if self.turn_procs.resetting_talents then return true end if self:knowTalent(self.T_HYMN_ADEPT) then local t2 = self:getTalentFromId(self.T_HYMN_ADEPT) self:setEffect(self.EFF_DAMAGE_SHIELD, t2.shieldDur(self, t2), {power=t2.shieldPower(self, t2)}) end return true end, info = function(self, t) local immunities = t.getImmunities(self, t) return ([[Chant the glory of the Moons, granting you %d%% stun, blindness and confusion resistance. You may only have one Hymn active at once.]]): tformat(100 * (immunities)) end, } -- Depreciated, but retained for compatability. newTalent{ name = "Hymn of Moonlight", type = {"celestial/hymns-hymns",1}, mode = "sustained", require = divi_req4, points = 5, cooldown = 12, sustain_negative = 20, no_energy = true, dont_provide_pool = true, tactical = { ATTACKAREA = { DARKNESS = 1 }, NEGATIVE = function(self, t, target) return t.getNegativeDrain(self, t)/15 -- negated for foes end }, range = 5, getDamage = function(self, t) return self:combatTalentSpellDamage(t, 7, 80) end, getTargetCount = function(self, t) return math.floor(self:combatTalentScale(t, 1, 5)) end, getNegativeDrain = function(self, t) return self:combatTalentLimit(t, 0, 7, 3) end, -- Limit > 0, no regen at high levels target = function(self, t) -- for AI only return {type="ball", friendlyfire=false, friendlyblock=false, radius=self:getTalentRange(t), range=0, talent=t} end, callbackOnActBase = function(self, t) local drain = t.getNegativeDrain(self, t) if self:getNegative() < drain then return end local tgts = {} local grids = core.fov.circle_grids(self.x, self.y, 5, true) for x, yy in pairs(grids) do for y, _ in pairs(grids[x]) do local a = game.level.map(x, y, Map.ACTOR) if a and self:reactionToward(a) < 0 then tgts[#tgts+1] = a end end end local drain = t.getNegativeDrain(self, t) -- Randomly take targets local tg = {type="hit", range=self:getTalentRange(t), talent=t} for i = 1, t.getTargetCount(self, t) do if #tgts <= 0 then break end if self:getNegative() - 1 < drain then break end local a, id = rng.table(tgts) table.remove(tgts, id) self:project(tg, a.x, a.y, DamageType.DARKNESS, rng.avg(1, self:spellCrit(t.getDamage(self, t)), 3)) game.level.map:particleEmitter(self.x, self.y, math.max(math.abs(a.x-self.x), math.abs(a.y-self.y)), "shadow_beam", {tx=a.x-self.x, ty=a.y-self.y}) game:playSoundNear(self, "talents/spell_generic") self:incNegative(-drain) end end, sustain_slots = 'celestial_hymn', activate = function(self, t) game:playSoundNear(self, "talents/spell_generic") game.logSeen(self, "#DARK_GREY#A shroud of shadow dances around %s!", self:getName()) return { name=("%s's %s"):tformat(self:getName():capitalize(), t.name) } end, deactivate = function(self, t, p) game.logSeen(self, "#DARK_GREY#The shroud of shadows around %s disappears.", self:getName()) return true end, info = function(self, t) local targetcount = t.getTargetCount(self, t) local damage = t.getDamage(self, t) local drain = t.getNegativeDrain(self, t) return ([[Chant the glory of the Moons, conjuring a shroud of dancing shadows that follows you as long as this spell is active. Each turn, a shadowy beam will hit up to %d of your foes within radius 5 for 1 to %0.2f damage. This powerful spell will drain %0.1f negative energy for each beam; no beam will fire if your negative energy is too low. You may only have one Hymn active at once. The damage will increase with your Spellpower.]]): tformat(targetcount, damDesc(self, DamageType.DARKNESS, damage), drain) end, } newTalent{ name = "Hymn Acolyte", type = {"celestial/hymns", 1}, require = divi_req1, points = 5, mode = "passive", negative = 0, -- forces learning of Negative pool passives = function(self, t) self:setTalentTypeMastery("celestial/hymns-hymns", self:getTalentMastery(t)) end, on_learn = function(self, t) self:learnTalent(self.T_HYMN_OF_SHADOWS, true, nil, {no_unlearn=true}) self:learnTalent(self.T_HYMN_OF_DETECTION, true, nil, {no_unlearn=true}) self:learnTalent(self.T_HYMN_OF_PERSEVERANCE, true, nil, {no_unlearn=true}) end, on_unlearn = function(self, t) self:unlearnTalent(self.T_HYMN_OF_SHADOWS) self:unlearnTalent(self.T_HYMN_OF_DETECTION) self:unlearnTalent(self.T_HYMN_OF_PERSEVERANCE) end, info = function(self, t) local ret = "" local old1 = self.talents[self.T_HYMN_OF_SHADOWS] local old2 = self.talents[self.T_HYMN_OF_DETECTION] local old3 = self.talents[self.T_HYMN_OF_PERSEVERANCE] self.talents[self.T_HYMN_OF_SHADOWS] = (self.talents[t.id] or 0) self.talents[self.T_HYMN_OF_DETECTION] = (self.talents[t.id] or 0) self.talents[self.T_HYMN_OF_PERSEVERANCE] = (self.talents[t.id] or 0) pcall(function() -- Be very paranoid, even if some addon or whatever manage to make that crash, we still restore values local t1 = self:getTalentFromId(self.T_HYMN_OF_SHADOWS) local t2 = self:getTalentFromId(self.T_HYMN_OF_DETECTION) local t3 = self:getTalentFromId(self.T_HYMN_OF_PERSEVERANCE) ret = ([[You have learned to sing the praises of the Moons, in the form of three defensive Hymns: Hymn of Shadows: Increases your movement speed by %d%% and your spell casting speed by %d%%. Hymn of Detection: Increases your ability to see stealthy creatures by %d and invisible creatures by %d, and increases your critical power by %d%%. Hymn of Perseverance: Increases your resistance to stun, confusion and blinding by %d%%. You may only have one Hymn active at a time.]]): tformat(t1.moveSpeed(self, t1), t1.castSpeed(self, t1), t2.getSeeStealth(self, t2), t2.getSeeInvisible(self, t2), t2.critPower(self, t2), t3.getImmunities(self, t3)*100) end) self.talents[self.T_HYMN_OF_SHADOWS] = old1 self.talents[self.T_HYMN_OF_DETECTION] = old2 self.talents[self.T_HYMN_OF_PERSEVERANCE] = old3 return ret end, } newTalent{ name = "Hymn Incantor", type = {"celestial/hymns", 2}, require = divi_req2, points = 5, mode = "passive", getDamageOnMeleeHit = function(self, t) return self:combatTalentSpellDamage(t, 10, 30) end, getDarkDamageIncrease = function(self, t) return self:combatTalentSpellDamage(t, 10, 30) end, info = function(self, t) return ([[Your Hymns now focus darkness near you, which increases your darkness damage by %d%% and does %0.2f darkness damage to anyone who hits you in melee. These values scale with your Spellpower.]]):tformat(t.getDarkDamageIncrease(self, t), damDesc(self, DamageType.DARKNESS, t.getDamageOnMeleeHit(self, t))) end, } -- Remember that Hymns can be swapped instantly. newTalent{ name = "Hymn Adept", type = {"celestial/hymns", 3}, require = divi_req3, points = 5, mode = "passive", getBonusInfravision = function(self, t) return math.floor(self:combatTalentScale(t, 0.75, 3.5, 0.75)) end, getSpeed = function(self, t) return self:combatTalentSpellDamage(t, 300, 600) end, shieldDur = function(self, t) return self:combatTalentSpellDamage(t, 5, 10) end, shieldPower = function(self, t) return self:combatTalentSpellDamage(t, 50, 500) end, invisDur = function(self, t) return self:combatTalentSpellDamage(t, 5, 10) end, invisPower = function(self, t) return math.ceil(self:combatTalentSpellDamage(t, 20, 30)) end, info = function(self, t) return ([[Your skill in Hymns now improves your sight in darkness, increasing your infravision radius by %d. Also, when you end a Hymn, you will gain a buff of a type based on which Hymn you ended. Hymn of Shadows increases your movement speed by %d%% for one turn. Hymn of Detection makes you invisible (power %d) for %d turns. Hymn of Perseverance grants a damage shield (power %d) for %d turns.]]):tformat(t.getBonusInfravision(self, t), t.getSpeed(self, t), t.invisPower(self, t), t.invisDur(self, t), self:getShieldAmount(t.shieldPower(self, t)), self:getShieldDuration(t.shieldDur(self, t))) end, } newTalent{ name = "Hymn Nocturnalist", type = {"celestial/hymns", 4}, require = divi_req4, points = 5, mode = "sustained", cooldown = 10, sustain_negative = 5, range = 5, tactical = { SELF = {NEGATIVE = 0.5}, -- this assumes a Hymn is active ATTACKAREA = { DARKNESS = 1 }, DISABLE = { blind = 0.5}, NEGATIVE = function(self, t, target) return t.getNegativeDrain(self, t)/15 -- negated for foes end }, target = function(self, t) -- for AI only return {type="ball", friendlyfire=false, friendlyblock=false, radius=self:getTalentRange(t), range=0, talent=t} end, getDamage = function(self, t) return self:combatTalentSpellDamage(t, 1, 50) end, getTargetCount = function(self, t) return math.floor(self:combatTalentScale(t, 1, 5)) end, getNegativeDrain = function(self, t) return 5 end, do_beams = function(self, t) if self:getNegative() < t.getNegativeDrain(self, t) then return end local tgts = {} local grids = core.fov.circle_grids(self.x, self.y, 5, true) for x, yy in pairs(grids) do for y, _ in pairs(grids[x]) do local a = game.level.map(x, y, Map.ACTOR) if a and self:reactionToward(a) < 0 then tgts[#tgts+1] = a end end end if #tgts <= 0 then return end local drain = t.getNegativeDrain(self, t) self:incNegative(-drain) -- Make sure to pay before Corona procs local dam = self:spellCrit(t.getDamage(self, t)) -- Randomly take targets local tg = {type="hit", range=self:getTalentRange(t), talent=t} for i = 1, t.getTargetCount(self, t) do if #tgts <= 0 then break end local a, id = rng.table(tgts) table.remove(tgts, id) self:project(tg, a.x, a.y, DamageType.DARKNESS_BLIND, dam) game.level.map:particleEmitter(self.x, self.y, math.max(math.abs(a.x-self.x), math.abs(a.y-self.y)), "shadow_beam", {tx=a.x-self.x, ty=a.y-self.y}) game:playSoundNear(self, "talents/spell_generic") end end, activate = function(self, t) game:onTickEnd(function() self.turn_procs.resetting_talents = true local hymn_id = self.sustain_slots and self.sustain_slots.celestial_hymn if self:isTalentActive(hymn_id) then self:forceUseTalent(hymn_id, {ignore_energy=true, ignore_cd=true, no_talent_fail=true, silent=true}) self:forceUseTalent(hymn_id, {ignore_energy=true, ignore_cd=true, no_talent_fail=true, silent=true}) end self.turn_procs.resetting_talents = nil end) return {} end, deactivate = function(self, t, p) game:onTickEnd(function() self.turn_procs.resetting_talents = true local hymn_id = self.sustain_slots and self.sustain_slots.celestial_hymn if self:isTalentActive(hymn_id) then self:forceUseTalent(hymn_id, {ignore_energy=true, ignore_cd=true, no_talent_fail=true, silent=true}) self:forceUseTalent(hymn_id, {ignore_energy=true, ignore_cd=true, no_talent_fail=true, silent=true}) end self.turn_procs.resetting_talents = nil end) return true end, info = function(self, t) return ([[Your passion for singing the praises of the Moons reaches its zenith. Your Hymns now fire shadowy beams that will hit up to %d of your foes within radius 5 for %0.2f damage, with a 25%% chance of blinding. This powerful effect will drain %0.1f negative energy each time it fires at at least 1 target; no beam will fire if your negative energy is too low. These values scale with your Spellpower.]]):tformat(t.getTargetCount(self, t), damDesc(self, DamageType.DARKNESS, t.getDamage(self, t)), t.getNegativeDrain(self, t)) end, }