diff --git a/game/engines/default/engine/interface/PlayerRun.lua b/game/engines/default/engine/interface/PlayerRun.lua index 91cbd06994a0153744bbe3f24cb803cbb54c0f36..2ac98847675221f8f00395e6a1a0f79443817748 100644 --- a/game/engines/default/engine/interface/PlayerRun.lua +++ b/game/engines/default/engine/interface/PlayerRun.lua @@ -367,6 +367,9 @@ function _M:runStop(msg) game:unregisterDialog(self.running.dialog) + if not msg and self.running.explore and self.running.path and self.running.cnt == #self.running.path + 1 then + msg = "at " .. self.running.explore + end if msg then game.log("Ran for %d turns (stop reason: %s).", self.running.cnt, msg) end diff --git a/game/modules/tome/class/Player.lua b/game/modules/tome/class/Player.lua index bb1a27b7666f91c9335f00cf6cc48b5fe72b89a6..7360214efa20e7c37219033916e120ee25f02a80 100644 --- a/game/modules/tome/class/Player.lua +++ b/game/modules/tome/class/Player.lua @@ -113,6 +113,9 @@ function _M:onEnterLevel(zone, level) -- Save where we entered self.entered_level = {x=self.x, y=self.y} + -- mark entrance (if applicable) as noticed + game.level.map.attrs(self.x, self.y, "noticed", true) + -- Fire random escort quest if self.random_escort_levels and self.random_escort_levels[zone.short_name] and self.random_escort_levels[zone.short_name][level.level] then self:grantQuest("escort-duty") @@ -645,16 +648,23 @@ function _M:runCheck(ignore_memory) -- Only notice interesting terrains, but allow auto-explore and A* to take us to the exit. Auto-explore can also take us through "safe" doors local grid = game.level.map(x, y, Map.TERRAIN) - if grid and grid.notice and not (self.running and self.running.path and (what ~= self and - (self.running.explore and grid.door_opened -- safe door + if grid and grid.notice and not (self.running and self.running.path and (game.level.map.attrs(x, y, "noticed") + or (what ~= self and (self.running.explore and grid.door_opened -- safe door or #self.running.path == self.running.cnt and (self.running.explore == "exit" -- auto-explore onto exit or not self.running.explore and grid.change_level)) -- A* onto exit or #self.running.path - self.running.cnt < 2 and (self.running.explore == "portal" -- auto-explore onto portal or not self.running.explore and grid.orb_portal) -- A* onto portal or self.running.cnt < 3 and grid.orb_portal and -- path from portal - game.level.map:checkEntity(self.running.path[1].x, self.running.path[1].y, Map.TERRAIN, "orb_portal"))) + game.level.map:checkEntity(self.running.path[1].x, self.running.path[1].y, Map.TERRAIN, "orb_portal")))) then - noticed = "interesting terrain"; return + if self.running and self.running.explore and self.running.path and self.running.explore ~= "unseen" and self.running.cnt == #self.running.path + 1 then + noticed = "at " .. self.running.explore + else + noticed = "interesting terrain" + end + -- let's only remember and ignore standard interesting terrain + if not ignore_memory and (grid.change_level or grid.orb_portal) then game.level.map.attrs(x, y, "noticed", true) end + return end if grid and grid.type and grid.type == "store" then noticed = "store entrance spotted"; return end @@ -689,7 +699,7 @@ function _M:mouseMove(tmx, tmy, force_move) end return true end - return engine.interface.PlayerMouse.mouseMove(self, tmx, tmy, spotHostiles, {recheck=true, astar_check=astar_check}, force_move) + return engine.interface.PlayerMouse.mouseMove(self, tmx, tmy, function() local spotted = spotHostiles(self) ; return #spotted > 0 end, {recheck=true, astar_check=astar_check}, force_move) end --- Called after running a step @@ -1003,7 +1013,8 @@ end function _M:useOrbPortal(portal) if portal.special then portal:special(self) return end - if spotHostiles(self) then game.logPlayer(self, "You can not use the Orb with foes in sight.") return end + local spotted = spotHostiles(self) + if #spotted > 0 then game.logPlayer(self, "You can not use the Orb with foes in sight.") return end if portal.on_preuse then portal:on_preuse(self) end diff --git a/game/modules/tome/class/interface/PlayerExplore.lua b/game/modules/tome/class/interface/PlayerExplore.lua index b48e918793bb7f09d5e9bcc38f06d5c78558cdb2..e26e2ee8fef00083470be11d3db45d10ac3ecba3 100644 --- a/game/modules/tome/class/interface/PlayerExplore.lua +++ b/game/modules/tome/class/interface/PlayerExplore.lua @@ -217,6 +217,7 @@ function _M:autoExplore() local portals = {} local values = {} values[node[3]] = 0 + local safe_doors = {} local door_values = {} local slow_values = {} local slow_tiles = {} @@ -329,6 +330,7 @@ function _M:autoExplore() node[4] = move_cost + 1 values[c] = move_cost + 1 current_tiles_next[#current_tiles_next + 1] = node + safe_doors[c] = true end -- go to next level, exit, previous level, or orb portal (in that order of precedence) elseif terrain.change_level then @@ -404,16 +406,45 @@ function _M:autoExplore() local choices = {} local distances = {} local mindist = 999999999999999 - -- try to explore cleanly--don't leave single unseen tiles by themselves - for _, c in ipairs(unseen_singlets) do - if values[c] <= minval + singlet_greed then - target_type = "unseen" - choices[#choices + 1] = c + -- If we already have a suitable target that we haven't reached yet, then use that as our target. This will be more useful + -- if or when we save info between between instances of running auto-explore. For now it's useful when dodging traps on the fly. + if self.running and (self.running.explore == "exit" or self.running.explore == "portal") and (self.x ~= self.running.target.x or self.y ~= self.running.target.y) then + -- verify that the target is currently reachable + for _, c in ipairs(exits) do local x, y = toDouble(c) - local dist = core.fov.distance(self.x, self.y, x, y, true) - distances[c] = dist - if dist < mindist then - mindist = dist + if x == self.running.target.x and y == self.running.target.y then + target_type = "exit" + choices[1] = c + distances[c] = 1 + mindist = 1 + break + end + end + if #choices == 0 then + for _, c in ipairs(portals) do + local x, y = toDouble(c) + if x == self.running.target.x and y == self.running.target.y then + target_type = "portal" + choices[1] = c + distances[c] = 1 + mindist = 1 + break + end + end + end + end + -- try to explore cleanly--don't leave single unseen tiles by themselves + if #choices == 0 then + for _, c in ipairs(unseen_singlets) do + if values[c] <= minval + singlet_greed then + target_type = "unseen" + choices[#choices + 1] = c + local x, y = toDouble(c) + local dist = core.fov.distance(self.x, self.y, x, y, true) + distances[c] = dist + if dist < mindist then + mindist = dist + end end end end @@ -421,7 +452,7 @@ function _M:autoExplore() if #choices == 0 and minval_items <= minval + item_greed then for _, c in ipairs(unseen_items) do if values[c] == minval_items then - target_type = "item" + target_type = "object" choices[#choices + 1] = c local x, y = toDouble(c) local dist = core.fov.distance(self.x, self.y, x, y, true) @@ -661,8 +692,9 @@ function _M:autoExplore() -- This results in dog-leg (or hockey stick)-like movement. If desired, we could try adding an A*-like heuristic -- to favor straighter line movement (i.e., alternate between diagonal and cardinal moves), but, meh, whatever ;) -- If our target is a door, it would be very nice to approach it from a cardinal direction, because this would - -- give a much better and safer view should the player choose to open the door - if #cardinals == 0 or min_diagonal < min_cardinal and not (idiot_counter == 1 and target_type == "door" and min_cardinal < min_diagonal + 2) then + -- give a much better and safer view should the player choose to open the door. Also do this for "safe" doors + local c = toSingle(path[#path].x, path[#path].y) + if #cardinals == 0 or min_diagonal < min_cardinal and not (min_cardinal < min_diagonal + 2 and (safe_doors[c] or door_values[c])) then current_val = min_diagonal for _, node in ipairs(diagonals) do if values[node[3]] == min_diagonal then @@ -698,27 +730,38 @@ function _M:autoExplore() -- take care of a couple fringe cases -- don't open adjacent or target doors if we've already been running if target_type == "door" then - if #path == 1 then return false - else path[#path] = nil end + if #path == 1 then + self:runStop("at door") + return false + else + path[#path] = nil + end end -- don't run into adjacent interesting terrain if we've already been running - local terrain = game.level.map(path[1].x, path[1].y, Map.TERRAIN) - if terrain.notice and not (#path == 1 and target_type == "exit") then - self:runStop("interesting terrain") - return false + local x, y = path[1].x, path[1].y + local terrain = game.level.map(x, y, Map.TERRAIN) + if terrain.notice and (target_type ~= "exit" and target_type ~= "portal" or #path ~= 1 and not game.level.map.attrs(x, y, "noticed")) then + if safe_doors[toSingle(x, y)] and not self.running.busy then + self.running.busy = { type = "opening door", do_move = true, no_energy = true } + else + if terrain.change_level or terrain.orb_portal then game.level.map.attrs(x, y, "noticed", true) end + self:runStop("interesting terrain") + return false + end end self.running.path = path self.running.cnt = 1 self.running.explore = target_type + self.running.target = {x=target_x, y=target_y} -- hack! self.running.ave_x = (self.running.ave_x*self.running.ave_N + 2*(target_x + self.x)) / (self.running.ave_N + 4) self.running.ave_y = (self.running.ave_y*self.running.ave_N + 2*(target_y + self.y)) / (self.running.ave_N + 4) self.running.ave_N = self.running.ave_N + 2 else -- another fringe case: if we target an item in an adjacent wall that we've probably already targeted, then mark it as seen and find a new target - if #path == 1 and target_type == "item" and game.level.map:checkEntity(target_x, target_y, Map.TERRAIN, "block_move", self, nil, true) then + if #path == 1 and target_type == "object" and game.level.map:checkEntity(target_x, target_y, Map.TERRAIN, "block_move", self, nil, true) then game.level.map.attrs(target_x, target_y, "obj_seen", true) return self:autoExplore() end @@ -732,6 +775,7 @@ function _M:autoExplore() self:runStop() end, false, true), explore = target_type, + target = {x=target_x, y=target_y}, -- hack! ave_x = 0.5*(target_x + self.x), ave_y = 0.5*(target_y + self.y), @@ -753,17 +797,23 @@ function _M:checkAutoExplore() if not self.running or not self.running.explore then return false end -- We can open a door and explore if the player initiated auto-explore directly adjacent to the target door. - -- If not, though, then stop, because the player *must* choose to open the door + -- If not, though, then stop, because the player *must* choose to open the door (except for "safe" doors) local node = self.running.path[self.running.cnt] - local terrain = node and game.level.map(node.x, node.y, Map.TERRAIN) - - -- this is either a "safe" door or a target adjacent door. Either way, we can open it + local cx, cy = node and node.x, node and node.y + local terrain = node and game.level.map(cx, cy, Map.TERRAIN) if terrain and terrain.door_opened then - -- we already tried to open the door but failed - if self.running.busy and self.running.busy.type == "opening door" then return false end - - self.running.busy = { type = "opening door", do_move = true, no_energy = true } - return true + -- we already tried to open the door but failed (always fails on vault doors) + if self.running.busy and self.running.busy.type == "opening door" then + self:runStop("checked door") + return false + -- we didn't know this was a door at the time, so explore a new path + elseif self.running.explore == "unseen" and self.running.cnt == #self.running.path and game.level.map.has_seens(cx, cy) then + return self:autoExplore() + -- this is either a "safe" door or a target adjacent door. Either way, we can open it, which takes a movement action but no energy to do + else + self.running.busy = { type = "opening door", do_move = true, no_energy = true } + return true + end end self.running.busy = nil @@ -773,53 +823,73 @@ function _M:checkAutoExplore() end -- if we're at the end of the path and we're searching for unseen tiles, then continue with a new path - local x, y = self.running.path[#self.running.path].x, self.running.path[#self.running.path].y - local obj = game.level.map:getObject(x, y, 1) + local tx, ty = self.running.path[#self.running.path].x, self.running.path[#self.running.path].y + local obj = game.level.map:getObject(tx, ty, 1) if not node then - if self.running.explore == "unseen" or self.running.explore == "item" and not obj then + if self.running.explore == "unseen" or self.running.explore == "object" and not obj then return self:autoExplore() else + self:runStop("at " .. self.running.explore) return false end end -- if the next spot in the path is blocked, explore a new path if we are searching for unseen tiles, otherwise stop - if game.level.map.has_seens(node.x, node.y) and game.level.map:checkEntity(node.x, node.y, Map.TERRAIN, "block_move", self, nil, true) then - -- game.level.map:checkAllEntities(node.x, node.y, "block_move", self) then + if game.level.map.has_seens(cx, cy) and game.level.map:checkEntity(cx, cy, Map.TERRAIN, "block_move", self, nil, true) then + -- game.level.map:checkAllEntities(cx, cy, "block_move", self) then if terrain.notice then + if terrain.change_level or terrain.orb_portal then game.level.map.attrs(cx, cy, "noticed", true) end self:runStop("interesting terrain") return false - elseif self.running.explore == "unseen" then + elseif self.running.explore == "unseen" or self.running.explore == "object" and self.running.cnt ~= #self.running.path then return self:autoExplore() else - self:runStop("the path is blocked") + if self.running.explore == "object" and self.running.cnt == #self.running.path then + game.level.map.attrs(cx, cy, "obj_seen", true) + self:runStop("at object (diggable)") + else + self:runStop("the path is blocked") + end return false end end + -- if we are about to step on a trap, then verify that we actually intend to do so + local trap = game.level.map(cx, cy, Map.TRAP) + if trap and trap:knownBy(self) and self.running.cnt ~= 1 then + return self:autoExplore() + end + -- One more kindness to the player: take advantage of asymmetric LoS in this one specific case. -- If an enemy is at '?', the player is able to prevent an ambush by moving to 'x' instead of 't'. -- This is the only sensibly preventable ambush (that I know of) in which the player can move -- in a way to see the would-be ambusher and the would-be ambusher can't see the player. - -- + -- However, don't do this if it will step onto a known trap + -- -- .tx Moving onto 't' puts us adjacent to an unseen tile, '?' -- ?#@ --> Pick 'x' instead - if math.abs(self.x - node.x) == 1 and math.abs(self.y - node.y) == 1 then - if game.level.map:checkAllEntities(self.x, node.y, "block_move", self) and not game.level.map:checkAllEntities(node.x, self.y, "block_move", self) and - game.level.map:isBound(self.x, 2*node.y - self.y) and not game.level.map.has_seens(self.x, 2*node.y - self.y) then - table.insert(self.running.path, self.running.cnt, {x=node.x, y=self.y}) - elseif game.level.map:checkAllEntities(node.x, self.y, "block_move", self) and not game.level.map:checkAllEntities(self.x, node.y, "block_move", self) and - game.level.map:isBound(2*node.x - self.x, self.y) and not game.level.map.has_seens(2*node.x - self.x, self.y) then - table.insert(self.running.path, self.running.cnt, {x=self.x, y=node.y}) + if math.abs(self.x - cx) == 1 and math.abs(self.y - cy) == 1 then + if game.level.map:checkAllEntities(self.x, cy, "block_move", self) and not game.level.map:checkAllEntities(cx, self.y, "block_move", self) and + game.level.map:isBound(self.x, 2*cy - self.y) and not game.level.map.has_seens(self.x, 2*cy - self.y) then + local trap = game.level.map(cx, self.y, Map.TRAP) + if not trap or not trap:knownBy(self) then + table.insert(self.running.path, self.running.cnt, {x=cx, y=self.y}) + end + elseif game.level.map:checkAllEntities(cx, self.y, "block_move", self) and not game.level.map:checkAllEntities(self.x, cy, "block_move", self) and + game.level.map:isBound(2*cx - self.x, self.y) and not game.level.map.has_seens(2*cx - self.x, self.y) then + local trap = game.level.map(self.x, cy, Map.TRAP) + if not trap or not trap:knownBy(self) then + table.insert(self.running.path, self.running.cnt, {x=self.x, y=cy}) + end end end -- continue current path if we haven't seen the target tile or object yet - if not game.level.map.has_seens(x, y) then return true end - if obj and not game.level.map.attrs(x, y, "obj_seen") then return true end + if not game.level.map.has_seens(tx, ty) then return true end + if obj and not game.level.map.attrs(tx, ty, "obj_seen") then return true end -- if we have explored the unseen node or the unseen item is no longer there, then continue auto-exploring somewhere else - if self.running.explore == "unseen" or self.running.explore == "item" and not obj then + if self.running.explore == "unseen" or self.running.explore == "object" and not obj then return self:autoExplore() else -- otherwise, try to continue running on the current path to reach our target diff --git a/game/modules/tome/data/timed_effects/other.lua b/game/modules/tome/data/timed_effects/other.lua index 4b38512b59c23438e5a9ed1446226a8f3dcd859a..993e9cfe32f7f947d75227f4c26560830f74a86e 100644 --- a/game/modules/tome/data/timed_effects/other.lua +++ b/game/modules/tome/data/timed_effects/other.lua @@ -1132,8 +1132,8 @@ newEffect{ if math.min(eff.unlockLevel, eff.level) >= 4 then -- build chance for a nightmare local def = self.tempeffect_def[self.EFF_CURSE_OF_NIGHTMARES] - eff.nightmareChance = (eff.nightmareChance or 0) def.getNightmareChance(eff.level) - + eff.nightmareChance = (eff.nightmareChance or 0) + def.getNightmareChance(eff.level) + -- invoke the nightmare if rng.percent(eff.nightmareChance) then local radius = def.getNightmareRadius(eff.level) diff --git a/game/modules/tome/dialogs/CharacterSheet.lua b/game/modules/tome/dialogs/CharacterSheet.lua index 4526c7181ceb934cd44b1a0301111e609fb1a8c8..d2bd3bba153dc5ca9bc43d20ee0636e68ca92a8e 100644 --- a/game/modules/tome/dialogs/CharacterSheet.lua +++ b/game/modules/tome/dialogs/CharacterSheet.lua @@ -866,15 +866,6 @@ function _M:drawDialog(kind, actor_to_compare) local list = {} for j, t in pairs(player.talents_def) do if player:knowTalent(t.id) and not t.hide then --- local typename = "talent" --- local status = tstring{{"color", "LIGHT_GREEN"}, "Active"} --- if self.actor:isTalentCoolingDown(t) then --- nodes = cooldowns --- status = tstring{{"color", "LIGHT_RED"}, self.actor:isTalentCoolingDown(t).." turns"} --- elseif t.mode == "sustained" then --- if self.actor:isTalentActive(t.id) then nodes = sustained end --- status = self.actor:isTalentActive(t.id) and tstring{{"color", "YELLOW"}, "Sustaining"} or tstring{{"color", "LIGHT_GREEN"}, "Sustain"} --- end local lvl = player:getTalentLevelRaw(t) list[#list+1] = { name = ("%s (%d)"):format(t.name, lvl), @@ -909,7 +900,7 @@ function _M:dump() local makelabel = function(s,r) while s:len() < labelwidth do s = s.." " end return ("%s: %s"):format(s, r) end local cur_exp, max_exp = player.exp, player:getExpChart(player.level+1) - nl(" [Tome 4.00 @ www.te4.org Character Dump]") + nl(" [ToME4 @ www.te4.org Character Dump]") nl() nnl(("%-32s"):format(makelabel("Sex", (player.descriptor and player.descriptor.sex) or (player.female and "Female" or "Male"))))