From 2b1c997c74b7657f4a347ca70ae1976f347b316e Mon Sep 17 00:00:00 2001
From: dg <dg@51575b47-30f0-44d4-a5cc-537603b46e54>
Date: Sat, 19 Mar 2011 12:38:22 +0000
Subject: [PATCH] Better running algorithm Recalling will fail when you can not
 move, to prevent being stuck on the worldmap

git-svn-id: http://svn.net-core.org/repos/t-engine4@3032 51575b47-30f0-44d4-a5cc-537603b46e54
---
 .../default/engine/interface/PlayerRun.lua    | 248 +++++++++++++-----
 game/modules/tome/class/Player.lua            |  27 +-
 .../data/general/objects/quest-artifacts.lua  |   2 +-
 .../tome/data/maps/towns/gates-of-morning.lua |  30 +--
 .../tome/data/maps/towns/last-hope.lua        |  36 +--
 game/modules/tome/data/talents/misc/misc.lua  |   2 +-
 game/modules/tome/data/timed_effects.lua      |   4 +-
 7 files changed, 243 insertions(+), 106 deletions(-)

diff --git a/game/engines/default/engine/interface/PlayerRun.lua b/game/engines/default/engine/interface/PlayerRun.lua
index 1f9e7febce..767f1520a2 100644
--- a/game/engines/default/engine/interface/PlayerRun.lua
+++ b/game/engines/default/engine/interface/PlayerRun.lua
@@ -26,59 +26,58 @@ module(..., package.seeall, class.make)
 
 local sides =
 {
-	[1] = {left=2, right=4},
-	[2] = {left=3, right=1},
-	[3] = {left=6, right=2},
-	[4] = {left=1, right=7},
-	[6] = {left=9, right=3},
-	[7] = {left=4, right=8},
-	[8] = {left=7, right=9},
-	[9] = {left=8, right=6},
-}
-
-local turn =
-{
-	[1] = {left=3, right=7},
-	[2] = {left=6, right=4},
-	[3] = {left=9, right=1},
-	[4] = {left=2, right=8},
-	[6] = {left=8, right=2},
-	[7] = {left=1, right=9},
-	[8] = {left=4, right=6},
-	[9] = {left=7, right=3},
+	[1] = {hard_left=3, soft_left=2, soft_right=4, hard_right=7},
+	[4] = {hard_left=2, soft_left=1, soft_right=7, hard_right=8},
+	[7] = {hard_left=1, soft_left=4, soft_right=8, hard_right=9},
+	[8] = {hard_left=4, soft_left=7, soft_right=9, hard_right=6},
+	[9] = {hard_left=7, soft_left=8, soft_right=6, hard_right=3},
+	[6] = {hard_left=8, soft_left=9, soft_right=3, hard_right=2},
+	[3] = {hard_left=9, soft_left=6, soft_right=2, hard_right=1},
+	[2] = {hard_left=6, soft_left=3, soft_right=1, hard_right=4},
 }
 
 local function checkDir(a, dir, dist)
 	dist = dist or 1
 	local dx, dy = dir_to_coord[dir][1], dir_to_coord[dir][2]
 	local x, y = a.x + dx * dist, a.y + dy * dist
-	return game.level.map:checkAllEntities(x, y, "block_move", a) and true or false
-end
-local function isEdge(a, dir, dist)
-	dist = dist or 1
-	local dx, dy = dir_to_coord[dir][1], dir_to_coord[dir][2]
-	local x, y = a.x + dx * dist, a.y + dy * dist
-	return not game.level.map:isBound(x, y) and true or false
+	-- don't treat other actors as terrain or as something to notice (let the module handle this)
+	if game.level.map(x, y, game.level.map.ACTOR) and not game.level.map:checkEntity(x, y, game.level.map.TERRAIN, "block_move") then return false end
+	return (game.level.map:checkAllEntities(x, y, "block_move", a) or not game.level.map:isBound(x, y)) and true or false
 end
 
 --- Initializes running
 -- We check the direction sides to know if we are in a tunnel, along a wall or in open space.
 function _M:runInit(dir)
