From 16e50d393c48eed025be52fdb65e221597579b10 Mon Sep 17 00:00:00 2001
From: dg <dg@51575b47-30f0-44d4-a5cc-537603b46e54>
Date: Fri, 29 Jun 2012 14:37:48 +0000
Subject: [PATCH] shhhh, as shadow we must be

git-svn-id: http://svn.net-core.org/repos/t-engine4@5317 51575b47-30f0-44d4-a5cc-537603b46e54
---
 game/modules/tome/class/Actor.lua             | 106 ++++++++-
 game/modules/tome/class/PlayerDisplay.lua     |   7 +
 game/modules/tome/class/interface/Combat.lua  |  12 +-
 .../tome/class/interface/TooltipsData.lua     |   6 +
 game/modules/tome/class/uiset/Minimalist.lua  |  37 +++
 .../tome/data/birth/classes/psionic.lua       |  41 +++-
 .../tome/data/general/events/mice-quest.lua   |  94 ++++++++
 .../gfx/shockbolt/terrain/pedestal_heart.png  | Bin 0 -> 7208 bytes
 game/modules/tome/data/talents/misc/npcs.lua  |   4 +-
 .../tome/data/talents/psionic/feedback.lua    | 221 ++++++++++++++++++
 .../tome/data/talents/psionic/psionic.lua     |   5 +
 .../tome/data/talents/psionic/solipsism.lua   | 151 ++++++++++++
 .../tome/data/timed_effects/mental.lua        |  43 ++++
 .../modules/tome/data/timed_effects/other.lua |  56 +++++
 14 files changed, 770 insertions(+), 13 deletions(-)
 create mode 100644 game/modules/tome/data/general/events/mice-quest.lua
 create mode 100644 game/modules/tome/data/gfx/shockbolt/terrain/pedestal_heart.png
 create mode 100644 game/modules/tome/data/talents/psionic/feedback.lua
 create mode 100644 game/modules/tome/data/talents/psionic/solipsism.lua

diff --git a/game/modules/tome/class/Actor.lua b/game/modules/tome/class/Actor.lua
index 6a1cfe7581..2b5f2a6f80 100644
--- a/game/modules/tome/class/Actor.lua
+++ b/game/modules/tome/class/Actor.lua
@@ -327,6 +327,24 @@ function _M:useEnergy(val)
 end
 
 function _M:actBase()
+	-- Solipsism speed effects
+	local current_psi_percentage = self:getPsi() / self:getMaxPsi()
+	if self:attr("solipsism_threshold") and current_psi_percentage < self:attr("solipsism_threshold") then
+		if self:hasEffect(self.EFF_CLARITY) then
+			self:removeEffect(self.EFF_CLARITY)
+		end
+		self:setEffect(self.EFF_SOLIPSISM, 1, {power = self:attr("solipsism_threshold") - current_psi_percentage})
+	elseif self:attr("clarity_threshold") and current_psi_percentage > self:attr("clarity_threshold") then
+		if self:hasEffect(self.EFF_SOLIPSISM) then
+			self:removeEffect(self.EFF_SOLIPSISM)
+		end
+		self:setEffect(self.EFF_CLARITY, 1, {power = current_psi_percentage - self:attr("clarity_threshold")})
+	elseif self:hasEffect(self.EFF_SOLIPSISM) then
+		self:removeEffect(self.EFF_SOLIPSISM)
+	elseif self:hasEffect(self.EFF_CLARITY) then
+		self:removeEffect(self.EFF_CLARITY)
+	end
+
 	self.energyBase = self.energyBase - game.energy_to_act
 
 	if self:attr("no_timeflow") then
@@ -361,6 +379,11 @@ function _M:actBase()
 	end
 
 	self:regenResources()
+	
+	-- update psionic feedback
+	if self.psionic_feedback and self.psionic_feedback > 0 then
+		self.psionic_feedback = math.max(0, self.psionic_feedback - 1)
+	end
 
 	-- Compute timed effects
 	self:timedEffects()
@@ -1304,7 +1327,23 @@ end
 --- Regenerate life, call it from your actor class act() method
 function _M:regenLife()
 	if self.life_regen and not self:attr("no_life_regen") then
-		self.life = util.bound(self.life + self.life_regen * util.bound((self.healing_factor or 1), 0, 2.5), self.die_at, self.max_life)
+		local regen = self.life_regen * util.bound((self.healing_factor or 1), 0, 2.5)
+		
+		-- Psionic Balance
+		if self:knowTalent(self.T_BALANCE) then
+			local t = self:getTalentFromId(self.T_BALANCE)
+			local ratio = t.getBalanceRatio(self, t)
+			local psi_increase = regen * ratio
+			self:incPsi(psi_increase)
+			-- Quality of life hack, doesn't decrease life regen while resting..  was way to painful
+			if not self.resting then
+				regen = regen - psi_increase
+			end
+		end
+		
+		self.life = util.bound(self.life + regen, self.die_at, self.max_life)
+		
+		-- Blood Lock
 		if self:attr("blood_lock") then
 			self.life = util.bound(self.life, self.die_at, self:attr("blood_lock"))
 		end
@@ -1355,6 +1394,15 @@ function _M:onHeal(value, src)
 		self:setEffect(self.EFF_REGENERATION, 6, {power=(value * self.fungal_growth / 100) / 6, no_wild_growth=true})
 	end
 
+	-- Psionic Balance
+	if self:knowTalent(self.T_BALANCE) then
+		local t = self:getTalentFromId(self.T_BALANCE)
+		local ratio = t.getBalanceRatio(self, t)
+		local psi_increase = value * ratio
+		self:incPsi(psi_increase)
+		value = value - psi_increase
+	end
+	
 	-- Must be last!
 	if self:attr("blood_lock") then
 		if self.life + value > self:attr("blood_lock") then
@@ -1532,13 +1580,27 @@ function _M:onTakeHit(value, src)
 			t.explode(self, t, dam)
 		end
 	end
+	
+	if self:attr("resonance_shield") then
+	-- Absorb damage into the shield
+		if value / 2 <= self.resonance_shield_absorb then
+			self.resonance_shield_absorb = self.resonance_shield_absorb - (value / 2)
+			value = value / 2
+		else
+			value = value - self.resonance_shield_absorb
+			self.resonance_shield_absorb = 0
+		end
+		if self.resonance_shield_absorb <= 0 then
+			self:removeEffect(self.EFF_RESONANCE_SHIELD)
+		end
+	end
 
 	if self:isTalentActive(self.T_BONE_SHIELD) then
 		local t = self:getTalentFromId(self.T_BONE_SHIELD)
 		t.absorb(self, t, self:isTalentActive(self.T_BONE_SHIELD))
 		value = 0
 	end
