From c23202b9d11c9889ba2db0f1a64fde1db0b6c69a Mon Sep 17 00:00:00 2001
From: dg <dg@51575b47-30f0-44d4-a5cc-537603b46e54>
Date: Fri, 14 Jan 2011 11:06:06 +0000
Subject: [PATCH] Changed Feed Shadows to a passive talent that increases
 attack, damage and gives hate back to the player when a shadow kills.
 Replaced Punishments' Cursed Ground with Infectious Thought (an attack that
 can spread to other nearby targets) Changed Punishments' Agony to ramp up
 damage over 5 turns (no longer based on proximity to caster). Reduced hate
 cost of Deflection Increased duration of a summoned shadow from 100 to 500
 Increased Willful Strike and Blast damage Fixed problems with shadows and
 feed effects not disappearing after leaving the level.

git-svn-id: http://svn.net-core.org/repos/t-engine4@2360 51575b47-30f0-44d4-a5cc-537603b46e54
---
 game/modules/tome/ai/shadow.lua               |   6 +
 game/modules/tome/class/Actor.lua             |   9 +-
 game/modules/tome/class/Player.lua            |  10 ++
 .../tome/data/gfx/particles/cursed_ground.lua |  60 ----------
 .../tome/data/gfx/particles/feed_hate.lua     |  12 +-
 .../tome/data/talents/cursed/cursed-form.lua  |  18 +--
 .../tome/data/talents/cursed/cursed.lua       |   4 +-
 .../data/talents/cursed/dark-sustenance.lua   |   4 +-
 .../tome/data/talents/cursed/darkness.lua     |  43 +++++---
 .../data/talents/cursed/force-of-will.lua     |  16 +--
 .../tome/data/talents/cursed/punishments.lua  |  86 ++++++++++++---
 .../tome/data/talents/cursed/rampage.lua      |   2 +-
 .../tome/data/talents/cursed/shadows.lua      |  83 ++++++++------
 game/modules/tome/data/timed_effects.lua      | 104 +++++++++++++++---
 14 files changed, 287 insertions(+), 170 deletions(-)

diff --git a/game/modules/tome/ai/shadow.lua b/game/modules/tome/ai/shadow.lua
index acd0516a3b..d4309addf9 100644
--- a/game/modules/tome/ai/shadow.lua
+++ b/game/modules/tome/ai/shadow.lua
@@ -198,6 +198,12 @@ 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)
diff --git a/game/modules/tome/class/Actor.lua b/game/modules/tome/class/Actor.lua
index 578f6b9c31..8103ac234e 100644
--- a/game/modules/tome/class/Actor.lua
+++ b/game/modules/tome/class/Actor.lua
@@ -1025,7 +1025,7 @@ function _M:die(src)
 	end
 
 	-- Adds hate
-	if src and src.knowTalent and src:knowTalent(self.T_HATE_POOL) then
+	if src and src.knowTalent and src:knowTalent(src.T_HATE_POOL) then
 		local hateGain = src.hate_per_kill
 		local hateMessage
 
@@ -1051,6 +1051,13 @@ function _M:die(src)
 			game.logPlayer(src, hateMessage.." (+%0.1f hate)", hateGain - src.hate_per_kill)
 		end
 	end
+	
+	if src and src.summoner and src.summoner_hate_per_kill then
+		if src.summoner.knowTalent and src.summoner:knowTalent(src.summoner.T_HATE_POOL) then
+			src.summoner.hate = math.min(src.summoner.max_hate, src.summoner.hate + src.summoner_hate_per_kill)
+			game.logPlayer(src.summoner, "%s feeds you hate from it's latest victim. (+%0.1f hate)", src.name:capitalize(), src.summoner_hate_per_kill)
+		end
+	end
 
 	if src and src.knowTalent and src:knowTalent(src.T_UNNATURAL_BODY) then
 		local t = src:getTalentFromId(src.T_UNNATURAL_BODY)
diff --git a/game/modules/tome/class/Player.lua b/game/modules/tome/class/Player.lua
index 0827f82de5..e5c62a18a6 100644
--- a/game/modules/tome/class/Player.lua
+++ b/game/modules/tome/class/Player.lua
@@ -133,6 +133,16 @@ function _M:onEnterLevelEnd(zone, level)
 end
 
 function _M:onLeaveLevel(zone, level)
+	-- clean up things that need to be removed before re-entering the level	
+	if self:isTalentActive(self.T_CALL_SHADOWS) then
+		local t = self:getTalentFromId(self.T_CALL_SHADOWS)
+		t.removeAllShadows(self, t)
+	end
+	
+	if self:hasEffect(self.EFF_FEED) then
+		self:removeEffect(self.EFF_FEED, true)
+	end
+
 	-- Fail past escort quests
 	local eid = "escort-duty-"..zone.short_name.."-"..level.level
 	if self:hasQuest(eid) and not self:hasQuest(eid):isEnded() then