-	local block_left, block_right = false, false
-
-	-- Check sides
-	if checkDir(self, sides[dir].left) then block_left = true end
-	if checkDir(self, sides[dir].right) then block_right = true end
+	if checkDir(self, dir) then return end
 
 	self.running = {
 		dir = dir,
-		block_left = block_left,
-		block_right = block_right,
+		block_left = false,
+		block_right = false,
+		block_hard_left = false,
+		block_hard_right = false,
 		cnt = 1,
 		dialog = Dialog:simplePopup("Running...", "You are running, press Enter to stop.", function()
 			self:runStop()
 		end, false, true),
 	}
+
+	-- Check sides
+	if checkDir(self, sides[dir].hard_left) then self.running.block_hard_left = true end
+	if checkDir(self, sides[dir].hard_right) then self.running.block_hard_right = true end
+
+	if checkDir(self, sides[dir].soft_left) then
+		self.running.block_left = true
+	else
+		self.running.ignore_left = 2
+	end
+
+	if checkDir(self, sides[dir].soft_right) then
+		self.running.block_right = true
+	else
+		self.running.ignore_right = 2
+	end
+
 	self.running.dialog.__showup = nil
 	self.running.dialog.__hidden = true
 
@@ -132,12 +131,50 @@ function _M:runStep()
 		return false
 	else
 		local oldx, oldy = self.x, self.y
+		local dir_is_cardinal = self.running.dir == 2 or self.running.dir == 4 or self.running.dir == 6 or self.running.dir == 8
 		if self.running.path then
 			if not self.running.path[self.running.cnt] then self:runStop()
 			else self:move(self.running.path[self.running.cnt].x, self.running.path[self.running.cnt].y) end
 		else
-			if isEdge(self, self.running.dir) then self:runStop()
-			else self:moveDir(self.running.dir) end
+			-- Try to move around known traps if possible
+			local dx, dy = dir_to_coord[self.running.dir][1], dir_to_coord[self.running.dir][2]
+			local x, y = self.x + dx, self.y + dy
+			local trap = game.level.map(x, y, game.level.map.TRAP)
+			if trap and trap:knownBy(self) then
+				-- Take a phantom step forward and check path; backup current data first
+				local running_bak = table.clone(self.running)
+				self.x, self.y = x, y
+				local ret2, msg2 = self:runCheck(true) -- don't remember other items or traps from phantom steps
+				if self.running.dir == sides[running_bak.dir].hard_left then
+					running_bak.dir = sides[running_bak.dir].soft_left
+				elseif self.running.dir == sides[running_bak.dir].hard_right then
+					running_bak.dir = sides[running_bak.dir].soft_right
+				else
+					ret2 = false
+				end
+				if self.running.ignore_left then
+					running_bak.ignore_left = running_bak.ignore_left - 1
+					if running_bak.ignore_left <= 0 then running_bak.ignore_left = nil end
+					if checkDir(self, sides[self.running.dir].soft_left) and (not checkDir(self, self.running.dir) or not dir_is_cardinal) then running_bak.block_left = true end
+				end
+				if self.running.ignore_right then
+					running_bak.ignore_right = running_bak.ignore_right - 1
+					if running_bak.ignore_right <= 0 then running_bak.ignore_right = nil end
+					if checkDir(self, sides[self.running.dir].soft_right) and (not checkDir(self, self.running.dir) or not dir_is_cardinal) then running_bak.block_right = true end
+				end
+				if self.running.block_left then running_bak.ignore_left = nil end
+				if self.running.block_right then running_bak.ignore_right = nil end
+				-- Put data back
+				self.x, self.y = oldx, oldy
+				self.running = running_bak
+				-- Can't run around the trap
+				if not ret2 then
+					self:runStop("trap spotted")
+					return false
+				end
+			end
+			-- Move!
+			self:moveDir(self.running.dir)
 		end
 		self:runMoved()
 
@@ -147,17 +184,17 @@ function _M:runStep()
 		if not self.running then return false end
 		self.running.cnt = self.running.cnt + 1
 
-		if self.running.newdir then
-			self.running.dir = self.running.newdir
-			self.running.newdir = nil
-		end
+		if self.running.block_left then self.running.ignore_left = nil end
 		if self.running.ignore_left then
 			self.running.ignore_left = self.running.ignore_left - 1
 			if self.running.ignore_left <= 0 then self.running.ignore_left = nil end
+			if checkDir(self, sides[self.running.dir].soft_left) and (not checkDir(self, self.running.dir) or not dir_is_cardinal) then self.running.block_left = true end
 		end
+		if self.running.block_right then self.running.ignore_right = nil end
 		if self.running.ignore_right then
 			self.running.ignore_right = self.running.ignore_right - 1
 			if self.running.ignore_right <= 0 then self.running.ignore_right = nil end
