diff --git a/game/modules/tome/ai/shadow.lua b/game/modules/tome/ai/shadow.lua index d4309addf9379c4dd1dbf63290763b6971611b8a..2cff0cf73c481ec40c1b9ff01d7e28bd722fd076 100644 --- a/game/modules/tome/ai/shadow.lua +++ b/game/modules/tome/ai/shadow.lua @@ -59,6 +59,20 @@ local function shadowChooseActorTarget(self) end local function shadowMoveToActorTarget(self) + local range = math.floor(core.fov.distance(self.x, self.y, self.ai_target.actor.x, self.ai_target.actor.y)) + + if range <= 1 and self.ai_state.close_attack_spell_chance and rng.percent(self.ai_state.close_attack_spell_chance) then + -- chance for close spell + if self:closeAttackSpell() then return true end + elseif range <= 6 and self.ai_state.far_attack_spell_chance and rng.percent(self.ai_state.far_attack_spell_chance) then + -- chance for a far spell + if self:farAttackSpell() then return true end + end + + if range <= 1 and self.ai_state.dominate_chance and rng.percent(self.ai_state.dominate_chance) then + if self:dominate() then return true end + end + -- use the target blindside chance if it was assigned; otherwise, use the normal chance local blindsideChance = self.ai_target.blindside_chance or self.ai_state.blindside_chance self.ai_target.blindside_chance = nil @@ -76,17 +90,8 @@ local function shadowMoveToActorTarget(self) end end - if self:canAttackSpell() then - -- use the attack spell chance if it was assigned; otherwise, use the normal chance - local attackSpellChance = self.ai_target.attack_spell_chance or self.ai_state.attack_spell_chance - self.ai_target.attack_spell_chance = nil - if rng.percent(attackSpellChance) and self:attackSpell() then - return true - end - end - -- chance to reset target next turn if we are attacking (unless we have been focused) - if math.floor(core.fov.distance(self.x, self.y, self.ai_target.actor.x, self.ai_target.actor.y)) <= 1 and rng.percent(20) then + if range <= 1 and rng.percent(20) then --game.logPlayer(self.summoner, "#PINK#%s is about to attack.", self.name:capitalize()) if not self.ai_state.focus_on_target then self.ai_state.target_time = self.ai_state.target_timeout @@ -198,32 +203,28 @@ newAI("shadow", function(self) clearTarget(self) end - -- apply feed - if self.summoner:knowTalent(self.summoner.T_FEED_SHADOWS) then - local t = self.summoner:getTalentFromId(self.summoner.T_FEED_SHADOWS) - self:feed(t) - end - -- shadow wall if self.ai_state.shadow_wall then + clearTarget(self) + + local defendant = self.ai_state.shadow_wall_target - if self.ai_state.shadow_wall_time <= 0 then + if self.ai_state.shadow_wall_time <= 0 or defendant.dead then self.ai_state.shadow_wall = false else self.ai_state.shadow_wall_time = self.ai_state.shadow_wall_time - 1 - local range = core.fov.distance(self.x, self.y, self.summoner.x, self.summoner.y) - if range > 3 then + local range = core.fov.distance(self.x, self.y, defendant.x, defendant.y) + if range >= 3 then -- phase door into range self:useTalent(self.T_SHADOW_PHASE_DOOR) return true elseif range > 1 then - self.ai_target.x = self.summoner.x - self.ai_target.y = self.summoner.y + self.ai_target.x = defendant.x + self.ai_target.y = defendant.y if shadowMoveToLocationTarget(self) then return true end end - -- no action..look for a target to attack local newX, newY local start = rng.range(0, 8) @@ -235,7 +236,7 @@ newAI("shadow", function(self) self:attackTarget(target, nil, 1, true) return true end - if not newX and math.floor(core.fov.distance(x, y, self.summoner.x, self.summoner.y)) <= 1 and self:canMove(x, y, false) then + if not newX and math.floor(core.fov.distance(x, y, defendant.x, defendant.y)) <= 1 and self:canMove(x, y, false) then newX, newY = x, y end end diff --git a/game/modules/tome/class/Actor.lua b/game/modules/tome/class/Actor.lua index 90ba745840da369eeaeea4c2a7834723d4f81cea..f489cff67fa81ed1b9f56f09dc067986d26fb98f 100644 --- a/game/modules/tome/class/Actor.lua +++ b/game/modules/tome/class/Actor.lua @@ -943,8 +943,7 @@ function _M:onTakeHit(value, src) self:forceUseTalent(self.T_SHIELD_OF_LIGHT, {ignore_energy=true}) end end - - + -- Second Life if self:isTalentActive(self.T_SECOND_LIFE) and value >= self.life then local sl = self.max_life * (0.05 + self:getTalentLevelRaw(self.T_SECOND_LIFE)/25) @@ -953,6 +952,18 @@ function _M:onTakeHit(value, src) game.logSeen(self, "%s has been saved by a blast of positive energy!", self.name:capitalize()) self:forceUseTalent(self.T_SECOND_LIFE, {ignore_energy=true}) end + + if value >= self.life and self.ai_state and self.ai_state.can_reform then + local t = self:getTalentFromId(self.T_SHADOW_REFORM) + if rng.percent(t.getChance(self, t)) then + value = 0 + self.life = self.max_life + game.logSeen(self, "%s fades for a moment and then reforms whole again!", self.name:capitalize()) + game.level.map:particleEmitter(self.x, self.y, 1, "teleport_out") + game:playSoundNear(self, "talents/heal") + game.level.map:particleEmitter(self.x, self.y, 1, "teleport_in") + end + end if self:knowTalent(self.T_LEECH) and src.hasEffect and src:hasEffect(src.EFF_VIMSENSE) then self:incVim(3 + self:getTalentLevel(self.T_LEECH) * 0.7) diff --git a/game/modules/tome/data/birth/classes/afflicted.lua b/game/modules/tome/data/birth/classes/afflicted.lua index 7335b31d1d206ee28aee2a17ef4aac22cd87989b..f0db10dfe30077627617158231cbc111df7fcbfd 100644 --- a/game/modules/tome/data/birth/classes/afflicted.lua +++ b/game/modules/tome/data/birth/classes/afflicted.lua @@ -56,7 +56,6 @@ newBirthDescriptor{ ["cursed/slaughter"]={true, 0.0}, ["cursed/endless-hunt"]={true, 0.0}, ["cursed/cursed-form"]={true, 0.0}, - ["cursed/traveler"]={true, 0.0}, ["technique/combat-training"]={true, 0.3}, ["cunning/survival"]={false, 0.0}, ["cursed/rampage"]={false, 0.0}, @@ -102,7 +101,7 @@ newBirthDescriptor{ ["cursed/shadows"]={true, 0.3}, ["cursed/darkness"]={true, 0.3}, ["cursed/cursed-form"]={true, 0.0}, - ["cursed/traveler"]={true, 0.0}, + ["cunning/survival"]={false, 0.0}, ["cursed/dark-figure"]={false, 0.0}, }, talents = { diff --git a/game/modules/tome/data/damage_types.lua b/game/modules/tome/data/damage_types.lua index 7bfa4b2a559962fcff5104940c2c50fb63544d4d..c24f527a03ba8488ad40e17f5551bad07d3dee66 100644 --- a/game/modules/tome/data/damage_types.lua +++ b/game/modules/tome/data/damage_types.lua @@ -1301,4 +1301,19 @@ newDamageType{ target:setEffect(target.EFF_SPEED, 3, {power=0.6}) end end, -} \ No newline at end of file +} + +newDamageType{ + name = "devour life", type = "DEVOUR_LIFE", + projector = function(src, x, y, type, dam) + if _G.type(dam) == "number" then dam = {dam=dam} end + local target = game.level.map(x, y, Map.ACTOR) -- Get the target first to make sure we heal even on kill + local realdam = DamageType:get(DamageType.PHYSICAL).projector(src, x, y, DamageType.PHYSICAL, dam.dam) + if target and realdam > 0 then + local heal = realdam * (dam.healfactor or 1) + src:heal(heal) + game.logSeen(target, "%s consumes %d life from %s!", src.name:capitalize(), heal, target.name) + end + end, + hideMessage=true, +} diff --git a/game/modules/tome/data/talents/cursed/dark-figure.lua b/game/modules/tome/data/talents/cursed/dark-figure.lua index 7290078a142b527a2b637d66f077cebbf6adcb9f..b3c5c04ba7172d05453b1b236db5a54895d8671f 100644 --- a/game/modules/tome/data/talents/cursed/dark-figure.lua +++ b/game/modules/tome/data/talents/cursed/dark-figure.lua @@ -31,6 +31,7 @@ newTalent{ getRadius = function(self, t) return 3 + math.floor((self:getTalentLevelRaw(t) - 1) / 2) end, getDuration = function(self, t) return 5 + math.floor(self:getTalentLevel(t) * 2) end, tactical = { DISABLE = 2 }, + requires_target = true, range = 6, action = function(self, t) local tg = {type="hit", range=self:getTalentRange(t), talent=t} diff --git a/game/modules/tome/data/talents/cursed/dark-sustenance.lua b/game/modules/tome/data/talents/cursed/dark-sustenance.lua index 2bfeeeecebffcb34bafd4997b0954b50c75276df..42487b89e44db4dba6e174adc29d8e6862e1c49f 100644 --- a/game/modules/tome/data/talents/cursed/dark-sustenance.lua +++ b/game/modules/tome/data/talents/cursed/dark-sustenance.lua @@ -17,6 +17,14 @@ -- Nicolas Casalini "DarkGod" -- darkgod@te4.org +local function combatTalentDamage(self, t, min, max) + return self:combatTalentSpellDamage(t, min, max, self.level + self:getWil()) +end + +local function combatPower(self, t, multiplier) + return (self.level + self:getWil()) * (multiplier or 1) +end + newTalent{ name = "Feed", type = {"cursed/dark-sustenance", 1}, @@ -26,25 +34,25 @@ newTalent{ cooldown = 6, range = 7, tactical = { BUFF = 2, DEFEND = 1 }, - requires_target = true, + requires_target = function(self, t) return self:getTalentLevel(t) >= 5 end, + direct_hit = true, getHateGain = function(self, t) return math.sqrt(self:getTalentLevel(t)) * 0.2 + self:getWil(0.15) end, action = function(self, t) - local tg = {type="hit", range=self:getTalentRange(t)} + local range = self:getTalentRange(t) + local tg = {type="hit", range=range} local x, y, target = self:getTarget(tg) - if not x or not y or not target then return nil end + if not x or not y or not target or core.fov.distance(self.x, self.y, x, y) > range then return nil end if self:reactionToward(target) >= 0 or target.summoner == self then game.logPlayer(self, "You can only gain sustenance from your foes!"); return nil end - print("*** targeted"); - -- remove old effect - if self:hasEffect(self.EFF_FEED_HATE) then - self:removeEffect(self.EFF_FEED_HATE) + if self:hasEffect(self.EFF_FEED) then + self:removeEffect(self.EFF_FEED) end local hateGain = t.getHateGain(self, t) @@ -53,11 +61,11 @@ newTalent{ local damageGain = 0 local resistGain = 0 - local tFeedHealth = self:getTalentFromId(self.T_FEED_HEALTH) - if tFeedHealth and self:getTalentLevelRaw(tFeedHealth) > 0 then - constitutionGain = tFeedHealth.getConstitutionGain(self, tFeedHealth, target) - lifeRegenGain = tFeedHealth.getLifeRegenGain(self, tFeedHealth) - end + --local tFeedHealth = self:getTalentFromId(self.T_FEED_HEALTH) + --if tFeedHealth and self:getTalentLevelRaw(tFeedHealth) > 0 then + -- constitutionGain = tFeedHealth.getConstitutionGain(self, tFeedHealth, target) + -- lifeRegenGain = tFeedHealth.getLifeRegenGain(self, tFeedHealth) + --end local tFeedPower = self:getTalentFromId(self.T_FEED_POWER) if tFeedPower and self:getTalentLevelRaw(tFeedPower) > 0 then @@ -75,11 +83,67 @@ newTalent{ end, info = function(self, t) local hateGain = t.getHateGain(self, t) - return ([[Feed from the essence of your enemy. Draws %0.2f hate per turn from a targeted foe as long as the foe remains in your line of sight. + return ([[Feed from the essence of your enemy. Draws %0.2f hate per turn from a targeted foe as long as the they remain in your line of sight. Improves with the Willpower stat.]]):format(hateGain) end, } +newTalent{ + name = "Devour Life", + type = {"cursed/dark-sustenance", 2}, + require = cursed_wil_req2, + points = 5, + random_ego = "attack", + cooldown = 6, + range = 7, + tactical = { BUFF = 2, DEFEND = 1 }, + direct_hit = true, + requires_target = true, + getLifeSteal = function(self, t, target) + return combatTalentDamage(self, t, 20, 120) + end, + action = function(self, t) + local effect = self:hasEffect(self.EFF_FEED) + if not effect then + if self:getTalentLevel(t) >= 5 then + local tFeed = self:getTalentFromId(self.T_FEED) + if not tFeed.action(self, tFeed) then return nil end + + effect = self:hasEffect(self.EFF_FEED) + else + game.logPlayer(self, "You must begin feeding before you can Devour Life."); + return nil + end + end + local target = effect.target + + if target and not target.dead then + local lifeSteal = t.getLifeSteal(self, t) + self:project({type="hit", x=target.x,y=target.y}, target.x, target.y, DamageType.DEVOUR_LIFE, { dam=lifeSteal }) + + game.level.map:particleEmitter(self.x, self.y, math.max(math.abs(target.x-self.x), math.abs(target.y-self.y)), "dark_torrent", {tx=target.x-self.x, ty=target.y-self.y}) + --local dx, dy = target.x - self.x, target.y - self.y + --game.level.map:particleEmitter(self.x, self.y,math.max(math.abs(dx), math.abs(dy)), "feed_hate", { tx=dx, ty=dy }) + game:playSoundNear(self, "talents/fire") + + return true + end + + return nil + end, + info = function(self, t) + local lifeSteal = t.getLifeSteal(self, t) + if self:getTalentLevel(t) >= 5 then + return ([[Devours life from the target of your feeding. %d life from the victim will be added to your own. Devour Life can be used like the Feed talent to begin feeding. + Improves with the Willpower stat.]]):format(lifeSteal) + else + return ([[Devours life from the target of your feeding. %d life from the victim will be added to your own. At level 5 Devour Life can be used like the Feed talent to begin feeding. + Improves with the Willpower stat.]]):format(lifeSteal) + end + end, +} + +--[[ newTalent{ name = "Feed Health", type = {"cursed/dark-sustenance", 2}, @@ -102,11 +166,11 @@ newTalent{ info = function(self, t) local constitutionGain = t.getConstitutionGain(self, t) local lifeRegenGain = t.getLifeRegenGain(self, t) - return ([[Enhances your feeding by transferring %d constitution and %0.1f life per turn from a targeted foe to you. - Improves with the Willpower stat.]]):format(constitutionGain, lifeRegenGain) + return ([Enhances your feeding by transferring %d constitution and %0.1f life per turn from a targeted foe to you. + Improves with the Willpower stat.]):format(constitutionGain, lifeRegenGain) end, } - +]] newTalent{ name = "Feed Power", type = {"cursed/dark-sustenance", 3}, diff --git a/game/modules/tome/data/talents/cursed/darkness.lua b/game/modules/tome/data/talents/cursed/darkness.lua index 04ef6baf0922fc6c6fd120381b6d6a692b262dfc..51dd4a84449db48e3bf82f1497bcc28a09938219 100644 --- a/game/modules/tome/data/talents/cursed/darkness.lua +++ b/game/modules/tome/data/talents/cursed/darkness.lua @@ -46,7 +46,7 @@ local function createDarkTendrils(summoner, x, y, target, damage, duration, pinD local done = false local hitTarget = false - local tCreepingDarkness = self.summoner:getTalentFromId(summoner.T_CREEPING_DARKNESS) + local tCreepingDarkness = self.summoner:getTalentFromId(self.summoner.T_CREEPING_DARKNESS) if self.finalizing then if self.duration <= 0 or self.target.dead or self.x ~= self.target.x or self.y ~= self.target.y then @@ -295,7 +295,6 @@ newTalent{ local locations = {} local grids = core.fov.circle_grids(x, y, radius, true) for darkX, yy in pairs(grids) do for darkY, _ in pairs(grids[darkX]) do - print("*** pairs", darkX, darkY) local l = line.new(x, y, darkX, darkY) local lx, ly = l() while lx and ly do @@ -307,7 +306,6 @@ newTalent{ if lx == darkX and ly == darkY and t.canCreep(darkX, darkY) then locations[#locations+1] = {darkX, darkY} - print("*** locations", darkX, darkY) end end end @@ -349,6 +347,17 @@ newTalent{ return 5 + (level - 3) * 3 end, + hasLOS = function(x1, y1, x2, y2) + local l = line.new(x1, y1, x2, y2) + local lx, ly = l() + while lx and ly do + local entity = game.level.map:checkAllEntities(lx, ly, "block_sight") + if entity and not game.level.map:checkAllEntities(lx, ly, "creepingDark") then return false end + + lx, ly = l() + end + return true + end, info = function(self, t) local damageIncrease = t.getDamageIncrease(self, t) if damageIncrease <= 0 then @@ -369,7 +378,6 @@ newTalent{ cooldown = 6, tactical = { ATTACK = 2, DISABLE = 1 }, range = 5, - direct_hit = true, reflectable = true, requires_target = true, getDamage = function(self, t) @@ -422,6 +430,8 @@ newTalent{ hate = 1.2, range = 6, tactical = { ATTACK = 2, DISABLE = 2 }, + direct_hit = true, + requires_target = true, getPinDuration = function(self, t) return 1 + math.floor(self:getTalentLevel(t) / 2) end, @@ -431,9 +441,10 @@ newTalent{ action = function(self, t) if self.dark_tendrils then return false end - local tg = {type="hit", range=self:getTalentRange(t), talent=t} + local range = self:getTalentRange(t) + local tg = {type="hit", range=range, talent=t} local x, y, target = self:getTarget(tg) - if not x or not y or not target then return nil end + if not x or not y or not target or core.fov.distance(self.x, self.y, x, y) > range then return nil end local pinDuration = t.getPinDuration(self, t) local damage = t.getDamage(self, t) diff --git a/game/modules/tome/data/talents/cursed/force-of-will.lua b/game/modules/tome/data/talents/cursed/force-of-will.lua index feaf482bb068f8767ac1225fd28604c2002faadc..79ba231536302f6c381cdb081b0751c5ee76ea98 100644 --- a/game/modules/tome/data/talents/cursed/force-of-will.lua +++ b/game/modules/tome/data/talents/cursed/force-of-will.lua @@ -103,6 +103,8 @@ newTalent{ cooldown = 4, hate = 0.5, tactical = { ATTACK = 2 }, + direct_hit = true, + requires_target = true, range = function(self, t) return 4 end, @@ -117,11 +119,10 @@ newTalent{ local tg = {type="hit", range=self:getTalentRange(t)} local x, y, target = self:getTarget(tg) - if not x or not y or not target then return nil end - - local distance = math.max(1, math.floor(core.fov.distance(self.x, self.y, x, y))) + if not x or not y or not target or core.fov.distance(self.x, self.y, x, y) > range then return nil end - local power = (1 - ((distance - 1) / range)) + --local distance = math.max(1, math.floor(core.fov.distance(self.x, self.y, x, y))) + local power = 1 --(1 - ((distance - 1) / range)) local damage = t.getDamage(self, t) * power local knockback = t.getKnockback(self, t) forceHit(self, target, self.x, self.y, damage, knockback, 15, power) @@ -130,7 +131,7 @@ newTalent{ info = function(self, t) local damage = t.getDamage(self, t) local knockback = t.getKnockback(self, t) - return ([[Focusing your hate you strike your foe with unseen force for up to %d damage and %d knockback at a range of 1. Damage decreases the further you are from your target. + return ([[Focusing your hate you strike your foe with unseen force for %d damage and %d knockback. Damage increases with the Willpower stat.]]):format(damDesc(self, DamageType.PHYSICAL, damage), knockback) end, } @@ -145,7 +146,6 @@ newTalent{ cooldown = 12, tactical = { DEFEND = 2 }, no_sustain_autoreset = true, - direct_hit = true, getMaxDamage = function(self, t) return combatTalentDamage(self, t, 20, 200) end, @@ -210,6 +210,7 @@ newTalent{ random_ego = "attack", cooldown = 10, tactical = { ATTACK = 2 }, + requires_target = true, hate = 1.5, range = function(self, t) return 4 @@ -231,7 +232,7 @@ newTalent{ local tg = {type="ball", nolock=true, pass_terrain=false, friendly_fire=false, nowarning=true, range=range, radius=radius, talent=t} local blastX, blastY = self:getTarget(tg) - if not blastX or not blastY then return nil end + if not blastX or not blastY or core.fov.distance(self.x, self.y, blastX, blastY) > range then return nil end local grids = self:project(tg, blastX, blastY, function(x, y, target, self) @@ -269,7 +270,6 @@ newTalent{ hate = 2, cooldown = 50, tactical = { ATTACKAREA = 2 }, - direct_hit = true, range = function(self, t) return math.floor(4 + self:getTalentLevel(t) / 2.3) end, diff --git a/game/modules/tome/data/talents/cursed/punishments.lua b/game/modules/tome/data/talents/cursed/punishments.lua index 780c6cdf0b6b76655c157c0dd4389dce2855683d..7eed24bbca6a58af678377675abf291cb96b3406 100644 --- a/game/modules/tome/data/talents/cursed/punishments.lua +++ b/game/modules/tome/data/talents/cursed/punishments.lua @@ -84,6 +84,8 @@ newTalent{ hate = 0.8, range = 7, tactical = { ATTACK = 2 }, + direct_hit = true, + requires_target = true, getDuration = function(self, t) return 10 end, @@ -103,7 +105,7 @@ newTalent{ local range = self:getTalentRange(t) local tg = {type="hit", range=range} local x, y, target = self:getTarget(tg) - if not x or not y or not target or target:hasEffect(target.EFF_HATEFUL_WHISPER) then return nil end + if not x or not y or not target or core.fov.distance(self.x, self.y, x, y) > range or target:hasEffect(target.EFF_HATEFUL_WHISPER) then return nil end local duration = t.getDuration(self, t) local damage = t.getDamage(self, t) @@ -260,6 +262,8 @@ newTalent{ hate = 0.5, range = 7, tactical = { ATTACK = 2 }, + direct_hit = true, + requires_target = true, getDuration = function(self, t) return 5 end, @@ -273,7 +277,7 @@ newTalent{ local range = self:getTalentRange(t) local tg = {type="hit", range=range} local x, y, target = self:getTarget(tg) - if not x or not y or not target then return nil end + if not x or not y or not target or core.fov.distance(self.x, self.y, x, y) > range then return nil end local damage = t.getDamage(self, t) local mindpower = t.getMindpower(self, t) diff --git a/game/modules/tome/data/talents/cursed/shadows.lua b/game/modules/tome/data/talents/cursed/shadows.lua index 39ecc72c0d2303fd17246367135da743f4e33038..b0502d8f92747fb2dbc89e4064e755dfea9b8a29 100644 --- a/game/modules/tome/data/talents/cursed/shadows.lua +++ b/game/modules/tome/data/talents/cursed/shadows.lua @@ -27,7 +27,7 @@ newTalent{ action = function(self, t) local x, y, range if self.ai_state.shadow_wall then - x, y, range = self.summoner.x, self.summoner.y, 1 + x, y, range = self.ai_state.shadow_wall_target.x, self.ai_state.shadow_wall_target.y, 1 elseif self.ai_target.x and self.ai_target.y then x, y, range = self.ai_target.x, self.ai_target.y, 1 else @@ -82,6 +82,82 @@ newTalent{ end, } +newTalent{ + short_name = "SHADOW_LIGHTNING", + name = "Shadow Lightning", + type = {"spell/other", 1}, + require = { }, + points = 5, + random_ego = "attack", + range = 1, + direct_hit = true, + requires_target = true, + getDamage = function(self, t) return self:combatTalentSpellDamage(t, 20, 200) end, + action = function(self, t) + local tg = {type="hit", range=self:getTalentRange(t), talent=t} + local x, y = self:getTarget(tg) + if not x or not y then return nil end + local dam = self:spellCrit(t.getDamage(self, t)) + self:project(tg, x, y, DamageType.LIGHTNING, rng.avg(dam / 3, dam, 3)) + local _ _, x, y = self:canProject(tg, x, y) + game.level.map:particleEmitter(self.x, self.y, math.max(math.abs(x-self.x), math.abs(y-self.y)), "lightning", {tx=x-self.x, ty=y-self.y}) + game:playSoundNear(self, "talents/lightning") + return true + end, + info = function(self, t) + local damage = t.getDamage(self, t) + return ([[Strikes the target with a spark of lightning doing %0.2f to %0.2f damage. + The damage will increase with the Magic stat]]): + format(damDesc(self, DamageType.LIGHTNING, damage / 3), + damDesc(self, DamageType.LIGHTNING, damage)) + end, +} + +newTalent{ + short_name = "SHADOW_FLAMES", + name = "Shadow Flames", + type = {"spell/other", 1}, + require = { }, + points = 5, + random_ego = "attack", + range = 6, + direct_hit = true, + requires_target = true, + getDamage = function(self, t) return self:combatTalentSpellDamage(t, 20, 140) end, + action = function(self, t) + local tg = {type="hit", range=self:getTalentRange(t), talent=t} + local x, y = self:getTarget(tg) + if not x or not y then return nil end + local dam = self:spellCrit(t.getDamage(self, t)) + self:project(tg, x, y, DamageType.FIRE, dam) + game.level.map:particleEmitter(x, y, 1, "flame") + game:playSoundNear(self, "talents/fire") + return true + end, + info = function(self, t) + local damage = t.getDamage(self, t) + return ([[Bathes the target in flames doing %0.2f damage + The damage will increase with the Magic stat]]): + format(damDesc(self, DamageType.FIREBURN, damage)) + end, +} + +newTalent{ + short_name = "SHADOW_REFORM", + name = "Reform", + type = {"spell/other", 1}, + require = { }, + points = 5, + getChance = function(self, t) + return 50 --10 + self:getMag() * 0.25 + self:getTalentLevel(t) * 2 + end, + info = function(self, t) + local chance = t.getChance(self, t) + return ([[When a shadow is hit and killed, there is a %d%% chance it will reform unhurt." + The chance will increase with the Magic stat]]):format(chance) + end, +} + local function createShadow(self, level, duration, target) return require("mod.class.NPC").new{ type = "undead", subtype = "shadow", @@ -105,8 +181,8 @@ local function createShadow(self, level, duration, target) stats = { str=10 + math.floor(level * 0.2), dex=15 + math.floor(level * 0.8), - mag=15 + math.floor(level * 0.5), - wil=10 + math.floor(level * 0.4), + mag=15 + math.floor(level * 0.6), + wil=10 + math.floor(level * 0.6), cun=10 + math.floor(level * 0.2), con=5, }, @@ -119,12 +195,15 @@ local function createShadow(self, level, duration, target) }, evasion = 30, mana = 100, + summoner_hate_per_kill = 0.8, resolvers.talents{ [self.T_SHADOW_PHASE_DOOR]=math.max(5, math.floor(1 + level * 0.1)), [self.T_SHADOW_BLINDSIDE]=math.max(5, math.floor(1 + level * 0.1)), - [self.T_LIGHTNING]=math.max(5, math.floor(1 + level * 0.1)), - [self.T_SHOCK]=math.max(5, math.floor(1 + level * 0.1)), + [self.T_SHADOW_LIGHTNING]=math.max(5, math.floor(1 + level * 0.1)), + [self.T_SHADOW_FLAMES]=math.max(5, math.floor(1 + level * 0.1)), + [self.T_SHADOW_REFORM]=math.max(5, math.floor(1 + level * 0.1)), [self.T_HEAL]=math.max(5, math.floor(1 + level * 0.1)), + [self.T_DOMINATE]=math.max(5, math.floor(1 + level * 0.1)), }, undead = 1, @@ -153,7 +232,10 @@ local function createShadow(self, level, duration, target) blindside_chance = 15, phasedoor_chance = 5, - attack_spell_chance = 5, + close_attack_spell_chance = 0, + far_attack_spell_chance = 0, + can_reform = false, + dominate_chance = 0, feed_level = 0 }, @@ -166,46 +248,40 @@ local function createShadow(self, level, duration, target) healSelf = function(self) self:useTalent(self.T_HEAL) end, - canAttackSpell = function(self) - local target = self.ai_target.actor - return target and math.floor(core.fov.distance(self.x, self.y, target.x, target.y)) <= 1 + closeAttackSpell = function(self) + return self:useTalent(self.T_SHADOW_LIGHTNING) end, - attackSpell = function(self) - if self:canAttackSpell() then - local choice = rng.range(1, 2) - if choice == 1 then - return self:useTalent(self.T_LIGHTNING) - else - return self:useTalent(self.T_SHOCK) - end + farAttackSpell = function(self) + return self:useTalent(self.T_SHADOW_FLAMES) + end, + dominate = function(self) + return self:useTalent(self.T_DOMINATE) + end, + feed = function(self) + if self.summoner:knowTalent(self.summoner.T_SHADOW_MAGES) then + local tShadowMages = self.summoner:getTalentFromId(self.summoner.T_SHADOW_MAGES) + self.ai_state.close_attack_spell_chance = tShadowMages.getCloseAttackSpellChance(self.summoner, tShadowMages) + self.ai_state.far_attack_spell_chance = tShadowMages.getFarAttackSpellChance(self.summoner, tShadowMages) + self.ai_state.can_reform = self.summoner:getTalentLevel(tShadowMages) >= 5 else - return false + self.ai_state.close_attack_spell_chance = 0 + self.ai_state.far_attack_spell_chance = 0 + self.ai_state.can_reform = false end - end, - feed = function(self, t) - local level = self.summoner:getTalentLevel(t) - if self.ai_state.feed_level == level then return end - self.ai_state.feed_level = level - - -- clear old values if self.ai_state.feed_temp1 then self:removeTemporaryValue("combat_atk", self.ai_state.feed_temp1) end self.ai_state.feed_temp1 = nil if self.ai_state.feed_temp2 then self:removeTemporaryValue("inc_damage", self.ai_state.feed_temp2) end self.ai_state.feed_temp2 = nil - self.summoner_hate_per_kill = nil - - if level and level > 0 then - -- set new values - self.ai_state.feed_temp1 = self:addTemporaryValue("combat_atk", t.getCombatAtk(self.summoner, t)) - self.ai_state.feed_temp2 = self:addTemporaryValue("inc_damage", {all=t.getIncDamage(self.summoner, t)}) - self.summoner_hate_per_kill = t.getHatePerKill(self.summoner, t) + if self.summoner:knowTalent(self.summoner.T_SHADOW_WARRIORS) then + local tShadowWarriors = self.summoner:getTalentFromId(self.summoner.T_SHADOW_WARRIORS) + self.ai_state.feed_temp1 = self:addTemporaryValue("combat_atk", tShadowWarriors.getCombatAtk(self.summoner, tShadowWarriors)) + self.ai_state.feed_temp2 = self:addTemporaryValue("inc_damage", {all=tShadowWarriors.getIncDamage(self.summoner, tShadowWarriors)}) + self.ai_state.dominate_chance = tShadowWarriors.getDominateChance(self.summoner, tShadowWarriors) + else + self.ai_state.dominate_chance = 0 end end, - shadowWall = function(self, t, duration) - self.ai_state.shadow_wall = true - self.ai_state.shadow_wall_time = duration - end, } end @@ -279,6 +355,7 @@ newTalent{ shadow:resolve(nil, true) shadow:forceLevelup(level) game.zone:addEntity(game.level, shadow, "actor", x, y) + shadow:feed() game.level.map:particleEmitter(x, y, 1, "teleport_in") game:playSoundNear(self, "talents/spell_generic") @@ -294,7 +371,7 @@ newTalent{ info = function(self, t) local maxShadows = t.getMaxShadows(self, t) local level = t.getLevel(self, t) - return ([[While this ability is active you will continually call up to %d level %d shadows to aid you in battle. Each shadow costs 1 hate to summon and will be equal in level to you when it appears.]]):format(maxShadows, level) + return ([[While this ability is active you will continually call up to %d level %d shadows to aid you in battle. Shadows are weak combatants that can Heal themselves, Blindside their opponents and Phase Door from place to place. Each shadow costs 1 hate to summon and will be equal in level to you when it appears.]]):format(maxShadows, level) end, } @@ -305,71 +382,98 @@ newTalent{ points = 5, random_ego = "attack", cooldown = 10, - hate = 1, + hate = 0.5, range = 6, requires_target = true, tactical = { ATTACK = 2 }, - getDuration = function(self, t) - return self:getTalentLevel(t) + getDefenseDuration = function(self, t) + return 3 + math.floor(self:getTalentLevel(t) * 1.5) end, getBlindsideChance = function(self, t) return math.min(100, 30 + self:getTalentLevel(t) * 10) end, - getAttackSpellChance = function(self, t) - return math.min(100, 5 + self:getTalentLevel(t) * 5) - end, action = function(self, t) - local target = { type="hit", range=self:getTalentRange(t) } + local range = self:getTalentRange(t) + local target = { type="hit", range=range, nowarning=true } local x, y, target = self:getTarget(target) - if not x or not y or not target then return nil end - - local blindsideChance = t.getBlindsideChance(self, t) - local attackSpellChance = t.getAttackSpellChance(self, t) - local shadowCount = 0 - for _, e in pairs(game.level.entities) do - if e.summoner and e.summoner == self and e.subtype == "shadow" then - -- reset target and set to focus - e.ai_target.x = nil - e.ai_target.y = nil - e.ai_target.actor = target - e.ai_target.focus_on_target = true - e.ai_target.blindside_chance = blindsideChance - e.ai_target.attack_spell_chance = attackSpellChance - - shadowCount = shadowCount + 1 + if not x or not y or not target or core.fov.distance(self.x, self.y, x, y) > range then return nil end + + if self:reactionToward(target) < 0 then + -- attack the target + local blindsideChance = t.getBlindsideChance(self, t) + local shadowCount = 0 + for _, e in pairs(game.level.entities) do + if e.summoner and e.summoner == self and e.subtype == "shadow" then + -- reset target and set to focus + e.ai_target.x = nil + e.ai_target.y = nil + e.ai_target.actor = target + e.ai_target.focus_on_target = true + e.ai_target.blindside_chance = blindsideChance + + shadowCount = shadowCount + 1 + end end - end - if shadowCount > 0 then - game.logPlayer(self, "#PINK#The shadows converge on %s!", target.name) - return true + if shadowCount > 0 then + game.logPlayer(self, "#PINK#The shadows converge on %s!", target.name) + return true + else + game.logPlayer(self, "Their are no shadows to heed the call!") + return false + end else - game.logPlayer(self, "Their are no shadows to heed the call!") - return false + -- defend the target + local defenseDuration = t.getDefenseDuration(self, t) + local shadowCount = 0 + for _, e in pairs(game.level.entities) do + if e.summoner and e.summoner == self and e.subtype == "shadow" then + e.ai_state.shadow_wall = true + e.ai_state.shadow_wall_target = target + e.ai_state.shadow_wall_time = defenseDuration + + shadowCount = shadowCount + 1 + end + end + + if shadowCount > 0 then + game.logPlayer(self, "#PINK#The shadows form around %s!", target.name) + return true + else + game.logPlayer(self, "Their are no shadows to heed the call!") + return false + end end end, info = function(self, t) - local duration = t.getDuration(self, t) + local defenseDuration = t.getDefenseDuration(self, t) local blindsideChance = t.getBlindsideChance(self, t) - local attackSpellChance = t.getAttackSpellChance(self, t) - return ([[Focus your shadows on a single target. There is a %d%% chance they will blindside the target and a %d%% chance they will use an attack spell.]]):format(blindsideChance, attackSpellChance) + return ([[Focus your shadows on a single target. Friendly targets will be defended for %d turns. Hostile targets will be attacked with a %d%% chance they will blindside the target.]]):format(defenseDuration, blindsideChance) end, } newTalent{ - name = "Feed Shadows", + name = "Shadow Mages", type = {"cursed/shadows", 3}, mode = "passive", require = cursed_mag_req3, points = 5, - getIncDamage = function(self, t) - return math.floor((math.sqrt(self:getTalentLevel(t)) - 0.5) * 17) + getCloseAttackSpellChance = function(self, t) + if self:getTalentLevelRaw(t) > 0 then + return math.min(100, math.sqrt(self:getTalentLevel(t)) * 5) + else + return 0 + end end, - getCombatAtk = function(self, t) - return math.floor((math.sqrt(self:getTalentLevel(t)) - 0.5) * 17) + getFarAttackSpellChance = function(self, t) + if self:getTalentLevelRaw(t) >= 3 then + return math.min(100, math.sqrt(self:getTalentLevel(t)) * 5) + else + return 0 + end end, - getHatePerKill = function(self, t) - return (self:getTalentLevel(t) / 8) * self.hate_per_kill + canReform = function(self, t) + return self:getTalentLevelRaw(t) >= 5 end, on_learn = function(self, t) if game and game.level and game.level.entities then @@ -394,46 +498,73 @@ newTalent{ return true end, info = function(self, t) - local combatAtk = t.getCombatAtk(self, t) - local incDamage = t.getIncDamage(self, t) - local hatePerKill = t.getHatePerKill(self, t) - return ([[Your hatred of all living things begins to feed your shadows. Their new viciousness gives them %d%% extra attack and %d%% extra damage and each kill they make transfers %0.2f hatred back to you.]]):format(combatAtk, incDamage, hatePerKill) + local closeAttackSpellChance = t.getCloseAttackSpellChance(self, t) + local farAttackSpellChance = t.getFarAttackSpellChance(self, t) + + local level = self:getTalentLevelRaw(t) + if level < 3 then + return ([[Infuse magic into your shadows to give them fearsome spells. + Your shadows can strike adjacent foes with Lightning (%d%% chance at range 1). + At 3 talent points they will gain Flames and at 5 talent points they will gain Reform.]]):format(closeAttackSpellChance) + elseif level < 5 then + return ([[Infuse magic into your shadows to give them fearsome spells. + Your shadows can strike adjacent foes with Lightning (%d%% chance at range 1). + Your shadows can sear their enemies from a distance with Flames (%d%% chance at range 2 to 6). + At 5 talent points they will gain Reform.]]):format(closeAttackSpellChance, farAttackSpellChance) + else + return ([[Infuse magic into your shadows to give them fearsome spells. + Your shadows can strike adjacent foes with Lightning (%d%% chance at range 1). + Your shadows can sear their enemies from a distance with Flames (%d%% chance at range 2 to 6). + When your shadows are struck down they will attempt to Reform becoming whole again.]]):format(closeAttackSpellChance, farAttackSpellChance) + end end, } newTalent{ - name = "Shadow Wall", + name = "Shadow Warriors", type = {"cursed/shadows", 4}, + mode = "passive", require = cursed_mag_req4, points = 5, - cooldown = 10, - hate = 1, - tactical = { DEFEND = 2 }, - getDuration = function(self, t) - return 2 + self:getTalentLevel(t) * 2 + getIncDamage = function(self, t) + return math.floor((math.sqrt(self:getTalentLevel(t)) - 0.5) * 23) end, - action = function(self, t) - - local duration = t.getDuration(self, t) - local shadowCount = 0 - for _, e in pairs(game.level.entities) do - if e.summoner and e.summoner == self and e.subtype == "shadow" then - e:shadowWall(t, duration) - - shadowCount = shadowCount + 1 + getCombatAtk = function(self, t) + return math.floor((math.sqrt(self:getTalentLevel(t)) - 0.5) * 23) + end, + getDominateChance = function(self, t) + if self:getTalentLevelRaw(t) > 0 then + return math.min(100, math.sqrt(self:getTalentLevel(t)) * 5) + else + return 0 + end + end, + on_learn = function(self, t) + if game and game.level and game.level.entities then + for _, e in pairs(game.level.entities) do + if e.summoner and e.summoner == self and e.subtype == "shadow" then + e:feed(t) + end end end - - if shadowCount > 0 then - game.logPlayer(self, "#PINK#The shadows form around %s!", self.name) - return true - else - game.logPlayer(self, "Their are no shadows to heed the call!") - return false + + return { } + end, + on_unlearn = function(self, t, p) + if game and game.level and game.level.entities then + for _, e in pairs(game.level.entities) do + if e.summoner and e.summoner == self and e.subtype == "shadow" then + e:feed(t) + end + end end + + return true end, info = function(self, t) - local duration = t.getDuration(self, t) - return ([[Summon your shadows to your side to form a wall against danger. Your shadows will stay beside you for %d turns and attack anyone nearby.]]):format(duration) + local combatAtk = t.getCombatAtk(self, t) + local incDamage = t.getIncDamage(self, t) + local dominateChance = t.getDominateChance(self, t) + return ([[Instill hate in your shadows strengthening their attacks. They gain %d%% extra attack and %d%% extra damage. The fury of their attacks gives them the ability to try to Dominate their foes, increasing all damage taken by that foe for 4 turns. (%d%% chance at range 1)]]):format(combatAtk, incDamage, dominateChance) end, } diff --git a/game/modules/tome/data/talents/cursed/slaughter.lua b/game/modules/tome/data/talents/cursed/slaughter.lua index a6e338758e9a72215d06b4371923caebe413ef96..88dac4a048ec09c704c7ec0026eeb744036b8af3 100644 --- a/game/modules/tome/data/talents/cursed/slaughter.lua +++ b/game/modules/tome/data/talents/cursed/slaughter.lua @@ -50,7 +50,8 @@ newTalent{ end, info = function(self, t) local multiplier = (0.17 + .23 * self:getTalentLevel(t)) - return ([[Slashes wildly at your target for 100%% (at 0 Hate) to %d%% (at 10+ Hate) damage.]]):format(multiplier * 100 + 100) + return ([[Slashes wildly at your target for 100%% (at 0 Hate) to %d%% (at 10+ Hate) damage. + Requires a one or two handed axe.]]):format(multiplier * 100 + 100) end, } @@ -96,7 +97,8 @@ newTalent{ end, info = function(self, t) local multiplier = self:combatTalentWeaponDamage(t, 0.2, 0.7) - return ([[Assault nearby foes with 4 fast attacks for %d%% (at 0 Hate) to %d%% (at 10+ Hate) damage each.]]):format(multiplier * 50, multiplier * 100) + return ([[Assault nearby foes with 4 fast attacks for %d%% (at 0 Hate) to %d%% (at 10+ Hate) damage each. + Requires a one or two handed axe.]]):format(multiplier * 50, multiplier * 100) end, } @@ -201,7 +203,8 @@ newTalent{ else size = "Small" end - return ([[Charge through your opponents, attacking anyone near your path for %d%% (at 0 Hate) to %d%% (at 10+ Hate) damage. %s opponents may be knocked from your path.]]):format(multiplier * 30, multiplier * 100, size) + return ([[Charge through your opponents, attacking anyone near your path for %d%% (at 0 Hate) to %d%% (at 10+ Hate) damage. %s opponents may be knocked from your path. + Requires a one or two handed axe.]]):format(multiplier * 30, multiplier * 100, size) end, } @@ -242,6 +245,7 @@ newTalent{ info = function(self, t) local chance = 28 + self:getTalentLevel(t) * 7 local multiplier = self:combatTalentWeaponDamage(t, 0.2, 0.7) - return ([[Every swing of your axe has a %d%% chance of striking a second target for %d%% (at 0 Hate) to %d%% (at 10+ Hate) damage.]]):format(chance, multiplier * 50, multiplier * 100) + return ([[Every swing of your axe has a %d%% chance of striking a second target for %d%% (at 0 Hate) to %d%% (at 10+ Hate) damage. + Requires a one or two handed axe.]]):format(chance, multiplier * 50, multiplier * 100) end, } diff --git a/game/modules/tome/data/timed_effects.lua b/game/modules/tome/data/timed_effects.lua index 9d5b8add06a15e5c221e6bba2abe3399981a5c2d..a53c522062a1c503e3eff64c15a2ea76768264b3 100644 --- a/game/modules/tome/data/timed_effects.lua +++ b/game/modules/tome/data/timed_effects.lua @@ -2507,9 +2507,25 @@ newEffect{ if eff.extension <= 0 then self:removeEffect(self.EFF_FEED) end - elseif eff.target.dead or not self:hasLOS(eff.target.x, eff.target.y, "block_move") then + return + end + + if eff.target.dead then eff.isSevered = true - + else + local t = self:getTalentFromId(self.T_DARK_VISION) + if t then + if not t.hasLOS(self.x, self.y, eff.target.x, eff.target.y) then + eff.isSevered = true + end + else + if not self:hasLOS(eff.target.x, eff.target.y) then + eff.isSevered = true + end + end + end + + if eff.isSevered then if eff.particles then -- remove old particle emitter eff.particles.x = nil