diff --git a/game/modules/tome/data/gfx/particles/cursed_ground.lua b/game/modules/tome/data/gfx/particles/cursed_ground.lua
index 3d090868ad..e69de29bb2 100644
--- a/game/modules/tome/data/gfx/particles/cursed_ground.lua
+++ b/game/modules/tome/data/gfx/particles/cursed_ground.lua
@@ -1,60 +0,0 @@
--- ToME - Tales of Maj'Eyal
--- Copyright (C) 2009, 2010 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
-
-base_size = 32
-
-local distributionOffset = math.rad(rng.range(0, 360))
-
-return { generator = function()
-	local life = rng.float(10, 15)
-	local size = 1
-	local angle = math.rad(rng.range(0, 360))
-	local distribution = (math.sin(angle + distributionOffset) + 1) / 2
-	local distance = engine.Map.tile_w * rng.float(0.3, 0.9)
-	local startX = distance * math.cos(angle)
-	local startY = distance * math.sin(angle)
-	local alpha = (80 - distribution * 50) / 255
-	
-	local speed = 0.03
-	local dirv = math.pi * 2 * speed
-	local vel = math.pi * distance * speed
-	
-	return {
-		trail = 1,
-		life = life,
-		size = size, sizev = size / life / 3, sizea = 0,
-
-		x = -size / 2 + startX, xv = -startX / life, xa = 0,
-		y = -size / 2 + startY, yv = -startY / life, ya = 0,
-		dir = angle + math.rad(90), dirv = dirv, dira = 0,
-		vel = vel, velv = -vel / life, vela = 0,
-		
-		r = (100 + distribution * 100) / 255,  rv = 0, ra = 0,
-		g = 64 / 255,  gv = 0, ga = 0,
-		b = (200 - distribution * 100) / 255,  bv = 0, ba = 0,
-		a = alpha,  av = -alpha / life / 2, aa = 0,
-	}
-end, },
-function(self)
-	self.nb = (self.nb or 0) + 1
-	if self.nb <= 4 then
-		self.ps:emit(1000)
-	end
-end,
-1000 * 4
diff --git a/game/modules/tome/data/gfx/particles/feed_hate.lua b/game/modules/tome/data/gfx/particles/feed_hate.lua
index da4b3b7462..7eb9d5fbd9 100644
--- a/game/modules/tome/data/gfx/particles/feed_hate.lua
+++ b/game/modules/tome/data/gfx/particles/feed_hate.lua
@@ -35,20 +35,20 @@ return { generator = function()
 
 	return {
 		life = 5,
-		size = rng.range(3, 5), sizev = -0.4, sizea = 0,
+		size = rng.range(4, 6), sizev = -0.4, sizea = 0,
 
 		x = r * math.cos(angle) + math.cos(rightAngle) * offset, xv = 0, xa = 0,
 		y = r * math.sin(angle) + math.sin(rightAngle) * offset, yv = 0, ya = 0,
 		dir = angle + math.rad(180), dirv = 0, dira = 0,
 		vel = rng.range(0.3, 0.6), velv = 0, vela = 0,
 
-		r = 32 / 255, rv = 0, ra = 0,
+		r = rng.range(48, 196) / 255, rv = 0, ra = 0,
 		g = 32 / 255, gv = 0, ga = 0,
-		b = 32 / 255, bv = 0, ba = 0,
-		a = rng.range(80, 150) / 255, av = 0, aa = 0,
+		b = rng.range(48, 164) / 255, bv = 0, ba = 0,
+		a = rng.range(80, 196) / 255, av = 0, aa = 0,
 	}
 end, },
 function(self)
-	self.ps:emit(5*tiles)
+	self.ps:emit(3*tiles)
 end,