+			if checkDir(self, sides[self.running.dir].soft_right) and (not checkDir(self, self.running.dir) or not dir_is_cardinal) then self.running.block_right = true end
 		end
 
 		return true
@@ -171,27 +208,109 @@ end
 -- @return true if we can continue to run, false otherwise
 function _M:runCheck()
 	if not self.running.path then
+		local dir_is_cardinal = self.running.dir == 2 or self.running.dir == 4 or self.running.dir == 6 or self.running.dir == 8
+		local blocked_ahead = checkDir(self, self.running.dir)
+		local blocked_soft_left = checkDir(self, sides[self.running.dir].soft_left)
+		local blocked_hard_left = checkDir(self, sides[self.running.dir].hard_left)
+		local blocked_soft_right = checkDir(self, sides[self.running.dir].soft_right)
+		local blocked_hard_right = checkDir(self, sides[self.running.dir].hard_right)
+
 		-- Do we change run direction ? We can only choose to change for left or right, never backwards.
 		-- We must also be in a tunnel (both sides blocked)
-		if self.running.block_left and self.running.block_right then
-			-- Turn left
-			if not checkDir(self, self.running.dir) and checkDir(self, self.running.dir, 2) and not checkDir(self, sides[self.running.dir].left) and checkDir(self, sides[self.running.dir].right) then
-				self.running.newdir = turn[self.running.dir].left
-				self.running.ignore_left = 2
-				return true
-			end
-
-			-- Turn right
-			if not checkDir(self, self.running.dir) and checkDir(self, self.running.dir, 2) and checkDir(self, sides[self.running.dir].left) and not checkDir(self, sides[self.running.dir].right) then
-				self.running.newdir = turn[self.running.dir].right
-				self.running.ignore_right = 2
-				return true
+		if (self.running.block_left or self.running.ignore_left) and (self.running.block_right or self.running.ignore_right) then
+			if blocked_ahead then
+				if blocked_soft_right and (blocked_hard_right or self.running.ignore_right) then
+					local blocked_back_left = checkDir(self, sides[sides[self.running.dir].hard_left].soft_left)
+					-- Turn soft left
+					if not blocked_soft_left and (blocked_hard_left or not dir_is_cardinal) then
+						if not dir_is_cardinal and not blocked_hard_left and not (checkDir(self, sides[self.running.dir].soft_left, 2) and blocked_back_left) then
+							return false, "terrain changed ahead"
+						end
+						self.running.dir = sides[self.running.dir].soft_left
+						self.running.block_right = true
+						if blocked_hard_left then self.running.block_left = true end
+						return true
+					end
+					-- Turn hard left
+					if not blocked_hard_left and (not self.running.ignore_left or (self.running.block_hard_left and self.running.block_right)) then
+						if dir_is_cardinal and not blocked_soft_left and not checkDir(self, sides[self.running.dir].hard_left, 2) then
+							return false, "terrain change on the left"
+						end
+						if not dir_is_cardinal and not blocked_back_left then
+							return false, "terrain ahead blocks"
+						end
+						self.running.dir = sides[self.running.dir].hard_left
+						if self.running.block_hard_left and self.running.ignore_left and self.running.ignore_left == 1 then
+							self.running.block_left = true
+						end
+						return true
+					end
+				end
+
+				if blocked_soft_left and (blocked_hard_left or self.running.ignore_left) then
+					local blocked_back_right = checkDir(self, sides[sides[self.running.dir].hard_right].soft_right)
+					-- Turn soft right
+					if not blocked_soft_right and (blocked_hard_right or not dir_is_cardinal) then
+						if not dir_is_cardinal and not blocked_hard_right and not (checkDir(self, sides[self.running.dir].soft_right, 2) and blocked_back_right) then
+							return false, "terrain changed ahead"
+						end
+						self.running.dir = sides[self.running.dir].soft_right
+						self.running.block_left = true
+						if blocked_hard_right then self.running.block_right = true end
+						return true
+					end
+					-- Turn hard right
+					if not blocked_hard_right and (not self.running.ignore_right or (self.running.block_hard_right and self.running.block_left)) then
+						if dir_is_cardinal and not blocked_soft_right and not checkDir(self, sides[self.running.dir].hard_right, 2) then
+							return false, "terrain change on the right"
+						end
+						if not dir_is_cardinal and not blocked_back_right then
+							return false, "terrain ahead blocks"
+						end
+						self.running.dir = sides[self.running.dir].hard_right
+						if self.running.block_hard_right and self.running.ignore_right and self.running.ignore_right == 1 then
+							self.running.block_right = true
+						end
+						return true
+					end
+				end
+			else
+				-- Favor cardinal directions if possible, otherwise we may miss something interesting
+				if not dir_is_cardinal then
+					-- Turn soft left
+					if blocked_soft_right and blocked_hard_left and not blocked_soft_left and checkDir(self, sides[self.running.dir].soft_left, 2) and (not self.running.ignore_left or self.running.ignore_left ~= 2) then
+						self.running.dir = sides[self.running.dir].soft_left
+						self.running.block_left = true
+						self.running.block_right = true
+						return true
+					end
+					-- Turn soft right
+					if blocked_soft_left and blocked_hard_right and not blocked_soft_right and checkDir(self, sides[self.running.dir].soft_right, 2) and (not self.running.ignore_right or self.running.ignore_right ~= 2) then
+						self.running.dir = sides[self.running.dir].soft_right
+						self.running.block_left = true
+						self.running.block_right = true
+						return true
+					end
+				end
+				if checkDir(self, self.running.dir, 2) then
+					if not dir_is_cardinal and ((self.running.block_left and not blocked_hard_left and not self.running.ignore_left) or (self.running.block_right and not blocked_hard_right and not self.running.ignore_right)) then
+						return false, "terrain changed ahead"
+					end
+					-- Continue forward so we may turn
+					if (blocked_soft_left and not blocked_soft_right) or (blocked_soft_right and not blocked_soft_left) then return true end
+				end
 			end
 		end
