diff --git a/game/engines/default/engine/Game.lua b/game/engines/default/engine/Game.lua
index 13b27922a7934cb783e375a7aa9ca4f145f0ddf8..789f72e08890d0277b891c7769d8ee84a9152dfc 100644
--- a/game/engines/default/engine/Game.lua
+++ b/game/engines/default/engine/Game.lua
@@ -50,9 +50,16 @@ function _M:init(keyhandler)
 end
 
 --- Log a message
-function _M:log() end
---- Log something that was seen
-function _M:logSeen() end
+-- Redefine as needed
+function _M.log(style, ...) end
+
+--- Log something associated with an entity that is seen by the player
+-- Redefine as needed
+function _M.logSeen(e, style, ...) end
+
+--- Log something associated with an entity if it is the player
+-- Redefine as needed
+function _M.logPlayer(e, style, ...) end
 
 --- Default mouse cursor
 function _M:defaultMouseCursor()
@@ -270,9 +277,11 @@ end
 
 --- This is the "main game loop", do something here
 function _M:tick()
-	-- Check out any possible errors
+	-- If any errors have occurred, save them and open the error dialog
 	local errs = core.game.checkError()
 	if errs then
+		if not self.errors or self.errors.turn ~= self.turn then self.errors = {turn=self.turn, first_error = errs} end
+		self.errors.last_error = errs table.insert(self.errors, (#self.errors%10) + 1, errs)
 		if config.settings.cheat then for id = #self.dialogs, 1, -1 do self:unregisterDialog(self.dialogs[id]) end end
 		self:registerDialog(require("engine.dialogs.ShowErrorStack").new(errs))
 	end
diff --git a/game/engines/default/engine/LogDisplay.lua b/game/engines/default/engine/LogDisplay.lua
index 943f2482b3ff259d709b449edb02bbeb90f457e3..5c5bd2dbba1494514d8a8133876fd72f5a45969e 100644
--- a/game/engines/default/engine/LogDisplay.lua
+++ b/game/engines/default/engine/LogDisplay.lua
@@ -86,7 +86,7 @@ function _M:resize(x, y, w, h)
 	self.mouse:registerZone(0, 0, self.w, self.h, function(button, x, y, xrel, yrel, bx, by, event) self:mouseEvent(button, x, y, xrel, yrel, bx, by, event) end)
 end
 
---- Returns the full log
+--- Returns a clone of the full log
 function _M:getLog(extra, timestamp)
 	local log = {}
 	for i = 1, #self.log do
@@ -140,9 +140,9 @@ function _M:call(str, ...)
 	self.changed = true
 end
 
---- Gets the last log line
+--- Gets the newest log line
 function _M:getNewestLine()
-	if self.log[1] then return self.log[1].str end
+	if self.log[1] then return self.log[1].str, self.log[1] end
 	return nil
 end
 
@@ -153,7 +153,35 @@ function _M:empty()
 	self.changed = true
 end
 
---- Get Last Lines From Log
+--- Remove some lines from the log, starting with the newest
+-- @param line = number of lines to remove (default 1) or the last line (table, reference) to leave in the log
+-- @param [type=table, optional] ret table in which to append removed lines
+-- @param [type=number, optional] timestamp of the oldest line to remove
+-- @return the table of removed lines or nil
+function _M:rollback(line, ret, timestamp)
+	local nb = line or 1
+	if type(line) == "table" then
+		nb = 0
+		for i, ln in ipairs(self.log) do
+			if ln == line then nb = i - 1 break end
+		end
+	end
+	if nb > 0 then
+		for i = 1, nb do
+			local removed = self.log[1]
+			if not timestamp or removed.timestamp >= timestamp then
+				print("[LOG][remove]", removed.timestamp, removed.str)
+				table.remove(self.log, 1)
+				if ret then ret[#ret+1] = removed end
+			else break
+			end
+		end
+		self.changed = true
+	end
+	return ret
+end
+
+--- Get the oldest lines from the log
 -- @param number number of lines to retrieve
 function _M:getLines(number)
 	local from = number
diff --git a/game/engines/default/engine/dialogs/ShowErrorStack.lua b/game/engines/default/engine/dialogs/ShowErrorStack.lua
index a8f3b90ddce2649c55dd63cfa3696e3af7d52b83..7a22e45e0e841a75c2f642e86f1215957230889d 100644
--- a/game/engines/default/engine/dialogs/ShowErrorStack.lua
+++ b/game/engines/default/engine/dialogs/ShowErrorStack.lua
@@ -94,6 +94,12 @@ If you are not currently connected to the internet, please report this bug when
 
 	self.key:addBinds{
 		EXIT = function() game:unregisterDialog(self) end,
+		LUA_CONSOLE = function()
+			if config.settings.cheat then
+				local DebugConsole = require "engine.DebugConsole"
+				game:registerDialog(DebugConsole.new())
+			end
+		end,
 	}
 end
 
diff --git a/game/engines/default/engine/interface/ActorLife.lua b/game/engines/default/engine/interface/ActorLife.lua
index c7b6f25d218c5d346be12d0da5a4070e23a25de0..3e7c8b74d9a6f4fea1064c8e57f225ee3829df9e 100644
--- a/game/engines/default/engine/interface/ActorLife.lua
+++ b/game/engines/default/engine/interface/ActorLife.lua
@@ -95,7 +95,7 @@ end
 
 --- Actor is being attacked!
 -- Module authors should rewrite it to handle combat, dialog, ...
--- @param target the actor attacking us
+-- @param target the actor being attacked
 -- @param x placeholder
 -- @param y placeholder
 function _M:attack(target, x, y)
diff --git a/game/engines/default/engine/interface/ActorTalents.lua b/game/engines/default/engine/interface/ActorTalents.lua
index 702fa8bec56905ff31f55e37d91d862f234772ec..9bab416fad8b8217bbdedb8b734e926859243fc9 100644
--- a/game/engines/default/engine/interface/ActorTalents.lua
+++ b/game/engines/default/engine/interface/ActorTalents.lua
@@ -117,7 +117,7 @@ function _M:resolveLevelTalents()
 	end
 end
 
--- Make the actor use the talent
+--- Make the actor use a talent
 -- @param id talent ID
 -- @param who talent user
 -- @param force_level talent level(raw) override
@@ -125,52 +125,80 @@ end
 -- @param force_target the target of the talent (override)
 -- @param silent do not display messages about use
 -- @param no_confirm  Never ask confirmation
+-- @return the return value from the talent code if successful or false:
+--	activated talents: value returned from the action function
+--	sustainable talents: value returned from the activate function (if inactive, should be a table of parameters)
+--		or value returned from the talent deactivate function (if active)
+--  talent code should always return a non false/nil result if successful
 function _M:useTalent(id, who, force_level, ignore_cd, force_target, silent, no_confirm)
 	who = who or self
-	local ab = _M.talents_def[id]
-	assert(ab, "trying to cast talent "..tostring(id).." but it is not defined")
+	local ab, ret = _M.talents_def[id]
+	assert(ab, "trying to use talent "..tostring(id).." but it is not defined")
 
+	self.talent_error = nil
 	local cancel = false
 	local co, success, err
+	
+	local msg, line
+
 	if ab.mode == "activated" and ab.action then
 		if self:isTalentCoolingDown(ab) and not ignore_cd then
 			game.logPlayer(who, "%s is still on cooldown for %d turns.", ab.name:capitalize(), self.talents_cd[ab.id])
-			return
+			return false
 		end
-		co = coroutine.create(function()
+		co = coroutine.create(function() -- coroutine to run activated talent code
 			if cancel then
 				success = false
 				return false
 			end
-			if not self:preUseTalent(ab, silent) then return end
+			
+			if not self:preUseTalent(ab, silent) then return false end
+			
+			msg, line = game.logNewest()
+			if not silent then self:logTalentMessage(ab) end
 			
 			local ok, ret, special = xpcall(function() return ab.action(who, ab) end, debug.traceback)
 			self.__talent_running = nil
+			if not ok then self:onTalentLuaError(ab, ret) error(ret) end
 
-			if not ok then error(ret) end
+			if not ret and self._silent_talent_failure then -- remove messages generated by failed talent
+				local msg, newline = game.logNewest()
+				if newline ~= line then game.logRollback(line) end
+			end
 
-			if not self:postUseTalent(ab, ret, silent) then return end
+			if not self:postUseTalent(ab, ret, silent) then return false end
 
 			-- Everything went ok? then start cooldown if any
 			if not ignore_cd and (not special or not special.ignore_cd) then self:startTalentCooldown(ab) end
+			return ret
 		end)
 	elseif ab.mode == "sustained" and ab.activate and ab.deactivate then
 		if self:isTalentCoolingDown(ab) and not ignore_cd then
 			game.logPlayer(who, "%s is still on cooldown for %d turns.", ab.name:capitalize(), self.talents_cd[ab.id])
-			return
+			return false
 		end
-		co = coroutine.create(function()
+		co = coroutine.create(function() -- coroutine to run sustainable talent code
 			if cancel then
 				success = false
 				return false
 			end
-			if not self:preUseTalent(ab, silent) then return end
-			if not self.sustain_talents[id] then
-				local ret = ab.activate(who, ab)
+			if not self:preUseTalent(ab, silent) then return false end
+			msg, line = game.logNewest()
+			if not silent then self:logTalentMessage(ab) end
+			
+			local ok, ret, special
+			if not self.sustain_talents[id] then -- activating
+				ok, ret, special = xpcall(function() return ab.activate(who, ab) end, debug.traceback)
+				if not ok then self:onTalentLuaError(ab, ret) error(ret) end
 				if ret == true then ret = {} end -- fix for badly coded talents
 				if ret then ret.name = ret.name or ab.name end
 
-				if not self:postUseTalent(ab, ret) then return end
+				if not ret and self._silent_talent_failure then -- remove messages generated by failed talent
+					local msg, newline = game.logNewest()
+					if newline ~= line then game.logRollback(line) end
+				end
+
+				if not self:postUseTalent(ab, ret, silent) then return false end
 
 				self.sustain_talents[id] = ret
 
@@ -186,7 +214,7 @@ function _M:useTalent(id, who, force_level, ignore_cd, force_target, silent, no_
 						table.insert(list, id)
 					end
 				end
-			else
+			else -- deactivating
 				if self.deactivating_sustain_talent == ab.id then return end
 
 				local p = self.sustain_talents[id]
@@ -202,10 +230,16 @@ function _M:useTalent(id, who, force_level, ignore_cd, force_target, silent, no_
 					end
 				end
 				p.__tmpparticles = nil
-				local ret = ab.deactivate(who, ab, p)
+				ok, ret, special = xpcall(function() return ab.deactivate(who, ab, p) end, debug.traceback)
+				if not ok then self:onTalentLuaError(ab, ret) error(ret) end
+				
+				if not ret and self._silent_talent_failure then -- remove messages generated by failed talent
+					local msg, newline = game.logNewest()
+					if newline ~= line then game.logRollback(line) end
+				end
 
 				self.deactivating_sustain_talent = ab.id
-				if not self:postUseTalent(ab, ret, silent) then self.deactivating_sustain_talent = nil return end
+				if not self:postUseTalent(ab, ret, silent) then self.deactivating_sustain_talent = nil return false end
 				self.deactivating_sustain_talent = nil
 
 				-- Everything went ok? then start cooldown if any
@@ -225,11 +259,12 @@ function _M:useTalent(id, who, force_level, ignore_cd, force_target, silent, no_
 					end
 				end
 			end
+			return ret
 		end)
 	else
-		print("Activating non activable or sustainable talent: "..id.." :: "..ab.name.." :: "..ab.mode)
+		print("[useTalent] Attempt to use non activated or sustainable talent: "..id.." :: "..ab.name.." :: "..ab.mode)
 	end
-	if co then
+	if co then -- talent is usable and has passed checks
 		-- Stub some stuff
 		local old_level, old_target, new_target = nil, nil, nil
 		if force_level then old_level = who.talents[id] end
@@ -237,23 +272,27 @@ function _M:useTalent(id, who, force_level, ignore_cd, force_target, silent, no_
 			if ab.onAIGetTarget and not who.player then old_target = rawget(who, "getTarget"); new_target = function() return ab.onAIGetTarget(self, ab) end end
 			if force_target and not old_target then old_target = rawget(who, "getTarget"); new_target = function(a) return force_target.x, force_target.y, not force_target.__no_self and force_target end end
 		end
-		local co_wrapper = coroutine.create(function()
+		
+		local co_wrapper = coroutine.create(function() -- coroutine for talent interface
 			success = true
 			local ok
 			while success do
 				if new_target then who.getTarget = new_target end
 				if force_level then who.talents[id] = force_level end
 				self.__talent_running = ab
-				ok, err = coroutine.resume(co)
+				ok, ret = coroutine.resume(co) -- ret == error or return value from co
 				success = success and ok
 				if new_target then who.getTarget = old_target end
 				if force_level then who.talents[id] = old_level end
 				self.__talent_running = nil
-				if ok and coroutine.status(co) == "dead" then
-					-- terminated
-					return
+				if ok and coroutine.status(co) == "dead" then -- coroutine terminated normally
+					if success and not ret then -- talent failed
+						print("[useTalent] TALENT FAILED:", ab.id, "for", self.name, self.uid, success)
+					--game.log("#ORANGE# %s TALENT USE FAILED [%s (silent_failure:%s at (%s, %s)]", ab.id, self.name, self._silent_talent_failure, self.x, self.y) -- debugging
+					end
+					return ret
 				end
-				if err then error(err) end  --propagate
+				if ret then error(ret) end --propagate error
 				coroutine.yield()
 			end
 		end)
@@ -264,27 +303,35 @@ function _M:useTalent(id, who, force_level, ignore_cd, force_target, silent, no_
 				if quit ~= false then
 					cancel = true
 				end
-				success, err = coroutine.resume(co_wrapper)
+				success, ret = coroutine.resume(co_wrapper)
+				if not success and ret then -- talent code error
+					self:onTalentLuaError(ab, ret)
+					--print("useTalent:", debug.traceback(co_wrapper), '\n')
+					error(ret)
+				end
 			end,
 			"Cancel","Continue")
 		else
-			-- cancel checked in coroutine
-			success, err = coroutine.resume(co_wrapper)
+			success, ret = coroutine.resume(co_wrapper) -- cancel checked in coroutine
 		end
 		-- Cleanup in case we coroutine'd out
 		self.__talent_running = nil
-		if not success and err then
-			print(debug.traceback(co_wrapper))
-			self:onTalentLuaError(err)
-			error(err)
+		if not success and ret then -- talent code error
+			self:onTalentLuaError(ab, ret)
+			--print("useTalent:", debug.traceback(co_wrapper), '\n')
+			error(ret)
 		end
 	end
 	self.changed = true
-	return true
-
+	
+	return ret -- return value from successfully used talent
 end
 
---- Replace some markers in a string with info on the talent
+--- Set true to remove game log messages generated by talents that started but did not complete
+--	affects messages logged after preUseTalent check when action/activate/deactivate function returns nil or false
+_M._silent_talent_failure = false
+
+--- Get the talent use message, replacing some markers in its message string with info on the talent
 function _M:useTalentMessage(ab)
 	if not ab.message then return nil end
 	local str = util.getval(ab.message, self, ab)
@@ -299,32 +346,56 @@ function _M:useTalentMessage(ab)
 	return str
 end
 
---- Called before a talent is used  
+--- Display the talent use message in the game log
 -- Redefine as needed
+-- called in useTalent after successful preUseTalent check
 -- @param[type=table] talent the talent (not the id, the table)
+-- uses ab.message if defined or generates default use text (ab.message == false suppresses)
+function _M:logTalentMessage(ab)
+	if ab.message == false then return
+	elseif ab.message then
+		game.logSeen(self, "%s", self:useTalentMessage(ab))
+	elseif ab.mode == "sustained" then
+		game.logSeen(self, "%s %s %s.", self.name:capitalize(), self:isTalentActive(ab.id) and "deactivates" or "activates", ab.name)
+	else
+		game.logSeen(self, "%s uses %s.", self.name:capitalize(), ab.name)
+	end
+end
+
+--- Called BEFORE a talent is used -- CAN it be used?
+-- Redefine as needed
+-- @param[type=table] ab the talent (not the id, the table)
 -- @param[type=boolean] silent no messages will be outputted
 -- @param[type=boolean] fake no actions are taken, only checks
 -- @return[1] true to continue
 -- @return[2] false to stop
-function _M:preUseTalent(talent, silent, fake)
+function _M:preUseTalent(ab, silent, fake)
 	return true
 end
 
---- Called before a talent is used  
+--- Called AFTER a talent is used -- WAS it successfully used?
 -- Redefine as needed
--- @param[type=table] talent the talent (not the id, the table)
--- @param ret the return of the talent action
+-- @param[type=table] ab the talent (not the id, the table)
+-- @param ret the return of the talent action, activate, or deactivate function
 -- @param[type=boolean] silent no messages will be outputted
 -- @return[1] true to continue
 -- @return[2] false to stop
-function _M:postUseTalent(talent, ret, silent)
+function _M:postUseTalent(ab, ret, silent)
 	return true
 end
 
---- Called if a talent errors out
--- @param ab the talent
--- @param err the error
+--- Called if a talent errors out when used
+-- Redefine as needed
+-- @param ab the talent table
+-- @param err the table of errors returned from xpcall
+-- sets self.talent_error and logs errors to _M._talent_errors
+-- 		data forma: {[ab.id]=ab, Actor=self, uid=, x=, y=, err=err, turn=game.turn}
 function _M:onTalentLuaError(ab, err)
+	if self.talent_error then return end -- handle only the first error
+	self.talent_error = {[ab.id]=ab, Actor=self, uid=self.uid, name=self.name, err=err, x=self.x, y=self.y, turn=game.turn}
+	print("##Use Talent Lua Error##", ab and ab.id, "Actor:", self.uid, self.name)
+	_M._talent_errors = _M._talent_errors or {} -- log the error globally
+	table.insert(_M._talent_errors, self.talent_error)
 	return
 end
 
@@ -552,7 +623,7 @@ function _M:updateTalentPassives(tid)
 	t.passives(self, t, self.talents_learn_vals[t.id])
 end
 
---- Checks the talent if learnable
+--- Checks if the talent can be learned
 -- @param t the talent to check
 -- @param offset the level offset to check, defaults to 1
 -- @param ignore_special ignore requirement of special
@@ -691,7 +762,7 @@ function _M:getTalentLevelRaw(id)
 end
 
 --- Talent level, 0 if not known
--- Includes mastery
+-- Includes mastery (defaults to 1)
 function _M:getTalentLevel(id)
 	local t
 
@@ -700,7 +771,7 @@ function _M:getTalentLevel(id)
 	else
 		t = _M.talents_def[id]
 	end
-	return (self:getTalentLevelRaw(id)) * ((self.talents_types_mastery[t.type[1]] or 0) + 1)
+	return t and (self:getTalentLevelRaw(id)) * ((self.talents_types_mastery[t.type[1]] or 0) + 1) or 0
 end
 
 --- Talent type level, sum of all raw levels of talents inside
@@ -823,21 +894,21 @@ function _M:isTalentCoolingDown(t)
 	if self.talents_cd[t.id] and self.talents_cd[t.id] > 0 then return self.talents_cd[t.id] else return false end
 end
 
---- Returns the range of a talent
+--- Returns the range of a talent (defaults to 1)
 function _M:getTalentRange(t)
 	if not t.range then return 1 end
 	if type(t.range) == "function" then return t.range(self, t) end
 	return t.range
 end
 
---- Returns the radius of a talent
+--- Returns the radius of a talent (defaults to 0)
 function _M:getTalentRadius(t)
 	if not t.radius then return 0 end
 	if type(t.radius) == "function" then return t.radius(self, t) end
 	return t.radius
 end
 
---- Returns the target type of a talent
+--- Returns the target table for a talent
 function _M:getTalentTarget(t)
 	if type(t.target) == "function" then return t.target(self, t) end
 	return t.target
@@ -992,7 +1063,7 @@ function _M:talentDialog(d)
 		local ok, err = coroutine.resume(co, dialog_returns[d])
 		if not ok and err then
 			print(debug.traceback(co))
-			self:onTalentLuaError(err)
+			self:onTalentLuaError(nil, err)
 			error(err)
 		end
 	end
diff --git a/game/engines/default/modules/boot/class/Game.lua b/game/engines/default/modules/boot/class/Game.lua
index 5baf53803acd39247e360066988dd3243dd66070..9bee703bb3e7df04971be40282cb18f1e32fa197 100644
--- a/game/engines/default/modules/boot/class/Game.lua
+++ b/game/engines/default/modules/boot/class/Game.lua
@@ -124,6 +124,8 @@ function _M:run()
 	self.log = function(style, ...) end
 	self.logSeen = function(e, style, ...) end
 	self.logPlayer = function(e, style, ...) end
+	self.logRollback = function(line, ...) return self.logdisplay:rollback(line, ...) end
+	self.logNewest = function() return self.logdisplay:getNewestLine() end
 	self.nicer_tiles = NicerTiles.new()
 
 	-- Starting from here we create a new game
diff --git a/game/modules/tome/ai/tactical.lua b/game/modules/tome/ai/tactical.lua
index 716e9be2a739de77e99dfc490f1165f3f6a2fac7..b6cf8ad7257696750c498160e59b22d66eee8057 100644
--- a/game/modules/tome/ai/tactical.lua
+++ b/game/modules/tome/ai/tactical.lua
@@ -469,6 +469,7 @@ newAI("tactical", function(self)
 			self.energy.used = true
 			self.ai_state.last_tid = used_talent
 		end
+		return true
 	end
 	return false
 end)
diff --git a/game/modules/tome/class/Actor.lua b/game/modules/tome/class/Actor.lua
index 1c497be76287cff7d13936313e3c715142a85022..2668018e5630ef54c6cd412ef11416d1bfb9b8f4 100644
--- a/game/modules/tome/class/Actor.lua
+++ b/game/modules/tome/class/Actor.lua
@@ -3434,8 +3434,11 @@ function _M:onTemporaryValueChange(prop, v, base)
 	end
 end
 
+--- Actor attacks target
+-- @param target the actor being attacked
+-- @param x, y coordinates of target grid
 function _M:attack(target, x, y)
-	self:bumpInto(target, x, y)
+	return self:bumpInto(target, x, y)
 end
 
 function _M:getMaxEncumbrance()
@@ -4891,24 +4894,24 @@ function _M:preUseTalent(ab, silent, fake)
 	-- Special checks -- AI
 	if not self.player and ab.on_pre_use_ai and not (ab.mode == "sustained" and self:isTalentActive(ab.id)) and not ab.on_pre_use_ai(self, ab, silent, fake) then return false end
 
-	if not silent then
-		-- Allow for silent talents
-		if ab.message ~= nil then
-			if ab.message then
-				game.logSeen(self, "%s", self:useTalentMessage(ab))
-			end
-		elseif ab.mode == "sustained" and not self:isTalentActive(ab.id) then
-			game.logSeen(self, "%s activates %s.", self.name:capitalize(), ab.name)
-		elseif ab.mode == "sustained" and self:isTalentActive(ab.id) then
-			game.logSeen(self, "%s deactivates %s.", self.name:capitalize(), ab.name)
+	return true
+end
+
+--- Display the talent use message in the game log
+-- called when the talent is used after successful preUseTalent check
+-- @param ab the talent (not the id, the table)
+function _M:logTalentMessage(ab)
+	if ab.message ~= false and not util.getval(ab.no_message, self, ab) then
+		if ab.message then
+			game.logSeen(self, "%s", self:useTalentMessage(ab))
+		elseif ab.mode == "sustained" then
+			game.logSeen(self, "%s %s %s.", self.name:capitalize(), self:isTalentActive(ab.id) and "deactivates" or "activates", ab.name)
 		elseif ab.is_spell then
 			game.logSeen(self, "%s casts %s.", self.name:capitalize(), ab.name)
-		elseif not ab.no_message then
+		else
 			game.logSeen(self, "%s uses %s.", self.name:capitalize(), ab.name)
 		end
 	end
-
-	return true
 end
 
 local sustainCallbackCheck = {
diff --git a/game/modules/tome/class/NPC.lua b/game/modules/tome/class/NPC.lua
index 9cce3305f9d2aa6904ccaf8cde3a85ad942bf7fb..e02fb5cfd59039dd57b08dc7cf1bece0df1e4696 100644
--- a/game/modules/tome/class/NPC.lua
+++ b/game/modules/tome/class/NPC.lua
@@ -34,6 +34,8 @@ function _M:init(t, no_default)
 	if not self.image and self.name ~= "unknown actor" then self.image = "npc/"..tostring(self.type or "unknown").."_"..tostring(self.subtype or "unknown"):lower():gsub("[^a-z0-9]", "_").."_"..(self.name or "unknown"):lower():gsub("[^a-z0-9]", "_")..".png" end
 end
 
+_M._silent_talent_failure = true
+
 function _M:actBase()
 	-- Reduce shoving pressure every turn
 	if self.shove_pressure then
@@ -109,6 +111,7 @@ local function spotHostiles(self)
 end
 
 function _M:onTalentLuaError(ab, err)
+	engine.interface.ActorTalents.onTalentLuaError(self, ab, err)
 	self:useEnergy()  -- prevent infinitely long erroring out turns
 end
 
diff --git a/game/modules/tome/class/interface/ActorObjectUse.lua b/game/modules/tome/class/interface/ActorObjectUse.lua
index 1ae770a2c94fc881a285968d8320082808edc164..4bd05d95c6c93cd02de5dea832f664b8134e265f 100644
--- a/game/modules/tome/class/interface/ActorObjectUse.lua
+++ b/game/modules/tome/class/interface/ActorObjectUse.lua
@@ -225,6 +225,7 @@ _M.useObjectBaseTalent ={
 			end
 		end)
 		coroutine.resume(co)
+		return ret and ret.used
 	end,
 	info = function(self, t)
 		local o = t.getObject(self, t)
diff --git a/game/modules/tome/class/interface/Combat.lua b/game/modules/tome/class/interface/Combat.lua
index bba2c0102af79c96830c203b5cafcec1e63aec1b..654b8ac2396947074a39a3e13bfc384552e6a9d9 100644
--- a/game/modules/tome/class/interface/Combat.lua
+++ b/game/modules/tome/class/interface/Combat.lua
@@ -31,10 +31,10 @@ module(..., package.seeall, class.make)
 -- Talk ? attack ? displace ?
 function _M:bumpInto(target, x, y)
 	local reaction = self:reactionToward(target)
-	if reaction < 0 then
+	if reaction < 0 then -- attack target if possible
 		if target.encounterAttack and self.player then self:onWorldEncounter(target, x, y) return end
 		if game.player == self and ((not config.settings.tome.actor_based_movement_mode and game.bump_attack_disabled) or (config.settings.tome.actor_based_movement_mode and self.bump_attack_disabled)) then return end
-		return self:useTalent(self.T_ATTACK, nil, nil, nil, target)
+		return self:enoughEnergy(game.energy_to_act*self:combatSpeed()) and self:useTalent(self.T_ATTACK, nil, nil, nil, target)
 	elseif reaction >= 0 then
 		-- Talk ? Bump ?
 		if self.player and target.on_bump then
diff --git a/game/modules/tome/class/uiset/Classic.lua b/game/modules/tome/class/uiset/Classic.lua
index a2ecf284d458b009a6a13a2d4b17a3639a85afa9..ab75975570e040814ff2df24eff24b3374ba06ff 100644
--- a/game/modules/tome/class/uiset/Classic.lua
+++ b/game/modules/tome/class/uiset/Classic.lua
@@ -92,6 +92,8 @@ function _M:activate()
 		game.uiset.logdisplay(...) else game.uiset.logdisplay(style, ...) end
 		if game.uiset.show_userchat then game.uiset.logdisplay.changed = old end
 	end
+	game.logRollback = function(line, ...) return self.logdisplay:rollback(line, ...) end
+	game.logNewest = function() return self.logdisplay:getNewestLine() end
 --	game.logSeen = function(e, style, ...) if e and e.player or (not e.dead and e.x and e.y and game.level and game.level.map.seens(e.x, e.y) and game.player:canSee(e)) then game.log(style, ...) end end
 	game.logPlayer = function(e, style, ...) if e == game.player or e == game.party then game.log(style, ...) end end
 end
diff --git a/game/modules/tome/class/uiset/Minimalist.lua b/game/modules/tome/class/uiset/Minimalist.lua
index 669f66cfd6d90f0bace4b9897bfb36cf0fb9bd9f..4bc3702d08a03570b44490cca39fcd32e62cb4a5 100644
--- a/game/modules/tome/class/uiset/Minimalist.lua
+++ b/game/modules/tome/class/uiset/Minimalist.lua
@@ -490,6 +490,8 @@ function _M:activate()
 		game.uiset.logdisplay(...) else game.uiset.logdisplay(style, ...) end
 		if game.uiset.show_userchat then game.uiset.logdisplay.changed = old end
 	end
+	game.logRollback = function(line, ...) return self.logdisplay:rollback(line, ...) end
+	game.logNewest = function() return self.logdisplay:getNewestLine() end
 --	game.logSeen = function(e, style, ...) if e and e.player or (not e.dead and e.x and e.y and game.level and game.level.map.seens(e.x, e.y) and game.player:canSee(e)) then game.log(style, ...) end end
 	game.logPlayer = function(e, style, ...) if e == game.player or e == game.party then game.log(style, ...) end end
 
diff --git a/game/modules/tome/data/chats/artifice-mastery.lua b/game/modules/tome/data/chats/artifice-mastery.lua
index f77068676cc95cdc7176c748bbf0f9727abfb7fe..d7acb1f57b681b681da2f98c4da24029b52ea948 100644
--- a/game/modules/tome/data/chats/artifice-mastery.lua
+++ b/game/modules/tome/data/chats/artifice-mastery.lua
@@ -18,40 +18,42 @@
 -- darkgod@te4.org
 
 local Talents = require("engine.interface.ActorTalents")
-
+chat_talent = player:getTalentFromId(chat_tid)
+chat_level = player:getTalentLevelRaw(chat_tid)
 local function generate_tools()
-	local answers = {}
-	local tools = 
-	{	
-	}
-	
-	if player:knowTalent(player.T_HIDDEN_BLADES) then tools[Talents.T_ASSASSINATE] = 1 end 
-	if player:knowTalent(player.T_SMOKESCREEN) then tools[Talents.T_SMOKESCREEN_MASTERY] = 1 end 
-	if player:knowTalent(player.T_ROGUE_S_BREW) then tools[Talents.T_ROGUE_S_BREW_MASTERY] = 1 end 
-	if player:knowTalent(player.T_DART_LAUNCHER) then tools[Talents.T_DART_LAUNCHER_MASTERY] = 1 end 
+	local answers = {{"Cancel"}}
+	for tid, m_tid in pairs(tool_ids) do
+		local t = player:getTalentFromId(tid)
+		local m_t = player:getTalentFromId(m_tid)
+		if m_t then
+			local master_talent = function(npc, player)
+				local old_mastery_level = player:getTalentLevelRaw(m_tid)
+				if old_mastery_level == chat_level then return end
+				-- unlearn mastery talent(s)
+				for tid, m_tid in pairs(tool_ids) do
+					if player:knowTalent(m_tid) then player:unlearnTalentFull(m_tid) end
+				end
+				
+				player:learnTalent(m_tid, true, chat_level, {no_unlearn=true})
+				player.artifice_tools_mastery = tid
+				
+				-- start talent cooldowns
+				if old_mastery_level == 0 then
+					player:startTalentCooldown(tid) player:startTalentCooldown(m_tid)
+					player:startTalentCooldown(chat_tid)
+					game.log("#LIGHT_BLUE# You enhance your preparation of %s.", t.name)
+				end
 
-	
-	if tools then
-		for tid, level in pairs(tools) do
-			local t = npc:getTalentFromId(tid)
-			level = math.min(t.points - game.player:getTalentLevelRaw(tid), level)
-			
-			local doit = function(npc, player)
-				if game.player:knowTalentType(t.type[1]) == nil then player:setTalentTypeMastery(t.type[1], 1.0) end
-				player:learnTalent(tid, true, level, {no_unlearn=true})
-				player:startTalentCooldown(tid)
 			end
-			answers[#answers+1] = {("[%s]"):format(t.name),
-				action=doit,
+			answers[#answers+1] = {("%s[%s -- mastery: %s]#LAST#"):format(player.artifice_tools_mastery == tid and "#YELLOW#" or "", t.name, m_t.name),
+				action=master_talent,
 				on_select=function(npc, player)
 					local mastery = nil
-					if player:knowTalentType(t.type[1]) == nil then mastery = 1.0 end
 					game.tooltip_x, game.tooltip_y = 1, 1
-					game:tooltipDisplayAtMap(game.w, game.h, "#GOLD#"..t.name.."#LAST#\n"..tostring(player:getTalentFullDescription(t, 1, nil, mastery)))
+					game:tooltipDisplayAtMap(game.w, game.h, "#GOLD#"..m_t.name.."#LAST#\n"..tostring(player:getTalentFullDescription(m_t, nil, {force_level=chat_level}, mastery)))
 				end,
 			}
 		end
-		answers[#answers+1] = {"Cancel"}
 	end
 	return answers
 end
diff --git a/game/modules/tome/data/chats/artifice.lua b/game/modules/tome/data/chats/artifice.lua
index 5587262325070b290a359f5ea7f6a61896f476cc..0ab43a18dec43ab302b6c4d7af8cd822cd921f08 100644
--- a/game/modules/tome/data/chats/artifice.lua
+++ b/game/modules/tome/data/chats/artifice.lua
@@ -18,63 +18,80 @@
 -- darkgod@te4.org
 
 local Talents = require("engine.interface.ActorTalents")
+chat_talent = player:getTalentFromId(chat_tid)
+chat_level = player:getTalentLevelRaw(chat_tid)
 
 local function generate_tools()
-	local answers = {}
-	local tools = 
-	{	
-	}
-
-	--populate the tool list, also apply a temp value so talents display correctly
-	if not player.artifice_hidden_blades then 
-		tools[Talents.T_HIDDEN_BLADES] = 1
-		player.artifice_hidden_blades = slot
-	end
-	if not player.artifice_smokescreen then 
-		tools[Talents.T_SMOKESCREEN] = 1
-		player.artifice_smokescreen = slot
-	end
-	if not player.artifice_rogue_s_brew then 
-		tools[Talents.T_ROGUE_S_BREW] = 1
-		player.artifice_rogue_s_brew = slot
-	end
-	if not player.artifice_dart_launcher then 
-		tools[Talents.T_DART_LAUNCHER] = 1
-		player.artifice_dart_launcher = slot
-	end
+	local answers = {{"[Cancel]"}}
+	local tool_ids = tool_ids or player.main_env.artifice_tool_tids
+	player.artifice_tools = player.artifice_tools or {}
 	
-	if tools then
-		for tid, level in pairs(tools) do
-			local t = npc:getTalentFromId(tid)
-			level = math.min(t.points - game.player:getTalentLevelRaw(tid), level)
-			
-			local doit = function(npc, player)
-				if game.player:knowTalentType(t.type[1]) == nil then player:setTalentTypeMastery(t.type[1], 1.0) end
-				player:learnTalent(tid, true, level, {no_unlearn=true})
-				--remove the temp values set earlier
-				if not (t.name=="Hidden Blades" or player:knowTalent(player.T_HIDDEN_BLADES)) then player.artifice_hidden_blades = null end
-				if not (t.name=="Smokescreen" or player:knowTalent(player.T_SMOKESCREEN)) then player.artifice_smokescreen = null end
-				if not (t.name=="Rogue's Brew" or player:knowTalent(player.T_ROGUE_S_BREW)) then player.artifice_rogue_s_brew = null end
-				if not (t.name=="Dart Launcher" or player:knowTalent(player.T_DART_LAUNCHER)) then player.artifice_dart_launcher = null end
+	for tid, m_tid in pairs(tool_ids) do
+		local t = player:getTalentFromId(tid)
+		if t then
+
+			local tool_level = player:getTalentLevelRaw(t)
+			local equip_tool = function(npc, player) -- equip a tool
+				if tool_level == chat_level then return end -- already selected and up to date
+				-- unlearn the previous talent
+				player:unlearnTalentFull(player.artifice_tools[chat_tid])
+				-- (re)learn the talent
+				player:unlearnTalentFull(tid)
+				player:learnTalent(tid, true, chat_level, {no_unlearn=true})
+				-- clear other tool slots
+				for slot, tool_id in pairs(player.artifice_tools) do
+					if tool_id == tid then player.artifice_tools[slot] = nil end
+				end
+				player.artifice_tools[chat_tid] = tid
+
+				-- start talent cooldowns and use energy
+				player.turn_procs._did_artifice = true -- controls energy use
 				player:startTalentCooldown(tid)
+				if player:getTalentFromId(m_tid) then player:startTalentCooldown(m_tid) end
 			end
-			answers[#answers+1] = {("[Equip %s]"):format(t.name),
-				action=doit,
+			local txt, slot
+			-- check for an existing slot
+			for slot_id, tool_id in pairs(player.artifice_tools) do
+				if tool_id == tid then slot = slot_id break end
+			end
+			if slot then
+				txt = ("[%sEquip %s%s#LAST#]"):format(slot==chat_tid and "#YELLOW#" or "", t.name, slot and (" (%s)"):format(player:getTalentFromId(slot).name) or "")
+			else
+				txt = ("[Equip %s]"):format(t.name)
+			end
+
+			answers[#answers+1] = {txt,
+				action=equip_tool,
 				on_select=function(npc, player)
-					local mastery = nil
-					if player:knowTalentType(t.type[1]) == nil then mastery = 1.0 end
+					local display_level
+					display_level = chat_level - tool_level
 					game.tooltip_x, game.tooltip_y = 1, 1
-					game:tooltipDisplayAtMap(game.w, game.h, "#GOLD#"..t.name.."#LAST#\n"..tostring(player:getTalentFullDescription(t, 1, nil, mastery)))
+
+					-- set up tooltip
+					local text = tstring{}
+					if display_level ~= 0 and player:knowTalent(t) then
+						local diff = function(i2, i1, res)
+							if i2 > i1 then
+								res:add({"color", "LIGHT_GREEN"}, i1, {"color", "LAST"}, " [->", {"color", "YELLOW_GREEN"}, i2, {"color", "LAST"}, "]")
+							elseif i2 < i1 then
+								res:add({"color", "LIGHT_GREEN"}, i1, {"color", "LAST"}, " [->", {"color", "LIGHT_RED"}, i2, {"color", "LAST"}, "]")
+							end
+						end
+						text:merge(player:getTalentFullDescription(t, display_level, nil):diffWith(player:getTalentFullDescription(t, 0, nil), diff))
+					else
+						text = player:getTalentFullDescription(t, nil, {force_level=chat_level})
+					end
+					game:tooltipDisplayAtMap(game.w, game.h, "#GOLD#"..t.name.."#LAST#\n"..tostring(text))
 				end,
 			}
 		end
-
 	end
+
 	return answers
 end
 
 newChat{ id="welcome",
-	text = [[Equip which tools?]],
+	text = ([[Equip which tool for #YELLOW#%s#LAST#?]]):format(chat_talent.name),
 	answers = generate_tools(),
 }
 
diff --git a/game/modules/tome/data/damage_types.lua b/game/modules/tome/data/damage_types.lua
index b8cd0bd78536c5ef0716bbb4aa8454e6c83536ef..795227a7adb551582c54318a9b233a78deffda27 100644
--- a/game/modules/tome/data/damage_types.lua
+++ b/game/modules/tome/data/damage_types.lua
@@ -239,7 +239,7 @@ setDefaultProjector(function(src, x, y, type, dam, state)
 			end
 		end
 
-		if src and dam > 0 and src.knowTalent and src:knowTalent(src.T_BACKSTAB) and src.__CLASSNAME ~= "mod.class.Grid" then
+		if dam > 0 and src and src.__is_actor and src:knowTalent(src.T_BACKSTAB) and src.__CLASSNAME ~= "mod.class.Grid" then
 			local power = src:callTalent("T_BACKSTAB", "getDamageBoost")
 			local nb = 0
 			for eff_id, p in pairs(target.tmp) do
@@ -3878,11 +3878,13 @@ newDamageType{
 
 newDamageType{
 	name = "terror", type = "TERROR",
+	text_color = "#YELLOW#",
 	projector = function(src, x, y, type, dam, state)
 		state = initState(state)
 		useImplicitCrit(src, state)
 		local target = game.level.map(x, y, Map.ACTOR)
 		if target then
+			game:delayedLogDamage(src, target, 0, ("%s<terror chance>#LAST#"):format(DamageType:get(type).text_color or "#aaaaaa#"), false)
 			if not src:checkHit(src:combatAttack(), target:combatMentalResist()) then return end
 			local effect = rng.range(1, 3)
 			if effect == 1 then
@@ -3908,6 +3910,7 @@ newDamageType{
 	end,
 }
 
+-- Random poison: 25% to be enhanced
 newDamageType{
 	name = "random poison", type = "RANDOM_POISON", text_color = "#LIGHT_GREEN#",
 	projector = function(src, x, y, t, dam, poison, state)
@@ -3915,10 +3918,10 @@ newDamageType{
 		useImplicitCrit(src, state)
 		local power
 		local target = game.level.map(x, y, Map.ACTOR)
-		if target and src:reactionToward(target) < 0 then
+		if target and src:reactionToward(target) < 0 and target:canBe("poison") then
 			local realdam = DamageType:get(DamageType.NATURE).projector(src, x, y, DamageType.NATURE, dam.dam / 6, state)
-			chance = rng.range(1, 3)
-			if target and target:canBe("poison") and rng.percent(25) then
+			if rng.percent(dam.random_chance or 25) then
+				local chance = rng.range(1, 3)
 				if chance == 1 then
 					target:setEffect(target.EFF_INSIDIOUS_POISON, 5, {src=src, power=dam.dam / 6, heal_factor=dam.power*2, apply_power=dam.apply_power or (src.combatAttack and src:combatAttack()) or 0})
 				elseif chance == 2 then
@@ -3926,7 +3929,7 @@ newDamageType{
 				elseif chance == 3 then
 					target:setEffect(target.EFF_CRIPPLING_POISON, 5, {src=src, power=dam.dam / 6, fail=dam.power, apply_power=dam.apply_power or (src.combatAttack and src:combatAttack()) or 0})
 				end
-			elseif target and target:canBe("poison") then
+			else
 				target:setEffect(target.EFF_POISONED, 5, {src=src, power=dam.dam / 6, apply_power=dam.apply_power or (src.combatAttack and src:combatAttack()) or 0})
 			end
 			return realdam
@@ -3936,11 +3939,13 @@ newDamageType{
 
 newDamageType{
 	name = "blinding powder", type = "BLINDING_POWDER",
+	text_color = "#GREY#",
 	projector = function(src, x, y, type, dam, state)
 		state = initState(state)
 		useImplicitCrit(src, state)
 		local target = game.level.map(x, y, Map.ACTOR)
 		if target then
+			game:delayedLogDamage(src, target, 0, ("%s<blinding powder>#LAST#"):format(DamageType:get(type).text_color or "#aaaaaa#"), false)
 			if not src:checkHit(src:combatAttack(), target:combatPhysicalResist()) then return end
 			
 			if target:canBe("blind") then
@@ -3955,12 +3960,13 @@ newDamageType{
 
 newDamageType{
 	name = "smokescreen", type = "SMOKESCREEN",
+	text_color = "#GREY#",
 	projector = function(src, x, y, type, dam, state)
 		state = initState(state)
 		useImplicitCrit(src, state)
 		local target = game.level.map(x, y, Map.ACTOR)
 		if target and src:reactionToward(target) < 0 then
-		
+			game:delayedLogDamage(src, target, 0, ("%s<smoke>#LAST#"):format(DamageType:get(type).text_color or "#aaaaaa#"), false)
 			if target:canBe("blind") then
 				target:setEffect(target.EFF_DIM_VISION, 2, {sight=dam.dam, apply_power=src:combatAttack(), no_ct_effect=true})
 			else
diff --git a/game/modules/tome/data/general/objects/world-artifacts.lua b/game/modules/tome/data/general/objects/world-artifacts.lua
index a75efb047cf17ac81c25ea970d2e8322d27235dc..1a0d13ba109ad73bb865df1cac56174fd18407a3 100644
--- a/game/modules/tome/data/general/objects/world-artifacts.lua
+++ b/game/modules/tome/data/general/objects/world-artifacts.lua
@@ -461,7 +461,7 @@ newEntity{ base = "BASE_SHIELD",
 		fatigue = 20,
 		learn_talent = { [Talents.T_BLOCK] = 5, },
 	},
-	on_block = {desc = "30% chance that you'll breath stunning fire on foes in a 6 radius cone.", fct = function(self, who, target, type, dam, eff)
+	on_block = {desc = "30% chance that you'll breath stunning fire in a cone at the attacker (if within range 6).", fct = function(self, who, target, type, dam, eff)
 	if rng.percent(30) then
 		if not target or not target.x or not target.y or core.fov.distance(who.x, who.y, target.x, target.y) > 6 then return end
 
@@ -531,18 +531,22 @@ newEntity{ base = "BASE_SHIELD",
 		resists = { [DamageType.BLIGHT] = 25, [DamageType.DARKNESS] = 25, },
 		inc_stats = { [Stats.STAT_WIL] = 5, },
 	},
-	on_block = {desc = "Pull up to 1 attacker per turn, up to 15 spaces away into melee range, pinning and asphixiating them", fct = function(self, who, src, type, dam, eff)
+	on_block = {desc = "Up to once per turn, pull an attacker up to 15 spaces away into melee range, pinning and asphyxiating it", fct = function(self, who, src, type, dam, eff)
 		if not src then return end
 		if who.turn_procs.black_mesh then return end
  
-		src:pull(who.x, who.y, 15)
-		game.logSeen(src, "Black tendrils shoot out of the mesh and pull %s to you!", src.name:capitalize())
+		who:logCombat(src, "#ORCHID#Black tendrils from #Source# grab #Target#!")
+		local kb = src:canBe("knockback")
+		if kb then
+			who:logCombat(src, "#ORCHID##Source#'s tendrils pull #Target# in!")
+			src:pull(who.x, who.y, 15)
+		else
+			game.logSeen(src, "#ORCHID#%s resists the tendrils' pull!", src.name:capitalize())
+		end
 		if core.fov.distance(who.x, who.y, src.x, src.y) <= 1 and src:canBe('pin') then
 			src:setEffect(src.EFF_CONSTRICTED, 6, {src=who})
 		end
- 
 		who.turn_procs.black_mesh = true
- 
 	end,}
 }
 
@@ -4536,7 +4540,7 @@ newEntity{ base = "BASE_SHIELD", --Thanks SageAcrin!
 		learn_talent = { [Talents.T_BLOCK] = 2, },
 		max_air = 20,
 	},
-	on_block = {desc = "30% chance that a blast of freezing water will spray at the target.", fct = function(self, who, target, type, dam, eff)
+	on_block = {desc = "30% chance to spray freezing water (radius 4 cone) at the target.", fct = function(self, who, target, type, dam, eff)
 		if rng.percent(30) then
 			if not target or target:attr("dead") or not target.x or not target.y then return end
 
@@ -4544,7 +4548,7 @@ newEntity{ base = "BASE_SHIELD", --Thanks SageAcrin!
 		
 			who:project(burst, target.x, target.y, engine.DamageType.ICE, 30)
 			game.level.map:particleEmitter(who.x, who.y, burst.radius, "breath_cold", {radius=burst.radius, tx=target.x-who.x, ty=target.y-who.y})
-			who:logCombat(target, "A wave of icy water bursts out from #Source#'s shield towards #Target#!")
+			who:logCombat(target, "A wave of icy water sprays out from #Source# towards #Target#!")
 		end
 	end,},
 }
diff --git a/game/modules/tome/data/talents/cunning/artifice.lua b/game/modules/tome/data/talents/cunning/artifice.lua
index 5cad988f587c46c1379b5188452f242bc4afb83f..180151675bfbd5a55f889b97da05cae2e0f472df 100644
--- a/game/modules/tome/data/talents/cunning/artifice.lua
+++ b/game/modules/tome/data/talents/cunning/artifice.lua
@@ -22,75 +22,111 @@ local Object = require "engine.Object"
 local Map = require "engine.Map"
 local Chat = require "engine.Chat"
 
+-- equipable artifice tool talents and associated mastery talents
+-- to add a new tool, define a tool talent and a mastery talent and update this table
+artifice_tool_tids = {T_HIDDEN_BLADES="T_ASSASSINATE", T_SMOKESCREEN="T_SMOKESCREEN_MASTERY", T_ROGUE_S_BREW="T_ROGUE_S_BREW_MASTERY", T_DART_LAUNCHER="T_DART_LAUNCHER_MASTERY"}
+
+--- initialize artifice tools, update mastery level and unlearn any unselected tools talents
+function artifice_tools_setup(self, t)
+	self.artifice_tools = self.artifice_tools or {}
+	self:setTalentTypeMastery("cunning/tools", self:getTalentMastery(t))
+	for tid, m_tid in pairs(artifice_tool_tids) do
+		if self:knowTalent(tid) then
+			local slot
+			for slot_id, tool_id in pairs(self.artifice_tools) do
+				if tool_id == tid then slot = slot_id break end
+			end
+			if not slot then self:unlearnTalentFull(tid) end
+		end
+		if self.artifice_tools_mastery == tid then
+			local m_level = self:getTalentLevelRaw(self.T_MASTER_ARTIFICER)
+			if self:getTalentLevelRaw(m_tid) ~= m_level then
+				self:unlearnTalentFull(m_tid)
+				self:learnTalent(m_tid, true, m_level, {no_unlearn=true})
+			end
+		elseif self:knowTalent(m_tid) then
+			self:unlearnTalentFull(m_tid)
+		end
+	end
+	return true
+end
+
+--- generate a textual list of available artifice tools
+function artifice_tools_get_descs(self, t)
+	if not self.artifice_tools then artifice_tools_setup(self, t) end
+	local tool_descs = {}
+	for tool_id, mt in pairs(artifice_tool_tids) do
+		local tool, desc = self:getTalentFromId(tool_id)
+		local prepped = self.artifice_tools[t.id] == tool_id
+		if prepped then
+			desc = ("#YELLOW#%s (prepared, level %s)#LAST#:\n"):format(tool.name, self:getTalentLevelRaw(tool))
+		else
+			desc = tool.name..":\n"
+		end
+		if tool.short_info then
+			desc = desc..tool.short_info(self, tool, t).."\n"
+		else
+			desc = desc.."#GREY#(see talent description)#LAST#\n"
+		end
+		tool_descs[#tool_descs+1] = desc
+	end
+	return table.concatNice(tool_descs, "\n\t")
+end
+
+--- NPC's automatically pick a tool for each tool slot if needed
+-- used as the talent on_pre_use_ai function
+-- this causes newly spawned NPC's to prepare their tools the first time they check for usable talents
+function artifice_tools_npc_select(self, t, silent, fake)
+	if not self.artifice_tools[t.id] then -- slot is empty: pick a tool
+		local tool_ids = table.keys(artifice_tool_tids)
+		local tid = rng.tableRemove(tool_ids)
+		while tid do
+			if not self:knowTalent(tid) then -- select the tool
+				self:learnTalent(tid, true, self:getTalentLevelRaw(t), {no_unlearn=true})
+				self.artifice_tools[t.id] = tid
+				if game.party:hasMember(self) then -- cooldowns for party members
+					self:startTalentCooldown(t); self:startTalentCooldown(tid)
+					self:useEnergy()
+				end
+				game.logSeen(self, "#GREY#You notice %s has prepared: %s.", self.name:capitalize(), self:getTalentFromId(tid).name)
+				break
+			end
+			tid = rng.tableRemove(tool_ids)
+		end
+	end
+	return false -- npc's don't need to actually use the tool slot talents
+end
+
 newTalent{
 	name = "Rogue's Tools",
 	type = {"cunning/artifice", 1},
 	points = 5,
 	require = cuns_req_high1,
 	cooldown = 10,
-	no_npc_use = true,
+	stamina = 0, -- forces learning stamina pool (npcs)
 	no_unlearn_last = true,
+	on_pre_use = artifice_tools_setup,
 	on_learn = function(self, t)
 		self:attr("show_gloves_combat", 1)
 	end,
 	on_unlearn = function(self, t)
 		self:attr("show_gloves_combat", -1)
 	end,
-	getHBDamage = function (self, t) return self:combatTalentWeaponDamage(t, 1.0, 1.8) end,
-	getRBDuration = function(self, t) return math.ceil(self:combatTalentScale(t, 4, 9)) end,
-	getRBResist = function(self, t) return self:combatTalentLimit(t, 1, 0.17, 0.5) end,
-	getRBRawHeal = function (self, t) return self:getTalentLevel(t) * 40 end,
-	getRBMaxHeal = function (self, t) return self:combatTalentLimit(t, 0.4, 0.10, 0.25) end,
-	getRBCure = function(self, t) return math.floor(self:combatTalentScale(t, 1, 3)) end,
-	getSSDuration = function(self, t) return math.ceil(self:combatTalentScale(t, 3, 5)) end,
-	getSSSightLoss = function(self, t) return math.floor(self:combatTalentScale(t,1, 6, "log", 0, 4)) end, -- 1@1 6@5
-	getDLDamage = function(self, t) return 15 + self:combatTalentStatDamage(t, "cun", 12, 150) end,
-	getDLSleepPower = function(self, t) return 15 + self:combatTalentStatDamage(t, "cun", 15, 180) end,
+	tactical = {BUFF = 2},
+	on_pre_use_ai = artifice_tools_npc_select, -- NPC's automatically pick a tool
 	action = function(self, t)
-		if self.artifice_hidden_blades==1 then 
-			self:unlearnTalent(self.T_HIDDEN_BLADES)
-			self.artifice_hidden_blades = null
-			if self:knowTalent(self.T_ASSASSINATE) then self:unlearnTalent(self.T_ASSASSINATE) end
-		end
-		if self.artifice_smokescreen==1 then 
-			self:unlearnTalent(self.T_SMOKESCREEN)
-			self.artifice_smokescreen = null
-			if self:knowTalent(self.T_SMOKESCREEN_MASTERY) then self:unlearnTalent(self.T_SMOKESCREEN_MASTERY) end
-		end		
-		if self.artifice_rogue_s_brew==1 then 
-			self:unlearnTalent(self.T_ROGUE_S_BREW)
-			self.artifice_rogue_s_brew = null
-			if self:knowTalent(self.T_ROGUE_S_BREW_MASTERY) then self:unlearnTalent(self.T_ROGUE_S_BREW_MASTERY) end
-		end		
-		if self.artifice_dart_launcher==1 then 
-			self:unlearnTalent(self.T_DART_LAUNCHER)
-			self.artifice_dart_launcher = null
-			if self:knowTalent(self.T_DART_LAUNCHER_MASTERY) then self:unlearnTalent(self.T_DART_LAUNCHER_MASTERY) end
-		end
-		
-		local chat = Chat.new("artifice", self, self, {player=self, slot=1})
+		local chat = Chat.new("artifice", self, self, {player=self, slot=1, chat_tid=t.id, tool_ids=artifice_tool_tids})
 		self:talentDialog(chat:invoke())
-		return true
+		artifice_tools_setup(self, t)
+		return self.turn_procs._did_artifice -- only use energy/cooldown if a tool was prepared
 	end,
 	info = function(self, t)
-		local tool = ""
-		if self:knowTalent(self.T_HIDDEN_BLADES) and self.artifice_hidden_blades==1 then
-			tool = ([[#YELLOW#Current Tool: Hidden Blades]]):format()
-		elseif self:knowTalent(self.T_SMOKESCREEN) and self.artifice_smokescreen==1 then
-			tool = ([[#YELLOW#Current Tool: Smokescreen]]):format()
-		elseif self:knowTalent(self.T_ROGUE_S_BREW) and self.artifice_rogue_s_brew==1 then
-			tool = ([[#YELLOW#Current Tool: Rogue's Brew]]):format()
-		elseif self:knowTalent(self.T_DART_LAUNCHER) and self.artifice_dart_launcher==1 then
-			tool = ([[#YELLOW#Current Tool: Dart Launcher]]):format()
-		end
-		return ([[You learn to create and equip a number of useful tools:
-Hidden Blades. Melee criticals inflict %d%% bonus unarmed damage. 4 turn cooldown.
-Smokescreen. Throw a vial of smoke that blocks vision in radius 2 for %d turns, and reduces the vision of enemies within by %d. 15 turn cooldown.
-Rogue’s Brew. Drink a potion that restores %d life (+%d%% of maximum), %d stamina (+%d%% of maximum) and cures %d negative physical effects. 20 turn cooldown.
-Dart Launcher. Fires a dart that deals %0.2f physical damage and puts the target to sleep for 4 turns. 10 turn cooldown.
-You can equip a single tool at first.
-%s]]):
-format(t.getHBDamage(self,t)*100, t.getSSDuration(self,t), t.getSSSightLoss(self,t), t.getRBRawHeal(self,t), t.getRBMaxHeal(self,t)*100, t.getRBRawHeal(self,t)/4, t.getRBMaxHeal(self,t)*40, t.getRBCure(self,t), damDesc(self, DamageType.PHYSICAL, t.getDLDamage(self,t)), tool)
+		local descs = artifice_tools_get_descs(self, t)
+		return ([[With some advanced preparation, you learn to create and equip one of a number of useful tools (at #YELLOW#level %d#WHITE#):
+
+%s
+Preparing a tool sets its talent level and puts it on cooldown.
+]]):format(self:getTalentLevelRaw(t), descs)
 	end,
 }
 
@@ -100,128 +136,51 @@ newTalent{
 	points = 5,
 	require = cuns_req_high2,
 	cooldown = 10,
-	no_npc_use = true,
+	stamina = 0, -- forces learning stamina pool (npcs)
 	no_unlearn_last = true,
-	getHBDamage = function (self, t) return self:combatTalentWeaponDamage(t, 1.0, 1.8) end,
-	getRBDuration = function(self, t) return math.ceil(self:combatTalentScale(t, 4, 9)) end,
-	getRBResist = function(self, t) return self:combatTalentLimit(t, 1, 0.17, 0.5) end,
-	getRBRawHeal = function (self, t) return self:getTalentLevel(t) * 40 end,
-	getRBMaxHeal = function (self, t) return self:combatTalentLimit(t, 0.4, 0.10, 0.25) end,
-	getRBCure = function(self, t) return math.floor(self:combatTalentScale(t, 1, 3)) end,
-	getSSDuration = function(self, t) return math.ceil(self:combatTalentScale(t, 3, 5)) end,
-	getSSSightLoss = function(self, t) return math.floor(self:combatTalentScale(t,1, 6, "log", 0, 4)) end, -- 1@1 6@5
-	getDLDamage = function(self, t) return 15 + self:combatTalentStatDamage(t, "cun", 12, 150) end,
-	getDLSleepPower = function(self, t) return 15 + self:combatTalentStatDamage(t, "cun", 15, 180) end,
+	on_pre_use = artifice_tools_setup,
+	tactical = {BUFF = 2},
+	on_pre_use_ai = artifice_tools_npc_select, -- NPC's automatically pick a tool
 	action = function(self, t)
-		if self.artifice_hidden_blades==2 then 
-			self:unlearnTalent(self.T_HIDDEN_BLADES)
-			self.artifice_hidden_blades = null
-			if self:knowTalent(self.T_ASSASSINATE) then self:unlearnTalent(self.T_ASSASSINATE) end
-		end
-		if self.artifice_smokescreen==2 then 
-			self:unlearnTalent(self.T_SMOKESCREEN)
-			self.artifice_smokescreen = null
-			if self:knowTalent(self.T_SMOKESCREEN_MASTERY) then self:unlearnTalent(self.T_SMOKESCREEN_MASTERY) end
-		end		
-		if self.artifice_rogue_s_brew==2 then 
-			self:unlearnTalent(self.T_ROGUE_S_BREW)
-			self.artifice_rogue_s_brew = null
-			if self:knowTalent(self.T_ROGUE_S_BREW_MASTERY) then self:unlearnTalent(self.T_ROGUE_S_BREW_MASTERY) end
-		end		
-		if self.artifice_dart_launcher==2 then 
-			self:unlearnTalent(self.T_DART_LAUNCHER)
-			self.artifice_dart_launcher = null
-			if self:knowTalent(self.T_DART_LAUNCHER_MASTERY) then self:unlearnTalent(self.T_DART_LAUNCHER_MASTERY) end
-		end
-
-		local chat = Chat.new("artifice", self, self, {player=self, slot=2})
+		local chat = Chat.new("artifice", self, self, {player=self, slot=2, chat_tid=t.id, tool_ids=artifice_tool_tids})
 		self:talentDialog(chat:invoke())
-		return true
+		return self.turn_procs._did_artifice -- only use energy/cooldown if a tool was prepared
 	end,
 	info = function(self, t)
-		local tool = ""
-		if self:knowTalent(self.T_HIDDEN_BLADES) and self.artifice_hidden_blades==2 then
-			tool = ([[#YELLOW#Current Tool: Hidden Blades]]):format()
-		elseif self:knowTalent(self.T_SMOKESCREEN) and self.artifice_smokescreen==2 then
-			tool = ([[#YELLOW#Current Tool: Smokescreen]]):format()
-		elseif self:knowTalent(self.T_ROGUE_S_BREW) and self.artifice_rogue_s_brew==2 then
-			tool = ([[#YELLOW#Current Tool: Rogue's Brew]]):format()
-		elseif self:knowTalent(self.T_DART_LAUNCHER) and self.artifice_dart_launcher==2 then
-			tool = ([[#YELLOW#Current Tool: Dart Launcher]]):format()
-		end
-		return ([[You learn to equip a second tool:
-Hidden Blades. Melee criticals inflict %d%% bonus unarmed damage. 4 turn cooldown.
-Smokescreen. Throw a vial of smoke that blocks vision in radius 2 for %d turns, and reduces the vision of enemies within by %d. 15 turn cooldown.
-Rogue’s Brew. Drink a potion that restores %d life (+%d%% of maximum), %d stamina (+%d%% of maximum) and cures %d negative physical effects. 20 turn cooldown.
-Dart Launcher. Fires a dart that deals %0.2f physical damage and puts the target to sleep for 4 turns. 10 turn cooldown.
-%s]]):
-format(t.getHBDamage(self,t)*100, t.getSSDuration(self,t), t.getSSSightLoss(self,t), t.getRBRawHeal(self,t), t.getRBMaxHeal(self,t)*100, t.getRBRawHeal(self,t)/4, t.getRBMaxHeal(self,t)*40, t.getRBCure(self,t), damDesc(self, DamageType.PHYSICAL, t.getDLDamage(self,t)), tool)
+		local descs = artifice_tools_get_descs(self, t)
+		return ([[With some advanced preparation, you learn to create and equip a second tool (at #YELLOW#level %d#WHITE#):
+
+%s
+Preparing a tool sets its talent level and puts it on cooldown.
+Only one tool of each type can be equipped at a time.
+]]):format(self:getTalentLevelRaw(t), descs)
 	end,
 }
 
-
 newTalent{
 	name = "Intricate Tools",
 	type = {"cunning/artifice", 3},
 	require = cuns_req_high3,
 	points = 5,
 	cooldown = 10,
-	no_npc_use = true,
+	stamina = 0, -- forces learning stamina pool (npcs)
 	no_unlearn_last = true,
-	getHBDamage = function (self, t) return self:combatTalentWeaponDamage(t, 1.0, 1.8) end,
-	getRBDuration = function(self, t) return math.ceil(self:combatTalentScale(t, 4, 9)) end,
-	getRBResist = function(self, t) return self:combatTalentLimit(t, 1, 0.17, 0.5) end,
-	getRBRawHeal = function (self, t) return self:getTalentLevel(t) * 40 end,
-	getRBMaxHeal = function (self, t) return self:combatTalentLimit(t, 0.4, 0.10, 0.25) end,
-	getRBCure = function(self, t) return math.floor(self:combatTalentScale(t, 1, 3)) end,
-	getSSDuration = function(self, t) return math.ceil(self:combatTalentScale(t, 3, 5)) end,
-	getSSSightLoss = function(self, t) return math.floor(self:combatTalentScale(t,1, 6, "log", 0, 4)) end, -- 1@1 6@5
-	getDLDamage = function(self, t) return 15 + self:combatTalentStatDamage(t, "cun", 12, 150) end,
-	getDLSleepPower = function(self, t) return 15 + self:combatTalentStatDamage(t, "cun", 15, 180) end,
+	on_pre_use = artifice_tools_setup,
+	tactical = {BUFF = 2},
+	on_pre_use_ai = artifice_tools_npc_select, -- NPC's automatically pick a tool
 	action = function(self, t)
-		if self.artifice_hidden_blades==3 then 
-			self:unlearnTalent(self.T_HIDDEN_BLADES)
-			self.artifice_hidden_blades = null
-			if self:knowTalent(self.T_ASSASSINATE) then self:unlearnTalent(self.T_ASSASSINATE) end
-		end
-		if self.artifice_smokescreen==3 then 
-			self:unlearnTalent(self.T_SMOKESCREEN)
-			self.artifice_smokescreen = null
-			if self:knowTalent(self.T_SMOKESCREEN_MASTERY) then self:unlearnTalent(self.T_SMOKESCREEN_MASTERY) end
-		end		
-		if self.artifice_rogue_s_brew==3 then 
-			self:unlearnTalent(self.T_ROGUE_S_BREW)
-			self.artifice_rogue_s_brew = null
-			if self:knowTalent(self.T_ROGUE_S_BREW_MASTERY) then self:unlearnTalent(self.T_ROGUE_S_BREW_MASTERY) end
-		end		
-		if self.artifice_dart_launcher==3 then 
-			self:unlearnTalent(self.T_DART_LAUNCHER)
-			self.artifice_dart_launcher = null
-			if self:knowTalent(self.T_DART_LAUNCHER_MASTERY) then self:unlearnTalent(self.T_DART_LAUNCHER_MASTERY) end
-		end
-		
-		local chat = Chat.new("artifice", self, self, {player=self, slot=3})
+		local chat = Chat.new("artifice", self, self, {player=self, slot=3, chat_tid=t.id, tool_ids=artifice_tool_tids})
 		self:talentDialog(chat:invoke())
-		return true
+		return self.turn_procs._did_artifice -- only use energy/cooldown if a tool was prepared
 	end,
 	info = function(self, t)
-		local tool = ""
-		if self:knowTalent(self.T_HIDDEN_BLADES) and self.artifice_hidden_blades==3 then
-			tool = ([[#YELLOW#Current Tool: Hidden Blades]]):format()
-		elseif self:knowTalent(self.T_SMOKESCREEN) and self.artifice_smokescreen==3 then
-			tool = ([[#YELLOW#Current Tool: Smokescreen]]):format()
-		elseif self:knowTalent(self.T_ROGUE_S_BREW) and self.artifice_rogue_s_brew==3 then
-			tool = ([[#YELLOW#Current Tool: Rogue's Brew]]):format()
-		elseif self:knowTalent(self.T_DART_LAUNCHER) and self.artifice_dart_launcher==3 then
-			tool = ([[#YELLOW#Current Tool: Dart Launcher]]):format()
-		end
-		return ([[You learn to equip a third tool:
-Hidden Blades. Melee criticals inflict %d%% bonus unarmed damage. 4 turn cooldown.
-Smokescreen. Throw a vial of smoke that blocks vision in radius 2 for %d turns, and reduces the vision of enemies within by %d. 15 turn cooldown.
-Rogue’s Brew. Drink a potion that restores %d life (+%d%% of maximum), %d stamina (+%d%% of maximum) and cures %d negative physical effects. 20 turn cooldown.
-Dart Launcher. Fires a dart that deals %0.2f physical damage and puts the target to sleep for 4 turns. 10 turn cooldown.
-%s]]):
-format(t.getHBDamage(self,t)*100, t.getSSDuration(self,t), t.getSSSightLoss(self,t), t.getRBRawHeal(self,t), t.getRBMaxHeal(self,t)*100, t.getRBRawHeal(self,t)/4, t.getRBMaxHeal(self,t)*40, t.getRBCure(self,t), damDesc(self, DamageType.PHYSICAL, t.getDLDamage(self,t)), tool)
+		local descs = artifice_tools_get_descs(self, t)
+		return ([[With some advanced preparation, you learn to create and equip a third tool (at #YELLOW#level %d#WHITE#):
+
+%s
+Preparing a tool sets its talent level and puts it on cooldown.
+Only one tool of each type can be equipped at a time.
+]]):format(self:getTalentLevelRaw(t), descs)
 	end,
 }
 
@@ -231,57 +190,83 @@ newTalent{
 	require = cuns_req_high4,
 	points = 5,
 	cooldown = 10,
-	no_npc_use = true,
+	stamina = 0, -- forces learning stamina pool (npcs)
+	no_energy = true,
 	no_unlearn_last = true,
-	getAssassinateDamage = function (self, t) return self:combatTalentWeaponDamage(t, 1.8, 3.0) end,
-	getBleed = function(self, t) return self:combatTalentScale(t, 0.2, 0.8) end,
-    getSSDamage = function (self, t) return 30 + self:combatTalentStatDamage(t, "cun", 10, 150) end,
-	getRBDieAt = function(self, t) return self:combatTalentScale(t, 100, 600) end,
-    getDLSlow = function(self, t) return self:combatTalentLimit(t, 50, 15, 40)/100 end,
+	on_pre_use = artifice_tools_setup,
+	tactical = {BUFF = 2},
+	on_pre_use_ai = function(self, t, silent, fake) -- npc's automatically master a tool they have prepared
+		if self.artifice_tools and not self.artifice_tools_mastery then
+			game:onTickEnd(function()
+				local tools = table.values(self.artifice_tools)
+				while #tools > 0 do
+					local tool_id = rng.tableRemove(tools)
+					local m_tid = artifice_tool_tids[tool_id]
+					if m_tid then -- note: talent level affects AI use
+						local tl = self:getTalentLevelRaw(m_tid)
+						if self:learnTalent(m_tid, true, self:getTalentLevelRaw(t) - tl) then
+							self.artifice_tools_mastery = tool_id
+							if game.party:hasMember(self) then -- cooldowns for party members
+								self:startTalentCooldown(t); self:startTalentCooldown(tool_id); self:startTalentCooldown(m_tid)
+							end
+							break
+						end
+					end
+				end
+			end)
+		end
+		return false
+	end,
 	action = function(self, t)
-		if self:knowTalent(self.T_ASSASSINATE) then self:unlearnTalent(self.T_ASSASSINATE) end
-		if self:knowTalent(self.T_SMOKESCREEN_MASTERY) then self:unlearnTalent(self.T_SMOKESCREEN_MASTERY) end
-		if self:knowTalent(self.T_ROGUE_S_BREW_MASTERY) then self:unlearnTalent(self.T_ROGUE_S_BREW_MASTERY) end
-		if self:knowTalent(self.T_DART_LAUNCHER_MASTERY) then self:unlearnTalent(self.T_DART_LAUNCHER_MASTERY) end
-		
-		local chat = Chat.new("artifice-mastery", self, self, {player=self})
+		local chat = Chat.new("artifice-mastery", self, self, {player=self, chat_tid=t.id, tool_ids=artifice_tool_tids})
 		self:talentDialog(chat:invoke())
-		return true
+		return false -- chat handles cooldowns
 	end,
 	info = function(self, t)
-		local tool = ""
-		if self:knowTalent(self.T_ASSASSINATE) then
-			tool = ([[#YELLOW#Current Mastery: Hidden Blades]]):format()
-		elseif self:knowTalent(self.T_SMOKESCREEN_MASTERY) then
-			tool = ([[#YELLOW#Current Mastery: Smokescreen]]):format()
-		elseif self:knowTalent(self.T_ROGUE_S_BREW_MASTERY) then
-			tool = ([[#YELLOW#Current Mastery: Rogue's Brew]]):format()
-		elseif self:knowTalent(self.T_DART_LAUNCHER_MASTERY) then
-			tool = ([[#YELLOW#Current Mastery: Dart Launcher]]):format()
+		local tool = "none"
+		if self.artifice_tools_mastery then
+			tool = self:getTalentFromId(self.artifice_tools_mastery).name
+		end
+		--- generate a textual list of available artifice tools enhancements
+		if not self.artifice_tools then artifice_tools_setup(self, t) end
+		local mastery_descs = {}
+		for tool_id, m_tid in pairs(artifice_tool_tids) do
+			local tool, mt = self:getTalentFromId(tool_id), self:getTalentFromId(m_tid)
+			if mt then
+				local desc
+				local prepped = self.artifice_tools_mastery == tool_id
+				if prepped then
+					desc = ("#YELLOW#%s (%s)#LAST#\n"):format(tool.name, mt.name)
+				else
+					desc = ("%s (%s)\n"):format(tool.name, mt.name)
+				end
+				if mt.short_info then
+					desc = desc..mt.short_info(self, mt).."\n"
+				else
+					desc = desc.."#GREY#(see talent description)#LAST#\n"
+				end
+				mastery_descs[#mastery_descs+1] = desc
+			end
 		end
-		return ([[You reach the height of your craft, allowing you to focus on a single tool to greatly improve its capabilities:
-Hidden Blades. Grants use of the Assassinate ability, striking twice with your hidden blades for %d%% unarmed damage as a guaranteed critical strike which ignores armor and resistances. Your Hidden Blades also inflict an additional %d%% damage as bleed.
-Smokescreen: Infuses your Smokescreen with chokedust, causing %0.2f nature damage each turn to enemies inside as well as silencing them.
-Rogue’s Brew. The brew strengthens you for 8 turns, preventing you from dying until you reach -%d life.
-Dart Launcher. The sleeping poison becomes potent enough to ignore immunity, and on waking the target will be slowed by %d%% for 4 turns.
-%s]]):
-format(t.getAssassinateDamage(self,t)*100, t.getBleed(self,t)*100, damDesc(self, DamageType.NATURE, t.getSSDamage(self,t)), t.getRBDieAt(self,t), t.getDLSlow(self,t)*100, tool)
+		mastery_descs = table.concatNice(mastery_descs, "\n\t")
+		return ([[You become a master of your craft, allowing you to focus on a single tool {#YELLOW#currently %s#LAST#) to greatly improve its capabilities:
+
+%s
+The effects depend on this talent's level.
+Mastering a new tool places it (and its special effects, as appropriate) on cooldown.]]):format(tool, mastery_descs)
 	end,
 }
 
+--====================--
+-- Rogue's tools and enhancements
+--====================--
 newTalent{
 	name = "Hidden Blades",
 	type = {"cunning/tools", 1},
 	mode = "passive",
 	points = 1,
 	cooldown = 4,
-	getDamage = function(self, t) 
-		if self.artifice_hidden_blades == 1 then return self:callTalent(self.T_ROGUE_S_TOOLS, "getHBDamage") 
-		elseif self.artifice_hidden_blades == 2 then return self:callTalent(self.T_CUNNING_TOOLS, "getHBDamage") 
-		elseif self.artifice_hidden_blades == 3 then return self:callTalent(self.T_INTRICATE_TOOLS, "getHBDamage") 
-		else return 0
-		end
-	end,
+	getDamage = function (self, t) return self:combatTalentWeaponDamage(t, 1.0, 1.8) end,
 	callbackOnCrit = function(self, t, kind, dam, chance, target)
 		if not target then return end
 		if target.turn_procs.hb then return end
@@ -289,6 +274,7 @@ newTalent{
 		if not self:isTalentCoolingDown(t) then
 			target.turn_procs.hb = true
 			local oldlife = target.life
+			self:logCombat(target, "#Source# strikes #target# with hidden blades!")
 			self:attackTarget(target, nil, t.getDamage(self,t), true, true)	
 
 			if self:knowTalent(self.T_ASSASSINATE) then
@@ -302,47 +288,112 @@ newTalent{
 			self:startTalentCooldown(t)
 		end	
 	end,
+	short_info = function(self, t, slot_talent)
+		return ([[Melee criticals trigger an extra unarmed attack, inflicting %d%% damage. 4 turn cooldown.]]):format(t.getDamage(self, slot_talent)*100)
+	end,
 	info = function(self, t)
 		local dam = t.getDamage(self, t)
-		return ([[You mount spring loaded blades on your wrists. On scoring a critical strike against an adjacent target, you follow up with your blades for %d%% unarmed damage.
-This talent has a cooldown.]]):
-		format(dam*100)
+		local slot = "not prepared"
+		for slot_id, tool_id in pairs(self.artifice_tools) do
+			if tool_id == t.id then slot = self:getTalentFromId(slot_id).name break end
+		end
+		return ([[You conceal spring loaded blades within your equipment.  On scoring a critical strike against an adjacent target, you follow up with your blades for %d%% damage (as an unarmed attack).
+This talent has a cooldown.
+#YELLOW#Prepared with: %s#LAST#]]):format(dam*100, slot)
 	end,
 }
 
 newTalent{
-	name = "Rogue's Brew",
+	name = "Assassinate",
 	type = {"cunning/tools", 1},
 	points = 1,
-	cooldown = 20,
-	tactical = { BUFF = 2 },
+	cooldown = 8,
+	stamina = 10,
+	message = false,
+	tactical = { ATTACK = 3 },
 	requires_target = true,
-	getRawHeal = function(self, t) 
-		if self.artifice_rogue_s_brew == 1 then return self:callTalent(self.T_ROGUE_S_TOOLS, "getRBRawHeal")
-		elseif self.artifice_rogue_s_brew == 2 then return self:callTalent(self.T_CUNNING_TOOLS, "getRBRawHeal") 
-		elseif self.artifice_rogue_s_brew == 3 then return self:callTalent(self.T_INTRICATE_TOOLS, "getRBRawHeal") 
-		else return 0
+	is_melee = true,
+	target = function(self, t) return {type="hit", range=self:getTalentRange(t)} end,
+	range = 1,
+	on_pre_use = function(self, t, silent, fake)
+		if not self:knowTalent(self.T_HIDDEN_BLADES) then
+			if not silent then game.logPlayer(self, "You must have Hidden Blades prepared to use this talent.") end
+			return
 		end
+		return true
 	end,
-	getMaxHeal = function(self, t) 
-		if self.artifice_rogue_s_brew == 1 then return self:callTalent(self.T_ROGUE_S_TOOLS, "getRBMaxHeal")
-		elseif self.artifice_rogue_s_brew == 2 then return self:callTalent(self.T_CUNNING_TOOLS, "getRBMaxHeal") 
-		elseif self.artifice_rogue_s_brew == 3 then return self:callTalent(self.T_INTRICATE_TOOLS, "getRBMaxHeal") 
-		else return 0
+	getDamage = function (self, t) return self:combatTalentWeaponDamage(self:getTalentFromId(self.T_MASTER_ARTIFICER), 1.8, 3.0) end,
+	getBleed = function(self, t) return self:combatTalentScale(self:getTalentFromId(self.T_MASTER_ARTIFICER), 0.3, 1) end,
+	action = function(self, t)
+		local tg = self:getTalentTarget(t)
+		local x, y, target = self:getTarget(tg)
+		if not target or not self:canSee(target) or not self:canProject(tg, x, y) then return nil end
+		
+		target.turn_procs.hb = true -- prevent a crit against this target from triggering an additional hidden blades attack
+		self.turn_procs.auto_melee_hit = true
+		-- store old values to restore later
+		local apr, rpen, evasion = self.combat_apr, self.resists_pen.PHYSICAL, target.evasion
+		self:attr("combat_apr", 10000)
+		self.resists_pen.PHYSICAL = 100
+		target.evasion = 0
+		local bleed = t.getBleed(self, t)
+		local oldlife = target.life
+
+		self:logCombat(target, "#Source# strikes at a vital spot on #target#!")
+		local do_attack = function() self:attackTarget(target, nil, t.getDamage(self, t), true, true) end
+		local ok, err = pcall(do_attack)
+		if ok then ok, err = pcall(do_attack) end
+		self.combat_apr, self.resists_pen.PHYSICAL, target.evasion = apr, rpen, evasion
+		if not ok then error(err) end
+		self.turn_procs.auto_melee_hit = nil
+		
+		local life_diff = oldlife - target.life
+		if life_diff > 0 and target:canBe('cut') and bleed then
+			target:setEffect(target.EFF_CUT, 5, {power=life_diff * bleed / 5, src=self})
 		end
+
+		return true
+	end,
+	short_info = function(self, t)
+		return ([[You prime your Hidden Blades to cause bleeding and facilitate the Assassinate ability, which allows you to strike twice for %d%% unarmed damage, hitting automatically while ignoring armor and resistance.]]):format(t.getDamage(self, t)*100)
+	end,
+	info = function(self, t)
+		local damage = t.getDamage(self, t) * 100
+		local bleed = t.getBleed(self,t) * 100
+		return ([[You strike your target with your Hidden Blades twice in a vital spot for %d%% unarmed (physical) damage.  You must be able to see your target to use this attack, but it always hits and ignores all armor and physical resistance.
+In addition, your hidden blades now inflict a further %d%% of all damage dealt as bleeding over 5 turns.]])
+		:format(damage, bleed)
 	end,
-	getCure = function(self,t) 
-		if self.artifice_rogue_s_brew == 1 then return self:callTalent(self.T_ROGUE_S_TOOLS, "getRBCure")
-		elseif self.artifice_rogue_s_brew == 2 then return self:callTalent(self.T_CUNNING_TOOLS, "getRBCure") 
-		elseif self.artifice_rogue_s_brew == 3 then return self:callTalent(self.T_INTRICATE_TOOLS, "getRBCure") 
-		else return 0
+}
+
+newTalent{
+	name = "Rogue's Brew",
+	type = {"cunning/tools", 1},
+	points = 1,
+	cooldown = 20,
+	tactical = { HEAL = 1.5, STAMINA = 1.5,
+		CURE = function(self, t, target)
+			local num, max = 0, t.getCure(self, t)
+			for eff_id, p in pairs(self.tmp) do
+				local e = self.tempeffect_def[eff_id]
+				if e.type == "physical" and e.status == "detrimental" then
+					num = num + 1
+					if num >= max then break end
+				end
+			end
+			return (2*num)^.5
 		end
+	},
+	getHeal = function(self, t)
+		return self:combatStatScale("cun", 10, 200, 0.7) + self:combatTalentScale(t, 20, 200, 0.7)
 	end,
-	getDieAt = function(self,t) return self:callTalent(self.T_MASTER_ARTIFICER, "getRBDieAt") end,
+	getStam = function(self, t)
+		return self:combatStatScale("cun", 5, 50, 0.75) + self:combatTalentScale(t, 5, 50, 0.75)
+	end,
+	getCure = function(self, t) return math.floor(self:combatTalentScale(t, 1, 3, "log")) end,
 	action = function(self, t)
-	
-		local life = t.getRawHeal(self,t) + (t.getMaxHeal(self,t) * self.max_life)
-		local sta = t.getRawHeal(self,t)/4 + (t.getMaxHeal(self,t) * self.max_stamina * 0.4)
+		local life = t.getHeal(self, t)
+		local sta = t.getStam(self, t)
 		self:incStamina(sta)
 		self:attr("allow_on_heal", 1)
 		self:heal(life, self)
@@ -370,20 +421,41 @@ newTalent{
 			game.logSeen(self, "%s is cured!", self.name:capitalize())
 		end
 
-		if self:knowTalent(self.T_ROGUE_S_BREW_MASTERY) then self:setEffect(self.EFF_ROGUE_S_BREW, 8, {power = t.getDieAt(self,t)}) end
+		if self:knowTalent(self.T_ROGUE_S_BREW_MASTERY) then self:setEffect(self.EFF_ROGUE_S_BREW, 8, {power = self:callTalent(self.T_ROGUE_S_BREW_MASTERY, "getDieAt")}) end
 				
 		return true
 
 	end,
+	short_info = function(self, t, slot_talent)
+		return ([[Prepare a potion that restores %d life, %d stamina, and cures %d negative physical effects. 20 turn cooldown.]]):format(t.getHeal(self, slot_talent), t.getStam(self, slot_talent), t.getCure(self, slot_talent))
+	end,
 	info = function(self, t)
-	local heal = t.getRawHeal(self,t) + (t.getMaxHeal(self,t) * self.max_life)
-	local sta = t.getRawHeal(self,t)/4 + (t.getMaxHeal(self,t) * self.max_stamina * 0.4)
+	local heal = t.getHeal(self, t)
+	local sta = t.getStam(self, t)
 	local cure = t.getCure(self,t)
-		return ([[Imbibe a potent mixture of energizing and restorative substances, restoring %d life, %d stamina and curing %d negative physical effects.]]):
-		format(heal, sta, cure)
+	local slot = "not prepared"
+	for slot_id, tool_id in pairs(self.artifice_tools) do
+		if tool_id == t.id then slot = self:getTalentFromId(slot_id).name break end
+	end
+	return ([[Imbibe a potent mixture of energizing and restorative substances, restoring %d life, %d stamina and curing %d detrimental physical effects.  The restorative effects improve with your Cunning.
+	#YELLOW#Prepared with: %s#LAST#]]):format(heal, sta, cure, slot)
    end,
 }
 
+newTalent{
+	name = "Rogue's Brew Mastery",
+	type = {"cunning/tools", 1},
+	mode = "passive",
+	points = 1,
+	getDieAt = function(self, t) return self:combatTalentScale(self:getTalentFromId(self.T_MASTER_ARTIFICER), 100, 600) end,
+	short_info = function(self, t)
+		return ([[Your Rogue's Brew fortifies you for 8 turns, preventing you from dying until you reach -%d life.]]):format(t.getDieAt(self, t))
+	end,
+	info = function(self, t)
+		return ([[Adjust your Rogue's Brew formulation so that it fortifies you for 8 turns, preventing you from dying until you reach -%d life.]]):format(t.getDieAt(self,t))
+	end,
+}
+
 newTalent{
 	name = "Smokescreen",
 	type = {"cunning/tools", 1},
@@ -392,32 +464,22 @@ newTalent{
 	stamina = 10,
 	range = 6,
 	direct_hit = true,
-	tactical = { DISABLE = 2 },
+	tactical = { ESCAPE = 2, DISABLE = {blind = 2} },
 	requires_target = true,
+	no_break_stealth = true,
 	radius = 2,
-	getSightLoss = function(self, t) 
-		if self.artifice_smokescreen == 1 then return self:callTalent(self.T_ROGUE_S_TOOLS, "getSSSightLoss")
-		elseif self.artifice_smokescreen == 2 then return self:callTalent(self.T_CUNNING_TOOLS, "getSSSightLoss") 
-		elseif self.artifice_smokescreen == 3 then return self:callTalent(self.T_INTRICATE_TOOLS, "getSSSightLoss") 
-		else return 0
-		end
-	end,
 	getDamage = function(self,t) 
 		if self:knowTalent(self.T_SMOKESCREEN_MASTERY) then
-			return self:callTalent(self.T_SMOKESCREEN_MASTERY, "getSSDamage")
+			return self:callTalent(self.T_SMOKESCREEN_MASTERY, "getDamage")
 		else
 			return 0
 		end
 	end,
-	getDuration = function(self, t)
-		if self.artifice_smokescreen == 1 then return self:callTalent(self.T_ROGUE_S_TOOLS, "getSSDuration")
-		elseif self.artifice_smokescreen == 2 then return self:callTalent(self.T_CUNNING_TOOLS, "getSSDuration") 
-		elseif self.artifice_smokescreen == 3 then return self:callTalent(self.T_INTRICATE_TOOLS, "getSSDuration") 
-		else return 0
-		end
-	end,
+	getDuration = function(self, t) return math.ceil(self:combatTalentScale(t, 3, 5)) end,
+	getSightLoss = function(self, t) return math.floor(self:combatTalentScale(t,1, 6, "log", 0, 4)) end, -- 1@1 6@5
+	target = function(self, t) return {type="ball", range=self:getTalentRange(t), radius=self:getTalentRadius(t), talent=t} end,
 	action = function(self, t)
-		local tg = {type="ball", range=self:getTalentRange(t), radius=self:getTalentRadius(t), talent=t}
+		local tg = self:getTalentTarget(t)
 		local x, y = self:getTarget(tg)
 		if not x or not y then return nil end
 
@@ -460,7 +522,7 @@ newTalent{
 			e.particles = Particles.new("creeping_dark", 1, { })
 			e.particles.x = px
 			e.particles.y = py
-		game.level.map:addParticleEmitter(e.particles)
+			game.level.map:addParticleEmitter(e.particles)
 
 		end, nil, {type="dark"})
 
@@ -468,80 +530,19 @@ newTalent{
 		game.level.map:redisplay()
 		return true
 	end,
-	info = function(self, t)
-		return ([[Throw a vial of sticky smoke that explodes in radius %d, blocking line of sight for 5 turns. Enemies within will have their vision range reduced by %d.
-		Creatures affected by smokescreen can never prevent you from stealthing, even if their proximity would normally forbid it.
-		Use of this will not break stealth.]]):
-		format(self:getTalentRadius(t), t.getSightLoss(self,t))
-	end,
-}
-
-newTalent{
-	name = "Assassinate",
-	type = {"cunning/tools", 1},
-	points = 1,
-	cooldown = 8,
-	message = "@Source@ lashes out with their hidden blades!",
-	tactical = { ATTACK = { weapon = 2 } },
-	requires_target = true,
-	is_melee = true,
-	target = function(self, t) return {type="hit", range=self:getTalentRange(t)} end,
-	range = 1,
-	getDamage = function(self, t) return self:callTalent(self.T_MASTER_ARTIFICER, "getAssassinateDamage") end,
-	getBleed = function(self, t) return self:combatTalentScale(t, 0.3, 1) end,
-	action = function(self, t)
-		local tg = self:getTalentTarget(t)
-		local x, y, target = self:getTarget(tg)
-		if not target or not self:canProject(tg, x, y) then return nil end
-		
-		target.turn_procs.hb = true -- we're already using our hidden blades for this attack
-		self.turn_procs.auto_melee_hit = true
-		
-		self:attr("combat_apr", 1000)
-		local penstore = self.resists_pen
-		local storeeva = target.evasion
-		target.evasion=0
-		self.resists_pen = nil
-		self.resists_pen = {all = 100}
-		
-		local scale = nil
-		scale = t.getBleed(self, t)
-		local oldlife = target.life
-
-		self:attackTarget(target, nil, t.getDamage(self, t), true, true)
-		self:attackTarget(target, nil, t.getDamage(self, t), true, true)
-		
-		local life_diff = oldlife - target.life
-		if life_diff > 0 and target:canBe('cut') and scale then
-			target:setEffect(target.EFF_CUT, 5, {power=life_diff * scale / 5, src=self})
-		end
-
-		self:attr("combat_apr", -1000)
-		self.turn_procs.auto_melee_hit = nil
-		target.evasion = storeeva
-		self.resists_pen = nil
-		self.resists_pen = penstore
-
-		return true
-	end,
-	info = function(self, t)
-		local damage = t.getDamage(self, t) * 100
-		local bleed = t.getBleed(self,t) * 100
-		return ([[Impale the target on your hidden blades, striking twice for %d%% unarmed damage. This attack always hits and ignores all armor and resistances.
-In addition, your hidden blades now inflict a further %d%% of all damage dealt as bleeding over 5 turns.]])
-		:format(damage, bleed)
+	short_info = function(self, t, slot_talent)
+		return ([[Throw a smokebomb creating a radius 2 cloud of smoke, lasting %d turns, that blocks sight and reduces enemies' vision by %d. 15 turn cooldown.]]):format(t.getSightLoss(self, slot_talent), t.getDuration(self, slot_talent))
 	end,
-}
-
-newTalent{
-	name = "Rogue's Brew Mastery",
-	type = {"cunning/tools", 1},
-	mode = "passive",
-	points = 1,
-	getDieAt = function(self,t) return self:callTalent(self.T_MASTER_ARTIFICER, "getRBDieAt") end,
 	info = function(self, t)
-		return ([[The brew strengthens you for 8 turns, preventing you from dying until you reach -%d life.]]):
-		format(t.getDieAt(self,t))
+		local slot = "not prepared"
+		for slot_id, tool_id in pairs(self.artifice_tools) do
+			if tool_id == t.id then slot = self:getTalentFromId(slot_id).name break end
+		end
+		return ([[Throw a vial of volatile liquid that explodes in a smoke cloud of radius %d, blocking line of sight for 5 turns. Enemies within will have their vision range reduced by %d.
+		Creatures affected by smokescreen can never prevent you from activating stealth, even if their proximity would normally forbid it.
+		Use of this talent will not break stealth.
+		#YELLOW#Prepared with: %s#LAST#]]):
+		format(self:getTalentRadius(t), t.getSightLoss(self,t), slot)
 	end,
 }
 
@@ -550,12 +551,13 @@ newTalent{
 	type = {"cunning/tools", 1},
 	points = 1,
 	mode = "passive",
-	getSSDamage = function (self,t) return self:callTalent(self.T_MASTER_ARTIFICER, "getSSDamage") end,
-	getSSEvasion = function (self,t) return self:callTalent(self.T_MASTER_ARTIFICER, "getSSEvasion") end,
-	no_npc_use = true,
+	getDamage = function (self, t) return 30 + self:combatTalentStatDamage(self:getTalentFromId(self.T_MASTER_ARTIFICER), "cun", 10, 150) end,
+	short_info = function(self, t)
+		return ([[Your Smokescreen is infused with chokedust. Enemies in the smoke take %0.2f nature damage and may be silenced.]]):format(t.getDamage(self, t))
+	end,
 	info = function(self, t)
-		return ([[Infuses your smoke bomb with chokedust, causing %0.2f nature damage each turn and silencing enemies inside.]]):
-		format(damDesc(self, DamageType.NATURE, t.getSSDamage(self,t)), t.getSSEvasion(self,t))
+		return ([[You infuse your smoke bomb with chokedust. Each turn, enemies in the smoke take %0.2f nature damage and are 50%% likely to be silenced.]]):
+		format(damDesc(self, DamageType.NATURE, t.getDamage(self,t)))
 	end,
 }
 
@@ -563,31 +565,23 @@ newTalent{
 	name = "Dart Launcher",
 	type = {"cunning/tools", 1},
 	points = 1,
-	tactical = { ATTACK = 2 },
+	tactical = { ATTACK = {PHYSICAL = 1},
+		DISABLE = function(self, t, target)
+			return target:checkClassification("unliving") and 0 or self:knowTalent(self.T_DART_LAUNCHER_MASTERY) and 2 or {sleep = 1, poison = 1}
+		end
+	},
 	range = 5,
 	no_energy = true,
 	cooldown = 10,
+	stamina = 5,
 	requires_target = true,
 	no_break_stealth = true,
-	getDamage = function(self, t) 
-		if self.artifice_dart_launcher == 1 then return self:callTalent(self.T_ROGUE_S_TOOLS, "getDLDamage")
-		elseif self.artifice_dart_launcher == 2 then return self:callTalent(self.T_CUNNING_TOOLS, "getDLDamage") 
-		elseif self.artifice_dart_launcher == 3 then return self:callTalent(self.T_INTRICATE_TOOLS, "getDLDamage") 
-		else return 0
-		end
-	end,
-	getSleepPower = function(self, t)
-		if self.artifice_dart_launcher == 1 then return self:callTalent(self.T_ROGUE_S_TOOLS, "getDLSleepPower")
-		elseif self.artifice_dart_launcher == 2 then return self:callTalent(self.T_CUNNING_TOOLS, "getDLSleepPower") 
-		elseif self.artifice_dart_launcher == 3 then return self:callTalent(self.T_INTRICATE_TOOLS, "getDLSleepPower") 
-		else return 0
-		end
-	end,
-    getSlow = function(self, t) return self:callTalent(self.T_MASTER_ARTIFICER, "getDLSlow") end,
+	getDamage = function(self, t) return 15 + self:combatTalentStatDamage(t, "cun", 12, 150) end,
+	getSleepPower = function(self, t) return 15 + self:combatTalentStatDamage(t, "cun", 15, 180) end,
 	target = function(self, t)
 		return {type="bolt", range=self:getTalentRange(t)}
 	end,
-		action = function(self, t)
+	action = function(self, t)
 		local tg = self:getTalentTarget(t)
 		local x, y = self:getTarget(tg)
 		if not x or not y then return nil end
@@ -595,29 +589,37 @@ newTalent{
 		
 		local slow = 0
 		
-		if self:knowTalent(self.T_DART_LAUNCHER_MASTERY) then slow = t.getSlow(self,t) end
+		if self:knowTalent(self.T_DART_LAUNCHER_MASTERY) then slow = self:callTalent(self.T_DART_LAUNCHER_MASTERY, "getSlow") end
 
 		self:project(tg, x, y, function(px, py)
 			local target = game.level.map(px, py, engine.Map.ACTOR)
 			if not target then return nil end
 			self:project(tg, x, y, DamageType.PHYSICAL, t.getDamage(self,t))
-			if (target:canBe("sleep") and target:canBe("poison")) or self:knowTalent(self.T_DART_LAUNCHER_MASTERY) then
+			if target:checkClassification("living") and (self:knowTalent(self.T_DART_LAUNCHER_MASTERY) or target:canBe("sleep") and target:canBe("poison")) then
 				target:setEffect(target.EFF_SEDATED, 4, {src=self, power=t.getSleepPower(self,t), slow=slow, insomnia=20, no_ct_effect=true, apply_power=self:combatAttack()})
 				game.level.map:particleEmitter(target.x, target.y, 1, "generic_charge", {rm=180, rM=200, gm=100, gM=120, bm=30, bM=50, am=70, aM=180})
 			else
-				game.logSeen(self, "%s resists the sleep!", target.name:capitalize())
+				game.logSeen(self, "%s resists the sedation!", target.name:capitalize())
 			end
 
 		end)
 
 		return true
 	end,
+	short_info = function(self, t, slot_talent)
+		return ([[Fire a poisoned dart dealing %0.2f physical damage that puts the target to sleep for 4 turns. 10 turn cooldown.]]):format(t.getDamage(self, slot_talent))
+	end,
 	info = function(self, t)
 		local dam = t.getDamage(self,t)
 		local power = t.getSleepPower(self,t)
-		return ([[Uses a wrist mounted launcher to fire a poisoned dart dealing %0.2f physical damage and putting the target to sleep for 4 turns, rendering them unable to act. Every %d points of damage the target take reduces the duration of the sleeping poison by 1 turn.
-This can be used without breaking stealth.]]):
-	format(damDesc(self, DamageType.PHYSICAL, dam), power)
+		local slot = "not prepared"
+		for slot_id, tool_id in pairs(self.artifice_tools) do
+			if tool_id == t.id then slot = self:getTalentFromId(slot_id).name break end
+		end
+		return ([[Fire a poisoned dart from a silent, concealed launcher on your person that deals %0.2f physical damage and puts the target (living only) to sleep for 4 turns, rendering them unable to act. Every %d points of damage the target takes brings it closer to waking by 1 turn.
+This can be used without breaking stealth.
+#YELLOW#Prepared with: %s#LAST#]]):
+	format(damDesc(self, DamageType.PHYSICAL, dam), power, slot)
 	end,
 }
 
@@ -626,9 +628,12 @@ newTalent{
 	type = {"cunning/tools", 1},
 	mode = "passive",
 	points = 1,
-    getSlow = function(self, t) return self:callTalent(self.T_MASTER_ARTIFICER, "getDLSlow") end,
+	getSlow = function(self, t) return self:combatTalentLimit(self:getTalentFromId(self.T_MASTER_ARTIFICER), 50, 15, 40)/100 end,
+	short_info = function(self, t)
+		return ([[Your darts ignore poison and sleep immunity and waking targets are slowed by %d%% for 4 turns.]]):format(t.getSlow(self, t)*100)
+	end,
 	info = function(self, t)
 		return ([[The sleeping poison of your Dart Launcher becomes potent enough to ignore immunity, and upon waking the target is slowed by %d%% for 4 turns.]]):
 		format(t.getSlow(self, t)*100)
 	end,
-}
\ No newline at end of file
+}
diff --git a/game/modules/tome/data/talents/misc/misc.lua b/game/modules/tome/data/talents/misc/misc.lua
index ac40370605954ac6ff34cdc89d4a12904f51ba1a..66a269f408274043ba6118c9e5a7d186a0d17ced 100644
--- a/game/modules/tome/data/talents/misc/misc.lua
+++ b/game/modules/tome/data/talents/misc/misc.lua
@@ -60,7 +60,7 @@ newTalent{
 		local target = game.level.map(x, y, game.level.map.ACTOR)
 		if not target then
 			if swap then doWardenWeaponSwap(self, t, "bow") end
-			return nil
+			return true -- Make sure this is done if an NPC attacks an emptry grid.
 		end
 
 		local did_alternate = false
diff --git a/game/modules/tome/data/talents/spells/staff-combat.lua b/game/modules/tome/data/talents/spells/staff-combat.lua
index 2b8ce6b1c5bbb9f457901f3bd7b05810c28ee125..c92ee4577b0a75016c866e48eac1fd51d0048de9 100644
--- a/game/modules/tome/data/talents/spells/staff-combat.lua
+++ b/game/modules/tome/data/talents/spells/staff-combat.lua
@@ -35,6 +35,7 @@ newTalent{
 			friendlyblock=false,
 		}
 	end,
+	on_pre_use = function(self, t, silent) if not self:hasStaffWeapon() then if not silent then game.logPlayer(self, "You need a staff to use this spell.") end return false end return true end,
 	getDamageMod = function(self, t) return self:combatTalentWeaponDamage(t, 0.4, 1.1) end,
 	action = function(self, t)
 		local weapon = self:hasStaffWeapon()
@@ -147,6 +148,7 @@ newTalent{
 	tactical = { ATTACK = 1, DISABLE = 2, ESCAPE = 1 },
 	range = 1,
 	requires_target = true,
+	on_pre_use = function(self, t, silent) if not self:hasStaffWeapon() then if not silent then game.logPlayer(self, "You need a staff to use this spell.") end return false end return true end,
 	target = function(self, t)
 		return {type="hit", range=self:getTalentRange(t)}
 	end,