-5*5*tiles
+5*3*tiles
diff --git a/game/modules/tome/data/talents/cursed/cursed-form.lua b/game/modules/tome/data/talents/cursed/cursed-form.lua
index 9e43dcfdd8..485c1a383e 100644
--- a/game/modules/tome/data/talents/cursed/cursed-form.lua
+++ b/game/modules/tome/data/talents/cursed/cursed-form.lua
@@ -125,20 +125,20 @@ newTalent{
 	random_ego = "utility",
 	require = cursed_wil_req2,
 	points = 5,
-	cooldown = 400,
+	cooldown = 500,
+	getHateGain = function(self, t)
+		return (math.sqrt(self:getTalentLevel(t)) - 0.5) * 2.3
+	end,
 	action = function(self, t)
-		self:incHate(2 + self:getTalentLevel(t) * 0.9)
-
-		local damage = self.max_life * 0.25
-		self:takeHit(damage, self)
-		game.level.map:particleEmitter(self.x, self.y, 5, "fireflash", {radius=2, tx=self.x, ty=self.y})
+		self:incHate(t.getHateGain(self, t))
+		
+		game.level.map:particleEmitter(self.x, self.y, 5, "fireflash", {radius=1, tx=self.x, ty=self.y})
 		game:playSoundNear(self, "talents/fireflash")
 		return true
 	end,
 	info = function(self, t)
-		local increase = 2 + self:getTalentLevel(t) * 0.9
-		local damage = self.max_life * 0.25
-		return ([[Focus your rage gaining %0.1f hate at the cost of %d life.]]):format(increase, damage)
+		local hateGain = t.getHateGain(self, t)
+		return ([[Focus your rage gaining %0.1f hate.]]):format(hateGain)
 	end,
 }
 