-
-		if not self.running.ignore_left and self.running.block_left ~= checkDir(self, sides[self.running.dir].left) then return false, "terrain change on left side" end
-		if not self.running.ignore_right and self.running.block_right ~= checkDir(self, sides[self.running.dir].right) then return false, "terrain change on right side" end
-		if checkDir(self, self.running.dir) then return false, "terrain ahead blocks" end
+		
+		if not self.running.ignore_left and (self.running.block_left ~= blocked_soft_left or self.running.block_left ~= blocked_hard_left) then
+			return false, "terrain change on left side"
+		end
+		if not self.running.ignore_right and (self.running.block_right ~= blocked_soft_right or self.running.block_right ~= blocked_hard_right) then
+			return false, "terrain change on right side"
+		end
+		if blocked_ahead then
+			return false, "terrain ahead blocks"
+		end
 	end
 
 	return true
@@ -221,14 +340,25 @@ function _M:runScan(fct)
 		fct(x, y, "ahead")
 
 		-- Ahead left
-		local dx, dy = dir_to_coord[sides[self.running.dir].left][1], dir_to_coord[sides[self.running.dir].left][2]
+		local dx, dy = dir_to_coord[sides[self.running.dir].soft_left][1], dir_to_coord[sides[self.running.dir].soft_left][2]
 		local x, y = self.x + dx, self.y + dy
 		fct(x, y, "ahead left")
 
 		-- Ahead right
-		local dx, dy = dir_to_coord[sides[self.running.dir].right][1], dir_to_coord[sides[self.running.dir].right][2]
+		local dx, dy = dir_to_coord[sides[self.running.dir].soft_right][1], dir_to_coord[sides[self.running.dir].soft_right][2]
 		local x, y = self.x + dx, self.y + dy
 		fct(x, y, "ahead right")
+
+		-- Left
+		local dx, dy = dir_to_coord[sides[self.running.dir].hard_left][1], dir_to_coord[sides[self.running.dir].hard_left][2]
+		local x, y = self.x + dx, self.y + dy
+		fct(x, y, "left")
+
+		-- Right
+		local dx, dy = dir_to_coord[sides[self.running.dir].hard_right][1], dir_to_coord[sides[self.running.dir].hard_right][2]
+		local x, y = self.x + dx, self.y + dy
+		fct(x, y, "right")
+
 	elseif self.running.path[self.running.cnt] then
 		-- Ahead
 		local x, y = self.running.path[self.running.cnt].x, self.running.path[self.running.cnt].y
