diff --git a/game/engines/default/engine/interface/ActorAI.lua b/game/engines/default/engine/interface/ActorAI.lua
index ba2f754c4d98973526e6ab53311831484c64ff6d..8c88d41165b3736125555fa9730f378b4eb48d0f 100644
--- a/game/engines/default/engine/interface/ActorAI.lua
+++ b/game/engines/default/engine/interface/ActorAI.lua
@@ -123,7 +123,7 @@ function _M:doAI()
 	local target_pos = self.ai_target.actor and self.fov and self.fov.actors and self.fov.actors[self.ai_target.actor]
 	if target_pos then
 		local tx, ty = self:aiSeeTargetPos(self.ai_target.actor)
-		self.ai_state.target_last_seen = {x=tx, y=ty, turn=self.fov_last_turn}
+		self.ai_state.target_last_seen=table.merge(self.ai_state.target_last_seen or {}, {x=tx, y=ty, turn=self.fov_last_turn}) -- Merge to keep obfuscation data
 	end
 
 	return self:runAI(self.ai)
@@ -148,39 +148,43 @@ function _M:setTarget(target, last_seen)
 		self.ai_state.target_last_seen = last_seen
 	else
 		local target_pos = target and self.fov and self.fov.actors and self.fov.actors[self.ai_target.actor] or {x=self.x, y=self.y}
-		self.ai_state.target_last_seen = {x=target_pos.x, y=target_pos.y, turn=game.turn}
+		self.ai_state.target_last_seen=table.merge(self.ai_state.target_last_seen or {}, {x=target_pos.x, y=target_pos.y, turn=game.turn}) -- Merge to keep obfuscation data
 	end
 end
 
 --- Returns the seen coords of the target
 -- This will usually return the exact coords, but if the target is only partially visible (or not at all)
--- it will return estimates, to throw the AI a bit off
+-- it will return estimates, to throw the AI a bit off (up to 10 tiles error)
 -- @param target the target we are tracking
 -- @return x, y coords to move/cast to
 function _M:aiSeeTargetPos(target)
 	if not target then return self.x, self.y end
 	local tx, ty = target.x, target.y
+	local LSeen = self.ai_state.target_last_seen
+	if type(LSeen) ~= "table" then return tx, ty end
 	local spread = 0