diff --git a/game/modules/tome/data/talents/cursed/cursed.lua b/game/modules/tome/data/talents/cursed/cursed.lua
index 64c48d42a9..22508ae2be 100644
--- a/game/modules/tome/data/talents/cursed/cursed.lua
+++ b/game/modules/tome/data/talents/cursed/cursed.lua
@@ -26,8 +26,8 @@ newTalentType{ allow_random=true, type="cursed/rampage", name = "rampage", descr
 -- Doomed
 newTalentType{ allow_random=true, type="cursed/dark-sustenance", name = "dark sustenance", generic = true, description = "Invoke the powerful force of your will." }
 newTalentType{ allow_random=true, type="cursed/force-of-will", name = "force of will", description = "Invoke the powerful force of your will." }
-newTalentType{ allow_random=true, type="cursed/darkness", name = "darkness", description = "Harness the power of darkness to envelop your foes." }
-newTalentType{ allow_random=true, type="cursed/shadows", name = "shades", description = "Summon shadows from the darkness to aid you." }
+newTalentType{ allow_random=true, type="cursed/darkness", is_spell=true, name = "darkness", description = "Harness the power of darkness to envelop your foes." }
+newTalentType{ allow_random=true, type="cursed/shadows", is_spell=true, name = "shades", description = "Summon shadows from the darkness to aid you." }
 newTalentType{ allow_random=true, type="cursed/punishments", name = "punishments", description = "Your hate becomes punishment in the minds of your foes." }
 
 -- Generic
diff --git a/game/modules/tome/data/talents/cursed/dark-sustenance.lua b/game/modules/tome/data/talents/cursed/dark-sustenance.lua
index b5d7f3ebdc..dc1d6bf933 100644
--- a/game/modules/tome/data/talents/cursed/dark-sustenance.lua
+++ b/game/modules/tome/data/talents/cursed/dark-sustenance.lua
@@ -38,6 +38,8 @@ newTalent{
 			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
@@ -66,7 +68,7 @@ newTalent{
 			resistGain = tFeedStrengths.getResistGain(self, tFeedStrengths, target)
 		end
 
-		self:setEffect(self.EFF_FEED, 99999, { target=target, hateGain=hateGain, constitutionGain=constitutionGain, lifeRegenGain=lifeRegenGain, damageGain=damageGain, resistGain=resistGain, extension=0 })
+		self:setEffect(self.EFF_FEED, 40, { target=target, hateGain=hateGain, constitutionGain=constitutionGain, lifeRegenGain=lifeRegenGain, damageGain=damageGain, resistGain=resistGain, extension=0 })
 
 		return true
 	end,
diff --git a/game/modules/tome/data/talents/cursed/darkness.lua b/game/modules/tome/data/talents/cursed/darkness.lua
index 1d5a0da5fb..e56021741c 100644
--- a/game/modules/tome/data/talents/cursed/darkness.lua
+++ b/game/modules/tome/data/talents/cursed/darkness.lua
@@ -290,24 +290,34 @@ newTalent{
 		if not x or not y then return nil end
 		local _ _, x, y = self:canProject(tg, x, y)
 
-		local dist = math.floor(radius)
+		-- get locations in line of movement from center
 		local locations = {}
-		for darkX = x - dist, x + dist do
-			for darkY = y - dist, y + dist do
-				if t.canCreep(darkX, darkY) then
-					locations[#locations+1] = {darkX, darkY}
-				end
+		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
+				if game.level.map:checkAllEntities(lx, ly, "block_move") then break end
+
+				lx, ly = l()
 			end
-		end
+			if not lx and not ly then lx, ly = darkX, darkY end
+
+			if lx == darkX and ly == darkY and t.canCreep(darkX, darkY) then
+				locations[#locations+1] = {darkX, darkY}
+				print("*** locations", darkX, darkY)
+			end
+		end end
+		
 		darkCount = math.min(darkCount, #locations)
 		if darkCount == 0 then return false end
 
 		for i = 1, darkCount do
-			-- move selected locations to first indices
-			local selection = rng.range(i, #locations)
-			locations[i], locations[selection] = locations[selection], locations[i]
-
-			t.createDark(self, locations[i][1], locations[i][2], damage, 8, 4, 70, 0)
+			local location, id = rng.table(locations)
+			table.remove(locations, id)
+			print("*** dark", location[1], location[2])
+			t.createDark(self, location[1], location[2], damage, 8, 4, 70, 0)
 		end
 
 		game:playSoundNear(self, "talents/breath")
@@ -341,9 +351,9 @@ newTalent{
 	info = function(self, t)
 		local damageIncrease = t.getDamageIncrease(self, t)
 		if damageIncrease <= 0 then
-			return ([[Your eyes penetrate the darkness to find anyone that may be hiding there. You can also see through the creeping dark. At level 3 you begin to do extra damage to anyone in the dark.]])
+			return ([[Your eyes penetrate the darkness to find anyone that may be hiding there. You can also see through the creeping dark but not well enough to directly target someone. At level 3 you begin to do extra damage to anyone in the dark.]])
 		else
-			return ([[Your eyes penetrate the darkness to find anyone that may be hiding there. You can also see through the creeping dark. You do %d%% more damage to anyone in the dark.]]):format(damageIncrease)
+			return ([[Your eyes penetrate the darkness to find anyone that may be hiding there. You can also see through the creeping dark but not well enough to directly target someone. You do %d%% more damage to anyone in the dark.]]):format(damageIncrease)
 		end
 	end,
 }
@@ -370,6 +380,7 @@ newTalent{
 		local tg = {type="beam", range=self:getTalentRange(t), talent=t}
 		local x, y = self:getTarget(tg)
 		if not x or not y then return nil end
+		local _ _, x, y = self:canProject(tg, x, y)
 
 		local damage = t.getDamage(self, t)
 
@@ -422,7 +433,7 @@ newTalent{
 
 		local tg = {type="hit", range=self:getTalentRange(t), talent=t}
 		local x, y, target = self:getTarget(tg)
-		if not target then return nil end
+		if not x or not y or not target then return nil end
 
 		local pinDuration = t.getPinDuration(self, t)
 		local damage = t.getDamage(self, t)
@@ -434,7 +445,7 @@ newTalent{
 	info = function(self, t)
 		local pinDuration = t.getPinDuration(self, t)
 		local damage = t.getDamage(self, t)
-		return ([[Send tendrils of creeping dark out to attack your target and pin them in the darkness for %d turns. The darkness does %d damage per turn.
+		return ([[Send tendrils of creeping dark out to attack your target and pin them in the darkness for %d turns. Creeping dark will trail behind the tendrils as they move. The darkness does %d damage per turn.
 		The damage will increase with the Magic stat.]]):format(pinDuration, damage)
 	end,
 }
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 553a4b22b3..c90a8d15a2 100644
--- a/game/modules/tome/data/talents/cursed/force-of-will.lua
+++ b/game/modules/tome/data/talents/cursed/force-of-will.lua
@@ -106,7 +106,7 @@ newTalent{
 		return 4
 	end,
 	getDamage = function(self, t)
-		return combatTalentDamage(self, t, 20, 160)
+		return combatTalentDamage(self, t, 20, 200)
 	end,
 	getKnockback = function(self, t)
 		return math.floor(self:getTalentLevel(t))
@@ -129,8 +129,8 @@ 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.
-		Damage increases with the Willpower stat but decreases with range.]]):format(damDesc(self, DamageType.PHYSICAL, damage), knockback)
+		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.
+		Damage increases with the Willpower stat.]]):format(damDesc(self, DamageType.PHYSICAL, damage), knockback)
 	end,
 }
 
@@ -148,7 +148,7 @@ newTalent{
 	no_sustain_autoreset = true,
 	direct_hit = true,
 	getMaxDamage = function(self, t)
-		return combatTalentDamage(self, t, 20, 160)
+		return combatTalentDamage(self, t, 20, 200)
 	end,
 	getDisplayName = function(self, t, p)
 		return ("Deflection (%d)"):format(p.value)
@@ -166,8 +166,8 @@ newTalent{
 	end,
 	do_act = function(self, t, p)
 		local maxDamage = t.getMaxDamage(self, t)
-		if p.value < maxDamage and self.hate >= 0.05 then
-			self:incHate(-0.05)
+		if p.value < maxDamage and self.hate >= 0.02 then
+			self:incHate(-0.02)
 
 			p.value = math.min(p.value + maxDamage / 50, maxDamage)
 
@@ -198,7 +198,7 @@ newTalent{
 	end,
 	info = function(self, t)
 		local maxDamage = t.getMaxDamage(self, t)
-		return ([[Deflect 50%% of incoming damage with the force of your will. You may deflect up to %d damage but first your hate must slowly feed your strength.
+		return ([[Deflect 50%% of incoming damage with the force of your will. You may deflect up to %d damage but first your hate must slowly feed your strength (-0.02 hate regeneration while building strength).
 		The maximum damage deflected increases with the Willpower stat.]]):format(maxDamage)
 	end,
 }
@@ -218,7 +218,7 @@ newTalent{
 		return math.floor(2 + self:getTalentLevel(t) / 3)
 	end,
 	getDamage = function(self, t)
-		return combatTalentDamage(self, t, 20, 160)
+		return combatTalentDamage(self, t, 20, 200)
 	end,
 	getKnockback = function(self, t)
 		return 2 + math.floor(self:getTalentLevel(t))
diff --git a/game/modules/tome/data/talents/cursed/punishments.lua b/game/modules/tome/data/talents/cursed/punishments.lua
index 560d62dfd2..ca653d374b 100644
--- a/game/modules/tome/data/talents/cursed/punishments.lua
+++ b/game/modules/tome/data/talents/cursed/punishments.lua
@@ -73,6 +73,64 @@ newTalent{
 	end,
 }
 
+newTalent{
+	name = "Hateful Whisper",
+	type = {"cursed/punishments", 2},
+	require = cursed_wil_req2,
+	points = 5,
+	random_ego = "attack",
+	cooldown = 10,
+	hate =  0.8,
+	range = 7,
+	getDuration = function(self, t)
+		return 10
+	end,
+	getDamage = function(self, t)
+		return combatTalentDamage(self, t, 20, 100)
+	end,
+	getMindpower = function(self, t)
+		return combatPower(self, t)
+	end,
+	getJumpRange = function(self, t)
+		return 0.7 + math.sqrt(self:getTalentLevel(t))
+	end,
+	getExtraJumpChance = function(self, t)
+		return 25 + 12 * math.sqrt(self:getTalentLevel(t))
+	end,
+	action = function(self, 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 or target:hasEffect(target.EFF_HATEFUL_WHISPER) then return nil end
+		
+		local duration = t.getDuration(self, t)
+		local damage = t.getDamage(self, t)
+		local mindpower = t.getMindpower(self, t)
+		local jumpRange = t.getJumpRange(self, t)
+		local extraJumpChance = t.getExtraJumpChance(self, t)
+		target:setEffect(target.EFF_HATEFUL_WHISPER, duration, {
+			source = self,
+			duration = duration,
+			damage = damage,
+			mindpower = mindpower,
+			jumpRange = jumpRange,
+			extraJumpChance = extraJumpChance
+		})
+		game.level.map:particleEmitter(target.x, target.y, 1, "reproach", { dx = self.x - target.x, dy = self.y - target.y })
+
+		return true
+	end,
+	info = function(self, t)
+		local damage = t.getDamage(self, t)
+		local mindpower = t.getMindpower(self, t)
+		local jumpRange = t.getJumpRange(self, t)
+		local extraJumpChance = t.getExtraJumpChance(self, t)
+		return ([[Send a whisper filled with hate to spread throughout your foes. When first heard they will suffer %d mind damage and the whisper can travel to another victim within a range of %0.2f and begin to spread from them. There is a %d%% chance the whisper will be passed to two victims instead of one. (%d mindpower vs mental resistance)
+		The damage and mindpower will increase with the Willpower stat.]]):format(damDesc(self, DamageType.MIND, damage), jumpRange, extraJumpChance, mindpower)
+	end,
+}
+
+--[[
 newTalent{
 	name = "Cursed Ground",
 	type = {"cursed/punishments", 2},
@@ -184,10 +242,11 @@ newTalent{
 		local mindpower = t.getMindpower(self, t)
 		local duration = t.getDuration(self, t)
 		local stunDuration = t.getStunDuration(self, t)
-		return ([[You mark the ground at your feet with a terrible curse. Anyone passing the mark suffers %d mind damage and has a chance to be stunned for %d turns. The mark lasts for %d turns but the will weaken each time it is triggered. (%d mindpower vs mental resistance)
-		The damage and mindpower will increase with the Willpower stat.]]):format(damDesc(self, DamageType.MIND, damage), stunDuration, duration, mindpower)
+		return ([You mark the ground at your feet with a terrible curse. Anyone passing the mark suffers %d mind damage and has a chance to be stunned for %d turns. The mark lasts for %d turns but the will weaken each time it is triggered. (%d mindpower vs mental resistance)
+		The damage and mindpower will increase with the Willpower stat.]):format(damDesc(self, DamageType.MIND, damage), stunDuration, duration, mindpower)
 	end,
 }
+]]
 
 newTalent{
 	name = "Agony",
@@ -195,42 +254,43 @@ newTalent{
 	require = cursed_wil_req3,
 	points = 5,
 	random_ego = "attack",
-	cooldown = 6,
+	cooldown = 3,
 	hate =  0.5,
 	range = 7,
 	getDuration = function(self, t)
-		return 8 + math.floor(self:getTalentLevel(t) * 1.4)
+		return 5
 	end,
 	getDamage = function(self, t)
-		return combatTalentDamage(self, t, 15, 45)
+		return combatTalentDamage(self, t, 20, 110)
 	end,
 	getMindpower = function(self, t)
-		return combatPower(self, t)
+		return combatPower(self, t, 1.2)
 	end,
 	action = function(self, 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
-
+		
 		local damage = t.getDamage(self, t)
 		local mindpower = t.getMindpower(self, t)
 		local duration = t.getDuration(self, t)
 		target:setEffect(target.EFF_AGONY, duration, {
 			source = self,
-			mindpower = combatPower(self, t) * mindpower / 100,
+			mindpower = mindpower,
 			damage = damage,
-			range = range,
+			duration = duration,
 		})
 
 		return true
 	end,
 	info = function(self, t)
-		local damage = t.getDamage(self, t)
-		local mindpower = t.getMindpower(self, t)
 		local duration = t.getDuration(self, t)
-		return ([[Unleash agony upon your target. The pain will grow as they near you inflicting up to %d damage. They will suffer for %d turns unless they manage to resist. (%d mindpower vs mental resistance)
-		The damage and mindpower will increase with the Willpower stat.]]):format(damDesc(self, DamageType.MIND, damage), duration, mindpower)
+		local maxDamage = t.getDamage(self, t)
+		local minDamage = maxDamage / duration
+		local mindpower = t.getMindpower(self, t)
+		return ([[Unleash agony upon your target. The pain will grow over the course of %d turns unless they manage to resist. The first turn will inflict %d damage and slowly increase to %d on the last turn. (%d mindpower vs mental resistance)
+		The damage and mindpower will increase with the Willpower stat.]]):format(duration, damDesc(self, DamageType.MIND, minDamage), damDesc(self, DamageType.MIND, maxDamage), mindpower)
 	end,
 }
 
diff --git a/game/modules/tome/data/talents/cursed/rampage.lua b/game/modules/tome/data/talents/cursed/rampage.lua
index 99609055f9..fba4097bd6 100644
--- a/game/modules/tome/data/talents/cursed/rampage.lua
+++ b/game/modules/tome/data/talents/cursed/rampage.lua
@@ -72,7 +72,7 @@ newTalent{
 
 		return true
 	end,
-	getHateLoss = function(self, t) return 0.5 - 0.05 * self:getTalentLevelRaw(t) end,
+	getHateLoss = function(self, t) return 0.75 - 0.05 * self:getTalentLevelRaw(t) end,
 	getCritical = function(self, t) return 10 + 8 * self:getTalentLevel(t) end,
 	onTakeHit = function(t, self, fractionDamage)
 		if fractionDamage < 0.08 then return false end
diff --git a/game/modules/tome/data/talents/cursed/shadows.lua b/game/modules/tome/data/talents/cursed/shadows.lua
index f2a6793b33..23880df41c 100644
--- a/game/modules/tome/data/talents/cursed/shadows.lua
+++ b/game/modules/tome/data/talents/cursed/shadows.lua
@@ -152,12 +152,15 @@ local function createShadow(self, level, duration, target)
 			blindside_chance = 15,
 			phasedoor_chance = 5,
 			attack_spell_chance = 5,
+			
+			feed_level = 0
 		},
 		ai_target = {
 			actor=target,
 			x = nil,
 			y = nil
 		},
+		
 		healSelf = function(self)
 			self:useTalent(self.T_HEAL)
 		end,
@@ -178,16 +181,24 @@ local function createShadow(self, level, duration, target)
 			end
 		end,
 		feed = function(self, t)
-			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.ai_state.blindside_chance = t.getBlindsideChance(self.summoner, t)
-		end,
-		unfeed = 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.ai_state.blindside_chance = 15
+			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)
+			end
 		end,
 		shadowWall = function(self, t, duration)
 			self.ai_state.shadow_wall = true
@@ -262,13 +273,7 @@ newTalent{
 		self:incHate(-1)
 
 		level = t.getLevel(self, t)
-		local shadow = createShadow(self, level, 100, nil)
-
-		-- feed the shadow
-		if self:isTalentActive(T_FEED_SHADOWS) then
-			local t = self:getTalentFromId(T_FEED_SHADOWS)
-			shadow:feed(t)
-		end
+		local shadow = createShadow(self, level, 1000, nil)
 
 		shadow:resolve()
 		shadow:resolve(nil, true)
@@ -279,6 +284,13 @@ newTalent{
 		game:playSoundNear(self, "talents/spell_generic")
 		return true
 	end,
+	removeAllShadows = function(self, t)
+		for _, e in pairs(game.level.entities) do
+			if e.summoner and e.summoner == self and e.subtype == "shadow" then
+				e:die()
+			end
+		end
+	end,
 	info = function(self, t)
 		local maxShadows = t.getMaxShadows(self, t)
 		local level = t.getLevel(self, t)
@@ -346,46 +358,45 @@ newTalent{
 newTalent{
 	name = "Feed Shadows",
 	type = {"cursed/shadows", 3},
-	mode = "sustained",
+	mode = "passive",
 	require = cursed_mag_req3,
 	points = 5,
-	cooldown = 10,
-	getBlindsideChance = function(self, t)
-		return 15 + self:getTalentLevel(t) * 5
-	end,
 	getIncDamage = function(self, t)
-		return 20 + self:getTalentLevel(t) * 7
+		return math.floor((math.sqrt(self:getTalentLevel(t)) - 0.5) * 17)
 	end,
 	getCombatAtk = function(self, t)
-		return 20 + self:getTalentLevel(t) * 5
+		return math.floor((math.sqrt(self:getTalentLevel(t)) - 0.5) * 17)
 	end,
-	activate = function(self, t)
-		for _, e in pairs(game.level.entities) do
-			if e.summoner and e.summoner == self and e.subtype == "shadow" then
-				e:feed(t)
+	getHatePerKill = function(self, t)
+		return (self:getTalentLevel(t) / 8) * self.hate_per_kill
+	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
-
-		local regenId = self:addTemporaryValue("hate_regen", -0.02)
-
-		return { regenId = regenId }
+		
+		return { }
 	end,
-	deactivate = function(self, t, p)
-		for _, e in pairs(game.level.entities) do
-			if e.summoner and e.summoner == self and e.subtype == "shadow" then
-				e:unfeed(t)
+	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
 
-		self:removeTemporaryValue("hate_regen", p.regenId)
-
 		return true
 	end,
 	info = function(self, t)
 		local combatAtk = t.getCombatAtk(self, t)
 		local incDamage = t.getIncDamage(self, t)
-		local blindsideChance = t.getBlindsideChance(self, t)
-		return ([[Feed your hate to your shadows increasing their attack by %d%%, damage by %d%% and chance of using blindside to %d%%. While active you will lose hate faster.]]):format(combatAtk, incDamage, blindsideChance)
+		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)
 	end,
 }
 
diff --git a/game/modules/tome/data/timed_effects.lua b/game/modules/tome/data/timed_effects.lua
index 079ec1dab5..f13379cd73 100644
--- a/game/modules/tome/data/timed_effects.lua
+++ b/game/modules/tome/data/timed_effects.lua
@@ -2529,7 +2529,7 @@ 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) then
+		elseif eff.target.dead or not self:hasLOS(eff.target.x, eff.target.y, "block_move") then
 			eff.isSevered = true
 
 			if eff.particles then
@@ -2563,6 +2563,7 @@ newEffect{
 newEffect{
 	name = "AGONY",
 	desc = "Agony",
+	long_desc = function(self, eff) return ("%s is writhing in agony suffering suffering from %d to %d damage over %d turns."):format(self.name:capitalize(), eff.damage / duration, eff.damage, eff.duration) end,
 	type = "mental",
 	status = "detrimental",
 	parameters = { damage=10, mindpower=10, range=10, minPercent=10 },
@@ -2575,17 +2576,16 @@ newEffect{
 		if eff.particle then self:removeParticles(eff.particle) end
 	end,
 	on_timeout = function(self, eff)
-
-		local power = 1 - (math.min(eff.range, core.fov.distance(eff.source.x, eff.source.y, self.x, self.y)) / eff.range)
-		if power > 0 then
-			if self:checkHit(eff.mindpower, self:combatMentalResist(), 0, 95, 5) then
-				local damage = math.floor(eff.damage * power)
-				if damage > 0 then
-					DamageType:get(DamageType.MIND).projector(eff.source, self.x, self.y, DamageType.MIND, damage)
-				end
-			else
-				return true
+		eff.turn = (eff.turn or 0) + 1
+	
+		if self:checkHit(eff.mindpower, self:combatMentalResist(), 0, 95, 5) then
+			local damage = math.floor(eff.damage * (eff.turn / eff.duration))
+			if damage > 0 then
+				DamageType:get(DamageType.MIND).projector(eff.source, self.x, self.y, DamageType.MIND, damage)
+				game:playSoundNear(self, "talents/fire")
 			end
+		else
+			return true
 		end
 
 		if self.dead then
@@ -2593,12 +2593,82 @@ newEffect{
 			return
 		end
 
-		if math.floor(power * 10) + 1 ~= eff.power then
-			eff.power = math.floor(power * 10) + 1
-			if eff.particle then self:removeParticles(eff.particle) end
-			eff.particle = nil
-			if eff.power > 0 then
-				eff.particle = self:addParticles(Particles.new("agony", 1, { power = eff.power }))
+		if eff.particle then self:removeParticles(eff.particle) end
+		eff.particle = nil
+		eff.particle = self:addParticles(Particles.new("agony", 1, { power = 10 * eff.turn / eff.duration }))
+	end,
+}
+
+newEffect{
+	name = "HATEFUL_WHISPER",
+	desc = "Hateful Whisper",
+	long_desc = function(self, eff) return ("%s has heard the hateful whisper."):format(self.name:capitalize()) end,
+	type = "mental",
+	status = "detrimental",
+	parameters = { },
+	on_gain = function(self, err) return "#Target# has heard the hateful whisper!", "+Hateful Whisper" end,
+	on_lose = function(self, err) return "#Target# no longer hears the hateful whisper.", "-Hateful Whisper" end,
+	activate = function(self, eff)
+		DamageType:get(DamageType.MIND).projector(eff.source, self.x, self.y, DamageType.MIND, eff.damage)
+		
+		if self.dead then
+			-- only spread on activate if the target is dead
+			self.tempeffect_def[self.EFF_HATEFUL_WHISPER].doSpread(self, eff)
+			eff.duration = 0
+		else
+			eff.particle = self:addParticles(Particles.new("hateful_whisper", 1, { }))
+		end
+		
+		game:playSoundNear(self, "talents/fire")
+	end,
+	deactivate = function(self, eff)
+		if eff.particle then self:removeParticles(eff.particle) end
+	end,
+	on_timeout = function(self, eff)
+		eff.duration = eff.duration - 1
+		if eff.duration <= 0 then return false end
+		
+		if (eff.state or 0) == 0 then
+			-- pause a turn before infecting others
+			eff.state = 1
+		elseif eff.state == 1 then
+			self.tempeffect_def[self.EFF_HATEFUL_WHISPER].doSpread(self, eff)
+			eff.state = 2
+		end
+	end,
+	doSpread = function(self, eff)
+		local targets = {}
+		local grids = core.fov.circle_grids(self.x, self.y, eff.jumpRange, true)
+		for x, yy in pairs(grids) do
+			for y, _ in pairs(grids[x]) do
+				local a = game.level.map(x, y, game.level.map.ACTOR)
+				if a and eff.source:reactionToward(a) < 0 and self:hasLOS(a.x, a.y) then
+					if not a:hasEffect(a.EFF_HATEFUL_WHISPER) then
+						targets[#targets+1] = a
+					end
+				end
+			end
+		end
+
+		if #targets > 0 then
+			local hitCount = 1
+			if rng.percent(eff.extraJumpChance) then hitCount = hitCount + 1 end
+
+			-- Randomly take targets
+			for i = 1, hitCount do
+				local target = rng.tableRemove(targets)
+				target:setEffect(target.EFF_HATEFUL_WHISPER, eff.duration, {
+					source = eff.source,
+					duration = eff.duration,
+					damage = eff.damage,
+					mindpower = eff.mindpower,
+					jumpRange = eff.jumpRange,
+					extraJumpChance = eff.extraJumpChance
+				})
+				
+				game.level.map:particleEmitter(target.x, target.y, 1, "reproach", { dx = self.x - target.x, dy = self.y - target.y })
+				
+				if #targets == 0 then break end
 			end
 		end
 	end,
-- 
GitLab