diff --git a/game/engines/default/engine/interface/PlayerRun.lua b/game/engines/default/engine/interface/PlayerRun.lua index 53f27062a9ddc931e55b4a8b8c8e9cf0f1310eb5..91cbd06994a0153744bbe3f24cb803cbb54c0f36 100644 --- a/game/engines/default/engine/interface/PlayerRun.lua +++ b/game/engines/default/engine/interface/PlayerRun.lua @@ -128,7 +128,7 @@ function _M:runStep() if not self.running then return false end local ret, msg = self:runCheck() - if not ret and self.running.cnt > 1 then + if not ret and (self.running.cnt > 1 or self.running.busy) then self:runStop(msg) return false else @@ -139,8 +139,21 @@ function _M:runStep() elseif 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) + -- Allow auto-explore to perform actions other than movement, which should be performed + -- or setup in "checkAutoExplore". Hence, modules can make auto-explore borg-like if desired. + -- For example, non-move actions can be picking up an item, using a talent, resting, etc. + -- Some actions can require moving into a tile, such as opening a door or bump-attacking an enemy. + -- "self.running.cnt" is not incremented while "self.running.busy" exists. + if not self.running.busy or self.running.busy.do_move then + self:move(self.running.path[self.running.cnt].x, self.running.path[self.running.cnt].y) + end + self:runMoved() + -- Did not move ? no use in running unless we were busy + if self.running and not self.running.busy and self.x == oldx and self.y == oldy then + self:runStop("didn't move") + end end + if not self.running then return false end else -- Try to move around known traps if possible local dir_is_cardinal = self.running.dir == 2 or self.running.dir == 4 or self.running.dir == 6 or self.running.dir == 8 @@ -182,6 +195,10 @@ function _M:runStep() end -- Move! self:moveDir(self.running.dir) + self:runMoved() + -- Did not move ? no use in running + if self.x == oldx and self.y == oldy then self:runStop() end + if not self.running then return false end if self.running.block_left then self.running.ignore_left = nil end if self.running.ignore_left then @@ -211,13 +228,12 @@ function _M:runStep() end end - self:runMoved() - - -- Did not move ? no use in running - if self.x == oldx and self.y == oldy then self:runStop() end - if not self.running then return false end - self.running.cnt = self.running.cnt + 1 + if not self.running.busy then + self.running.cnt = self.running.cnt + 1 + elseif self.running.busy.no_energy then + return self:runStep() + end return true end end diff --git a/game/modules/tome/class/Player.lua b/game/modules/tome/class/Player.lua index 32be26a74bc73fd0dd37478934d5a510ee392093..798a9f0a69f7309d083546a17dad6e7f5a27569b 100644 --- a/game/modules/tome/class/Player.lua +++ b/game/modules/tome/class/Player.lua @@ -636,9 +636,17 @@ function _M:runCheck(ignore_memory) end end - -- Only notice interesting terrains, but allow auto-explore to take us to the exit + -- 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 (what ~= "self" and self.running and self.running.explore == "exit" and #self.running.path == self.running.cnt) then + 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 + 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"))) + then noticed = "interesting terrain"; return end if grid and grid.type and grid.type == "store" then noticed = "store entrance spotted"; return end diff --git a/game/modules/tome/class/interface/PlayerExplore.lua b/game/modules/tome/class/interface/PlayerExplore.lua index cd5e67abfa70ad81697233b409dcffd6f9abb250..910dd93ce80d6d3ba7b82424ff216763f98cad1b 100644 --- a/game/modules/tome/class/interface/PlayerExplore.lua +++ b/game/modules/tome/class/interface/PlayerExplore.lua @@ -214,6 +214,7 @@ function _M:autoExplore() local unseen_items = {} local unseen_doors = {} local exits = {} + local portals = {} local values = {} values[node[3]] = 0 local door_values = {} @@ -223,6 +224,7 @@ function _M:autoExplore() local running = true local minval = 999999999999999 local minval_items = 999999999999999 + local minval_portals = 999999999999999 local val, _, anode, tile_list -- a few tunable parameters @@ -287,7 +289,6 @@ function _M:autoExplore() if (not values[c] or values[c] > move_cost or is_slow) and (not is_slow or not slow_values[c] or slow_values[c] > move_cost) then -- if not game.level.map:checkEntity(x, y, Map.TERRAIN, "block_move", self, nil, true) then -- if not game.level.map:checkAllEntities(x, y, "block_move", self) then --- if not (terrain.does_block_move or terrain.is_door and terrain.door_opened then -- This is a sinful man's "block_move". If it messes up, then players can explore the level themselves! -- (and they can always interrupt running if something terrible happens) @@ -310,26 +311,45 @@ function _M:autoExplore() minval_items = move_cost end -- default to reasonable targets if there are no accessible unseen tiles or objects left on the map - elseif #unseen_tiles == 0 and #unseen_items == 0 then - -- only go to closed doors with unseen grids behind them - if terrain.door_opened and do_unseen then - local is_unexplored = false - for _, anode in ipairs(listAdjacentTiles(node)) do - if not game.level.map.has_seens(anode[1], anode[2]) then - is_unexplored = true - break - end + -- only go to closed doors with unseen grids behind them. We can go through "safe" doors + elseif terrain.door_opened and do_unseen then + local is_unexplored = false + for _, anode in ipairs(listAdjacentTiles(node)) do + if not game.level.map.has_seens(anode[1], anode[2]) then + is_unexplored = true + break end - if is_unexplored then - unseen_doors[#unseen_doors + 1] = c - if not door_values[c] or door_values[c] > move_cost then - door_values[c] = move_cost - end + end + if is_unexplored then + unseen_doors[#unseen_doors + 1] = c + if not door_values[c] or door_values[c] > move_cost then + door_values[c] = move_cost + end + else -- door is safe to move through + node[4] = move_cost + 1 + values[c] = move_cost + 1 + current_tiles_next[#current_tiles_next + 1] = node + end + -- go to next level, exit, previous level, or orb portal (in that order of precedence) + elseif terrain.change_level then + exits[#exits + 1] = c + values[c] = move_cost + elseif terrain.orb_portal then + local is_portal_center = true + local is_small_portal = true + for _, anode in ipairs(listAdjacentTiles(node)) do + if not game.level.map:checkEntity(anode[1], anode[2], Map.TERRAIN, "orb_portal") then + is_portal_center = false + else + is_small_portal = false end - -- go to next level, exit, or previous level (in that order of precedence) - elseif terrain.change_level then - exits[#exits + 1] = c + end + if is_portal_center or is_small_portal then + portals[#portals + 1] = c values[c] = move_cost + if move_cost < minval_portals then + minval_portals = move_cost + end end end end @@ -379,7 +399,7 @@ function _M:autoExplore() -- Negligible time is spent below -- Choose target - if #unseen_tiles > 0 or #unseen_items > 0 or #unseen_doors > 0 or #exits > 0 then + if #unseen_tiles > 0 or #unseen_items > 0 or #unseen_doors > 0 or #exits > 0 or #portals > 0 then local target_type local choices = {} local distances = {} @@ -470,12 +490,36 @@ function _M:autoExplore() end -- if no destination yet, go to nearest unexplored closed non-vault door if #choices == 0 then + local add_values = {} for _, c in ipairs(unseen_doors) do local x, y = toDouble(c) local terrain = game.level.map(x, y, Map.TERRAIN) if not terrain.door_player_check then target_type = "door" choices[#choices + 1] = c + -- we may take an extra step to approach a door squarely from a cardinal direction, so let's account for this + local door_val = door_values[c] + local min_diagonal = door_val + local min_cardinal = door_val + for _, node in ipairs(listAdjacentTiles(c, true)) do + if values[node[3]] and values[node[3]] < min_cardinal then + min_cardinal = values[node[3]] + end + end + for _, node in ipairs(listAdjacentTiles(c, false, true)) do + if values[node[3]] and values[node[3]] < min_diagonal then + min_diagonal = values[node[3]] + end + end + if min_cardinal > min_diagonal then + for _, node in ipairs(listAdjacentTiles(c, false, true)) do + if values[node[3]] then + add_values[node[3]] = values[node[3]] + 1 + door_values[c] = door_val + 1 + end + end + end + local dist = core.fov.distance(self.x, self.y, x, y, true) + 10*door_values[c] distances[c] = dist if dist < mindist then @@ -483,6 +527,9 @@ function _M:autoExplore() end end end + for _, c in ipairs(add_values) do + values[c] = values[c] + add_values[c] + end end -- ...or vault door if #choices == 0 then @@ -548,6 +595,21 @@ function _M:autoExplore() end end end + -- ...or orb portal + if #choices == 0 then + for _, c in ipairs(portals) do + if values[c] == minval_portals then + target_type = "portal" + 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 -- if multiple choices, then choose nearest one based on fov distance metric if #choices > 1 then @@ -633,8 +695,12 @@ function _M:autoExplore() if #path > 0 then if self.running and self.running.explore then -- take care of a couple fringe cases - -- don't open adjacent doors if we've already been running - if #path == 1 and target_type == "door" then return false end + -- 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 + 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 @@ -655,6 +721,9 @@ function _M:autoExplore() game.level.map.attrs(target_x, target_y, "obj_seen", true) return self:autoExplore() end + -- don't open non-adjacent target doors + if target_type == "door" and #path > 1 then path[#path] = nil end + self.running = { path = path, cnt = 1, @@ -686,26 +755,20 @@ function _M:checkAutoExplore() -- If not, though, then stop, because the player *must* choose to open the door local node = self.running.path[self.running.cnt] local terrain = node and game.level.map(node.x, node.y, Map.TERRAIN) - if self.running.explore == "door" and #self.running.path == self.running.cnt then - if self.running.cnt == 1 then - -- let's not make assumptions. Double-check that there is a closed door there and that we open it - if terrain.door_opened then - local sx, sy = self.x, self.y - self:move(node.x, node.y) - self:runMoved() - -- check if there are enemies behind the open door - local ret, msg = self:runCheck() - if not ret then - self:runStop(msg) - return false - end - terrain = game.level.map(node.x, node.y, Map.TERRAIN) - if not terrain.door_opened and sx == self.x and sy == self.y then - return self:autoExplore() - end - end - end - return false + + -- this is either a "safe" door or a target adjacent door. Either way, we can open it + 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 + end + self.running.busy = nil + + -- if we opened the adjacent target door, then continue exploring elsewhere + if self.running.explore == "door" and #self.running.path == self.running.cnt and self.running.cnt == 1 and terrain.door_closed then + return self:autoExplore() end -- if we're at the end of the path and we're searching for unseen tiles, then continue with a new path diff --git a/game/modules/tome/dialogs/CharacterSheet.lua b/game/modules/tome/dialogs/CharacterSheet.lua index b393181d3bf2f01bd4954f7e4b5570598cdf3b7f..2315c5b2c4d99673057a8224fcff728e733a9419 100644 --- a/game/modules/tome/dialogs/CharacterSheet.lua +++ b/game/modules/tome/dialogs/CharacterSheet.lua @@ -306,9 +306,9 @@ function _M:drawDialog(kind, actor_to_compare) local cur_exp, max_exp = player.exp, player:getExpChart(player.level+1) h = 0 w = 0 - s:drawStringBlended(self.font, "Sex : "..(player.descriptor.sex or (player.female and "Female" or "Male")), w, h, 0, 200, 255, true) h = h + self.font_h - s:drawStringBlended(self.font, "Race : "..(player.descriptor.subrace or player.type:capitalize()), w, h, 0, 200, 255, true) h = h + self.font_h - s:drawStringBlended(self.font, "Class: "..(player.descriptor.subclass or player.subtype:capitalize()), w, h, 0, 200, 255, true) h = h + self.font_h + s:drawStringBlended(self.font, "Sex : "..((player.descriptor and player.descriptor.sex) or (player.female and "Female" or "Male")), w, h, 0, 200, 255, true) h = h + self.font_h + s:drawStringBlended(self.font, (player.descriptor and "Race : " or "Type : ")..((player.descriptor and player.descriptor.subrace) or player.type:capitalize()), w, h, 0, 200, 255, true) h = h + self.font_h + s:drawStringBlended(self.font, (player.descriptor and "Class: " or "Stype: ")..((player.descriptor and player.descriptor.subclass) or player.subtype:capitalize()), w, h, 0, 200, 255, true) h = h + self.font_h s:drawStringBlended(self.font, "Size : "..(player:TextSizeCategory():capitalize()), w, h, 0, 200, 255, true) h = h + self.font_h h = h + self.font_h @@ -375,8 +375,10 @@ function _M:drawDialog(kind, actor_to_compare) text = compare_fields(player, actor_to_compare, function(actor) return actor.combat_physspeed - 1 end, "%.2f%%", "%+.2f%%", 100) self:mouseTooltip(self.TOOLTIP_SPEED_ATTACK, s:drawColorStringBlended(self.font, ("Attack speed : #00ff00#%s"):format(text), w, h, 255, 255, 255, true)) h = h + self.font_h h = h + self.font_h - text = compare_fields(player, actor_to_compare, function(actor) return #actor.died_times end, "%3d", "%+.0f") - self:mouseTooltip(self.TOOLTIP_LIVES, s:drawColorStringBlended(self.font, ("Times died : #00ff00#%s"):format(text), w, h, 255, 255, 255, true)) h = h + self.font_h + if player.died_times then + text = compare_fields(player, actor_to_compare, function(actor) return #actor.died_times end, "%3d", "%+.0f") + self:mouseTooltip(self.TOOLTIP_LIVES, s:drawColorStringBlended(self.font, ("Times died : #00ff00#%s"):format(text), w, h, 255, 255, 255, true)) h = h + self.font_h + end if player.easy_mode_lifes then text = compare_fields(player, actor_to_compare, "easy_mode_lifes", "%3d", "%+.0f") self:mouseTooltip(self.TOOLTIP_LIVES, s:drawColorStringBlended(self.font, ("Lives left : #00ff00#%s"):format(text), w, h, 255, 255, 255, true)) h = h + self.font_h @@ -670,7 +672,7 @@ function _M:drawDialog(kind, actor_to_compare) end end end - + local inc_damage_actor_types = {} if player.inc_damage_actor_type then for i, t in pairs(player.inc_damage_actor_type) do @@ -871,13 +873,13 @@ function _M:dump() nl(" [Tome 4.00 @ www.te4.org Character Dump]") nl() - nnl(("%-32s"):format(makelabel("Sex", player.descriptor.sex or (player.female and "Female" or "Male")))) + nnl(("%-32s"):format(makelabel("Sex", (player.descriptor and player.descriptor.sex) or (player.female and "Female" or "Male")))) nl(("STR: %d"):format(player:getStr())) - nnl(("%-32s"):format(makelabel("Race", player.descriptor.subrace or player.type:capitalize()))) + nnl(("%-32s"):format(makelabel("Race", (player.descriptor and player.descriptor.subrace) or player.type:capitalize()))) nl(("DEX: %d"):format(player:getDex())) - nnl(("%-32s"):format(makelabel("Class", player.descriptor.subclass or player.subtype:capitalize()))) + nnl(("%-32s"):format(makelabel("Class", (player.descriptor and player.descriptor.subclass) or player.subtype:capitalize()))) nl(("MAG: %d"):format(player:getMag())) nnl(("%-32s"):format(makelabel("Level", ("%d"):format(player.level)))) @@ -934,8 +936,8 @@ function _M:dump() else nnl(("%-32s"):format(" ")) end - nl(makelabel("Difficulty", player.descriptor.difficulty or "???")) - nl(makelabel("Permadeath", player.descriptor.permadeath or "???")) + nl(makelabel("Difficulty", (player.descriptor and player.descriptor.difficulty) or "???")) + nl(makelabel("Permadeath", (player.descriptor and player.descriptor.permadeath) or "???")) nnl(("%-32s"):format(strings[3])) if player:knowTalent(player.T_MANA_POOL) then diff --git a/game/modules/tome/dialogs/MapMenu.lua b/game/modules/tome/dialogs/MapMenu.lua index a2db360ca00bbb1052a304949ebad4f1b5b6c36f..ca16498ae37c361205ecd5b97a9b8a75ca70d1f7 100644 --- a/game/modules/tome/dialogs/MapMenu.lua +++ b/game/modules/tome/dialogs/MapMenu.lua @@ -72,7 +72,7 @@ function _M:use(item) elseif act == "order" then game.party:giveOrders(item.actor) elseif act == "change_level" then game.key:triggerVirtual("CHANGE_LEVEL") elseif act == "pickup" then game.key:triggerVirtual("PICKUP_FLOOR") - elseif act == "character_sheet" then game.key:triggerVirtual("SHOW_CHARACTER_SHEET") + elseif act == "character_sheet" then game:registerDialog(require("mod.dialogs.CharacterSheet").new(item.actor)) elseif act == "quests" then game.key:triggerVirtual("SHOW_QUESTS") elseif act == "levelup" then game.key:triggerVirtual("LEVELUP") elseif act == "inventory" then game.key:triggerVirtual("SHOW_INVENTORY") @@ -118,8 +118,8 @@ function _M:generateList() if a and not self.on_player and game.party:canOrder(a, false) then list[#list+1] = {name="Give order", action="order", color=colors.simple(colors.TEAL), actor=a} end if self.on_player then list[#list+1] = {name="Rest a while", action="rest", color=colors.simple(colors.ANTIQUE_WHITE)} end if self.on_player then list[#list+1] = {name="Inventory", action="inventory", color=colors.simple(colors.ANTIQUE_WHITE)} end - if self.on_player then list[#list+1] = {name="Character Sheet", action="character_sheet", color=colors.simple(colors.ANTIQUE_WHITE)} end if self.on_player then list[#list+1] = {name="Quest Log", action="quests", color=colors.simple(colors.ANTIQUE_WHITE)} end + if a then list[#list+1] = {name="Inspect Creature", action="character_sheet", color=colors.simple(colors.ANTIQUE_WHITE), actor=a} end if not self.on_player and a and profile.auth and profile.hash_valid then list[#list+1] = {name="Link creature in chat", action="chat-link"} end if self.on_player and (player.unused_stats > 0 or player.unused_talents > 0 or player.unused_generics > 0 or player.unused_talents_types > 0) then list[#list+1] = {name="Levelup!", action="levelup", color=colors.simple(colors.YELLOW)} end