diff --git a/game/modules/tome/class/Player.lua b/game/modules/tome/class/Player.lua
index f9a8e10391..1eb931cd52 100644
--- a/game/modules/tome/class/Player.lua
+++ b/game/modules/tome/class/Player.lua
@@ -522,8 +522,10 @@ function _M:restCheck()
 end
 
 --- Can we continue running?
--- We can run if no hostiles are in sight, and if we no interesting terrains are next to us
-function _M:runCheck()
+-- We can run if no hostiles are in sight, and if no interesting terrain or characters are next to us.
+-- Known traps aren't interesting.  We let the engine run around traps, or stop if it can't.
+-- 'ignore_memory' is only used when checking for paths around traps.  This ensures we don't remember items "obj_seen" that we aren't supposed to
+function _M:runCheck(ignore_memory)
 	local spotted = spotHostiles(self)
 	if spotted then return false, ("hostile spotted (%s%s)"):format(spotted.actor.name, game.level.map:isOnScreen(spotted.x, spotted.y) and "" or " - offscreen") end
 
@@ -532,22 +534,27 @@ function _M:runCheck()
 	-- Notice any noticeable terrain
 	local noticed = false
 	self:runScan(function(x, y, what)
-		-- Only notice interesting terrains
-		local grid = game.level.map(x, y, Map.TERRAIN)
-		if grid and grid.notice then noticed = "interesting terrain" end
-
 		-- Objects are always interesting, only on curent spot
 		if what == "self" and not game.level.map.attrs(x, y, "obj_seen") then
 			local obj = game.level.map:getObject(x, y, 1)
 			if obj then
 				noticed = "object seen"
-				game.level.map.attrs(x, y, "obj_seen", true)
+				if not ignore_memory then game.level.map.attrs(x, y, "obj_seen", true) end
+				return
 			end
 		end
 
-		-- Traps are always interesting if known
-		local trap = game.level.map(x, y, Map.TRAP)
-		if trap and trap:knownBy(self) then noticed = "trap spotted" end
+		-- Only notice interesting terrains
+		local grid = game.level.map(x, y, Map.TERRAIN)
+		if grid and grid.notice then noticed = "interesting terrain"; return end
+		if grid and grid.type and grid.type == "store" then noticed = "store entrance spotted"; return end
+
+		-- Only notice interesting characters
+		local actor = game.level.map(x, y, Map.ACTOR)
+		if actor and actor.can_talk then noticed = "interesting character"; return end
+
+		-- We let the engine take care of traps, but we should still notice "trap" stores.
+		if game.level.map:checkAllEntities(x, y, "store") then noticed = "store entrance spotted"; return end
 	end)
 	if noticed then return false, noticed end
 