+	LSeen.GCache_turn = LSeen.GCache_turn or game.turn -- Guess Cache turn to update position guess (so it's consistent during a turn)
+	LSeen.GCknown_turn = LSeen.GCknown_turn or game.turn -- Guess Cache known turn for spread calculation (self.ai_state.target_last_seen.turn can't be used because it's needed by FOV code)
 
-	-- Adding some type-safety checks, but this isn't fixing the source of the errors
-	if target == self.ai_target.actor and self.ai_state.target_last_seen and type(self.ai_state.target_last_seen) == "table" and self.ai_state.target_last_seen.x and not self:hasLOS(self.ai_state.target_last_seen.x, self.ai_state.target_last_seen.y) then
-		tx, ty = self.ai_state.target_last_seen.x, self.ai_state.target_last_seen.y
-		spread = spread + math.floor((game.turn - (self.ai_state.target_last_seen.turn or game.turn)) / (game.energy_to_act / game.energy_per_tick))
-	end
-	
+	-- Check if target is currently seen
 	local see, chance = self:canSee(target)
-
-	-- Compute the maximum spread if we need to obfuscate 
-	local spread = see and 0 or math.floor((100 - chance) / 10)
-	
-
-	-- We don't know the exact position, so we obfuscate
-	if spread > 0 then
-		tx = tx + rng.range(0, spread * 2) - spread
-		ty = ty + rng.range(0, spread * 2) - spread
-		return util.bound(tx, 0, game.level.map.w - 1), util.bound(ty, 0, game.level.map.h - 1)
-	-- Directly seeing it, no spread at all
+	if see and self:hasLOS(target.x, target.y) then -- canSee doesn't check LOS
+		LSeen.GCache_x, LSeen.GCache_y = nil, nil
+		LSeen.GCknown_turn = game.turn
+		LSeen.GCache_turn = game.turn
 	else
-		return util.bound(tx, 0, game.level.map.w - 1), util.bound(ty, 0, game.level.map.h - 1)
+		if target == self.ai_target.actor and (LSeen.GCache_turn or 0) + 10 <= game.turn and LSeen.x then
+			spread = spread + math.min(10, math.floor((game.turn - (LSeen.GCknown_turn or game.turn)) / (game.energy_to_act / game.energy_per_tick))) -- Limit spread to 10 tiles
+			tx, ty = util.bound(tx + rng.range(-spread, spread), 0, game.level.map.w - 1), util.bound(ty + rng.range(-spread, spread), 0, game.level.map.h - 1)
+			-- Inertial average with last guess: can specify another method here to make the targeting position less random
+			if LSeen.GCache_x then -- update guess with new random position. Could use util.findFreeGrid here at cost of speed
+				tx = math.floor(LSeen.GCache_x + (tx-LSeen.GCache_x)/2)
+				ty = math.floor(LSeen.GCache_y + (ty-LSeen.GCache_y)/2)
+			end
+			LSeen.GCache_x, LSeen.GCache_y = tx, ty
+			LSeen.GCache_turn = game.turn
+		end
+		if LSeen.GCache_x then return LSeen.GCache_x, LSeen.GCache_y end
 	end
-end
+	return tx, ty -- Fall through to correct coords
+end
\ No newline at end of file
diff --git a/game/modules/tome/class/GameState.lua b/game/modules/tome/class/GameState.lua
index 581e67f85e1b638e80e0ba0c2db4e9641adfbc48..0a055641a247c8e708709338174feec56b1ba95b 100644
--- a/game/modules/tome/class/GameState.lua
+++ b/game/modules/tome/class/GameState.lua
@@ -355,6 +355,7 @@ function _M:generateRandart(data)
 	-- Add ego properties
 	-----------------------------------------------------------
 	local nb_egos = data.egos or 3
+	local gr_egos = data.greater_egos_bias or math.floor(nb_egos*2/3) -- 2/3 greater egos by default
 	if o.egos and nb_egos > 0 then
 		local legos = {}
 		local been_greater = 0
@@ -365,7 +366,7 @@ function _M:generateRandart(data)
 			local egos = rng.table(legos)
 			local list = {}
 			local filter = nil
-			if rng.percent(lev) and been_greater < 2 then been_greater = been_greater + 1 filter = function(e) return e.greater_ego end end
+			if rng.percent(100*lev/(lev+50)) and been_greater < gr_egos then been_greater = been_greater + 1 filter = function(e) return e.greater_ego end end --RE Phase out (but don't eliminate) lesser egos with level
 			for z = 1, #egos do list[#list+1] = egos[z].e end
 
 			local ef = self:egoFilter(game.zone, game.level, "object", "randartego", o, {special=filter, forbid_power_source=data.forbid_power_source, power_source=data.power_source}, list, {})
@@ -1266,6 +1267,32 @@ function _M:entityFilterPost(zone, level, type, e, filter)
 					end
 					if data.user_post then data.user_post(b, data) end
 				end,
+				post = function(b, data)
+					if data.level <= 20 then
+						b.inc_damage = b.inc_damage or {}
+						b.inc_damage.all = (b.inc_damage.all or 0) - 40 * (20 - data.level + 1) / 20
+					end
+
+					-- Drop
+					for i = 1, data.nb_rares do -- generate rares as weak (1 ego) randarts
+						local fil = {lev=lev, egos=1, greater_egos_bias = 0, forbid_power_source=b.not_power_source,
+							base_filter = {no_tome_drops=true, ego_filter={keep_egos=true, ego_chance=-1000}, 
+							special=function(e)
+								return (not e.unique and e.randart_able) and (not e.material_level or e.material_level >= 1) and true or false
+							end}
+						}
+						local o = game.state:generateRandart(fil,nil, true)
+						if o then
+--							print("[entityFilterPost]: Generated random object for", tostring(b.name)) --RE
+							o.unique, o.randart, o.rare = nil, nil, true
+							b:addObject(b.INVEN_INVEN, o)
+							game.zone:addEntity(game.level, o, "object")
+						else
+							print("[entityFilterPost]: Failed to generate random object for", tostring(b.name))
+						end
+					end
+					if data.user_post then data.user_post(b, data) end
+				end,
 			}
 			e = self:createRandomBoss(e, table.merge(base, filter.random_elite, true))
 		end
@@ -1273,6 +1300,8 @@ function _M:entityFilterPost(zone, level, type, e, filter)
 		if filter.random_object and not e.unique and e.randart_able then
 			local data = _G.type(filter.random_object) == "table" and filter.random_object or {}
 			local lev = math.max(1, game.zone:level_adjust_level(game.level, game.zone, "object"))
+			print("[entityFilterPost]: Generating obsolete random_object")
+			print(debug.traceback())
 			e = game.state:generateRandart{
 				lev = lev,
 				egos = 0,
diff --git a/game/modules/tome/class/NPC.lua b/game/modules/tome/class/NPC.lua
index be6ebd9468f1601c24d7b77ba0b7fea644cc724a..f3e6edbe59f52f773517ebf0d5dc079f97a48fe2 100644
--- a/game/modules/tome/class/NPC.lua
+++ b/game/modules/tome/class/NPC.lua
@@ -409,14 +409,28 @@ function _M:tooltip(x, y, seen_by)
 	local str = mod.class.Actor.tooltip(self, x, y, seen_by)
 	if not str then return end
 	local killed = game:getPlayer(true).all_kills and (game:getPlayer(true).all_kills[self.name] or 0) or 0
-
+	local target = self.ai_target.actor
+	
 	str:add(
 		true,
 		("Killed by you: %s"):format(killed), true,
-		"Target: ", self.ai_target.actor and self.ai_target.actor.name or "none"
+		"Target: ", target and target.name or "none"
 	)
-	if config.settings.cheat then str:add(true, "UID: "..self.uid, true, self.image) end
-
+	-- Give hints to stealthed/invisible players about where the NPC is looking (if they have LOS)
+	if target == game.player and (game.player:attr("stealth") or game.player:attr("invisible")) and game.player:hasLOS(self.x, self.y) then
+		local tx, ty = self:aiSeeTargetPos(self.ai_target.actor)
+		local dx, dy = tx - self.ai_target.actor.x, ty - self.ai_target.actor.y
+		local offset = engine.Map:compassDirection(dx, dy)
+		if offset then
+			str:add(" looking " ..offset)
+			if config.settings.cheat then str:add((" (%+d, %+d)"):format(dx, dy)) end
+		else
+			str:add(" looking at you.")
+		end
+	end
+	if config.settings.cheat then
+		str:add(true, "UID: "..self.uid, true, self.image)
+	end
 	return str
 end
 
diff --git a/game/modules/tome/class/Player.lua b/game/modules/tome/class/Player.lua
index f6224d44c7e9fec1cde6c834fc0498a8c088c30a..216ffe6468de26a3efe307850edfe43e1d62ec61 100644
--- a/game/modules/tome/class/Player.lua
+++ b/game/modules/tome/class/Player.lua
@@ -154,6 +154,14 @@ function _M:onEnterLevel(zone, level)
 		end
 	end
 	for i, eff_id in ipairs(effs) do self:removeEffect(eff_id) end
+
+	-- Clear existing player created effects on the map
+	for i, eff in ipairs(level.map.effects) do
+		if eff.src and eff.src == game.player then
+			eff.duration = 0
+			eff.grids = {}
+		end
+	end
 end
 
 function _M:onEnterLevelEnd(zone, level)
diff --git a/game/modules/tome/data/talents/cunning/traps.lua b/game/modules/tome/data/talents/cunning/traps.lua
index cdb5f53cb510e34b861d4032f1b66705eb2789e6..881f45ff9f16f0ff1ca90341f8bf1a6814b1e99a 100644
--- a/game/modules/tome/data/talents/cunning/traps.lua
+++ b/game/modules/tome/data/talents/cunning/traps.lua
@@ -648,6 +648,7 @@ newTalent{
 		if not x or not y then return nil end
 		local _ _, x, y = self:canProject(tg, x, y)
 		if game.level.map(x, y, Map.TRAP) then game.logPlayer(self, "You somehow fail to set the trap.") return nil end
+		if game.level.map:checkEntity(x, y, Map.TERRAIN, "block_move") then game.logPlayer(self, "You somehow fail to set the trap.") return nil end
 
 		local dam = t.getDamage(self, t)
 		-- Need to pass the actor in to the triggered function for the apply_power to work correctly
diff --git a/game/modules/tome/data/talents/misc/races.lua b/game/modules/tome/data/talents/misc/races.lua
index e97c39f47acdbc251eb4a117706f2b3e60be91bc..37bd92e09480a97f5315a909af3237534ff3014c 100644
--- a/game/modules/tome/data/talents/misc/races.lua
+++ b/game/modules/tome/data/talents/misc/races.lua
@@ -358,7 +358,7 @@ newTalent{
 			local x, y = util.findFreeGrid(tx, ty, 5, true, {[Map.ACTOR]=true})
 			if not x then
 				game.logPlayer(self, "Not enough space to summon!")
-				return
+				if i == 1 then return else break end
 			end
 
 			local NPC = require "mod.class.NPC"
diff --git a/game/modules/tome/data/talents/spells/aether.lua b/game/modules/tome/data/talents/spells/aether.lua
index e7e152f578c3baa651b2deab65bc5ad8826edd08..a783f57458212696035e626b1a0815b5761dfcbf 100644
--- a/game/modules/tome/data/talents/spells/aether.lua
+++ b/game/modules/tome/data/talents/spells/aether.lua
@@ -62,6 +62,7 @@ newTalent{
 		if not x or not y then return nil end
 		local _ _, x, y = self:canProject(tg, x, y)
 		if game.level.map(x, y, Map.TRAP) then game.logPlayer(self, "You somehow fail to set the aether beam.") return nil end
+		if game.level.map:checkEntity(x, y, Map.TERRAIN, "block_move") then game.logPlayer(self, "You somehow fail to set the aether beam.") return nil end
 
 		local t = basetrap(self, t, x, y, 44, {
 			type = "aether", name = "aether beam", color=colors.VIOLET, image = "trap/trap_glyph_explosion_01_64.png",