-
+	
 	if self.knowTalent and (self:knowTalent(self.T_SEETHE) or self:knowTalent(self.T_GRIM_RESOLVE)) then
 		if not self:hasEffect(self.EFF_CURSED_FORM) then
 			self:setEffect(self.EFF_CURSED_FORM, 1, { increase=0 })
@@ -1571,13 +1633,6 @@ function _M:onTakeHit(value, src)
 		if value >= 6000 then world:gainAchievement("DAMAGE_6000", rsrc) end
 	end
 
-	-- Stoned ? SHATTER !
-	if self:attr("stoned") and value >= self.max_life * 0.3 then
-		-- Make the damage high enough to kill it
-		value = self.max_life + 1
-		game.logSeen(self, "%s shatters into pieces!", self.name:capitalize())
-	end
-
 	-- Frozen: absorb some damage into the iceblock
 	if self:attr("encased_in_ice") then
 		local eff = self:hasEffect(self.EFF_FROZEN)
@@ -1588,6 +1643,36 @@ function _M:onTakeHit(value, src)
 			eff.begone = game.turn
 		end
 	end
+	
+	-- Feedback pool: Stores damage as energy to use later
+	if self.psionic_feedback then
+		local current = self.psionic_feedback
+		local max = self.psionic_feedback_max or 100
+		self.psionic_feedback = math.min(self.psionic_feedback_max, self.psionic_feedback + value)
+	end
+		
+	-- Solipsism
+	if self.knowTalent and self:knowTalent(self.T_SOLIPSISM) then
+		local t = self:getTalentFromId(self.T_SOLIPSISM)
+		local damage_to_psi = value * t.damageToPsi(self, t)
+		if self:getPsi() > damage_to_psi then
+			self:incPsi(-damage_to_psi)
+		else
+			damage_to_psi = self:getPsi()
+			self:incPsi(-damage_to_psi)
+		end
+		if damage_to_psi > 0 then
+			game.logSeen(self, "%s's mind suffers #BLUE#%d psi#LAST# damage from the attack.", self.name:capitalize(), damage_to_psi)
+		end
+		value = value - damage_to_psi
+	end
+
+	-- Stoned ? SHATTER !
+	if self:attr("stoned") and value >= self.max_life * 0.3 then
+		-- Make the damage high enough to kill it
+		value = self.max_life + 1
+		game.logSeen(self, "%s shatters into pieces!", self.name:capitalize())
+	end
 
 	-- Adds hate
 	if self:knowTalent(self.T_HATE_POOL) then
@@ -3286,6 +3371,9 @@ function _M:breakStepUp()
 	if self:hasEffect(self.EFF_REFLEXIVE_DODGING) then
 		self:removeEffect(self.EFF_REFLEXIVE_DODGING)
 	end
+	if self:hasEffect(self.EFF_DISMISSAL) then
+		self:removeEffect(self.EFF_DISMISSAL)
+	end
 end
 
 --- Breaks lightning speed if active
diff --git a/game/modules/tome/class/PlayerDisplay.lua b/game/modules/tome/class/PlayerDisplay.lua
index 3f135f17d9..85c985fd2d 100644
--- a/game/modules/tome/class/PlayerDisplay.lua
+++ b/game/modules/tome/class/PlayerDisplay.lua
@@ -372,6 +372,13 @@ function _M:display()
 			{r=colors.BLUE.r / 5, g=colors.BLUE.g / 5, b=colors.BLUE.b / 5}
 		)) h = h + self.font_h
 	end
+	
+	if player.psionic_feedback_max then
+		self:mouseTooltip(self.TOOLTIP_FEEDBACK, self:makeTextureBar("#7fffd4#Feedback:", nil, player.psionic_feedback or 0, player.psionic_feedback_max, -1, x, h, 255, 255, 255,
+			{r=colors.YELLOW.r / 2, g=colors.YELLOW.g / 2, b=colors.YELLOW.b / 2},
+			{r=colors.YELLOW.r / 5, g=colors.YELLOW.g / 5, b=colors.YELLOW.b / 5}
+		)) h = h + self.font_h
+	end
 
 	local quiver = player:getInven("QUIVER")
 	local ammo = quiver and quiver[1]
diff --git a/game/modules/tome/class/interface/Combat.lua b/game/modules/tome/class/interface/Combat.lua
index e50d082836..906c363d54 100644
--- a/game/modules/tome/class/interface/Combat.lua
+++ b/game/modules/tome/class/interface/Combat.lua
@@ -1295,7 +1295,17 @@ function _M:combatPhysicalResist(fake)
 	if self:knowTalent(self.T_POWER_IS_MONEY) then
 		add = add + util.bound(self.money / (90 - self:getTalentLevelRaw(self.T_POWER_IS_MONEY) * 5), 0, self:getTalentLevelRaw(self.T_POWER_IS_MONEY) * 7)
 	end
-	return self:rescaleCombatStats(self.combat_physresist + (self:getCon() + self:getStr() + (self:getLck() - 50) * 0.5) * 0.35 + add)
+	
+	-- To return later
+	local total = self:rescaleCombatStats(self.combat_physresist + (self:getCon() + self:getStr() + (self:getLck() - 50) * 0.5) * 0.35 + add)
+	
+	-- Psionic Balance
+	if self:knowTalent(self.T_BALANCE) then
+		local t = self:getTalentFromId(self.T_BALANCE)
+		local ratio = t.getBalanceRatio(self, t)
+		total = (1 - ratio)*total + self:combatMentalResist(fake)*ratio
+	end
+	return total
 end
 
 --- Computes spell resistance
diff --git a/game/modules/tome/class/interface/TooltipsData.lua b/game/modules/tome/class/interface/TooltipsData.lua
index de7a62cbe3..5b1c44f799 100644
--- a/game/modules/tome/class/interface/TooltipsData.lua
+++ b/game/modules/tome/class/interface/TooltipsData.lua
@@ -115,6 +115,12 @@ To get meaningful amounts back in combat, you must absorb it through shields or
 Your capacity for storing energy is determined by your Willpower.
 ]]
 