diff --git a/game/modules/tome/data/general/objects/quest-artifacts.lua b/game/modules/tome/data/general/objects/quest-artifacts.lua
index 3be1449c48..34f3bd1d59 100644
--- a/game/modules/tome/data/general/objects/quest-artifacts.lua
+++ b/game/modules/tome/data/general/objects/quest-artifacts.lua
@@ -314,7 +314,7 @@ You have heard of such items before, they are very useful to adventurers, allowi
 	max_power = 1000, power_regen = 1,
 	use_power = { name = "recall the user to the worldmap", power = 1000,
 		use = function(self, who)
-			if who:canBe("worldport") then
+			if who:canBe("worldport") and not self:attr("never_move") then
 				who:setEffect(who.EFF_RECALL, 40, {})
 				game.logPlayer(who, "Space around you starts to dissolve...")
 			else
diff --git a/game/modules/tome/data/maps/towns/gates-of-morning.lua b/game/modules/tome/data/maps/towns/gates-of-morning.lua
index 3b54e4b505..4f9ad0028b 100644
--- a/game/modules/tome/data/maps/towns/gates-of-morning.lua
+++ b/game/modules/tome/data/maps/towns/gates-of-morning.lua
@@ -56,21 +56,21 @@ defineTile('s', "FLOOR", nil, mod.class.NPC.new{
 	can_quest = true,
 })
 
-quickEntity('1', {show_tooltip=true, name="Closed store", display='1', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"}, {no_teleport=true})
-quickEntity('2', {show_tooltip=true, name="Armour Smith", display='2', color=colors.UMBER, resolvers.store("ARMOR"), image="terrain/wood_store_armor.png"}, {no_teleport=true})
-quickEntity('3', {show_tooltip=true, name="Weapon Smith", display='3', color=colors.UMBER, resolvers.store("WEAPON"), image="terrain/wood_store_weapon.png"}, {no_teleport=true})
-quickEntity('4', {show_tooltip=true, name="Alchemist", display='4', color=colors.LIGHT_BLUE, resolvers.store("POTION"), image="terrain/wood_store_potion.png"}, {no_teleport=true})
-quickEntity('5', {show_tooltip=true, name="Scribe", display='5', color=colors.WHITE, resolvers.store("SCROLL"), resolvers.chatfeature("magic-store"), image="terrain/wood_store_book.png"}, {no_teleport=true})
-quickEntity('6', {show_tooltip=true, name="Closed store", display='6', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"}, {no_teleport=true})
-quickEntity('7', {show_tooltip=true, name="Closed store", display='7', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"}, {no_teleport=true})
-quickEntity('8', {show_tooltip=true, name="Closed store", display='8', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"}, {no_teleport=true})
-quickEntity('9', {show_tooltip=true, name="Closed store", display='9', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"}, {no_teleport=true})
-quickEntity('0', {show_tooltip=true, name="Closed store", display='0', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"}, {no_teleport=true})
-quickEntity('a', {show_tooltip=true, name="Closed store", display='*', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"}, {no_teleport=true})
-quickEntity('b', {show_tooltip=true, name="Closed store", display='*', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"}, {no_teleport=true})
-quickEntity('c', {show_tooltip=true, name="Closed store", display='*', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"}, {no_teleport=true})
-quickEntity('d', {show_tooltip=true, name="Closed store", display='*', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"}, {no_teleport=true})
-quickEntity('e', {show_tooltip=true, name="Zemekkys Home", display='+', color=colors.LIGHT_UMBER, resolvers.chatfeature("zemekkys"), image="terrain/wood_store_closed.png"}, {no_teleport=true})
+quickEntity('1', {type="store", show_tooltip=true, name="Closed store", display='1', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"}, {no_teleport=true})
+quickEntity('2', {type="store", show_tooltip=true, name="Armour Smith", display='2', color=colors.UMBER, resolvers.store("ARMOR"), image="terrain/wood_store_armor.png"}, {no_teleport=true})
+quickEntity('3', {type="store", show_tooltip=true, name="Weapon Smith", display='3', color=colors.UMBER, resolvers.store("WEAPON"), image="terrain/wood_store_weapon.png"}, {no_teleport=true})
+quickEntity('4', {type="store", show_tooltip=true, name="Alchemist", display='4', color=colors.LIGHT_BLUE, resolvers.store("POTION"), image="terrain/wood_store_potion.png"}, {no_teleport=true})
+quickEntity('5', {type="store", show_tooltip=true, name="Scribe", display='5', color=colors.WHITE, resolvers.store("SCROLL"), resolvers.chatfeature("magic-store"), image="terrain/wood_store_book.png"}, {no_teleport=true})
+quickEntity('6', {type="store", show_tooltip=true, name="Closed store", display='6', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"}, {no_teleport=true})
+quickEntity('7', {type="store", show_tooltip=true, name="Closed store", display='7', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"}, {no_teleport=true})
+quickEntity('8', {type="store", show_tooltip=true, name="Closed store", display='8', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"}, {no_teleport=true})
+quickEntity('9', {type="store", show_tooltip=true, name="Closed store", display='9', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"}, {no_teleport=true})
+quickEntity('0', {type="store", show_tooltip=true, name="Closed store", display='0', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"}, {no_teleport=true})
+quickEntity('a', {type="store", show_tooltip=true, name="Closed store", display='*', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"}, {no_teleport=true})
+quickEntity('b', {type="store", show_tooltip=true, name="Closed store", display='*', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"}, {no_teleport=true})
+quickEntity('c', {type="store", show_tooltip=true, name="Closed store", display='*', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"}, {no_teleport=true})
+quickEntity('d', {type="store", show_tooltip=true, name="Closed store", display='*', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"}, {no_teleport=true})
+quickEntity('e', {type="store", show_tooltip=true, name="Zemekkys Home", display='+', color=colors.LIGHT_UMBER, resolvers.chatfeature("zemekkys"), image="terrain/wood_store_closed.png"}, {no_teleport=true})
 
 startx = 0
 starty = 27
diff --git a/game/modules/tome/data/maps/towns/last-hope.lua b/game/modules/tome/data/maps/towns/last-hope.lua
index 35daee7fe4..5b68959881 100644
--- a/game/modules/tome/data/maps/towns/last-hope.lua
+++ b/game/modules/tome/data/maps/towns/last-hope.lua
@@ -34,25 +34,25 @@ quickEntity(',', {name='dirt', display='.', color=colors.LIGHT_UMBER, image="ter
 quickEntity('I', {name='tunneled wall', show_tooltip=true, display='#', color=colors.WHITE, image="terrain/wood_wall1.png"})
 quickEntity('M', {name='tunneled hills', show_tooltip=true, display='^', color=colors.SLATE, image="terrain/mountain.png"})
 
-quickEntity('1', {show_tooltip=true, name="Closed store", display='1', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"})
-quickEntity('2', {show_tooltip=true, name="Armour Smith", display='2', color=colors.UMBER, resolvers.store("ARMOR"), image="terrain/wood_store_armor.png"})
-quickEntity('3', {show_tooltip=true, name="Weapon Smith", display='3', color=colors.UMBER, resolvers.store("WEAPON"), resolvers.chatfeature("last-hope-weapon-store"), image="terrain/wood_store_weapon.png"})
-quickEntity('4', {show_tooltip=true, name="Alchemist", display='4', color=colors.LIGHT_BLUE, resolvers.store("POTION"), image="terrain/wood_store_potion.png"})
-quickEntity('5', {show_tooltip=true, name="Scribe", display='5', color=colors.WHITE, resolvers.store("SCROLL"), resolvers.chatfeature("magic-store"), image="terrain/wood_store_book.png"})
-quickEntity('6', {show_tooltip=true, name="Closed store", display='6', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"})
-quickEntity('7', {show_tooltip=true, name="Closed store", display='7', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"})
-quickEntity('8', {show_tooltip=true, name="Closed store", display='8', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"})
-quickEntity('9', {show_tooltip=true, name="Closed store", display='9', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"})
-quickEntity('0', {show_tooltip=true, name="Closed store", display='0', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"})
-quickEntity('a', {show_tooltip=true, name="Closed store", display='*', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"})
-quickEntity('b', {show_tooltip=true, name="Hall of the King", display='*', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"})
-quickEntity('c', {show_tooltip=true, name="Library", display='*', color=colors.LIGHT_RED, resolvers.store("LAST_HOPE_LIBRARY"), image="terrain/wood_store_book.png"})
-quickEntity('d', {show_tooltip=true, name="Closed store", display='*', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"})
-quickEntity('e', {show_tooltip=true, name="Rare goods", display='*', color=colors.AQUAMARINE, resolvers.store("LOST_MERCHANT"), resolvers.chatfeature("last-hope-lost-merchant"), image="terrain/wood_store_weapon.png"})
-quickEntity('g', {show_tooltip=true, name="Rich merchant", display='*', color=colors.AQUAMARINE, resolvers.chatfeature("last-hope-melinda-father"), image="terrain/wood_store_closed.png"})
+quickEntity('1', {type="store", show_tooltip=true, name="Closed store", display='1', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"})
+quickEntity('2', {type="store", show_tooltip=true, name="Armour Smith", display='2', color=colors.UMBER, resolvers.store("ARMOR"), image="terrain/wood_store_armor.png"})
+quickEntity('3', {type="store", show_tooltip=true, name="Weapon Smith", display='3', color=colors.UMBER, resolvers.store("WEAPON"), resolvers.chatfeature("last-hope-weapon-store"), image="terrain/wood_store_weapon.png"})
+quickEntity('4', {type="store", show_tooltip=true, name="Alchemist", display='4', color=colors.LIGHT_BLUE, resolvers.store("POTION"), image="terrain/wood_store_potion.png"})
+quickEntity('5', {type="store", show_tooltip=true, name="Scribe", display='5', color=colors.WHITE, resolvers.store("SCROLL"), resolvers.chatfeature("magic-store"), image="terrain/wood_store_book.png"})
+quickEntity('6', {type="store", show_tooltip=true, name="Closed store", display='6', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"})
+quickEntity('7', {type="store", show_tooltip=true, name="Closed store", display='7', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"})
+quickEntity('8', {type="store", show_tooltip=true, name="Closed store", display='8', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"})
+quickEntity('9', {type="store", show_tooltip=true, name="Closed store", display='9', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"})
+quickEntity('0', {type="store", show_tooltip=true, name="Closed store", display='0', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"})
+quickEntity('a', {type="store", show_tooltip=true, name="Closed store", display='*', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"})
+quickEntity('b', {type="store", show_tooltip=true, name="Hall of the King", display='*', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"})
+quickEntity('c', {type="store", show_tooltip=true, name="Library", display='*', color=colors.LIGHT_RED, resolvers.store("LAST_HOPE_LIBRARY"), image="terrain/wood_store_book.png"})
+quickEntity('d', {type="store", show_tooltip=true, name="Closed store", display='*', color=colors.LIGHT_UMBER, block_move=true, block_sight=true, image="terrain/wood_store_closed.png"})
+quickEntity('e', {type="store", show_tooltip=true, name="Rare goods", display='*', color=colors.AQUAMARINE, resolvers.store("LOST_MERCHANT"), resolvers.chatfeature("last-hope-lost-merchant"), image="terrain/wood_store_weapon.png"})
+quickEntity('g', {type="store", show_tooltip=true, name="Rich merchant", display='*', color=colors.AQUAMARINE, resolvers.chatfeature("last-hope-melinda-father"), image="terrain/wood_store_closed.png"})
 
-quickEntity('E', {show_tooltip=true, name="The Elder", display='*', color=colors.VIOLET, resolvers.chatfeature("last-hope-elder"), image="terrain/wood_store_closed.png"})
-quickEntity('f', {show_tooltip=true, name="Tannen's Tower", display='*', color=colors.VIOLET, resolvers.chatfeature("tannen"), image="terrain/wood_store_closed.png"})
+quickEntity('E', {type="store", show_tooltip=true, name="The Elder", display='*', color=colors.VIOLET, resolvers.chatfeature("last-hope-elder"), image="terrain/wood_store_closed.png"})
+quickEntity('f', {type="store", show_tooltip=true, name="Tannen's Tower", display='*', color=colors.VIOLET, resolvers.chatfeature("tannen"), image="terrain/wood_store_closed.png"})
 
 quickEntity('@', {show_tooltip=true, name="Statue of King Tolak the Fair", display='@', image="terrain/grass.png", add_displays = {mod.class.Grid.new{image="terrain/statue1.png"}}, color=colors.LIGHT_BLUE, block_move=function(self, x, y, e, act, couldpass) if e and e.player and act then e:learnLore("last-hope-tolak-statue") end return true end})
 quickEntity('Z', {show_tooltip=true, name="Statue of King Toknor the Brave", display='@', image="terrain/grass.png", add_displays = {mod.class.Grid.new{image="terrain/statue1.png"}}, color=colors.LIGHT_BLUE, block_move=function(self, x, y, e, act, couldpass) if e and e.player and act then e:learnLore("last-hope-toknor-statue") end return true end})
diff --git a/game/modules/tome/data/talents/misc/misc.lua b/game/modules/tome/data/talents/misc/misc.lua
index 405fe12905..664374d648 100644
--- a/game/modules/tome/data/talents/misc/misc.lua
+++ b/game/modules/tome/data/talents/misc/misc.lua
@@ -178,7 +178,7 @@ newTalent{
 	no_npc_use = true,
 	no_silence=true, is_spell=true,
 	action = function(self, t)
-		if not self:canBe("worldport") then
+		if not self:canBe("worldport") or self:attr("never_move") then
 			game.logPlayer(self, "The spell fizzles...")
 			return
 		end
diff --git a/game/modules/tome/data/timed_effects.lua b/game/modules/tome/data/timed_effects.lua
index c974bd7de0..2ee8c33546 100644
--- a/game/modules/tome/data/timed_effects.lua
+++ b/game/modules/tome/data/timed_effects.lua
@@ -2206,7 +2206,7 @@ newEffect{
 	activate = function(self, eff)
 	end,
 	deactivate = function(self, eff)
-		if self:canBe("worldport") then
+		if self:canBe("worldport") and not self:attr("never_move") then
 			game:onTickEnd(function()
 				game.logPlayer(self, "You are yanked out of this place!")
 				game:changeLevel(1, game.player.last_wilderness)
@@ -2238,7 +2238,7 @@ newEffect{
 			return
 		end
 
-		if self:canBe("worldport") then
+		if self:canBe("worldport") and not self:attr("never_move") then
 			game:onTickEnd(function()
 				game.logPlayer(self, "You are yanked out of this place!")
 				game:changeLevel(1, "town-angolwen")
-- 
GitLab