+TOOLTIP_FEEDBACK = [[#GOLD#Feedback#LAST#
+Feedback represents energy you've stored up from being attacked. It decays quickly over time.
+To get meaningful amounts back in combat, you must take damage.
+Your capacity for storing feedback is determined by how many talents you know that harness it.
+]]
+
 TOOLTIP_NECROTIC_AURA = [[#GOLD#Necrotic Aura#LAST#
 Represents the raw materials for creating undead minions.
 It increases each time you or your minions kill something that is inside the aura radius.
diff --git a/game/modules/tome/class/uiset/Minimalist.lua b/game/modules/tome/class/uiset/Minimalist.lua
index bd00e18f9f..d088dccab2 100644
--- a/game/modules/tome/class/uiset/Minimalist.lua
+++ b/game/modules/tome/class/uiset/Minimalist.lua
@@ -72,6 +72,8 @@ hate_c = {0xF5/255, 0x3C/255, 0xBE/255}
 hate_sha = Shader.new("resources", {color=hate_c, speed=1000, distort={0.4,0.4}})
 psi_c = {colors.BLUE.r/255, colors.BLUE.g/255, colors.BLUE.b/255}
 psi_sha = Shader.new("resources", {color=psi_c, speed=2000, distort={0.4,0.4}})
+feedback_c = {colors.YELLOW.r/255, colors.YELLOW.g/255, colors.YELLOW.b/255}
+feedback_sha = Shader.new("resources", {color=feedback_c, speed=2000, distort={0.4,0.4}})
 save_c = pos_c
 save_sha = pos_sha
 
@@ -103,6 +105,8 @@ fshat_vim = {core.display.loadImage("/data/gfx/ui/resources/front_vim.png"):glTe
 fshat_vim_dark = {core.display.loadImage("/data/gfx/ui/resources/front_vim_dark.png"):glTexture()}
 fshat_psi = {core.display.loadImage("/data/gfx/ui/resources/front_psi.png"):glTexture()}
 fshat_psi_dark = {core.display.loadImage("/data/gfx/ui/resources/front_psi_dark.png"):glTexture()}
+fshat_feedback = {core.display.loadImage("/data/gfx/ui/resources/front_psi.png"):glTexture()}
+fshat_feedback_dark = {core.display.loadImage("/data/gfx/ui/resources/front_psi_dark.png"):glTexture()}
 fshat_air = {core.display.loadImage("/data/gfx/ui/resources/front_air.png"):glTexture()}
 fshat_air_dark = {core.display.loadImage("/data/gfx/ui/resources/front_air_dark.png"):glTexture()}
 
@@ -511,6 +515,7 @@ function _M:showResourceTooltip(x, y, w, h, id, desc, is_first)
 						if player:knowTalent(player.T_VIM_POOL) then list[#list+1] = {name="Vim", id="vim"} end
 						if player:knowTalent(player.T_HATE_POOL) then list[#list+1] = {name="Hate", id="hate"} end
 						if player:knowTalent(player.T_PSI_POOL) then list[#list+1] = {name="Psi", id="psi"} end
+						if player.psionic_feedback_max then list[#list+1] = {name="Feedback", id="feedback"} end
 						Dialog:listPopup("Display/Hide resources", "Toggle:", list, 300, 300, function(sel)
 							if not sel or not sel.id then return end
 							game.player["_hide_resource_"..sel.id] = not game.player["_hide_resource_"..sel.id]
@@ -969,6 +974,38 @@ function _M:displayResources(scale, bx, by, a)
 			self:showResourceTooltip(bx+x*scale, by+y*scale, fshat[6], fshat[7], "res:psi", self.TOOLTIP_PSI)
 			x, y = self:resourceOrientStep(orient, bx, by, scale, x, y, fshat[6], fshat[7])
 		elseif game.mouse:getZone("res:psi") then game.mouse:unregisterZone("res:psi") end
+		
+		-----------------------------------------------------------------------------------
+		-- Feedback
+		if player.psionic_feedback_max and not player._hide_resource_feedback then
+			sshat[1]:toScreenFull(x-6, y+8, sshat[6], sshat[7], sshat[2], sshat[3], 1, 1, 1, a)
+			bshat[1]:toScreenFull(x, y, bshat[6], bshat[7], bshat[2], bshat[3], 1, 1, 1, a)
+			if feedback_sha.shad then feedback_sha:setUniform("a", a) feedback_sha.shad:use(true) end
+			local p = player.psionic_feedback / player.psionic_feedback_max
+			shat[1]:toScreenPrecise(x+49, y+10, shat[6] * p, shat[7], 0, p * 1/shat[4], 0, 1/shat[5], feedback_c[1], feedback_c[2], feedback_c[3], a)
+			if feedback_sha.shad then feedback_sha.shad:use(false) end
+
+			if not self.res.feedback or self.res.feedback.vc ~= player.psionic_feedback or self.res.feedback.vm ~= player.psionic_feedback_max or self.res.feedback.vr ~= -1 then
+				self.res.feedback = {
+					hidable = "Feedback",
+					vc = player.psionic_feedback, vm = player.psionic_feedback_max, vr = -1,
+					cur = {core.display.drawStringBlendedNewSurface(font_sha, ("%d/%d"):format(player.psionic_feedback, player.psionic_feedback_max), 255, 255, 255):glTexture()},
+					regen={core.display.drawStringBlendedNewSurface(sfont_sha, ("%+0.2f"):format(-1), 255, 255, 255):glTexture()},
+				}
+			end
+			local dt = self.res.feedback.cur
+			dt[1]:toScreenFull(2+x+64, 2+y+10 + (shat[7]-dt[7])/2, dt[6], dt[7], dt[2], dt[3], 0, 0, 0, 0.7 * a)
+			dt[1]:toScreenFull(x+64, y+10 + (shat[7]-dt[7])/2, dt[6], dt[7], dt[2], dt[3], 1, 1, 1, a)
+			dt = self.res.feedback.regen
+			dt[1]:toScreenFull(2+x+144, 2+y+10 + (shat[7]-dt[7])/2, dt[6], dt[7], dt[2], dt[3], 0, 0, 0, 0.7 * a)
+			dt[1]:toScreenFull(x+144, y+10 + (shat[7]-dt[7])/2, dt[6], dt[7], dt[2], dt[3], 1, 1, 1, a)
+
+			local front = fshat_feedback_dark
+			if player.psionic_feedback >= player.psionic_feedback_max then front = fshat_feedback end
+			front[1]:toScreenFull(x, y, front[6], front[7], front[2], front[3], 1, 1, 1, a)
+			self:showResourceTooltip(bx+x*scale, by+y*scale, fshat[6], fshat[7], "res:feedback", self.TOOLTIP_FEEDBACK)
+			x, y = self:resourceOrientStep(orient, bx, by, scale, x, y, fshat[6], fshat[7])
+		elseif game.mouse:getZone("res:feedback") then game.mouse:unregisterZone("res:feedback") end
 
 		-----------------------------------------------------------------------------------
 		-- Ammo
diff --git a/game/modules/tome/data/birth/classes/psionic.lua b/game/modules/tome/data/birth/classes/psionic.lua
index fb7b66e327..3e9565c426 100644
--- a/game/modules/tome/data/birth/classes/psionic.lua
+++ b/game/modules/tome/data/birth/classes/psionic.lua
@@ -32,12 +32,12 @@ newBirthDescriptor{
 			__ALL__ = "disallow",
 			Mindslayer = "allow",
 			Psion = "allow",
+			Solipsist = "allow",
 		},
 	},
 	copy = {
 		psi_regen = 0.2,
 	},
-	body = { PSIONIC_FOCUS = 1, QS_PSIONIC_FOCUS = 1,},
 }
 
 newBirthDescriptor{
@@ -85,6 +85,7 @@ newBirthDescriptor{
 		[ActorTalents.T_TELEKINETIC_SMASH] = 1,
 		[ActorTalents.T_SHOOT] = 1,
 	},
+	body = { PSIONIC_FOCUS = 1, QS_PSIONIC_FOCUS = 1,},
 	copy = {
 		max_life = 110,
 		resolvers.equip{ id=true,
@@ -147,3 +148,41 @@ newBirthDescriptor{
 		life_rating = -4,
 	},
 }
+
+newBirthDescriptor{
+	type = "subclass",
+	name = "Solipsist",
+	locked = function() return profile.mod.allow_build.psionic_solipsist and true or "hide"  end,
+	locked_desc = "TODO",
+	desc = {
+		"blahblah",
+		"Their most important stats are: Willpower and Cunning",
+		"#GOLD#Stat modifiers:",
+		"#LIGHT_BLUE# * +0 Strength, +0 Dexterity, +0 Constitution",
+		"#LIGHT_BLUE# * +0 Magic, +5 Willpower, +4 Cunning",
+		"#GOLD#Life per level:#LIGHT_BLUE# -4 (*special*)",
+	},
+	power_source = {psionic=true},
+	stats = { str=0, wil=5, cun=4, },
+	talents_types = {
+		["psionic/feedback"]={true, 0.3},
+		["psionic/psychic-assault"]={true, 0.3},
+		["psionic/solipsism"]={true, 0.3},
+	},
+	talents = {
+		[ActorTalents.T_FEEDBACK] = 1,
+		[ActorTalents.T_PSYCHIC_LOBOTOMY] = 1,
+		[ActorTalents.T_SOLIPSISM] = 1,
+	},
+	copy = {
+		max_life = 90,
+		resolvers.equip{ id=true,
+			{type="armor", subtype="cloth", name="linen robe", autoreq=true, ego_chance=-1000},
+			{type="weapon", subtype="mindstar", name="mossy mindstar", autoreq=true, ego_chance=-1000},
+			{type="weapon", subtype="mindstar", name="mossy mindstar", autoreq=true, ego_chance=-1000},
+		},
+	},
+	copy_add = {
+		life_rating = -4,
+	},
+}
diff --git a/game/modules/tome/data/general/events/mice-quest.lua b/game/modules/tome/data/general/events/mice-quest.lua
new file mode 100644
index 0000000000..1ead996be4
--- /dev/null
+++ b/game/modules/tome/data/general/events/mice-quest.lua
@@ -0,0 +1,94 @@
+-- ToME - Tales of Maj'Eyal
+-- Copyright (C) 2009, 2010, 2011, 2012 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
+
+-- Find a random spot
+local x, y = rng.range(1, level.map.w - 2), rng.range(1, level.map.h - 2)
+local tries = 0
+while not game.state:canEventGrid(level, x, y) and tries < 100 do
+	x, y = rng.range(1, level.map.w - 2), rng.range(1, level.map.h - 2)
+	tries = tries + 1
+end
+if tries >= 100 then return false end
+
+local id = "mouse-quest-"..game.turn
+
+local changer = function(id)
+	local npcs = mod.class.NPC:loadList{"/data/general/npcs/thieve.lua"}
+	local objects = mod.class.Object:loadList("/data/general/objects/objects.lua")
+	local terrains = mod.class.Grid:loadList("/data/general/grids/cave.lua")
+	local zone = mod.class.Zone.new(id, {
+		name = "Unknown cave",
+		level_range = {1, 1},
+		level_scheme = "player",
+		max_level = 1,
+		actor_adjust_level = function(zone, level, e) return zone.base_level + e:getRankLevelAdjust() + level.level-1 + rng.range(-1,2) end,
+		width = 20, height = 20,
+		ambient_music = "Suspicion.ogg",
+		reload_lists = false,
+		persistent = "zone",
+		min_material_level = 1,
+		max_material_level = 1,
+		generator =  {
+			map = {
+				class = "engine.generator.map.Cavern",
+				zoom = 4,
+				min_floor = 120,
+				floor = "CAVEFLOOR",
+				wall = "CAVEWALL",
+				up = "CAVE_LADDER_UP_WILDERNESS",
+				door = "CAVEFLOOR",
+			},
+			actor = {
+				class = "mod.class.generator.actor.Random",
+				nb_npc = {14, 14},
+				guardian = {random_elite={life_rating=function(v) return v * 1.5 + 4 end, nb_rares=3}},
+			},
+			object = {
+				class = "engine.generator.object.Random",
+				filters = {{type="gem"}},
+				nb_object = {6, 9},
+			},
+			trap = {
+				class = "engine.generator.trap.Random",
+				nb_trap = {6, 9},
+			},
+		},
+--		levels = { [1] = { generator = { map = { up = "CAVEFLOOR", }, }, }, },
+		npc_list = npcs,
+		grid_list = terrains,
+		object_list = objects,
+		trap_list = {}},
+	})
+	return zone
+end
+
+local g = game.level.map(x, y, engine.Map.TERRAIN):cloneFull()
+g.name = ""
+g.display='>' g.color_r=0 g.color_g=0 g.color_b=255 g.notice = true
+g.change_level=1 g.change_zone=id
+g.add_displays = g.add_displays or {}
+g.add_displays[#g.add_displays+1] = mod.class.Grid.new{image="terrain/pedestal_heart.png", z=5}
+g.nice_tiler = nil
+g.block_move = function(self)
+	game:changeLevel(1, self.real_change(self.change_zone), {temporary_zone_shift=true})
+	return true
+end
+game.zone:addEntity(game.level, g, "terrain", x, y)
+
+return true
diff --git a/game/modules/tome/data/gfx/shockbolt/terrain/pedestal_heart.png b/game/modules/tome/data/gfx/shockbolt/terrain/pedestal_heart.png
new file mode 100644
index 0000000000000000000000000000000000000000..a3f0cf662ec0a9db89c1a5355443499aa3f52cbc
GIT binary patch
literal 7208
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
aC5Ul3T*H`x0(kX}M`2=UVNk2@lK6j83eM61

literal 0
HcmV?d00001

diff --git a/game/modules/tome/data/talents/misc/npcs.lua b/game/modules/tome/data/talents/misc/npcs.lua
index ee35842b9c..2d8daebf62 100644
--- a/game/modules/tome/data/talents/misc/npcs.lua
+++ b/game/modules/tome/data/talents/misc/npcs.lua
@@ -1062,7 +1062,7 @@ newTalent{
 		local tg = self:getTalentTarget(t)
 		local x, y = self:getTarget(tg)
 		if not x or not y then return nil end
-		self:project(tg, x, y, DamageType.MIND, self:spellCrit(self:combatTalentMindDamage(t, 10, 340)), {type="mind"})
+		self:project(tg, x, y, DamageType.MIND, self:mindCrit(self:combatTalentMindDamage(t, 10, 340)), {type="mind"})
 		game:playSoundNear(self, "talents/spell_generic")
 		return true
 	end,
@@ -1112,7 +1112,7 @@ newTalent{
 		local tg = self:getTalentTarget(t)
 		local x, y = self:getTarget(tg)
 		if not x or not y then return nil end
-		self:project(tg, x, y, DamageType.MINDKNOCKBACK, self:spellCrit(self:combatTalentMindDamage(t, 10, 170)), {type="mind"})
+		self:project(tg, x, y, DamageType.MINDKNOCKBACK, self:mindCrit(self:combatTalentMindDamage(t, 10, 170)), {type="mind"})
 		game:playSoundNear(self, "talents/spell_generic")
 		return true
 	end,
diff --git a/game/modules/tome/data/talents/psionic/feedback.lua b/game/modules/tome/data/talents/psionic/feedback.lua
new file mode 100644
index 0000000000..9c8f0a9624
--- /dev/null
+++ b/game/modules/tome/data/talents/psionic/feedback.lua
@@ -0,0 +1,221 @@
+-- ToME - Tales of Maj'Eyal
+-- Copyright (C) 2009, 2010, 2011, 2012 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
+
+-- TODO: Sounds and particles
+
+newTalent{
+	name = "Feedback",
+	type = {"psionic/feedback", 1},
+	points = 5,
+	require = psi_wil_req1,
+	cooldown = 10,
+	psi = 10,
+	tactical = { PSI = 2 },
+	on_pre_use = function(self, t, silent) if self.psionic_feedback <= 0 then if not silent then game.logPlayer(self, "You have no feedback to power this talent.") end return false end return not self:hasEffect(self.EFF_REGENERATION) end,
+	getConversionRatio = function(self, t) return self:combatTalentMindDamage(t, 50, 150)/100 end,
+	on_learn = function(self, t)
+		if self:getTalentLevelRaw(t) == 1 then
+			if not self.psionic_feedback then
+				self.psionic_feedback = 0
+			end
+			self.psionic_feedback_max = (self.psionic_feedback_max or 0) + 50
+		end
+		return true
+	end,
+	on_unlearn = function(self, t)
+		if not self:knowTalent(t) then
+			self.psionic_feedback_max = self.psionic_feedback_max - 50
+			if self.psionic_feedback_max <= 0 then
+				self.psionic_feedback_max = nil
+				self.psionic_feedback = nil
+			end
+		end
+		return true
+	end,
+	action = function(self, t)
+		local power = self.psionic_feedback *  t.getConversionRatio(self, t)
+		self:setEffect(self.EFF_REGENERATION, 5, {power = self:mindCrit(power/5)})
+		self.psionic_feedback = 0
+		return true
+	end,
+	info = function(self, t)
+		local conversion = t.getConversionRatio(self, t)
+		return ([[You now store damage you take as psionic feedback.  Activating this talent removes all stored feedback, converting %d%% of the stored energy into life regen over the next five turns.
+		Learning this talent will increase the amount of feedback you can store by 50 (first talent point only).
+		The conversion ratio will scale with your mindpower.]]):format(conversion * 100)
+	end,
+}
+
+newTalent{
+	name = "Discharge",
+	type = {"psionic/feedback", 2},
+	points = 5, 
+	require = psi_wil_req2,
+	cooldown = 10,
+	psi = 10,
+	tactical = { DISABLE = 2},
+	range = 0,
+	direct_hit = true,
+	requires_target = true,
+	getConversionRatio = function(self, t) return 100 - math.min(50, self:combatTalentMindDamage(t, 0, 50)) end,
+	getDuration = function(self, t)
+		local power = (self.psionic_feedback or 0) / t.getConversionRatio(self, t)
+		local duration = 1 + math.floor(power)
+		return duration
+	end,
+	radius = function(self, t) return math.ceil(self:getTalentLevel(t)) end,
+	target = function(self, t)
+		return {type="ball", range=self:getTalentRange(t), radius=self:getTalentRadius(t), selffire=false}
+	end,
+	on_pre_use = function(self, t, silent) if self.psionic_feedback <= 0 then if not silent then game.logPlayer(self, "You have no feedback to power this talent.") end return false end return true end,
+	on_learn = function(self, t)
+		if self:getTalentLevelRaw(t) == 1 then
+			if not self.psionic_feedback then
+				self.psionic_feedback = 0
+			end
+			self.psionic_feedback_max = (self.psionic_feedback_max or 0) + 50
+		end
+		return true
+	end,
+	on_unlearn = function(self, t)
+		if not self:knowTalent(t) then
+			self.psionic_feedback_max = self.psionic_feedback_max - 50
+			if self.psionic_feedback_max <= 0 then
+				self.psionic_feedback_max = nil
+				self.psionic_feedback = nil
+			end
+		end
+		return true
+	end,
+	action = function(self, t)
+		local tg = self:getTalentTarget(t)
+
+		self:project(tg, self.x, self.y, function(px, py)
+			local target = game.level.map(px, py, engine.Map.ACTOR)
+			if not target then return end
+			
+			if target:canBe("stun") then
+				target:setEffect(target.EFF_DAZED, math.floor(self:mindCrit(t.getDuration(self, t))), {apply_power=self:combatMindpower()})
+			else
+				game.logSeen(target, "%s resists the daze!", target.name:capitalize())
+			end
+			game.level.map:particleEmitter(px, py, 1, "light")
+		end)
+		
+		self.psionic_feedback = 0
+		
+		return true
+	end,
+	info = function(self, t)
+		local conversion = t.getConversionRatio(self, t)
+		local radius = self:getTalentRadius(t)
+		return ([[Activate to discharge all stored feedback, dazing creatures in a radius of %d for 1 turn.  The duration of the effect will be increased by 1 for every %d feedback you have stored.
+		Learning this talent will increase the amount of feedback you can store by 50 (first talent point only).
+		The conversion ratio will scale with your mindpower.]]):format(radius, conversion)
+	end,
+}
+
+newTalent{
+	name = "Resonance Shield",
+	type = {"psionic/feedback", 3},
+	points = 5, 
+	require = psi_wil_req3,
+	cooldown = 15,
+	psi = 10,
+	tactical = { DEFEND = 2, ATTACK = {MIND = 2}},
+	on_pre_use = function(self, t, silent) if self.psionic_feedback <= 0 then if not silent then game.logPlayer(self, "You have no feedback to power this talent.") end return false end return true end,
+	getConversionRatio = function(self, t) return self:combatTalentMindDamage(t, 50, 150) / 100 end,
+	getDamage = function(self, t) return self:combatTalentMindDamage(t, 10, 50) end,
+	on_learn = function(self, t)
+		if self:getTalentLevelRaw(t) == 1 then
+			if not self.psionic_feedback then
+				self.psionic_feedback = 0
+			end
+			self.psionic_feedback_max = (self.psionic_feedback_max or 0) + 50
+		end
+		return true
+	end,
+	on_unlearn = function(self, t)
+		if not self:knowTalent(t) then
+			self.psionic_feedback_max = self.psionic_feedback_max - 50
+			if self.psionic_feedback_max <= 0 then
+				self.psionic_feedback_max = nil
+				self.psionic_feedback = nil
+			end
+		end
+		return true
+	end,
+	action = function(self, t)
+		local power = (self.psionic_feedback or 0) * t.getConversionRatio(self, t)
+		self:setEffect(self.EFF_RESONANCE_SHIELD, 10, {power = self:mindCrit(power), dam = self:mindCrit(t.getDamage(self, t))})
+		self.psionic_feedback = 0
+		return true
+	end,
+	info = function(self, t)
+		local conversion = t.getConversionRatio(self, t)
+		local damage = t.getDamage(self, t)
+		return ([[Activate to remove all stored feedback, converting %d%% of the stored energy into a resonance field that will absorb 50%% of all damage you take and inflict %0.2f mind damage to melee attackers.
+		Learning this talent will increase the amount of feedback you can store by 50 (first talent point only).
+		The conversion ratio will scale with your mindpower and the effect lasts up to ten turns.]]):format(conversion * 100, damDesc(self, DamageType.MIND, damage))
+	end,
+}
+
+newTalent{
+	name = "Feedback Loop",
+	type = {"psionic/feedback", 4},
+	points = 5, 
+	require = psi_wil_req4,
+	cooldown = 24,
+	psi = 10,
+	tactical = { FEEDBACK = 2 },
+	getConversionRatio = function(self, t) return math.min(100, self:combatTalentMindDamage(t, 20, 100))/100 end,
+	getFeedbackIncrease = function(self, t) return (self.psionic_feedback_max or 0) * t.getConversionRatio(self, t) end,
+	no_energy = true,
+	on_learn = function(self, t)
+		if self:getTalentLevelRaw(t) == 1 then
+			if not self.psionic_feedback then
+				self.psionic_feedback = 0
+			end
+			self.psionic_feedback_max = (self.psionic_feedback_max or 0) + 50
+		end
+		return true
+	end,
+	on_unlearn = function(self, t)
+		if not self:knowTalent(t) then
+			self.psionic_feedback_max = self.psionic_feedback_max - 50
+			if self.psionic_feedback_max <= 0 then
+				self.psionic_feedback_max = nil
+				self.psionic_feedback = nil
+			end
+		end
+		return true
+	end,
+	action = function(self, t)
+		self.psionic_feedback = math.min(self.psionic_feedback_max or 0, (self.psionic_feedback or 0) + t.getFeedbackIncrease(self, t))
+		return true
+	end,
+	info = function(self, t)
+		local conversion = t.getConversionRatio(self, t)
+		local feedback = t.getFeedbackIncrease(self, t)
+		return ([[Activate to instantly convert psi (the cost of the talent) into %d%% of your maximum feedback (currently %d).
+		Learning this talent will increase the amount of feedback you can store by 50 (first talent point only).
+		The feedback gain will scale with your mindpower.
+		This talent takes no time to use.]]):format(conversion * 100, feedback)
+	end,
+}
\ No newline at end of file
diff --git a/game/modules/tome/data/talents/psionic/psionic.lua b/game/modules/tome/data/talents/psionic/psionic.lua
index 5792ff0ce7..1f03e7bcd1 100644
--- a/game/modules/tome/data/talents/psionic/psionic.lua
+++ b/game/modules/tome/data/talents/psionic/psionic.lua
@@ -37,7 +37,9 @@ newTalentType{ allow_random=true, type="psionic/brainstorm", name = "brainstorm"
 -- Secret Project...
 -- Solipsist Talent Trees
 newTalentType{ allow_random=true, type="psionic/psychic-assault", name = "psychic assault", description = "Directly attack your opponents minds." }
+newTalentType{ allow_random=true, type="psionic/solipsism", name = "solipsism", description = "Nothing exists outside the minds ability to perceive it." }
 -- Generic Solipsist Trees
+newTalentType{ allow_random=true, type="psionic/feedback", generic = true, name = "feedback", description = "Store and discharge psychic feedback." }
 
 newTalentType{ allow_random=true, type="psionic/possession", name = "possession", description = "You have learnt to shed away your body, allowing you to possess any other." }
 
@@ -169,7 +171,10 @@ load("/data/talents/psionic/psi-archery.lua")
 load("/data/talents/psionic/grip.lua")
 
 -- Solipsist
+load("/data/talents/psionic/feedback.lua")
 load("/data/talents/psionic/psychic-assault.lua")
+load("/data/talents/psionic/solipsism.lua")
+
 
 load("/data/talents/psionic/possession.lua")
 
diff --git a/game/modules/tome/data/talents/psionic/solipsism.lua b/game/modules/tome/data/talents/psionic/solipsism.lua
new file mode 100644
index 0000000000..e1c2f35601
--- /dev/null
+++ b/game/modules/tome/data/talents/psionic/solipsism.lua
@@ -0,0 +1,151 @@
+-- ToME - Tales of Maj'Eyal
+-- Copyright (C) 2009, 2010, 2011, 2012 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
+
+
+-- TODO: Sounds and particles
+
+newTalent{
+	name = "Solipsism",
+	type = {"psionic/solipsism", 1},
+	points = 5,
+	require = psi_wil_req1,
+	mode = "passive",
+	no_unlearn_last = true,
+	damageToPsi = function(self, t) return math.min(self:getTalentLevel(t) * 0.15, 1) end,
+	on_learn = function(self, t)
+		self:incMaxPsi(10)
+		if self:getTalentLevelRaw(t) == 1 then
+			self.life_rating = 0
+			self.psi_rating =  self.psi_rating + 10
+			self.solipsism_threshold = (self.solipsism_threshold or 0) + 0.2
+		end
+		return true
+	end,
+	on_unlearn = function(self, t)
+		self:incMaxPsi(-10)
+		if not self:knowTalent(t) then
+			self.solipsism_threshold = self.solipsism_threshold - 0.2
+		end
+		return true
+	end,
+	info = function(self, t)
+		local damage_to_psi = t.damageToPsi(self, t)
+		return ([[You believe that your mind is the center of everything.  Permanently increases the amount of psi you gain per level by 10 and reduces your life rating (affects life at level up) to 0 (one time only adjustment).
+		You also have learned to overcome physical damage with your mind alone and convert %d%% of all damage into psi damage.
+		Increases your solipsism threshold by 20%% (first point only), reducing global speed if your Psi falls below the threshold (currently %d%%).
+		Each talent point invested will also increase your max Psi by 10.]]):format(damage_to_psi * 100, self.solipsism_threshold * 100)
+	end,
+}
+
+newTalent{
+	name = "Balance",
+	type = {"psionic/solipsism", 2},
+	points = 5,
+	require = psi_wil_req2,
+	mode = "passive",
+	getBalanceRatio = function(self, t) return math.min(self:getTalentLevel(t) * 0.15, 1) end,
+	on_learn = function(self, t)
+		self:incMaxPsi(10)
+		if self:getTalentLevelRaw(t) == 1 then
+			self.solipsism_threshold = (self.solipsism_threshold or 0) + 0.1
+		end
+		return true
+	end,
+	on_unlearn = function(self, t)
+		self:incMaxPsi(-10)
+		if not self:knowTalent(t) then
+			self.solipsism_threshold = self.solipsism_threshold - 0.1
+		end
+		return true
+	end,
+	info = function(self, t)
+		local ratio = t.getBalanceRatio(self, t) * 100
+		return ([[%d%% of your healing and life regen now recovers Psi instead of life.  You now use %d%% of your physical save value and %d%% of your mental save value for physical saving throws.
+		Increases your solipsism threshold by 10%% (first point only), reducing global speed if your Psi falls below the threshold (currently %d%%).
+		Each talent point invested will also increase your max Psi by 10.]]):format(ratio, ratio, ratio, self.solipsism_threshold * 100)
+	end,
+}
+
+newTalent{
+	name = "Clarity",
+	type = {"psionic/solipsism", 3},
+	points = 5,
+	require = psi_wil_req3,
+	mode = "passive",
+	getClarityThreshold = function(self, t) return math.max(0.5, 1 - self:getTalentLevelRaw(t) / 10) end,
+	on_learn = function(self, t)
+		self:incMaxPsi(10)
+		self.solipsism_threshold = (self.solipsism_threshold or 0) + 0.1
+		if self:getTalentLevelRaw(t) == 1 then
+			self.clarity_threshold = t.getClarityThreshold(self, t)
+		end
+		return true
+	end,
+	on_unlearn = function(self, t)
+		self:incMaxPsi(-10)
+		if not self:knowTalent(t) then
+			self.solipsism_threshold = self.solipsism_threshold - 0.1
+			self.clarity_threshold = nil
+		else
+			self.clarity_threshold = t.getClarityThreshold(self, t)
+		end
+		return true
+	end,
+	info = function(self, t)
+		local threshold = t.getClarityThreshold(self, t)
+		return ([[For every percent that your Psi pool exceeds %d%% you gain 1%% global speed.
+		Increases your solipsism threshold by 10%% (first point only), reducing global speed if your Psi falls below the threshold (currently %d%%).
+		Each talent point invested will also increase your max Psi by 10.]]):format(threshold * 100, self.solipsism_threshold * 100)
+	end,
+}
+
+newTalent{
+	name = "Dismissal",
+	type = {"psionic/solipsism", 4},
+	points = 5,
+	require = psi_wil_req4,
+	cooldown = 12,
+	psi = 20,
+	tactical = { DEFEND = 2},
+	getDuration = function(self, t) return 1 + math.ceil(self:getTalentLevel(t)) end,
+	on_learn = function(self, t)
+		self:incMaxPsi(10)
+		if self:getTalentLevelRaw(t) == 1 then
+			self.solipsism_threshold = (self.solipsism_threshold or 0) + 0.1
+		end
+		return true
+	end,
+	on_unlearn = function(self, t)
+		self:incMaxPsi(-10)
+		if not self:knowTalent(t) then
+			self.solipsism_threshold = self.solipsism_threshold - 0.1
+		end
+		return true
+	end,
+	action = function(self, t)
+		self:setEffect(self.EFF_DISMISSAL, t.getDuration(self, t), {})
+		return true
+	end,
+	info = function(self, t)
+		local duration = t.getDuration(self, t)
+		return ([[You dismiss 'reality' as merely a figment of your mind.  For the next %d turns you are immune to all damage and ignore new status effects.  Performing any action other then movement will reaffirm your belief in 'reality' and end the effect.
+		Increases your solipsism threshold by 10%% (first point only), reducing global speed if your Psi falls below the threshold (currently %d%%).
+		Each talent point invested will also increase your max Psi by 10.]]):format(duration, self.solipsism_threshold * 100)
+	end,
+}
diff --git a/game/modules/tome/data/timed_effects/mental.lua b/game/modules/tome/data/timed_effects/mental.lua
index 2fe15cffed..f17a324a8d 100644
--- a/game/modules/tome/data/timed_effects/mental.lua
+++ b/game/modules/tome/data/timed_effects/mental.lua
@@ -2317,3 +2317,46 @@ newEffect{
 		self.mental_negative_status_effect_immune = nil
 	end,
 }
+
+
+newEffect{
+	name = "FEEDBACK", image = "talents/feedback.png",
+	desc = "Feedback",
+	long_desc = function(self, eff) return ("The target is converting feedback into Psi regen at the rate of %0.2f per turn."):format(eff.power) end,
+	type = "mental",
+	subtype = { psionic=true },
+	status = "beneficial",
+	parameters = { power=1 },
+	on_gain = function(self, err) return "#target# is recovering Psi quickly.", "+Feedback" end,
+	on_lose = function(self, err) return "#target#'s Psi recover has returned to normal.", "-Feedback" end,
+	activate = function(self, eff)
+		eff.tid = self:addTemporaryValue("psi_regen", eff.power)
+	end,
+	deactivate = function(self, eff)
+		self:removeTemporaryValue("psi_regen", eff.tid)
+	end,
+}
+
+newEffect{
+	name = "RESONANCE_SHIELD", image = "talents/resonance_shield.png",
+	desc = "Resonance Shield",
+	long_desc = function(self, eff) return ("The target is surrounded by a psychic shield, absorbing 50%% of all damage (up to %d/%d).  Additionally melee attackers will suffer %0.2f mind damage on contact."):format(self.resonance_shield_absorb, eff.power, eff.dam) end,
+	type = "mental",
+	subtype = { psionic=true, shield=true },
+	status = "beneficial",
+	parameters = { power=100, dam = 1 },
+	on_gain = function(self, err) return "A psychic shield forms around #target#.", "+Resonance Shield" end,
+	on_lose = function(self, err) return "The psychic shield around #target# crumbles.", "-Resonance Shield" end,
+	activate = function(self, eff)
+		self.resonance_shield_absorb = eff.power
+		eff.particle = self:addParticles(Particles.new("damage_shield", 1))
+		eff.sid = self:addTemporaryValue("resonance_shield", eff.power)
+		eff.did = self:addTemporaryValue("on_melee_hit", {[DamageType.MIND]=eff.dam})
+	end,
+	deactivate = function(self, eff)
+		self.resonance_shield_absorb = nil
+		self:removeParticles(eff.particle)
+		self:removeTemporaryValue("resonance_shield", eff.sid)
+		self:removeTemporaryValue("on_melee_hit", eff.did)
+	end,
+}
diff --git a/game/modules/tome/data/timed_effects/other.lua b/game/modules/tome/data/timed_effects/other.lua
index d5d5167f5d..78c8404bfd 100644
--- a/game/modules/tome/data/timed_effects/other.lua
+++ b/game/modules/tome/data/timed_effects/other.lua
@@ -1541,3 +1541,59 @@ newEffect{
 		self:removeTemporaryValue("invulnerable", eff.tmpid)
 	end,
 }
+
+newEffect{
+	name = "SOLIPSISM", image = "talents/solipsism.png",
+	desc = "Solipsism",
+	long_desc = function(self, eff) return ("This creature has fallen into a solipsistic state and is caught up in its own thoughts (-%d%% global speed)."):format(eff.power * 100) end,
+	type = "other",
+	subtype = { psionic=true },
+	status = "detrimental",
+	decrease = 0,
+	no_stop_enter_worlmap = true, no_stop_resting = true,
+	parameters = { },
+	activate = function(self, eff)
+		eff.tmpid = self:addTemporaryValue("global_speed_add", -eff.power)
+	end,
+	deactivate = function(self, eff)
+		self:removeTemporaryValue("global_speed_add", eff.tmpid)
+	end,
+}
+
+newEffect{
+	name = "CLARITY", image = "talents/clarity.png",
+	desc = "Clarity",
+	long_desc = function(self, eff) return ("The creature has found a state of clarity (+%d%% global speed)."):format(eff.power * 100) end,
+	type = "other",
+	subtype = { psionic=true },
+	status = "beneficial",
+	decrease = 0,
+	no_stop_enter_worlmap = true, no_stop_resting = true,
+	parameters = { },
+	activate = function(self, eff)
+		eff.tmpid = self:addTemporaryValue("global_speed_add", eff.power)
+	end,
+	deactivate = function(self, eff)
+		self:removeTemporaryValue("global_speed_add", eff.tmpid)
+	end,
+}
+
+newEffect{
+	name = "DISMISSAL", image = "talents/dismissal.png",
+	desc = "Dismissal",
+	long_desc = function(self, eff) return "The target has dismissed reality.  For the duration it is immune to new status effects and all damage." end,
+	type = "other",
+	subtype = { psionic=true },
+	status = "beneficial",
+	parameters = {},
+	on_gain = function(self, err) return "#Target# dismisses reality!", "+Dismissal" end,
+	on_lose = function(self, err) return "#Target# reaffirms it's belief in reality.", "-Dismissal" end,
+	activate = function(self, eff)
+		eff.iid = self:addTemporaryValue("invulnerable", 1)
+		eff.imid = self:addTemporaryValue("status_effect_immune", 1)
+	end,
+	deactivate = function(self, eff)
+		self:removeTemporaryValue("invulnerable", eff.iid)
+		self:removeTemporaryValue("status_effect_immune", eff.imid)
+	end,
+}
\ No newline at end of file
-- 
GitLab