# https://github.com/Implojin/ff-rc # # ff.rc : a dungeon crawl script # # ----- Fast-Forward ----- # # 'Wake me up when we get to the good part.' # # Have you ever thought, "Hey, I *want* to like Crawl, but it really only takes like 5 keys and pressing them is pretty boring"? # # Do pillardancing and breadswinging take more effort than you want to put in? # # Do you tend to mash 'o tab' until 'o shit' happens? # # Maybe you'd like it if the game was a little more bite-sized. Closer to coffeebreak duration? # # Or maybe you'd have more fun if playing the game didn't involve things like food, stairs, and dropping items all the time. # # Are you the kind of person who unironically wants to automate dumb stuff like excluding every tile you've stepped on? # (At least until the devteam removes it?) # # What if you could get some chump to write the ancient, cursed crawl Lua, so that YOU don't have to? # # "It's MY interesting tactical combat, and I WANT IT NOW." # # ----- Fast-Forward ----- #### # For a list of thresholds and conditions which return control to the player, # see the check_kickbacks() function below. ## # BIG TODOs: # TODO: fancy fight logic # TODO: shaft handling # Fast-Forward is bound to 'Tab' by default, #macros += M \{9} ===toggle_auto macros += M \{9} ===maybe_auto # by commenting out the above line, and uncommenting below, it can be bound to 'Shift+Tab' instead: #macros += M \{-233} ===toggle_auto # or if you would prefer, it can be bound to 'o' by uncommenting the lines below: #bindkey = [o] CMD_NO_CMD_DEFAULT #bindkey = [O] CMD_EXPLORE #macros += M 'o' ===toggle_auto #bindkey = [1] CMD_NO_CMD_DEFAULT macros += M \{-1073741882} ===mpr_item_debug macros += M \{-1073741884} ===are_we_standing_on_a_valid_altar macros += M \{-1073741885} ===echo_kfeats_table tile_update_rate = 50 tile_runrest_rate = 50 travel_delay = -1 explore_delay = -1 rest_delay = -1 view_delay = 0 # enable travel trails to help contextualize player control handoffs show_travel_trail = true # highlight threatening mons tiles to help quickly contextualize m:threat() player control handoffs # this option can interfere with quick assessment of tile HP bars, so disable this if the auras bother you tile_show_threat_levels = tough, nasty auto_butcher = very hungry confirm_butcher = never auto_eat_chunks = true easy_eat_chunks = true flush.failure = false # let's see if this gets rid of mons projectile anim delay... use_animations = show_more = false # dangerous for manual play, but convenient if the game would otherwise pop -mores- while auto is active force_more_message = # manual skill training to save a couple of keystrokes at newgame, this isn't strictly required for the script but okay default_manual_training = true # try to set game options to prevent interference with custom Lua autopick() autopickup_starting_ammo = false autopickup = autopickup_exceptions = # these should be true by default, but let's make sure default_autopickup = true explore_greedy = true pickup_thrown = true easy_unequip = true # set game option to ensure consistent ring equip keystrokes #jewellery_prompt = true # TODO: revisit crawl/options_guide.txt to ensure that explore_stop options are appropriately set # leaving this disabled for now, but it could be useful later if I want auto() to use items automagically #bad_item_prompt = false ## For Crawl rcfile Lua parsing reasons, we must ensure no line in this file contains only the brace char '}'. ## If you are declaring a table, the closing brace cannot be the only thing on its line. ## I'm pretty sure this is the reason qw's tables all have "--hack" following the closing brace. { -- valid options: -- "zin", "tso", "kiku", "yred", "xom", "veh", "oka", "makh", "sif", "trog", -- "nem", "ely", "lucy", "beogh", "jiyva", "fedhas", "chei", "ash", "dith", -- "gozag", "qaz", "ru", "usk", "hep", "wjc", "random", "none" local target_gods_table = { "oka", } -- "trog", } --m:attitude() enums --local ATT_FRIENDLY = 4 local ATT_NEUTRAL = 1 --local ATT_HOSTILE = 0 local auto = false local HP_KICKBACK_THRESHOLD = 60 local HP_MULTIBUMP_THRESHOLD = 75 local POISON_KICKBACK_THRESHOLD = 20 local SKILL_DRAIN_KICKBACK_THRESHOLD = 80 local bump_attacks_remaining = 0 local bump_attack_dir = nil local bump_mons_target = nil local ONLINE_PLAY = true local ONLINE_DELAY_MS = 100 function go() auto = true end function stop() auto = false end function toggle_auto() auto = (not auto) end -- TODO: "if there are dangerous monsters in view that would prevent the triggering of auto(), then let this function -- trigger whatever would ordinarily be bound to Tab, instead." (===hit_closest by default, could also be ===hit_closest_nomove) function maybe_auto() auto = (not auto) -- monster_in_view(true) check_kickbacks() if not auto then hit_closest() end end -- vectoring fn pulled from autofight.lua local function delta_to_cmd(dx, dy) local d2v = { [-1] = { [-1] = "CMD_MOVE_UP_LEFT", [0] = "CMD_MOVE_LEFT", [1] = "CMD_MOVE_DOWN_LEFT"}, [0] = { [-1] = "CMD_MOVE_UP", [1] = "CMD_MOVE_DOWN"}, [1] = { [-1] = "CMD_MOVE_UP_RIGHT", [0] = "CMD_MOVE_RIGHT", [1] = "CMD_MOVE_DOWN_RIGHT"}, } return d2v[dx][dy] end -- adapted from qw.rc vi_to_delta(c) function cmd_to_delta(command) local d2v = { [-1] = { [-1] = "CMD_MOVE_UP_LEFT", [0] = "CMD_MOVE_LEFT", [1] = "CMD_MOVE_DOWN_LEFT"}, [0] = { [-1] = "CMD_MOVE_UP", [1] = "CMD_MOVE_DOWN"}, [1] = { [-1] = "CMD_MOVE_UP_RIGHT", [0] = "CMD_MOVE_RIGHT", [1] = "CMD_MOVE_DOWN_RIGHT"}, } local x,y for x = -1, 1 do for y = -1, 1 do if d2v[x][y] == command then return x,y end end end end -- travel.set_waypoint() uses player-centered coordinates function set_local_waypoint(num) -- local x,y = you.pos() travel.set_waypoint(num, 0, 0) end function vector_to_waypoint(num) local dx, dy = travel.waypoint_delta(num) crawl.mpr("dx = " .. tostring(dx) .. " , dy = " .. tostring(dy)) local cmd = delta_to_cmd(dx, dy) crawl.mpr("cmd = " .. cmd) crawl.do_commands({cmd}) end function init_swing(command, target, how_many) bump_attack_dir = command bump_mons_target = target bump_attacks_remaining = how_many -- crawl.mpr(tostring(bump_attack_dir) .. ", " .. tostring(bump_attacks_remaining)) end function stop_swing() bump_attack_dir = nil bump_mons_target = nil bump_attacks_remaining = 0 end -- you.feel_safe() "do you feel safe"; this might be a replacement builtin c fn for the firewood checks -- TODO: look into this function is_hostile(m) if m then if m:attitude() > ATT_NEUTRAL then return false end if m:is_firewood() then if not string.find(m:name(), "ballistomycete") then return false end end if m:name() == "butterfly" then return false end return true end end -- look ahead in the direction of a move_cmd and check for presence of a hostile enemy function is_valid_target(x,y) m = monster.get_monster_at(x, y) if m and is_hostile(m) then return m end return nil end -- should we auto swing? function auto_swing() if bump_attacks_remaining > 0 and bump_attack_dir ~= nil then local current_target = is_valid_target(cmd_to_delta(bump_attack_dir)) if current_target ~= nil and current_target:name() == bump_mons_target:name() then return true else -- stop the swing if our target is no longer there stop_swing() return false end end return false end -- initialize a repeat swing command, if something would cancel the repeat, issue a single swing instead function maybe_repeat_swing(command) local mons_target = is_valid_target(cmd_to_delta(command)) if mons_target ~= nil then init_swing(command, mons_target, 5) check_kickbacks() end if not auto_swing() then crawl.flush_input() crawl.redraw_screen() crawl.do_commands({command}) end end -- the swing should have already been validated before calling this function repeat_swing() if not auto_swing() then kickback("Invalid repeat_swing() command! (Is something wrong with the auto_swing script?)") return false end crawl.flush_input() crawl.redraw_screen() crawl.do_commands({bump_attack_dir}) bump_attacks_remaining = bump_attacks_remaining - 1 return true end function kickback(reason) crawl.formatted_mpr("Returning player control: " .. reason) stop() stop_swing() -- don't think there's a need to bother with an if do_bump_attacks check here end function low_hp(threshold) local hp, mhp = you.hp() return (100*hp <= threshold*mhp) end -- if I really want to save time with this, I need to give it vectoring behavior to deal with mthreat:2's -- and have it pop a basic god abil / self buff, so that it can handle more scenarios without dropping auto -- that way, I'd only have to drop into TACTICS for severe threats -- also, I probably need to rewrite its action logic to default fallthrough into player kickback. -- right now, lots of little unimplemented catches are being missed by the logic, and it's trying to continue -- on, despite the errors. -- I need to ensure that its default action is kickback, and only do a thing for one turn, per turn, -- if it's sure there's nothing else going on. function m_is_unique(m) -- uh, do i really need this wrapper when i'm probably going to assign it to a local anyway if m:is_unique() then return true end end -- apparently i do -- TODO: maybe move this to be a local function inside of monster_in_view ? is this worth it? -- a local mockup, using clua, of the explorer.rare_ood() implementation in /dat/dlua/explorer.dlua function check_rare_ood(m) local you_depth = you.depth() -- assigning crawl module function calls as locals here, we're running this on an inner loop local mdepth = m:avg_local_depth() -- this is premature optimization, but it probably won't hurt local mprob = m:avg_local_prob() local br_depth = you.depth_fraction() -- dgn.br_depth() appears to return brdepth (branch-local depth, I think); you.depth() will probably work here (?) -- local ood_threshold = math.max(2, you_depth / 3) local ood_threshold = 3 -- big ood only, and no pesky inner loop division? TODO: revisit this return mdepth > you_depth + ood_threshold and mprob < 2 end function monster_in_view(do_kickback) local LOS = you.los() -- properly handle nonstatic LOS as from nightwalker, robe of night, etc. local safe_los = true local m = nil local name = nil local threat = nil local weapon_desc = nil local mid_threat_count = 0 for i = -LOS,LOS do for j = -LOS,LOS do m = monster.get_monster_at(i,j) -- if m and is_hostile(m) and you.see_cell_no_trans(i,j) and view.can_reach(i,j) then if m and is_hostile(m) and you.see_cell_no_trans(i,j) then safe_los = false if do_kickback then name = m:name() threat = m:threat() weapon_desc = check_mons_weapon_desc(m) if weapon_desc then kickback("Monster with dangerous weapon: " .. name .. " (" .. weapon_desc .. ")") end if m_is_unique(m) then kickback("Unique Monster! Careful! : " .. name .. "") end if check_rare_ood(m) then kickback("OOD Mons, careful! : " .. name .. "") end if threat >= 3 then kickback("Extremely Dangerous Mons, RIP AND TEAR: " .. name .. "") elseif threat >= 2 then mid_threat_count = mid_threat_count + 1 -- Lua doesn't have ++ or += operators, apparently? -- By kicking back only when there are *multiple* mthreat: 2 monsters in view, we allow -- the script to handle more situations like "a single wyvern". This is risky, though: -- Some mons like hydras can be mthreat: 2 at their normal spawn depth; this behavior can result -- in very dangerous low-HP kickbacks for weaker characters. -- TODO: Probably revisit this. (Maybe add a script option for the user to choose this behavior?) -- (Maybe write fully-custom threat logic, and don't use m:threat() at all?) (that's a lot of work..) if mid_threat_count >= 2 then kickback("Dangerous Mid-Threat Pack: " .. name .. "") end end end end if view.invisible_monster(i,j) then safe_los = false if do_kickback then kickback("Invisible monster nearby?") end end end end return (not safe_los) end function you_are_unholy() local species = you.race() if species == "Mummy" or species == "Ghoul" or species == "Demonspawn" -- or (species == "Vampire" and you.hunger_name() == "bloodless") or species == "Vampire" or you.transform() == "lich" then return true end return false end -- {"name", int stop_warning_past_this_xl} local dangerous_brands = { {"electrocution", 12}, {"distortion", 27}, {"chaos", 27}, {"venom", 12}, {"speed", 12}, {"flaming", 12}, {"freezing", 12}, {"vorpal", 15}, } local holy_brands = { {"holy wrath", 27}, {"sacred scourge", 27}, {"trishula", 27}, {"eudemon blade", 27}, } function check_mons_weapon_desc(m) -- this pattern match should handle large decimal enchantments, but it won't capture the Axe of Woe local plus = tonumber(string.match(m:desc(true), "%+(%d+)")) -- the ingame text is "its +#" local brand = nil local you_xl = you.xl() -- try to extract a weapon descriptor from m:desc(), by matching everything from '+%d' to '\n'ewline, -- try not to return the '.' at the end of the weapon descriptor, -- also try not to erroneously return AC/EV/MR descriptors like '+++' -- TODO: This currently returns a poorly-trimmed string against Wraiths, at least, and probably others. -- See if I can further improve the pattern match? local weapon_desc = string.match(m:desc(true), "(%+%d+[^\n%.]+)") if weapon_desc then for _, which_brand in ipairs(dangerous_brands) do if string.find(weapon_desc, which_brand[1]) then if you_xl <= which_brand[2] then brand = which_brand[1] end end end if you_are_unholy() then for _, holy_brand in ipairs(holy_brands) do if string.find(weapon_desc, holy_brand[1]) then if you_xl <= holy_brand[2] then brand = holy_brand[1] end end end end end if (plus and (plus >=3)) or brand then return weapon_desc end return nil end function hp_not_full() local hp, mhp = you.hp() return (hp < mhp) end function mp_not_full() local mp, mmp = you.mp() return (mp < mmp) end function hp_mp_not_full() return (hp_not_full() or mp_not_full()) end function hungry() if you.hunger() <= 1 then return true end end --view.feature_at(x,y) returns the second name string in feature-data.h, feature_def feat_defs[] , -- this is the "vaultname" string that includes underscores local should_feature_auto_stop = { {"runed_door", true}, {"sealed_door", true}, {"sealed_clear_door", true}, {"closed_clear_door", true}, {"runed_clear_door", true}, {"malign_gateway", true}, -- {"teleporter", true}, -- old ver {"transporter", true}, {"sealed_stair_up", true}, {"sealed_stair_down", true}, {"exit_abyss", true}, {"abyssal_stair", true}, {"enter_shop", true}, -- {"enter_labyrinth", true}, -- old ver {"enter_dis", true}, {"enter_gehenna", true}, {"enter_cocytus", true}, {"enter_tartarus", true}, {"enter_hell", true}, {"enter_abyss", true}, {"enter_pandemonium", true}, {"transit_pandemonium", true}, {"enter_vaults", true}, {"enter_zot", true}, -- {"enter_portal_vault", true}, -- old ver {"enter_ziggurat", true}, {"enter_bazaar", true}, {"enter_trove", true}, {"enter_sewer", true}, {"enter_ossuary", true}, {"enter_bailey", true}, {"enter_gauntlet", true}, {"enter_ice_cave", true}, {"enter_volcano", true}, {"enter_wizlab", true}, {"enter_desolation", true}, {"enter_slime_pits", true}, {"enter_orcish_mines", true}, -- {"enter_dwarven_hall", true}, -- really old ver but its still in the code -- {"enter_forest", true}, -- old ver -- {"enter_hall_of_blades", true}, -- old ver {"enter_lair", true}, {"enter_crypt", true}, {"enter_temple", true}, {"enter_snake_pit", true}, {"enter_tomb", true}, {"enter_swamp", true}, {"enter_shoals", true}, {"enter_spider_nest", true}, {"enter_depths", true}, {"unknown_altar", true}, {"altar_zin", true}, {"altar_the_shining_one", true}, {"altar_kikubaaqudgha", true}, {"altar_yredelemnul", true}, {"altar_xom", true}, {"altar_vehumet", true}, {"altar_okawaru", true}, {"altar_makhleb", true}, {"altar_sif_muna", true}, {"altar_trog", true}, {"altar_nemelex_xobeh", true}, {"altar_elyvilon", true}, {"altar_lugonu", true}, {"altar_beogh", true}, {"altar_jiyva", true}, {"altar_fedhas", true}, {"altar_cheibriados", true}, {"altar_ashenzari", true}, {"altar_dithmenos", true}, {"altar_gozag", true}, {"altar_qazlal", true}, {"altar_ru", true}, {"altar_ecumenical", true}, -- {"altar_pakellas", true}, -- old ver {"altar_uskayaw", true}, {"altar_hepliaklqana", true}, {"altar_wu_jian", true}, } -- currently, this function only kicks back once per-feature, per-floor -- this is not a good way to handle features that may have floor-local duplicates, like shops or transporters: -- we're handling shops separately with a combination of a TravelDelay interrupt hook and post-runrest hook, -- that approach notifies the player once for every unique shop, when autoexplore travels to its location. -- Other floor-local duplicate features are currently unhandled. -- TODO: update this function to properly handle other important floor-local duplicate features function check_feature_kickbacks() local LOS = you.los() -- properly handle nonstatic LOS as from nightwalker, robe of night, etc. local feature = nil local where = you.where() for i = -LOS, LOS do for j = -LOS, LOS do feature = view.feature_at(i,j) for _,v in ipairs(should_feature_auto_stop) do if feature == v[1] and v[2] then -- if we've already found this feature on the current floor, then don't kickback if update_found_kfeats_table(feature, where) then if feature:find("altar") and (c_persist.HAVE_GOD or you.branch() == "Temple") then -- don't kickback for altars if we already have a god or if we're in Temple -- (we still record these in the kfeats table above) else kickback("Pause for important dungeon feature: " .. feature) end end end end end end end -- This function checks if we should disable the script, and return control to the player. -- This is called at the beginning of ready(), it's important not to issue any commands here. function check_kickbacks() -- check the crawl.messages() log for notifications, like portal announcements check_for_message_log_kickbacks() -- look for important KFEATS in visible LOS, like altars and branch entries check_feature_kickbacks() -- are we standing in a dangerous cloud if safe_cloud() == false then kickback("Standing in dangerous cloud: " .. view.cloud_at(0,0)) end -- is there a hostile monster in view, and should we stop for it? -- (monster_in_view() also contains kickback logic for "dangerous" mons, using its own heuristics) -- TODO: revise this logic, and the monster_in_view() logic if monster_in_view(true) then -- stop for status effects, if any hostile monster is present do_player_status_kickbacks() -- moving status check inside the mons LOS check so it won't kickback with no mons onscreen -- placing status here might break the script, for effects that root after monsters are dead -- stop for low HP, if any hostile monster is present if low_hp(HP_KICKBACK_THRESHOLD) then kickback("Low HP") end if low_hp(HP_MULTIBUMP_THRESHOLD) and auto_swing() then kickback("HP below multiswing threshold") end end -- TODO: replacement for the above logic -- if there are any hostile monsters in LOS, (return a table of m's, here) -- for each monster in the table, do we have a reason to stop just because of the monster? -- or should we stop because or low HP, -- or should we stop because of status -- are we severely poisoned? -- (we need to check for fatal poison even with no mons in view) -- TODO: revisit if any other status should be pulled out of the mons view check if you.poisoned() then local hp, mhp = you.hp() if (100*you.poison_survival() <= POISON_KICKBACK_THRESHOLD*mhp) then kickback("Severe Poison") -- i hope you have !curing dude end end -- For technical reasons, some kickbacks are handled elsewhere in the script: -- Hunger is being checked further down the main ready() loop, to prevent interference with autoexplore. -- Shops are being checked as a combination TravelDelay interrupt and post-runrest hook, to enable items being -- added to the shopping list, before the player sees the shop window open. end -- player status kickbacks -- TODO: rewrite this as a table comparison, piped from you.status() ? function do_player_status_kickbacks() if you.paralysed() then kickback("Paralysed!!") end if you.confused() then kickback("Confused") end if you.petrifying() then kickback("Petrifying!") end if you.caught() then kickback("Caught in a " .. you.caught()) end -- this might not work very well with auto() in spider if you.constricted() then kickback("Being constricted") end if you.corrosion() > 0 then kickback("Corroded!") end if you.mesmerised() then kickback("Being mesmerised") end if you.on_fire() and you.res_fire() < 0 then kickback("Burning with rF-") end if you.rooted() then kickback("Rooted") end if you.slowed() then kickback("Slowed!") end -- how to check player draining for wights? -- TODO: verify that this skill drain comparison is working properly; -- TODO: i think this isn't working properly -- if it's not, I could probably also check you.status("drain") if ( 100 * you.skill(you.best_skill()) < SKILL_DRAIN_KICKBACK_THRESHOLD * you.base_skill(you.best_skill()) ) then kickback("Drained!") end local transform = you.transform() -- form strings pulled from form-data.h -- in testing, you.transform() appears to use the third string form in the header (wiz-name) if transform == "bat" then kickback("Hostile polymorph: Batform") end if transform == "pig" then kickback("Hostile polymorph: Pigform (Kirke?)") end if transform == "fungus" then kickback("Hostile polymorph: Fungus") end if transform == "wisp" then kickback("Hostile polymorph: Wisp") end if transform == "tree" then kickback("Treeform: (Did the script auto-quaff !lig ?)") end if you.under_penance() then kickback("Under god penance, maybe the script broke a god conduct?") end -- TODO: fixup these stat-zero checks, I think they aren't working properly local int, mint = you.intelligence() local str, mstr = you.strength() local dex, mdex = you.dexterity() if int <= 0 then kickback("Stat-zero (int)") end -- galaxy if str <= 0 then kickback("Stat-zero (str)") end if dex <= 0 then kickback("Stat-zero (dex)") end end -- is it safe for auto() to retain character control while standing in this type of cloud? -- {string cloud_name, bool is_this_cloud_safe} -- -- cloud name strings pulled from cloud.cc, cloud_data clouds[] , first name field (terse) -- -- i don't know how to get conditional function return values working, here, so i'm handling a couple of those -- as a special case in the calling function -- (this seems to initialize the function retvals when the Lua interpreter is initialized and not update them later) -- (is this because I've declared this table as local to the toplevel player cLua context?) -- (this is a closure thing, I think?) -- TODO: read about Lua closures and fix this local safe_cloud_types = { {"?", false}, -- CLOUD_NONE {"flame", false}, -- CLOUD_FIRE -- {"noxious fumes", (function () if you.res_poison() > 0 then return true else return false end end)}, -- CLOUD_MEPHITIC {"noxious fumes", false}, -- CLOUD_MEPHITIC {"freezing vapour", false}, -- CLOUD_COLD {"poison gas", false}, -- CLOUD_POISON {"black smoke", true}, -- CLOUD_BLACK_SMOKE {"grey smoke", true}, -- CLOUD_GREY_SMOKE {"blue smoke", true}, -- CLOUD_BLUE_SMOKE {"purple smoke", true}, -- CLOUD_PURPLE_SMOKE {"translocational energy", false}, -- CLOUD_TLOC_ENERGY {"spreading flames", false}, -- CLOUD_FOREST_FIRE -- {"steam", (function () if you.res_fire() > 0 then return true else return false end end)}, -- CLOUD_STEAM -- {"steam", you_steam_res()}, {"steam", false}, -- CLOUD_STEAM {"gloom", false}, -- old ver -- CLOUD_GLOOM {"ink", false}, -- CLOUD_INK -- strictly, this might not be dangerous on its own, but there's probably a kraken nearby.. {"calcifying dust", false}, -- CLOUD_PETRIFY {"blessed fire", false}, -- CLOUD_HOLY {"foul pestilence", false}, -- CLOUD_MIASMA {"thin mist", true}, -- CLOUD_MIST -- marking this safe for now -- TODO: find a full list of what spawns this // ETC_MIST {"seething chaos", false}, -- CLOUD_CHAOS {"rain", false}, -- CLOUD_RAIN -- TODO: figure out the full list of when ETC_MIST // TILE_CLOUD_RAIN can spawn {"mutagenic fog", false}, -- CLOUD_MUTAGENIC {"magical condensation", false}, -- CLOUD_MAGIC_TRAIL -- first instance of this, labelled // CLOUD_MAGIC_TRAIL {"raging winds", false}, -- CLOUD_TORNADO {"sparse dust", false}, -- CLOUD_DUST {"spectral mist", false}, -- CLOUD_SPECTRAL {"acidic fog", false}, -- CLOUD_ACID {"thunder", false}, -- CLOUD_STORM {"negative energy", false}, -- CLOUD_NEGATIVE_ENERGY {"white fluffiness", true}, -- CLOUD_FLUFFY -- i sure hope this is safe {"magical condensation", false}, -- CLOUD_XOM_TRAIL --second instance of this, labelled // CLOUD_XOM_TRAIL {"salt", true}, -- CLOUD_SALT , this isn't dangerous by itself? manual control in desolation might be smarter, though.. {"golden dust", false}, -- CLOUD_GOLD_DUST {"smoldering embers", false}, -- CLOUD_EMBERS {"wisps of flame", false}, } -- CLOUD_FLAME -- through the fire and flames we actually don't carry on -- is it safe to continue standing in the cloud at (0,0)? -- -- returns true if we are standing in a safe cloud, -- false if we are standing in an unsafe cloud, -- nil if we are not standing in a cloud function safe_cloud() local cloud = view.cloud_at(0,0) local res_fire = you.res_fire() local res_poison = you.res_poison() if cloud then for _, v in ipairs(safe_cloud_types) do if cloud == "steam" then -- if you.res_fire() > 0 then if res_fire > 0 then return true else return false end end if cloud == "noxious fumes" then -- if you.res_poison() > 0 then if res_poison > 0 then return true else return false end end if cloud == v[1] then return v[2] end end return false -- this will disable auto() if we are standing in an unrecognized cloud type else return nil end end function rest() crawl.mpr("Rest!") crawl.sendkeys("5") end -- TODO: fancier fight logic; at least hit_closest_nomove with throwing checks function fight() crawl.mpr("Fight!") hit_closest() end function inventory() return iter.invent_iterator:new(items.inventory()) end function eat(it) local name = it.name() crawl.mpr("Eating: " .. name) crawl.sendkeys("e" .. items.index_to_letter(it.slot)) end function drop(it) local name = it.name() crawl.mpr("Dropping " .. name) crawl.sendkeys("d" .. items.index_to_letter(it.slot) .. string.char(13)) end function read(it) local itname = it.name() if it.class(true) == "scroll" then crawl.mpr("Reading scroll: " .. itname) crawl.sendkeys("r" .. items.index_to_letter(it.slot)) else crawl.mpr("Couldn't read scroll, is it the wrong item class? : " .. itname) end end function quaff(it) local itname = it.name() if it.class(true) == "potion" then crawl.mpr("Quaffing potion: " .. itname) crawl.sendkeys("q" .. items.index_to_letter(it.slot)) else crawl.mpr("Couldn't quaff potion, is it the wrong item class? : " .. itname) end end function scroll_identify(scroll, it) local scname = scroll.name() local itname = it.name() crawl.mpr("Using scroll: " .. scname .. ", on target: " .. itname) crawl.sendkeys("r" .. items.index_to_letter(scroll.slot) .. items.index_to_letter(it.slot)) end function read_identify(it) -- we're already doing this from a safe location, just input the commands local itname = it.name() local qty = it.quantity crawl.mpr("Trying to read-id a quantity " .. tostring(qty) .. " scroll stack: " .. itname) crawl.sendkeys("r" .. items.index_to_letter(it.slot) .. string.char(27) .. string.char(13)) end function enchant(scroll, it) local itname = it.name() crawl.mpr("Scroll enchanting: " .. itname) scroll_identify(scroll, it) -- TODO: rename the scroll_identify function? having a wrapper here is kind of dumb end -- TODO: possibly wrap some logic in here to add to that acquirement Lua hook that gammafunk added function acquire(scroll) crawl.mpr("Reading acquirement!") read(scroll) kickback("Acquired something?") end function wear_identify(it) local itname = it.name() crawl.mpr("Trying to wear-identify item: " .. itname) equip(it) end function wield_identify(it) local itname = it.name() crawl.mpr("Trying to wield-identify item: " .. itname) wield(it) end function wield(it) local it_name = it.name() local it_class = it.class(true) local success = nil if it_class == "magical staff" or it_class == "weapon" or it_class == "food" then -- Breadswinging, ho! crawl.mpr("Trying to wield item: " .. it_name) success = it.wield() else kickback("Trying to wield item: " .. it_name .. " , but it's a bad class to wield.") success = it.wield() end if success ~= nil and success == false then crawl.mpr("Couldn't wield item: " .. itname .. " , are we wearing a shield?, (or is there a species size incompatibility?)") end end function unwield(it) local it_name = it.name() crawl.mpr("Trying to unwield item: " .. it_name) crawl.sendkeys("w-") end --function equip(it) -- TODO: turns out i probably need to use items.wear(it) for this instead -- local itname = it.name() -- of a dumb sendkeys because of a potential silent slot-unavailability failure -- crawl.mpr("Trying to (W)ear armour: " .. itname) -- crawl.sendkeys("W" .. items.index_to_letter(it.slot)) --end function equip(it) local itname = it.name() local itclass = it.class(true) if itclass == "armour" then crawl.mpr("Trying to equip armour: " .. itname) it.wear() elseif itclass == "jewellery" then if it.equip_type == 101 then crawl.mpr("Trying to equip ring: " .. itname) local worst_ring = check_worst_equipped_ring() if worst_ring ~= nil then local worst_slot_key = items.index_to_letter(worst_ring.slot) crawl.sendkeys("P" .. items.index_to_letter(it.slot) .. worst_slot_key) else it.puton() end else crawl.mpr("Trying to equip jewellery: " .. itname) it.puton() end else crawl.mpr("Couldn't equip item: " .. itname .. " , is it not armour or jewellery?") end end function check_inventory_for_food() local chunk = nil local ration = nil for it in inventory() do if it.class(true) == "food" then if it.name():find("chunk") then chunk = it end if it.name():find("ration") then -- this string matches to "disintegration", careful ration = it end end end if chunk then return chunk elseif ration then return ration else return nil end end function try_to_eat() local food = check_inventory_for_food() if food then eat(food) else kickback("Hungry: Couldn't find any food!") end end -- sourced from output.cc, *s_equip_slot_names[] local equip_slot_names_table = { "Weapon", "Cloak", "Helmet", "Gloves", "Boots", "Shield", "Armour", "Left Ring", "Right Ring", "Amulet", "First Ring", "Second Ring", "Third Ring", "Fourth Ring", "Fifth Ring", "Sixth Ring", "Seventh Ring", "Eighth Ring", "Amulet Ring", } -- these are in order of slot enchantment preference: -- helm first because helm egos are not great, -- then boots because running and flying are pretty rare, -- then gloves because glove egos can be nice, -- lastly cloaks because scarves can supplant the slot now local aux_slot_names_table = { "Helmet", "Boots", "Gloves", "Cloak", } -- technically, we should probably be enchanting body armour before cloaks now that scarves exist, but it's not a big deal local body_armour_slot_name_table = { "Armour", } -- this should result in shield ench only if the other armour slots are all capped local shield_slot_name_table = { "Shield", } local enchant_armour_targets_table = { aux_slot_names_table, body_armour_slot_name_table, shield_slot_name_table, } local ring_slot_names_table = { "Left Ring", "Right Ring", } local octopode_ring_slot_names_table = { "First Ring", "Second Ring", "Third Ring", "Fourth Ring", "Fifth Ring", "Sixth Ring", "Seventh Ring", "Eighth Ring", } local macabre_ring_slot_name_table = { "Amulet Ring", } function get_you_ring_slot_tables() local ring_slot_table = {} if you.race() == "Octopode" then table.insert(ring_slot_table, octopode_ring_slot_names_table) else table.insert(ring_slot_table, ring_slot_names_table) end local it = items.equipped_at("Amulet") -- name string from /dat/descript/unrand.txt if it ~= nil and it.name():find("macabre finger necklace") then table.insert(ring_slot_table, macabre_ring_slot_name_table) end return ring_slot_table end function mpr_ring_slot_tables() local ring_slot_tables = get_you_ring_slot_tables() for _,subtable in ipairs(ring_slot_tables) do for _,slot_name in ipairs(subtable) do crawl.mpr(slot_name) end end end function check_worst_equipped_ring() local it = nil local itval = nil local worst_val = nil local worst_it = nil local ring_slot_tables = get_you_ring_slot_tables() for _,subtable in ipairs(ring_slot_tables) do for _,slot_name in ipairs(subtable) do it = items.equipped_at(slot_name) if it == nil then return it else itval = get_prop_value(it) if worst_val == nil or itval < worst_val then worst_val = itval worst_it = it end end end end return worst_it end function check_worst_equipped_at(equip_type) local equipped = nil -- a quick in-game test shows ring equip_type is 101 on both octopode and merfolk characters, -- i don't know if this is always true -- TODO: figure out if this assumption is safe if equip_type == 101 then equipped = check_worst_equipped_ring() else equipped = items.equipped_at(equip_type) end return equipped end -- items.equipped_at() returns nil for nothing equipped OR invalid slot -- i do not see any exposed lua hooks like "is_slot_available"; this will create problems with melded, -- sacrificed, and species-restricted slots -- qw appears to handle this with special-cased individual checks for all of the above, scattered throughout its code -- that seems horrible -- -- permanently-restricted slots should be able to be handled with items.is_useless, i think (?), -- i don't know if there's a you.() flag to check things like SACRIFICE_HAND, -- TODO: Look into this -- and I don't see any exposure at all through Lua for whether a _slot_ is currently unavailable due to melding: -- there is a check for item:is_melded, but that's not helpful if a slot is currently empty AND temporarily melded-unavailable -- -- possibly, the best i can do here is to lockout attempts at gear-swapping while you.transform() returns anything -- -- items.wear(item) returns a bool for success ; probably I will have to check against this, too, instead of equipping with -- a dumb sendkeys() function check_eligible_equip() for it in inventory() do if it.class(true) == "armour" then local equipped = nil equipped = items.equipped_at(it.equip_type) -- equipped_at() returns nil for nothing equipped OR invalid slot if equipped == nil then -- i do not see any lua hooks like "is_slot_available", this will create problems return it -- with melded, sacrificed, and species-restricted slots end if equipped.name():find("robe") and not it.name():find("robe") then return it end -- TODO: if not it.cursed, it.NEGATIVE_SLAY, etc end end return nil end -- "autoequip()" -- if the player has any items in inventory that are "better" than what's equipped in that slot, -- this function will return the first such item. -- it evaluates "better" by relative prop value, as returned from get_prop_value() function check_better_equips() for it in inventory() do -- possible logic problem here: this function will never identify unid stuff, and thus won't consider it for equip, -- even if the unid thing should obviously be better than what's equipped (e.g. earlygame armour) if it.fully_identified and not it.equipped then if it.class(true) == "armour" or (it.class(true) == "jewellery" and it.name():find("ring")) then local equipped = nil local equip_value = nil -- i need to replace this call with a wrapper fn that grabs the worst equipped item, -- to handle things like ring slots -- equipped = items.equipped_at(it.equip_type) equipped = check_worst_equipped_at(it.equip_type) local it_value = get_prop_value(it) if equipped ~= nil then equip_value = get_prop_value(equipped) -- i need this to work with non-artefact items before using this fn -- there's no guarantee that we have remove curse available, so we bail if the equipped slot is cursed if it_value > equip_value and not is_slot_cursed(equipped) then return it end end if equipped == nil and it_value >= 0 then return it end end end end return nil end local last_explore_turn = nil -- get the current CMD_EXPLORE keybind once, when the script is loaded, to permit binding Fast-Forward to 'o' -- this assumes the player doesn't rebind CMD_EXPLORE while playing local explore_keybind = crawl.get_command("CMD_EXPLORE") function set_current_goal(new_goal_string) c_persist.CURRENT_GOAL = new_goal_string end if c_persist.CURRENT_GOAL == nil then set_current_goal("goal_god") end function get_current_goal() return c_persist.CURRENT_GOAL end -- god name strings pulled from religion.cc, string god_name() -- case GOD_NO_GOD: return "No God"; -- case GOD_RANDOM: return "random"; -- case GOD_NAMELESS: return "nameless"; -- case GOD_ZIN: return "Zin"; -- case GOD_SHINING_ONE: return "the Shining One"; -- case GOD_KIKUBAAQUDGHA: return "Kikubaaqudgha"; -- case GOD_YREDELEMNUL: return "Yredelemnul"; -- case GOD_VEHUMET: return "Vehumet"; -- case GOD_OKAWARU: return "Okawaru"; -- case GOD_MAKHLEB: return "Makhleb"; -- case GOD_SIF_MUNA: return "Sif Muna"; -- case GOD_TROG: return "Trog"; -- case GOD_NEMELEX_XOBEH: return "Nemelex Xobeh"; -- case GOD_ELYVILON: return "Elyvilon"; -- case GOD_LUGONU: return "Lugonu"; -- case GOD_BEOGH: return "Beogh"; -- case GOD_FEDHAS: return "Fedhas"; -- case GOD_CHEIBRIADOS: return "Cheibriados"; -- case GOD_XOM: return "Xom"; -- case GOD_ASHENZARI: return "Ashenzari"; -- case GOD_DITHMENOS: return "Dithmenos"; -- case GOD_GOZAG: return "Gozag"; -- case GOD_QAZLAL: return "Qazlal"; -- case GOD_RU: return "Ru"; --#if TAG_MAJOR_VERSION == 34 -- case GOD_PAKELLAS: return "Pakellas"; --#endif -- case GOD_USKAYAW: return "Uskayaw"; -- case GOD_HEPLIAKLQANA: return "Hepliaklqana"; -- case GOD_WU_JIAN: return "Wu Jian"; -- case GOD_JIYVA: // This is handled at the beginning of the function -- case GOD_ECUMENICAL: return "an unknown god"; -- case NUM_GODS: return "Buggy"; -- -- string name = "Jiyva"; -- altar data pulled from feature-data.h, #define ALTAR -- {"kfeat_name", "ff.rc abbrev for local godname table", "crawl godname", "altar kfeat long description for ^f in-game"} local altar_kfeats_table = { {"unknown_altar", "", "", "detected altar"}, {"altar_zin", "zin", "Zin", "glowing silver altar of Zin"}, {"altar_the_shining_one", "tso", "the Shining One", "glowing golden altar of the Shining One"}, {"altar_kikubaaqudgha", "kiku", "Kikubaaqudgha", "ancient bone altar of Kikubaaqudgha"}, {"altar_yredelemnul", "yred", "Yredelemnul", "basalt altar of Yredelemnul"}, {"altar_xom", "xom", "Xom", "shimmering altar of Xom"}, {"altar_vehumet", "veh", "Vehumet", "radiant altar of Vehumet"}, {"altar_okawaru", "oka", "Okawaru", "iron altar of Okawaru"}, {"altar_makhleb", "makh", "Makhleb", "burning altar of Makhleb"}, {"altar_sif_muna", "sif", "Sif Muna", "shimmering blue altar of Sif Muna"}, {"altar_trog", "trog", "Trog", "bloodstained altar of Trog"}, {"altar_nemelex_xobeh", "nem", "Nemelex Xobeh", "sparkling altar of Nemelex Xobeh"}, {"altar_elyvilon", "ely", "Elyvilon", "white marble altar of Elyvilon"}, {"altar_lugonu", "lucy", "Lugonu", "corrupted altar of Lugonu"}, {"altar_beogh", "beogh", "Beogh", "roughly hewn altar of Beogh"}, {"altar_jiyva", "jiyva", "Jiyva", "viscous altar of Jiyva"}, {"altar_fedhas", "fedhas", "Fedhas", "blossoming altar of Fedhas"}, {"altar_cheibriados", "chei", "Cheibriados", "snail-covered altar of Cheibriados"}, {"altar_ashenzari", "ash", "Ashenzari", "shattered altar of Ashenzari"}, {"altar_dithmenos", "dith", "Dithmenos", "shadowy altar of Dithmenos"}, {"altar_gozag", "gozag", "Gozag", "opulent altar of Gozag"}, {"altar_qazlal", "qaz", "Qazlal", "stormy altar of Qazlal"}, {"altar_ru", "ru", "Ru", "sacrificial altar of Ru"}, {"altar_ecumenical", "", "an unknown god", "faded altar of an unknown god"}, -- {"altar_pakellas", "pak", "Pakellas", "oddly glowing altar of Pakellas"}, -- old ver {"altar_uskayaw", "usk", "Uskayaw", "hide-covered altar of Uskayaw"}, {"altar_hepliaklqana", "hep", "Hepliaklqana", "hazy altar of Hepliaklqana"}, {"altar_wu_jian", "wjc", "Wu Jian", "ornate altar of the Wu Jian Council"}, } -- target_gods_table is at the top of this file for ease of player access function is_valid_target_god_kfeat(local_kfeat) for _,which_god in ipairs(target_gods_table) do for _,which_kfeat in ipairs(altar_kfeats_table) do if string.lower(which_god) == which_kfeat[2] or string.lower(which_god) == "random" then if local_kfeat == which_kfeat[1] then crawl.mpr("local kfeat is a valid altar") return true end end end end crawl.mpr("local kfeat is not a valid altar") return false end function are_we_standing_on_a_valid_altar() return is_valid_target_god_kfeat(view.feature_at(0,0)) end -- this will return nil if there are no matches in the target_gods_table, be careful! function get_valid_god_kfeats() local valid_altars = {} for _,which_god in ipairs(target_gods_table) do for _,which_kfeat in ipairs(altar_kfeats_table) do if string.lower(which_god) == which_kfeat[2] then table.insert(valid_altars, which_kfeat[1]) end end end return valid_altars end function get_valid_gods() local valid_gods = {} for _,which_god in ipairs(target_gods_table) do for _,entry in ipairs(altar_kfeats_table) do if string.lower(which_god) == entry[2] then table.insert(valid_gods, entry[3]) end end end return valid_gods end function have_target_god() if you.race() == "Demigod" then return true end for _,which_god in ipairs(target_gods_table) do if which_god == "none" then return true end end local you_god = you.god() local valid_gods = get_valid_gods() for _,entry in ipairs(valid_gods) do if you_god == entry then return true end end return false end function reset_strategic_goal_flags() c_persist.HAVE_GOD = false c_persist.CLEARED_TEMPLE = false c_persist.CLEARED_LAIR = false c_persist.CLEARED_ORC = false c_persist.CLEARED_DUNGEON = false c_persist.LAIRBRANCHES = nil c_persist.CLEARED_LAIRBRANCH_1 = false c_persist.CLEARED_LAIRBRANCH_2 = false c_persist.CLEARED_VAULTS_4 = false c_persist.CLEARED_DEPTHS = false c_persist.CLEARED_VAULTS = false c_persist.CLEARED_ZOT = false c_persist.HAVE_ORB = false end -- TODO: table-ize all of these, this code is really dumb function initialize_strategic_goal_flags() if c_persist.HAVE_GOD == nil then c_persist.HAVE_GOD = false end if c_persist.CLEARED_TEMPLE == nil then c_persist.CLEARED_TEMPLE = false end if c_persist.CLEARED_LAIR == nil then c_persist.CLEARED_LAIR = false end if c_persist.CLEARED_ORC == nil then c_persist.CLEARED_ORC = false end if c_persist.CLEARED_DUNGEON == nil then c_persist.CLEARED_DUNGEON = false end if c_persist.CLEARED_LAIRBRANCH_1 == nil then c_persist.CLEARED_LAIRBRANCH_1 = false end if c_persist.CLEARED_LAIRBRANCH_2 == nil then c_persist.CLEARED_LAIRBRANCH_1 = false end if c_persist.CLEARED_VAULTS_4 == nil then c_persist.CLEARED_VAULTS_4 = false end if c_persist.CLEARED_DEPTHS == nil then c_persist.CLEARED_DEPTHS = false end if c_persist.CLEARED_VAULTS == nil then c_persist.CLEARED_VAULTS = false end if c_persist.CLEARED_ZOT == nil then c_persist.CLEARED_ZOT = false end if c_persist.HAVE_ORB == nil then c_persist.HAVE_ORB = false end end function fn_goal_god() -- have_religion, set next major goal if you.race() == "Demigod" or have_target_god() == true then c_persist.HAVE_GOD = true set_current_goal("goal_lair") crawl.mpr("goal_god complete! setting goal_lair.") crawl.sendkeys(".") return true end -- crawl.do_commands({CMD_NO_CMD_DEFAULT}) doesn't work properly for scripted non-actions intended to end the turn (as above); -- it processes as no command for the turn and ready() completes with auto() still active, -- but the turn doesn't finish and the game continues to wait for player input. -- I probably need to bind CMD_NO_CMD_DEFAULT to something and sendkeys that if I want the script to continue here: -- (Using "." wastes an in-game turn and isn't ideal.) -- TODO: replace all crawl.sendkeys(".") in the fn_goal functions with a sendkeys bound to CMD_NO_CMD_DEFAULT? -- pray_altar if is_valid_target_god_kfeat(view.feature_at(0,0)) then crawl.sendkeys(">" .. string.char(13)) return true end local valid_god_kfeats = get_valid_god_kfeats() -- if found_altar, travel_altar for _,entry in ipairs(c_persist.FOUND_KFEATS_TABLE) do for _,valid_altar in ipairs(valid_god_kfeats) do if entry[1] == valid_altar then local god_altar_string = nil for _,gods in ipairs(altar_kfeats_table) do if entry[1] == gods[1] then god_altar_string = gods[4] end end crawl.mpr("Trying to travel to altar: " .. god_altar_string) crawl.sendkeys("*f" .. god_altar_string .. string.char(13) .. "a" .. string.char(13)) return true end end end -- if cleared_temple, exit_temple if you.branch() == "Temple" and not c_persist.CLEARED_TEMPLE then c_persist.CLEARED_TEMPLE = true crawl.mpr("Leaving Temple, target god altar not found. (Overflow?)") crawl.sendkeys("GD" .. string.char(13)) return true end -- if found_temple and not cleared_temple, enter_temple for _,entry in ipairs(c_persist.FOUND_KFEATS_TABLE) do if entry[1] == "enter_temple" and not c_persist.CLEARED_TEMPLE then crawl.mpr("Trying to travel to Temple.") crawl.sendkeys("GT" .. string.char(13)) return true end end -- else, go downstairs crawl.mpr("Going Down!") crawl.sendkeys("G>") return false end function fn_goal_lair() -- cleared_lair, set next major goal if c_persist.CLEARED_LAIR == true then set_current_goal("goal_orc") crawl.mpr("goal_lair complete! setting goal_orc.") crawl.sendkeys(".") return true end -- if found_lair and not in_lair, enter_lair if you.branch() ~= "Lair" then for _,entry in ipairs(c_persist.FOUND_KFEATS_TABLE) do if entry[1] == "enter_lair" and not c_persist.CLEARED_LAIR then crawl.mpr("Trying to travel to Lair.") crawl.sendkeys("GL" .. string.char(13)) return true end end end -- if cleared lair, set cleared_lair -- TODO: "if not you_shafted()" if you.where() == "Lair:6" then c_persist.CLEARED_LAIR = true crawl.mpr("Cleared lair!") crawl.sendkeys(".") return true end -- if we're in deep D and we haven't found Lair, kickback if you.branch() == "D" and you.depth() > 11 then kickback("Failed to find Lair entry. (Maybe it's in a disconnected area?)") crawl.sendkeys(".") return false end -- if we're still in Temple, go back to D if you.branch() == "Temple" then crawl.mpr("Trying to travel to Dungeon.") crawl.sendkeys("GD" .. string.char(13)) end -- else, go downstairs crawl.mpr("Going Down!") crawl.sendkeys("G>") return false end function fn_goal_orc() -- cleared_orc, set next major goal if c_persist.CLEARED_ORC == true then set_current_goal("goal_d15") crawl.mpr("goal_orc complete! setting goal_d15.") crawl.sendkeys(".") return true end -- if found_orc and not in_orc, enter_orc if you.branch() ~= "Orc" then for _,entry in ipairs(c_persist.FOUND_KFEATS_TABLE) do if entry[1] == "enter_orcish_mines" and not c_persist.CLEARED_ORC then crawl.mpr("Trying to travel to Orc.") crawl.sendkeys("GO" .. string.char(13)) return true end end end -- if cleared orc, set cleared_orc -- TODO: "if not you_shafted()" if you.where() == "Orc:2" then c_persist.CLEARED_ORC = true crawl.mpr("Cleared orc!") crawl.sendkeys(".") return true end -- if we're in deep D and we haven't found Orc, kickback if you.branch() == "D" and you.depth() > 12 then kickback("Failed to find Orc entry. (Maybe it's in a disconnected area?)") crawl.sendkeys(".") return false end -- if we're still in Lair, go back to D if you.branch() == "Lair" then crawl.mpr("Trying to travel to Dungeon.") crawl.sendkeys("GD" .. string.char(13)) end -- else, go downstairs crawl.mpr("Going Down!") crawl.sendkeys("G>") return false end function fn_goal_d15() -- cleared_dungeon, set next major goal if c_persist.CLEARED_DUNGEON == true then set_current_goal("goal_s1") crawl.mpr("goal_d15 complete! setting goal_s1.") crawl.sendkeys(".") return true end -- if not in_d, enter_d if you.branch() ~= "D" and not c_persist.CLEARED_DUNGEON then crawl.mpr("Trying to travel to D.") crawl.sendkeys("GD" .. string.char(13)) return true end -- if cleared dungeon, set cleared_dungeon -- TODO: "if not you_shafted()" if you.where() == "D:15" then c_persist.CLEARED_DUNGEON = true crawl.mpr("Cleared Dungeon!") crawl.sendkeys(".") return true end -- else, go downstairs crawl.mpr("Going Down!") crawl.sendkeys("G>") return false end -- rune names sourced from item-name.cc, rune_type_name() -- case RUNE_DIS: return "iron"; -- case RUNE_GEHENNA: return "obsidian"; -- case RUNE_COCYTUS: return "icy"; -- case RUNE_TARTARUS: return "bone"; -- case RUNE_SLIME: return "slimy"; -- case RUNE_VAULTS: return "silver"; -- case RUNE_SNAKE: return "serpentine"; -- case RUNE_ELF: return "elven"; -- case RUNE_TOMB: return "golden"; -- case RUNE_SWAMP: return "decaying"; -- case RUNE_SHOALS: return "barnacled"; -- case RUNE_SPIDER: return "gossamer"; -- case RUNE_FOREST: return "mossy"; -- -- // pandemonium and abyss runes: -- case RUNE_DEMONIC: return "demonic"; -- case RUNE_ABYSSAL: return "abyssal"; -- -- // special pandemonium runes: -- case RUNE_MNOLEG: return "glowing"; -- case RUNE_LOM_LOBON: return "magical"; -- case RUNE_CEREBOV: return "fiery"; -- case RUNE_GLOORX_VLOQ: return "dark"; -- default: return "buggy"; -- these are in order of rough preference so we don't have to sort the table later -- TODO: check for branch preference by available character resistances? local lairbranch_prefs = { {"Snake", "enter_snake_pit", "P", "serpentine"}, {"Swamp", "enter_swamp", "S", "decaying"}, {"Spider", "enter_spider_nest", "N", "gossamer"}, {"Shoals", "enter_shoals", "A", "barnacled"}, } function get_lairbranch_table() local lairbranches = {} for _,which_branch in ipairs(lairbranch_prefs) do for _,entry in ipairs(c_persist.FOUND_KFEATS_TABLE) do if entry[1] == which_branch[2] then table.insert(lairbranches, which_branch) end end end return lairbranches end function fn_goal_s1() if c_persist.LAIRBRANCHES == nil or next(c_persist.LAIRBRANCHES) == nil then c_persist.LAIRBRANCHES = get_lairbranch_table() end if next(c_persist.LAIRBRANCHES) == nil then kickback("The available lairbranch table is empty! (Did we save/load across characters and nuke the table?)") return false end -- cleared_s1, set next major goal if c_persist.CLEARED_LAIRBRANCH_1 == true then set_current_goal("goal_s2") crawl.mpr("goal_s1 complete! setting goal_s2.") crawl.sendkeys(".") return true end -- if found_s1 and not in_s1, enter_s1 if you.branch() ~= c_persist.LAIRBRANCHES[1][1] then for _,entry in ipairs(c_persist.FOUND_KFEATS_TABLE) do if entry[1] == c_persist.LAIRBRANCHES[1][2] and not c_persist.CLEARED_LAIRBRANCH_1 then crawl.mpr("Trying to travel to " .. c_persist.LAIRBRANCHES[1][1] .. ".") crawl.sendkeys("G" .. c_persist.LAIRBRANCHES[1][3] .. string.char(13)) return true end end end -- if found rune_s1, set cleared_s1 if you.have_rune(c_persist.LAIRBRANCHES[1][4]) then c_persist.CLEARED_LAIRBRANCH_1 = true crawl.mpr("Cleared " .. c_persist.LAIRBRANCHES[1][1] .. "!") crawl.sendkeys(".") return true end -- if we've cleared branch_s1 and we haven't found rune_s1, kickback if you.branch() == c_persist.LAIRBRANCHES[1][1] and you.depth() == 4 then kickback("Failed to find " .. c_persist.LAIRBRANCHES[1][4] .. " rune. (Maybe it's in a disconnected area?)") crawl.sendkeys(".") return false end -- else, go downstairs crawl.mpr("Going Down!") crawl.sendkeys("G>") return false end function fn_goal_s2() if c_persist.LAIRBRANCHES == nil or next(c_persist.LAIRBRANCHES) == nil then c_persist.LAIRBRANCHES = get_lairbranch_table() end if next(c_persist.LAIRBRANCHES) == nil then kickback("The available lairbranch table is empty! (Did we save/load across characters and nuke the table?)") return false end -- cleared_s2, set next major goal if c_persist.CLEARED_LAIRBRANCH_2 == true then set_current_goal("goal_v4") crawl.mpr("goal_s2 complete! setting goal_v4.") crawl.sendkeys(".") return true end -- if found_s2 and not in_s2, enter_s2 if you.branch() ~= c_persist.LAIRBRANCHES[2][1] then for _,entry in ipairs(c_persist.FOUND_KFEATS_TABLE) do if entry[1] == c_persist.LAIRBRANCHES[2][2] and not c_persist.CLEARED_LAIRBRANCH_2 then crawl.mpr("Trying to travel to " .. c_persist.LAIRBRANCHES[2][1] .. ".") crawl.sendkeys("G" .. c_persist.LAIRBRANCHES[2][3] .. string.char(13)) return true end end end -- if found rune_s2, set cleared_s2 if you.have_rune(c_persist.LAIRBRANCHES[2][4]) then c_persist.CLEARED_LAIRBRANCH_2 = true crawl.mpr("Cleared " .. c_persist.LAIRBRANCHES[2][1] .. "!") crawl.sendkeys(".") return true end -- if we've cleared branch_s2 and we haven't found rune_s2, kickback if you.branch() == c_persist.LAIRBRANCHES[2][1] and you.depth() == 4 then kickback("Failed to find " .. c_persist.LAIRBRANCHES[2][4] .. " rune. (Maybe it's in a disconnected area?)") crawl.sendkeys(".") return false end -- else, go downstairs crawl.mpr("Going Down!") crawl.sendkeys("G>") return false end function fn_goal_v4() -- cleared_v4, set next major goal if c_persist.CLEARED_VAULTS_4 == true then set_current_goal("goal_u") crawl.mpr("goal_v4 complete! setting goal_u.") crawl.sendkeys(".") return true end -- if found_v and not in_v, enter_v if you.branch() ~= "Vaults" then for _,entry in ipairs(c_persist.FOUND_KFEATS_TABLE) do if entry[1] == "enter_vaults" and not c_persist.CLEARED_VAULTS_4 then crawl.mpr("Trying to travel to Vaults.") crawl.sendkeys("GV" .. string.char(13)) return true end end end -- if cleared v4, set cleared_v4 if you.branch() == "Vaults" and you.depth() == 4 then c_persist.CLEARED_VAULTS_4 = true crawl.mpr("Cleared Vaults:4.") crawl.sendkeys(".") return true end -- if found rune_v, set cleared_v -- this doesn't *quite* belong here, but maybe later if I change the dive behavior if you.have_rune("silver") then c_persist.CLEARED_VAULTS = true crawl.mpr("Cleared Vaults!") crawl.sendkeys(".") return true end -- else, go downstairs crawl.mpr("Going Down!") crawl.sendkeys("G>") return false end function fn_goal_u() -- cleared_u, set next major goal if c_persist.CLEARED_DEPTHS == true then set_current_goal("goal_v5") crawl.mpr("goal_u complete! setting goal_v5.") crawl.sendkeys(".") return true end -- if found_u and not in_u, enter_u if you.branch() ~= "Depths" then for _,entry in ipairs(c_persist.FOUND_KFEATS_TABLE) do if entry[1] == "enter_depths" and not c_persist.CLEARED_DEPTHS then crawl.mpr("Trying to travel to Depths.") crawl.sendkeys("GU" .. string.char(13)) return true end end end -- if cleared u, set cleared_u if you.branch() == "Depths" and you.depth() == 5 then c_persist.CLEARED_DEPTHS = true crawl.mpr("Cleared Depths:5.") crawl.sendkeys(".") return true end -- else, go downstairs crawl.mpr("Going Down!") crawl.sendkeys("G>") return false end function fn_goal_v5() -- cleared_v5, set next major goal if c_persist.CLEARED_VAULTS == true then set_current_goal("goal_zot") crawl.mpr("goal_v5 complete! setting goal_zot.") crawl.sendkeys(".") return true end -- if found_v and not in_v, enter_v if you.branch() ~= "Vaults" then for _,entry in ipairs(c_persist.FOUND_KFEATS_TABLE) do if entry[1] == "enter_vaults" and not c_persist.CLEARED_VAULTS then crawl.mpr("Trying to travel to Vaults.") crawl.sendkeys("GV" .. string.char(13)) return true end end end -- if found rune_v, set cleared_v if you.have_rune("silver") then c_persist.CLEARED_VAULTS = true crawl.mpr("Cleared Vaults!") crawl.sendkeys(".") return true end -- else, go downstairs crawl.mpr("Going Down!") crawl.sendkeys("G>") return false end function fn_goal_zot() -- cleared_zot, set next major goal if c_persist.CLEARED_ZOT == true then set_current_goal("goal_orb_pickup") crawl.mpr("goal_zot complete! setting goal_orb_pickup.") crawl.sendkeys(".") return true end -- if found_zot and not in_zot, enter_zot if you.branch() ~= "Zot" then for _,entry in ipairs(c_persist.FOUND_KFEATS_TABLE) do if entry[1] == "enter_zot" and not c_persist.CLEARED_ZOT then crawl.mpr("Trying to travel to Zot.") crawl.sendkeys("GZ" .. string.char(13)) return true end end end -- if cleared zot, set cleared_zot if you.branch() == "Zot" and you.depth() == 5 then c_persist.CLEARED_ZOT = true crawl.mpr("Cleared Zot:5.") crawl.sendkeys(".") return true end -- else, go downstairs crawl.mpr("Going Down!") crawl.sendkeys("G>") return false end -- Zot:5 should already be clear by the time we're calling this function function fn_goal_orb_pickup() -- have_orb, set next major goal if c_persist.HAVE_ORB == true then set_current_goal("goal_orbrun") crawl.mpr("goal_orb_pickup complete! setting goal_orbrun.") crawl.sendkeys(".") return true end -- -- if found_zot and not in_zot, enter_zot -- if you.branch() ~= "Zot" then -- for _,entry in ipairs(c_persist.FOUND_KFEATS_TABLE) do -- if entry[1] == "enter_zot" and not c_persist.CLEARED_ZOT then -- crawl.mpr("Trying to travel to Zot.") -- crawl.sendkeys("GZ" .. string.char(13)) -- return true -- end -- end -- end -- if have orb, set orbrun if you.have_orb() then c_persist.HAVE_ORB = true crawl.mpr("Picked up the Orb.") crawl.sendkeys(".") return true end local local_stuff = items.get_items_at(0,0) if local_stuff then for _,it in ipairs(local_stuff) do if it.name():find("Orb of Zot") then crawl.mpr("Trying to pickup the Orb.") -- i don't know why items.pickup isn't working here -- items.pickup(it) -- crawl.sendkeys("g" .. items.index_to_letter(it.slot) .. string.char(13)) -- items.pickup({it,1}) crawl.sendkeys("ga" .. string.char(13)) -- dumb workaround, this will break if the orb isn't in floor slot 'a' return true end end end -- else, ^F the orb and go there crawl.mpr("Trying to travel to the Orb...") crawl.sendkeys("*f" .. "Orb of Zot" .. string.char(13) .. "a" .. string.char(13)) return false -- -- else, go downstairs -- crawl.mpr("Going Down!") -- crawl.sendkeys("G>") -- return false end local ORBRUN_REST_THRESHOLD = 70 function orbrun_rest_check() local hp, mhp = you.hp() return (hp*100 < mhp*ORBRUN_REST_THRESHOLD) end -- note: orbrun rest ("5") behavior now has its own handler; this does not currently check for MP -- TODO: if you rework the script to handle offensive spellcasting, orbrun rest (MP) behavior will need adjustment function fn_goal_orbrun() if view.feature_at(0,0) == "exit_dungeon" then crawl.mpr("Ascending! :)") crawl.sendkeys("<") return true end if orbrun_rest_check() then if view.feature_at(0,0):find("up") or view.feature_at(0,0):find("exit") then if not monster_in_view() then -- check for ambrosia -- TODO: this? -- breadswing? -- okay, just '5' crawl.sendkeys("5") end end end if monster_in_view() then if view.feature_at(0,0):find("up") or view.feature_at(0,0):find("exit") then crawl.sendkeys("<") return true else fight() return true end end if hungry() then try_to_eat() return true end if you.where() == "D:1" then crawl.mpr("Trying to go to exit_dungeon.") crawl.sendkeys("X<" .. string.char(13)) return true end -- else, go to D:1 crawl.mpr("Trying to go to D:1 (orbrun)") crawl.sendkeys("GD1" .. string.char(13)) return false end local strategic_goal_table = { {"goal_orbrun", fn_goal_orbrun} , {"goal_orb_pickup", fn_goal_orb_pickup} , {"goal_zot", fn_goal_zot} , {"goal_v5", fn_goal_v5} , {"goal_u", fn_goal_u} , {"goal_v4", fn_goal_v4} , {"goal_s2", fn_goal_s2} , {"goal_s1", fn_goal_s1} , {"goal_d15", fn_goal_d15} , {"goal_orc", fn_goal_orc} , {"goal_lair", fn_goal_lair} , {"goal_god", fn_goal_god} , } -- TODO: write an unshaft_self override goal function do_current_goal_action() for _,goal in ipairs(strategic_goal_table) do if get_current_goal() == goal[1] then goal[2]() end end end function explore() crawl.mpr("Explore!") local read_id = nil local gear = nil -- "should we go downstairs" comparison : this only checks if the turn counter has failed to increment since our last -- explore attempt. This isn't a very reliable check. It breaks when autotravel breaks; such as with monsters in view -- but with blocked pathing. if (you.turns() ~= last_explore_turn) then -- TODO: come up with a better implementation for this crawl.sendkeys(explore_keybind) last_explore_turn = you.turns() else if not c_persist.ID_SCROLLS_RECOGNIZED then -- check if we need to read-id read_id = check_for_largest_unid_stack("scroll") if read_id and read_id.quantity <= 1 then -- attempts limited to stacks of 3+, this slightly delays id depth but saves some scrolls read_id = nil end end local rc_scroll = nil local wear_id_it = nil local wield_id_it = nil rc_scroll = check_for_known_scroll("remove curse") wear_id_it = can_we_wear_id_something() wield_id_it = can_we_wield_id_something() -- gear = check_eligible_equip() -- old "autoequip()" function, currently unused gear = check_better_equips() -- "autoequip()" local ea_scroll = nil local ea_target = nil ea_scroll, ea_target = should_we_enchant_something() if you_are_cursed() and rc_scroll then read(rc_scroll) elseif wear_id_it and rc_scroll then wear_identify(wear_id_it) elseif wield_id_it and rc_scroll then wield_identify(wield_id_it) elseif gear then equip(gear) elseif ea_target then if you.silenced() then crawl.sendkeys('5') else enchant(ea_scroll, ea_target) end elseif read_id then if you.silenced() then -- if we're silenced, wait before trying to read-id crawl.sendkeys('5') else read_identify(read_id) end else autoskill() -- this function tracks gamestate and issues "G>", "GT", etc., as appropriate do_current_goal_action() end end end -- local branch = you.branch() -- D -- local depth = you.depth() -- 1 -- local brdep = you.where() -- D:1 -- note: it's safer to use "G>" than "X>\{13}" because autotravel won't use hatches by mistake -- you.branch() and you.where() return the branch name in "abbrev name" in branch-data.h ; -- this is the third branch name string for each branch in the enums. --local branch_names = { -- {"D"}, -- {"Temple"}, -- {"Orc"}, -- {"Elf"}, -- {"Dwarf"}, -- {"Lair"}, -- {"Swamp"}, -- {"Shoals"}, -- {"Snake"}, -- {"Spider"}, -- {"Slime"}, -- {"Vaults"}, -- {"Blade"}, -- {"Crypt"}, -- {"Tomb"}, -- {"Depths"}, -- {"Hell"}, -- {"Dis"}, -- {"Geh"}, -- {"Coc"}, -- {"Tar"}, -- {"Zot"}, -- {"Forest"}, -- {"Abyss"}, -- {"Pan"}, -- {"Zig"}, -- {"Lab"}, -- {"Bazaar"}, -- {"Trove"}, -- {"Sewer"}, -- {"Ossuary"}, -- {"Bailey"}, -- {"Gauntlet"}, -- {"IceCv"}, -- {"Volcano"}, -- {"WizLab"}, -- {"Desolation"}, --} -- To find a list of portal messages in a DCSS repo, try: -- git grep -n "initmsg" -- The below portal messages are current as of 2020-03-05 for DCSS version 0.25. local portal_kickbacks = { {"Bailey", "You hear the roar of battle"}, {"Bazaar", "You hear coins being counted"}, {"Desolation", "You hear a distant wind"}, {"Gauntlet", "You hear a distant snort"}, {"IceCv", "You feel a wave of frost pass over you"}, {"Ossuary", "You hear the hiss of flowing sand"}, {"Sewer", "You hear the sound of rushing water"}, {"Volcano", "You feel an oppressive heat about you"}, {"WizLab", "You hear the crackle of arcane power"}, } -- NOTE: The raw output of crawl.messages() is not easily searchable for "what has happened since the previous player action". -- Message log kickback comparisons, including the borked script kickback, previously blocked auto() re-activation for -- several turns (the length of our crawl.messages() buffer). This should be "fixed" now, with the implementation of -- current_messages, but this is reliant on printing a comparison string to the messages log with every action the script -- takes. This is _very_ spammy. If you attempt to reduce ACTIONMARKER message log spam in the future, -- you'll need to work around this script-blocking in some other way. local borked_script_kickback = { {"borked scripting", "Infinite lua"}, } local shaft_kickback = { {"shafted!", "into a shaft and drop"}, } local invis_monster_kickbacks = { {"invis monster nearby", "There is a strange disturbance"}, {"invis monster nearby", "Something hits you"}, } local environmental_danger_kickbacks = { {"standing next to acid wall", "You\'re standing next to a slime covered wall"}, } -- TODO: Get rid of this, it won't be needed after transitioning fight() to no longer use autofight code local autofight_pathing_kickback = { {"failed autofight pathing (deep water/lava)", "No reachable target in view"}, } -- Ideally the inventory management scripts will handle this, but if they don't, auto() needs to stop while the player fixes it local autopick_autodrop_failure_kickback = { {"full inventory, (d)rop some stuff", "Your pack is full"}, } -- TODO: Rewrite the 'G>' logic to be a bit smarter, so this isn't needed local autopathing_target_failure_kickbacks = { {"branch end autotravel failure, (G)o somewhere manually", "already at the bottom of this branch"}, {"portal / branch autotravel failure, try leaving manually with (X<)", "Sorry, you can\'t auto%-travel out of here"}, {"autotravel failure, (travel exclusion, transporter vault, or shaft?), try leaving manually", "Sorry, I don\'t know how to get there"}, } local message_log_kickbacks = { portal_kickbacks, shaft_kickback, invis_monster_kickbacks, environmental_danger_kickbacks, autofight_pathing_kickback, autopick_autodrop_failure_kickback, autopathing_target_failure_kickbacks, borked_script_kickback, } -- message log kickbacks, for situations that need to be handled which are most easily identified through the message log -- -- Each of these kickbacks uses string:find() on the recent message log buffer, *EVERY SINGLE TURN*. -- It's better to handle scripting exceptions by checking exposed crawl Lua gamestate instead, if at all possible. -- -- Some situations are only exposed through the message log buffer, though, so here we are. function check_for_message_log_kickbacks() -- thought: at this point, could I instead take the message buffer and create a table from it using an exposed c function, -- then do the comparison against my predefined kickback table values without ever needing to involve string creation? local recent_messages = crawl.messages(20) -- we're looking for everything in the log that happened since our turn marker local last_turn_marker = ("ACTIONMARKER" .. tostring(c_persist.LAST_ACTION_MARKER)) -- doing it this way with gmatch [^\n] ends up mangling the strings before inserting them into the table, -- apparently crawl.messages drops random newlines in, (possibly depending on the main crawl window / console width?) -- -- it's probably best to keep the crawl.messages output as untouched as possible here, crawl appears to also be doing -- invisible things with string formatting which might not play nicely with breaking the message buffer output up into a table -- ---- create a table from the last 20 messages -- local t = {} -- for w in string.gmatch(recent_messages, "([^\n]+)") do ---- crawl.formatted_mpr(w) -- table.insert(t, 1, w) -- end -- local pr = true -- for v,_ in ipairs(t) do -- if t[v]:find(last_turn_marker) then -- pr = false -- end -- if pr then crawl.mpr(t[v]) end -- end -- get the string indices of our previous turn marker in the message buffer local i, j = recent_messages:find(last_turn_marker) local current_messages = "" -- if we found last turn's marker, copy everything after that into current_messages if j then -- crawl.mpr(string.sub(recent_messages, j+1)) current_messages = string.sub(recent_messages, j+1) end for _, subtable in ipairs(message_log_kickbacks) do for _, value in ipairs(subtable) do -- if recent_messages:find(value[2]) then if current_messages:find(value[2]) then -- now we can do string:find for kickback messages against the previous turn only kickback(value[1]) end end end end -- If this table existed, it would probably be sourced from item-prop.cc, weapon_def Weapon_prop[] , -- but subtype() comparisons are useless for OBJ_WEAPONS because of a limitation in items.subtype(). -- Need to use a different eval method for weapon autopick here; I'm just going to special case it. local weapon_types = { {nil, false}, {"", false}, } -- as in item-prop.cc, missile_def Missile_prop[] -- TODO: setup a conditional ammo pick fn with species logic and weapon_skill logic, a subtype table is insufficient here --{string missile_subtype, bool do_we_want_this_type, int ignore_type_above_this_xl} local missile_types = { {nil, true, 27}, -- this should only ever be the case if some dev makes an artefact missile {"dart", true, 27}, {"needle", true, 27}, -- old ver {"stone", true, 5}, {"arrow", false, 27}, -- TODO: i need to add launcher Skilling checks here {"bolt", false, 27}, {"large rock", false, 27}, -- I really need species logic here too {"sling bullet", false, 27}, {"javelin", true, 27}, {"throwing net", true, 27}, {"boomerang", true, 13}, } -- XL checks are probably not the greatest way to handle this: weaker species want item advantage for longer, -- and often have an XL aptitude penalty. TODO: maybe revisit this later --{string egoname, bool do_we_want_this_ego, int ignore_ego_above_this_xl} -- don't need the bool here after adding the int -- as in item-name.cc, missile_brand_name() ; items.ego(true) appears to return the MBN_TERSE form local missile_egos = { -- the MBN_NAME // MBN_TERSE table is all kinds of screwed up, half the checks are backwards {nil, true, 27}, -- i really don't know what identification behavior items.ego() has -- TODO: figure this out {"flame", true, 27}, -- old v {"frost", true, 27}, -- old v {"poison", true, 8}, -- some of these check MBN_NAME, some check MBN_TERSE, {"curare", true, 27}, -- i'll call this with terse = true and reference those forms where they exist {"explode", true, 5}, -- old v {"steel", true, 27}, -- old v {"return", true, 27}, -- old v {"penet", true, 27}, -- old v {"silver", true, 27}, {"paralysis", false, 0}, -- old v {"slow", false, 0}, -- old v {"sleep", false, 0}, -- old v {"conf", false, 0}, -- old v {"sick", false, 0}, -- old v {"datura", false, 0}, -- SPMSL_FRENZY {"chaos", false, 0}, {"disperse", true, 5}, {"atropa", true, 8}, -- SPMSL_BLINDING {"damnation", true, 27}, -- DAMNATION_BOLT_KEY , apparently this fixedart will return a special case missile ego_type_string() {"", true, 27}, -- SPMSL_NORMAL , leaving 27 cap here may cause problems with lategame plain javelins but its either that or change logic for launcher ammo {"buggy", true, 27}, } -- as in invent.cc, item_slot_name() -- TODO: setup an armour pick fn with better logic, a subtype table won't cut it here local armour_types = { {nil, true}, -- this should actually never happen because OBJ_ARMOUR is special cased, see items.subtype() limitations below {"cloak", true}, {"helmet", true}, {"gloves", true}, {"boots", true}, {"shield", true}, {"body", false}, -- eh, I need full logic here {"", true}, } -- default case item_slot_name() fallthrough, I hope this will catch the empty string -- as in item-name.cc, _wand_type_name() local wand_types = { {nil, true, 27}, -- autopick unid wands, please {"flame", false, 8}, {"paralysis", true, 13}, -- TODO: if i'm adding XL checks here, I need to also add an Evocations check {"digging", true, 27}, {"iceblast", true, 19}, {"polymorph", true, 13}, {"enslavement", false, 10}, {"acid", true, 27}, {"random effects", false, 5}, {"disintegration", true, 10}, {"clouds", true, 27}, {"scattershot", true, 19}, {"removedness", true, 27}, {"bugginess", true, 27}, } -- as in item-prop.cc , food_def Food_prop[] , *name field local food_types = { {nil, true}, -- see items.subtype() limitations below, nil will almost always be the case for food {"ration", true}, {"chunk", true}, {"buggy pizza", false}, -- handle buggy old ver stuff too, but this probably isn't needed {"buggy jelly", true}, {"buggy ration", true}, {"buggy fruit", false}, {"snozzcumber", true}, } -- don't forget the snozz ;_; -- for convenience, this follows the ordering in scroll_type_name() in item-name.cc -- if this ordering seems nonsensical, blame that function's author local scroll_types = { {nil, true}, -- autopick unid scrolls, please {"identify", true}, -- read-id convenience notes here: -- brings up a selection menu {"teleportation", true}, {"fear", true}, {"noise", false}, {"remove curse", true}, {"summoning", true}, {"enchant weapon", true}, -- brings up a selection menu -- can I handle these prompts with message log :find handlers? {"enchant armour", true}, -- brings up a selection menu {"torment", true}, {"random uselessness", false}, {"immolation", true}, {"blinking", true}, -- dump you into a position targeter {"magic mapping", true}, {"fog", true}, {"acquirement", true}, -- brings up a selection menu {"brand weapon", true}, -- brings up a selection menu {"holy word", true}, {"vulnerability", false}, {"silence", true}, -- prevents you from reading scrolls for a bit, remember to check you.silenced() before read-id {"amnesia", false}, -- dumps you into a spell selection menu {"curse weapon", true}, -- old ver -- i'm not going to bother writing read-id handlers for removed scrolls {"curse armour", true}, -- old ver {"curse jewellery", true}, -- old ver {"removedness", true}, {"bugginess", true}, } -- go ahead and pickup removed + buggy -- ugh -- as in item-name.cc, jewellery_type_name() -- "%s %s%s" -- _jewellery_class_name(), -- _jewellery_effect_prefix(), -- _jewellery_effect_name() local jewellery_types = { {nil, true}, -- unid or artefact jewellery *should* return nil type, but the jewellery subtype string constructor code is nasty. -- Hopefully I've gotten some of this right? See items.subtype() limitations below. {"ring of regeneration", true}, -- old ver {"buggy", true}, -- sure, why not {"amulet of the gourmand", false}, -- apparently this amulet is the only case for _jewellery_effect_prefix, thank you crawlcode. {"ring of obsoleteness", true}, {"ring of protection", true}, {"ring of protection from fire", true}, {"ring of poison resistance", true}, {"ring of protection from cold", true}, {"ring of strength", true}, {"ring of slaying", true}, -- mikee {"ring of see invisible", false}, -- TODO: consider picking these to help the script handle invis mons? {"ring of resist corrosion", true}, {"ring of attention", false}, {"ring of teleportation", false}, {"ring of evasion", true}, {"ring of sustain attributes", true}, -- old ver {"ring of stealth", false}, {"ring of dexterity", true}, {"ring of intelligence", false}, {"ring of wizardry", false}, {"ring of magical power", false}, {"ring of flight", true}, {"ring of positive energy", false}, {"ring of protection from magic", true}, {"ring of fire", true}, {"ring of ice", false}, {"ring of teleport control", true}, -- old ver {"amulet of rage", true}, {"amulet of harm", false}, {"amulet of magic regeneration", false}, {"amulet of the acrobat", true}, -- skipping gourmand here; apparently *this* amulet can have 'the' appended without a helper fn {"amulet of conservation", true}, -- old ver, what a blast from the past {"amulet of controlled flight", true}, -- old ver {"amulet of inaccuracy", false}, {"amulet of nothing", true}, -- what the heck, crawlcode? this isn't behind a removed major version tag or anything. {"amulet of guardian spirit", true}, {"amulet of faith", true}, {"amulet of reflection", true}, {"amulet of regeneration", true}, {"buggy jewellery", true}, -- i really don't know if this is prepended with "amulet of" ingame or not, but whatever {"buggy buggy jewellery", true}, {"ring of buggy jewellery", true}, {"amulet of buggy jewellery", true}, } local potion_types = { {nil, true}, -- do pickup unidentified potions, please {"curing", true}, {"heal wounds", true}, {"haste", true}, {"might", true}, {"stabbing", false}, {"brilliance", false}, {"gain strength", true}, -- old ver {"gain dexterity", true}, -- old ver {"gain intelligence", true}, -- old ver {"strong poison", false}, -- old ver {"porridge", true}, -- old ver {"slowing", false}, -- old ver {"flight", true}, {"poison", false}, -- old ver {"cancellation", true}, {"ambrosia", true}, {"invisibility", true}, {"degeneration", false}, {"decay", false}, -- old ver {"experience", true}, {"magic", true}, {"restore abilities", true}, -- old ver {"berserk rage", true}, {"cure mutation", true}, -- old ver {"mutation", true}, {"blood", true}, -- old ver {"coagulated blood", true}, -- old ver {"resistance", true}, {"lignification", true}, {"beneficial mutation", true}, -- old ver {"bugginess", true}, } -- Note: Current autopick logic should never reach this table for books. -- I'm autopicking all books by class now; apparently each manual gets an individualized subtype return, -- it's easiest to just pick all books. -- -- as in item-name.cc, sub_type_string(), case OBJ_BOOKS: local book_types = { {nil, true}, -- most books will id to nil, see items.subtype() limitations below {"manual", true}, } -- unidentified manuals get their own type return, apparently? -- item-name.cc , staff_type_name() local staff_types = { {nil, true}, -- staves id to nil before they've been positively identified {"wizardry", false}, {"power", false}, {"fire", false}, {"cold", false}, {"poison", false}, {"energy", true}, {"death", false}, {"conjuration", false}, {"air", true}, {"earth", false}, {"summoning", false}, {"bugginess", true}, } -- sure, why not -- setting orb autopick to false for now so the Zot goal can do Z:5 without autoexplore prematurely starting the orbrun local orb_types = { -- {nil, true}, -- see items.subtype() limitations below -- {"orb of Zot", true}, } {nil, false}, -- see items.subtype() limitations below {"orb of Zot", false}, } -- item-name.cc, misc_type_name() -- TODO: probably just want to turn all of these misc types off later local misc_types = { {nil, true}, -- this should never happen unless some dev adds an artefact misc, see items.subtype() limitations below {"crystal ball of energy", false}, {"box of beasts", true}, {"removed ebony casket", false}, {"fan of gales", false}, {"lamp of fire", false}, {"removed lantern of shadows", true}, {"horn of Geryon", true}, {"lightning rod", false}, {"empty flask", false}, {"obsolete rune of zot", true}, {"removed stone of tremors", false}, {"quad damage", true}, {"phial of floods", false}, {"sack of spiders", false}, {"phantom mirror", true}, {"figurine of a ziggurat", true}, {"removed chess piece", false}, {"tremorstone", false}, -- don't know how useful this item will end up being, initialize to true for now {"buggy miscellaneous item", true}, } -- again, why not -- pulled from item-name.cc , sub_type_string(), case: OBJ_CORPSES -- {nil, false} will remove corpses from autopick, but it also messes with auto_butcher behavior, -- by preventing autotravel from seeing corpses as potential travel targets. -- It's probably better to set this to {nil, true}, because the game prevents corpse pickup in current -- versions, regardless of autopick settings. This should (?) allow auto_butcher to work properly. local corpse_types = { {nil, true}, -- see items.subtype() limitations below {"corpse", false}, } local gold_types = { {nil, true}, -- see items.subtype() limitations below {"gold", true}, } local rune_types = { {nil, true}, -- runes of zot should always return nil subtype, see items.subtype() limitations below {"rune of Zot", true}, } -- untyped items that have been positively identified could actually return a subtype(), -- but as this type is a fallthrough and such items shouldn't exist, there are no identified subtype -- matches to add here. local untyped_types = { {nil, true}, -- unidentified untyped items should return nil, see items.subtype() limitations below {"", true}, } -- Item type autopickup tables. -- -- The possible returns that need to be handled here are defined within: -- base_type_string() in /source/item-name.cc contains the base item class string returns; -- item_class_name() in /source/invent.cc wraps around the base values. When called with (terse=true), as in it.class(true), -- this returns a shortform string, overwriting "miscellaneous" and "staff", with "misc" and "magical staff" local item_types = { {"weapon", weapon_types}, -- {"missile", missile_types}, -- handling pickup / evaluation for missiles separately, don't want them being assessed here {"armour", armour_types}, -- {"wand", wand_types}, -- handling wand eval separately too, i guess {"food", food_types}, {"scroll", scroll_types}, {"jewellery", jewellery_types}, {"potion", potion_types}, {"book", book_types}, {"magical staff", staff_types}, {"orb", orb_types}, {"misc", misc_types}, {"corpse", corpse_types}, {"gold", gold_types}, {"rune", rune_types}, {"", untyped_types}, } -- will this empty string untyped fallthrough work? function mpr_artprops(it) -- debug utility function for printing the artprops of the top item in the floor stack at (0,0) crawl.mpr(it.name()) if not it.artprops then crawl.mpr("This item has no artprops, maybe it's not an artefact?") else for k, v in pairs(it.artprops) do crawl.mpr("it.artprops: " .. tostring(k) .. ": " .. tostring(v)) end end crawl.mpr("Done printing artprops.") end function mpr_name(it) if not it.name() then crawl.mpr("This item has no name, maybe it's unidentified?") else crawl.mpr("it.name(): " .. it.name()) end end function mpr_class(it) if not it.class(true) then crawl.mpr("This item has no class, maybe it's unidentified?") else crawl.mpr("it.class(true): " .. it.class(true)) end end function mpr_subtype(it) if not it.subtype() then crawl.mpr("This item has nil subtype, maybe it's unidentified") else crawl.mpr("it.subtype(): " .. it.subtype()) end end function mpr_plus(it) if not it.plus then crawl.mpr("This item has no plus, is it unidentified?") else crawl.mpr("it.plus: " .. it.plus) end end function mpr_ego_terse(it) if not it.ego() then crawl.mpr("This item has no terse ego, is it unidentified or artefact?") else crawl.mpr("it.ego(true): " .. it.ego(true)) end end function mpr_ego_false(it) if not it.ego(false) then crawl.mpr("This item has no longform ego, is it unidentified or artefact?") else crawl.mpr("it.ego(false): " .. it.ego(false)) end end function mpr_prop_value(it) crawl.mpr("Relative prop value: " .. tostring(get_prop_value(it))) end function mpr_equip_type(it) crawl.mpr("Equip type (slot value): " .. tostring(it.equip_type)) end function mpr_max_plus(it) crawl.mpr("Armour type max plus: " .. tostring(get_armour_max_plus(it))) end function mpr_item_debug() -- debug utility fn, prints to mpr most stats of the first item on the ground at (0,0), local itn = items.get_items_at(0,0) -- can be used in clua context local it = itn[1] mpr_name(it) mpr_class(it) mpr_subtype(it) mpr_plus(it) mpr_ego_terse(it) mpr_ego_false(it) mpr_artprops(it) mpr_prop_value(it) mpr_equip_type(it) mpr_max_plus(it) end -- all weapon type tables sourced from item-prop.cc, weapon_def Weapon_prop[] -- -- "name", basedamage, 10*basedelay -- leaving accuracy out of these tables for now in favor of simple dam delay comparison, -- probably i should do this the not-lazy way and include accuracy calcs but whatever -- it's not like players pay attention to accuracy anyway -- -- TODO: I should include acc in here so I can use it at .7 dam / .3 acc weightings in weapon comparisons --local maces_and_flails_types = { -- {"club", 5, 13}, ---- {"spiked flail", 5, 13}, -- old v -- {"whip", 6, 11}, ---- {"hammer", 7, 13}, -- old v -- {"mace", 8, 14}, -- {"flail", 10, 14}, -- {"morningstar", 13, 15}, -- {"demon whip", 11, 11}, -- {"sacred scourge", 12, 11}, -- {"dire flail", 13, 13}, -- {"eveningstar", 15, 15}, -- {"great mace", 17, 17}, -- {"giant club", 20, 16}, -- {"giant spiked club", 22, 18}, } --local short_blades_types = { -- {"dagger", 4, 10}, -- {"quick blade", 5, 7}, -- {"short sword", 6, 11}, -- {"rapier", 8, 12}, ---- {"cutlass", 8, 12}, } -- old v --local long_blades_types = { -- {"falchion", 7, 13}, -- {"long sword", 9, 14}, -- {"scimitar", 11, 14}, -- {"demon blade", 12, 13}, -- {"eudemon blade", 13, 12}, -- {"double sword", 14, 15}, -- {"great sword", 15, 17}, -- {"triple sword", 17, 19}, ---- {"old falchion", 7, 13}, -- old ver blessed long blades ---- {"old long sword", 9, 14}, ---- {"old scimitar", 11, 14}, ---- {"old double sword", 14, 15}, ---- {"old great sword", 14, 16}, ---- {"old triple sword", 17, 19}, } --local axes_types = { -- {"hand axe", 7, 13}, -- {"war axe", 11, 15}, -- {"broad axe", 13, 16}, -- {"battleaxe", 15, 17}, -- {"executioner's axe", 18, 20}, } --local polearms_types = { -- {"spear", 6, 11}, -- {"trident", 9, 13}, -- {"halberd", 13, 15}, -- {"scythe", 14, 20}, -- {"demon trident", 12, 13}, -- {"trishula", 13, 13}, -- for some reason, i'm surprised this is still in the game -- {"glaive", 15, 17}, -- {"bardiche", 18, 20}, } --local staves_types = { -- {"staff", 5, 12}, -- magical staves only -- {"quarterstaff", 10, 13}, -- {"lajatang", 16, 14}, } -- i may want to separate these into tables by wskill --local launchers_types = { ---- {"blowgun", 0, 10}, -- old v -- {"hunting sling", 5, 12}, -- {"fustibalus", 8, 14}, -- {"hand crossbow", 12, 15}, -- {"arbalest", 18, 19}, -- {"triple crossbow", 22, 23}, -- {"shortbow", 9, 13}, -- {"longbow", 15, 17}, } --local sling_types = { -- {"hunting sling", 5, 12}, -- {"fustibalus", 8, 14}, } --local xbow_types = { -- {"hand crossbow", 12, 15}, -- {"arbalest", 18, 19}, -- {"triple crossbow", 22, 23}, } --local bow_types = { -- {"shortbow", 9, 13}, -- {"longbow", 15, 17}, } --local weapon_types_table = { -- {"Short Blades", short_blades_types}, -- {"Long Blades", long_blades_types}, -- {"Axes", axes_types}, -- {"Maces & Flails", maces_and_flails_types}, -- {"Polearms", polearms_types}, -- {"Staves", staves_types}, -- {"Slings", sling_types}, -- {"Bows", bow_types}, -- {"Crossbows", xbow_types}, } -- thought: i need to programmatically build a "best item per equip slot" table and store it; -- i can update it like once per floor, or per slot whenever i would swap that slot, -- it's only like 8 to 10 items to store with some adjustments for species and it will save -- iterating over the inventory for every slot countless times -- defining the "best" item when comparing against differing egos, plus levels, and artprops is a little challenging, but -- it's kind of needed here -- i could do "best item per skill type" for weapons, and "best item per aux slot" for armour, -- defining "best jewellery" is going to require some kind of relative artprop evaluation function -- (artprop eval will also need to be applied to armour) --function get_best_inventory_weapon(skill) -- local temp = nil -- for it in inventory() do -- if it.weap_skill == skill then -- if it.name():find(" -- -- -- return it --end --function check_desired_weapon_v2(it) -- local itskill = it.weap_skill -- -- -- if it doesn't use our best weapon or launcher skills, then we don't want it -- if not (itskill == skwrap("melee") or itskill == skwrap("launcher")) then -- return false -- end -- local itname = it.name() -- -- for _, which_skill in ipairs(weapon_types_table) do -- if itskill == which_skill[1] then -- for _, weapon_name in ipairs(which_skill[2]) do -- -- -- --end -- i was duplicating everything in this function, that was dumb -- TODO: come back to this when i'm awake function check_desired_weapon(it) -- if we have this weapon equipped, we're keeping it for now if it.equipped then return true end local weapon_skill = skwrap("melee") local launcher_skill = skwrap("launcher") local compare_skill = nil if it.weap_skill == weapon_skill then compare_skill = weapon_skill elseif it.weap_skill == launcher_skill then compare_skill = launcher_skill end -- this weapon does not use our best weapon or launcher skills, we don't want it if not compare_skill then return false end -- try to find our best weapon in inventory local best_it = nil -- i shouldn't be running this repeatedly inside of autopick, -- this needs to be run once per turn, at most for stuff in inventory() do -- careful with autodrop recursion here if stuff.class(true) == "weapon" then if stuff.weap_skill ~= nil then if stuff.weap_skill == compare_skill then if best_it == nil or stuff.damage > best_it.damage then best_it = stuff end end end end end if best_it == nil and it.weap_skill == compare_skill then -- somehow we have a weapon skill, but no weapon? return true end -- higher damage base type than our best in inventory, pick it up if it.damage > best_it.damage then return true end -- i think this check isn't working on not-fully-id'd weapons? -- artefact or ego weapon/launcher with a base type equivalent or better to what we have, pick it up -- TODO: figure out if it.damage returns a value for unid weapons? if ( it.artefact or it.ego(true) ) and it.damage >= best_it.damage then return true end -- equal or better plus and equal base type, pick it up -- (i think it wont let me compare plus against unid) -- if it.damage == best_it.damage and it.plus >= best_it.plus then -- unid same base type if it.damage == best_it.damage and it.fully_identified == false then return true end -- again, i think the it.damage checks aren't working with unid stuff -- i probably need a set of weapon class comparison tables against it.name(), -- assuming that it.damage can't be checked properly against not-fully-id'd stuff -- i'm not sure how what's written here will interact with unidentified items, -- TODO: test all of this and verify the it.damage, it.ego, it.artefact behavior for unid weapons return false end -- armour type tables, sourced from item-prop.cc, armour_def Armour_prop[] -- -- {"name", ac, raw evp, normalized ["EV"] artprop bonus, bool should_we_ench_this} -- native body armour resistances are handled by the dragon_armour_base_props_table below -- ["EV"] bonus values are normalized against -150 evp (chain mail), with a ceiling of +- 6 -- this is a quick and very dirty normalization that does not account for player Armour skill or Strength: -- evp is not used this way in-game, and this approach certainly gets some relative values wrong. -- -- TODO: rewrite the ["EV"] calcs properly, to handle player Armour skill and Str -- TODO: break this table up by type? local armour_base_def_table = { -- EQ_BODY_ARMOUR {"animal skin", 2, 0, 6, false}, {"robe", 2, 0, 6, false}, {"leather armour", 3, -40, 6, false}, {"ring mail", 5, -70, 6, false}, {"scale mail", 6, -100, 5, false}, {"chain mail", 8, -150, 0, false}, {"plate armour", 10, -180, -3, true}, {"crystal plate armour", 14, -230, -6, true}, -- {"removed troll hide", 0, 0}, -- old v {"troll leather armour", 4, -40, 6, false}, -- EQ_CLOAK {"cloak", 1, 0, nil, true}, {"scarf", 0, 0, nil, false}, -- EQ_GLOVES {"gloves", 1, 0, nil, true}, -- EQ_HELMET {"helmet", 1, 0, nil, true}, -- {"cap", 0, 0}, -- old v {"hat", 0, 0, nil, true}, -- EQ_BOOTS {"boots", 1, 0, nil, true}, {"centaur barding", 4, -60, nil, true}, -- not weighting ["EV"] against bardings, *yet* {"naga barding", 4, -60, nil, true}, -- EQ_SHIELD -- also not weighting ["EV"] against shields yet {"buckler", 3, -8, nil, false}, -- "Note: shields use ac-value as sh-value, EV pen is used as the basis {"kite shield", 8, -30, nil, false}, -- to calculate adjusted shield penalty." {"tower shield", 13, -50, nil, true}, -- DRAGON_ARMOUR {"steam dragon scales", 5, 0, 6, false}, {"acid dragon scales", 6, -50, 6, false}, {"quicksilver dragon scales", 9, -70, 6, false}, {"swamp dragon scales", 7, -70, 6, true}, {"fire dragon scales", 8, -110, 4, true}, {"ice dragon scales", 9, -110, 4, true}, {"pearl dragon scales", 10, -110, 4, true}, {"storm dragon scales", 10, -150, 0, true}, {"shadow dragon scales", 10, -150, 0, true}, {"gold dragon scales", 12, -230, -6, true}, } -- dragon armour + TLA base props table, in artprop format, to be used in armour value comparisons local dragon_armour_base_props_table = { {"steam dragon scales", {["rSteam"] = 1,} }, {"acid dragon scales", {["rCorr"] = 1,} }, {"quicksilver dragon scales", {["MR"] = 1,} }, {"swamp dragon scales", {["rPois"] = 1,} }, {"fire dragon scales", {["rF"] = 2, ["rC"] = -1,} }, {"ice dragon scales", {["rC"] = 2, ["rF"] = -1,} }, {"pearl dragon scales", {["rN"] = 1, } }, {"storm dragon scales", {["rElec"] = 1, } }, {"shadow dragon scales", {["Stlth"] = 1, } }, -- this should be Stlth + 4, but i maybe don't want the script to tank its value *that* much {"gold dragon scales", {["rF"] = 1, ["rC"] = 1, ["rPois"] = 1, } }, {"troll leather armour", {["Regen"] = 1, } }, } -- what about things like moon troll armour? robe of vines? -- TODO: add this? or are those vals listed in their respective artprops? -- TODO: find this out -- artprop weight table -- if i can get this right, it should provide a backbone for v2 armour and jewellery assessment fns -- -- {"name", int relative_value} -- relative value here is a custom weighting, per point of the prop, to be used in autoequip / autopick / etc. -- these weights were assigned relative to 1 point of Slaying == 100 value -- -- the intent is to allow the script to equip items that are relatively-stronger, -- by choosing the highest-weighted available items to equip, -- -- this baseline table does not account for situationally-relative weightings like rF or +Int, that is handled separately -- -- artprop names pulled from artefact.cc, artefact_prop_data artp_data[] -- -- i know there are some props missing from this table; things like spirit shield, "resistance", possibly others -- also, uniquely-cased unrandart behavior generally isn't captured by this table -- TODO: try to figure out what's still missing from this table, and fix it up local artprop_weight_table = { {"Slay", 100}, -- baseline value for building this table {"Brand", 0}, -- SPWPN_FREEZING, SPWPN_FLAMING, etc. -- TODO: revisit this if i want to apply this table to weapons {"AC", 67}, {"EV", 75}, {"Str", 50}, {"Int", 0}, -- needs special case function to check for damage spell offense {"Dex", 40}, {"rF", 200}, -- needs special case fn to check for pre-existing rF, {300 with rF-, 200 default, 0 with rF++} {"rC", 125}, -- needs special case fn to check for rC, {125 or 0} {"rElec", 300}, -- needs special case fn, {300 or 0} {"rPois", 150}, -- needs special case fn, {150 or 0} {"rN", 30}, -- needs special case fn, {30 or 0 with rN3} {"MR", 150}, -- needs special case fn, {150 or 0} with enough MR to resist Mark {"SInv", 30}, -- needs special case fn, {30 or 0} {"+Inv", 250}, -- needs special case fn, {250 or 0}, also may need logic for script to use +inv {"+Fly", 0}, -- needs special case fn, {0 or 75} if this prop is on boots {"+Fog", 250}, -- needs special case fn, {250 or 0}, (but maybe it doesn't since this is really only on Thief?) {"+Blink", 0}, {"+Rage", 180}, -- needs special case fn, {250 or 0} -- +Rage is at *least* 250, but i want its relative amu rating to be a little lower for now {"*Noise", -200}, {"-Cast", -200}, -- needs special case fn, {-200 or 0 or -1000}, depending if using minimal spells / no spells / damage spells -- probably -Cast should be worse than -200 even in the minimal case, it's pretty bad {"*Tele", -1000}, {"-Tele", -1000}, {"*Rage", -75}, -- {"Hungry", -200}, -- old ver {"*Contam", 0}, -- needs special case fn, to never remove this unless !cancellation is available (nor equip this?) -- {"Acc", 30}, -- old ver -- {"Slay", 100}, -- baseline {"*Curse", -1000}, -- likely to break autoequip, scythe of curses isn't worth that {"Stlth", -150}, -- this may need special case fn: stlth multi needs to be absolute value, negative stealth is also bad {"MP", 0}, -- needs special case fn, {0 or _something_} depending if using damage spells {this value relative to Int val} {"Delay", 0}, -- don't yet know where this is used, leaving it unvalued for now {"HP", 20}, {"Clar", 200}, -- only exists on 2 items, no real need to special case this {"BAcc", 30}, {"BDam", 70}, {"RMsl", 200}, -- needs special case fn, {200 or 0}, with a check for equipped RMsl and a spell-known check for RMsl and DMsl {"Regen", 225}, -- have this written as {250} originally, but {225} feels right -- {"SustAt", 40}, -- old ver -- this is removed, otherwise would need special case {40 or 0} {"nupgr", 0}, {"rCorr", 125}, -- needs special case, {125 or 0} {"rMut", 300}, -- only exists on 2 items, no real need to add {300 or 0} special case -- {"+Twstr", 0}, -- old ver -- this code must be very old {"*Corrode", -200}, {"*Drain", -100}, -- needs special case, cannot remove this unless swap-to item is massively better {"*Slow", -200}, {"Fragile", 0}, -- needs special case, cannot remove this unless we *really* want to destroy it {"SH", 40}, {"Harm", -1000}, } -- apparently some of the armour and jewellery egos have baked-in values, instead of using the item's plus -- i need to figure out which ones these are, -- so i can bake their baked-in plus multis into my armour/jewellery ego val tables -- armour ego todos: -- run, ponderous, Fly (SPARM_FLY is not +Fly), Archmagi, Spirit, archery, cloud immunity, buggy -- jewellery ego todos: -- attention (not sure if this is the same as *Noise), wizardry, fire, ice, teleport control, magic regeneration, -- gourmand, acrobat, conservation, controlled flight, inaccuracy, nothing, guardian spirit, faith, buggy jewellery -- armour ego type table -- -- these ego type tables are called with (terse = false), because the code in item-name.cc omits some information -- and returns some ego types out of order when called with (terse = true) -- -- I *could* merge these tables with the artprop table above, but some armour egos and some jewellery egos -- grant bonuses at fixed multipliers of those in the artprop table; things like "AC+3". -- For now, it's easier to have the data from each table here separately, even though it means some value duplication across -- the three tables. These tables will probably be merged after I've confirmed the in-game behavior of each ego. -- -- armour ego names in this table were sourced from item-name.cc, armour_ego_name(), (terse = false) local armour_ego_weight_table = { {"", 0}, -- SPARM_NORMAL -- TODO: make sure that having an empty string match at the top here won't mess with armour ego code {"running", 175}, -- SPARM_RUNNING, "run" -- i'd take 2 slay over run because fuck manual kiting, -- but -rundelay is quite strong and i don't want to rate this *too* low either. -- if this script ever does auto-kiting, running should be changed to extremely high value. {300}? +? {"speedy slithering", 175}, -- SPARM_RUNNING, with its non-terse ego type return mangled for naga bardings -- "to avoid player confusion, it used to be {sslith}." -- Thank you, crawlcode. {"fire resistance", 200}, -- SPARM_FIRE_RESISTANCE, "rF+" {"cold resistance", 125}, -- SPARM_COLD_RESISTANCE, "rC+" {"poison resistance", 150}, -- SPARM_POISON_RESISTANCE, "rPois" {"see invisible", 30}, -- SPARM_SEE_INVISIBLE, "SInv" {"invisibility", 250}, -- SPARM_INVISIBILITY, "+Inv" {"strength", 150}, -- SPARM_STRENGTH, "Str+3" {"dexterity", 120}, -- SPARM_DEXTERITY, "Dex+3" {"intelligence", 0}, -- SPARM_INTELLIGENCE, "Int+3" -- TODO: revisit this {"ponderousness", -175}, -- SPARM_PONDEROUSNESS, "ponderous" -- negative running seems about right, -- the ac bonus will be factored in with the armour's plus {"flying", 200}, -- SPARM_FLYING, "Fly" -- this is boot perma-Fly, I think? -- this can help the script to not get stuck on terrain; it is accordingly rated higher here than it would be for manual play. {"magic resistance", 150}, -- SPARM_MAGIC_RESISTANCE, "MR+" {"protection", 200}, -- SPARM_PROTECTION, "AC+3" {"stealth", -150}, -- SPARM_STEALTH, "Stlth+" {"resistance", 325}, -- SPARM_RESISTANCE, "rC+ rF+" {"positive energy", 30}, -- SPARM_POSITIVE_ENERGY, "rN+" {"the Archmagi", 0}, -- SPARM_ARCHMAGI, "Archmagi" -- currently, we do not value offensive spellcaster equipment -- TODO: this -- {"jumping", 150}, -- SPARM_JUMPING, "obsolete" -- old ver -- jumping was like, wu jian for 1 version or something, i think? this rating is probably off but it doesn't matter. -- {"preservation", 250}, -- SPARM_PRESERVATION, "obsolete" -- old ver {"reflection", 40}, -- SPARM_REFLECTION, "reflect" -- this ego uses shield + to determine reflect val, i need to special case this to multiply by SH base val + item plus {"spirit shield", 250}, -- SPARM_SPIRIT_SHIELD, "Spirit" {"archery", 100}, -- SPARM_ARCHERY, "archery" -- 4 slay for ranged attacks only, {400 / 2} == {200} ? this is a really dumb way to value it, but.. -- a more appropriate weighting would be against frequency of using ranged attacks -- i don't really want the script to take archery over gloves of strength; i need to reduce this to around {120} or {100} {"repulsion", 200}, -- SPARM_REPULSION, "repulsion" {"cloud immunity", 35}, -- SPARM_CLOUD_IMMUNE, "cloud immunity" -- technically, the script will kickback to player atop any dangerous cloud, so this is useless to the script itself, (at -- least unless the cloud kickback behavior is made conditional), but it may still have marginal value to the player. -- lower bound {30}, rN+ vs. miasma, upper bound {40}, i'd still rather take 1 dex -- TODO: i might want to revisit this to allow the script the chance to ignore clouds, that probably has value {"bugginess", 0}, } -- default armour ego fallthrough in the c switch case, "buggy" -- jewellery ego names sourced from item-name.cc, jewellery_effect_name(), (terse = false) local jewellery_ego_weight_table = { -- {"obsoleteness", 225}, -- RING_REGENERATION -- old ver -- {"protection", 67}, -- RING_PROTECTION -- moved to plus table, see below {"protection from fire", 200}, -- RING_PROTECTION_FROM_FIRE {"poison resistance", 150}, -- RING_POISON_RESISTANCE {"protection from cold", 125}, -- RING_PROTECTION_FROM_COLD -- {"strength", 50}, -- RING_STRENGTH -- moved to plus table -- {"slaying", 100}, -- RING_SLAYING -- moved to plus table {"see invisible", 30}, -- RING_SEE_INVISIBLE {"resist corrosion", 125}, -- RING_RESIST_CORROSION {"attention", -200}, -- RING_ATTENTION -- rating this as *Noise for now, although I'm not sure if they use identical behavior {"teleportation", -1000}, -- RING_TELEPORTATION -- {"evasion", 75}, -- RING_EVASION -- moved to plus table -- {"sustain attributes", 40}, -- RING_SUSTAIN_ATTRIBUTES -- old ver {"stealth", -150}, -- RING_STEALTH -- {"dexterity", 40}, -- RING_DEXTERITY -- moved to plus table -- {"intelligence", 0}, -- RING_INTELLIGENCE -- TODO: revisit -- moved to plus table {"wizardry", 0}, -- RING_WIZARDRY -- TODO: revisit function logic for things like Wiz, +Int, +MP {"magical power", 0}, -- RING_MAGICAL_POWER -- this is a static MP+9 , I think? {"flight", 0}, -- RING_FLIGHT {"positive energy", 30}, -- RING_LIFE_PROTECTION {"protection from magic", 150}, -- RING_PROTECTION_FROM_MAGIC {"fire", 75}, -- RING_FIRE -- rF+ rC- and we don't care about enhancers yet, these are simple {"ice", -75}, -- RING_ICE -- {"teleport control", 150}, -- RING_TELEPORT_CONTROL -- old ver -- this was very handy for rune vaults, i kind of miss it {"rage", 180}, -- AMU_RAGE -- +rage is much more useful than this value implies, but i don't want the script to take it all the time and use it poorly -- TODO: I should probably add scripted rage logic, so I can value it appropriately {"harm", -1000}, -- AMU_HARM {"magic regeneration", 0}, -- AMU_MANA_REGENERATION -- TODO: revisit {"gourmand", 0}, -- AMU_THE_GOURMAND -- TODO: maybe revisit with the spellcasting stuff, maybe not {"the acrobat", 200}, -- AMU_ACROBAT -- i don't value acrobat super highly in manual play, but for the script making "dumb" moves, this might be more helpful -- {"conservation", 250}, -- AMU_CONSERVATION -- old ver -- if we're playing ancient crawl, conservation is at least 250 value -- {"controlled flight", 0}, -- AMU_CONTROLLED_FLIGHT -- old ver {"inaccuracy", -150}, -- AMU_INACCURACY -- (possibly {-30} * whatever inacc effect strength is?) -- it looks like inacc loses 5 to-hit per point of inacc, so this would be {-150}; i eyeballed it at {-200}, so, sure. {"nothing", 0}, -- AMU_NOTHING {"guardian spirit", 250}, -- AMU_GUARDIAN_SPIRIT {"faith", 220}, -- AMU_FAITH -- faith is really good, but the script is not likely to make best use of it -- {"reflection", 40}, -- AMU_REFLECTION -- moved to plus table {"regeneration", 225}, -- AMU_REGENERATION {"buggy jewellery", 0}, } -- default jewellery ego fallthrough in the c switch case -- these are the only jewellery types that get pluses, so they get their own table: -- sourced from item-prop.cc, bool jewellery_has_pluses() local jewellery_plus_ego_weight_table = { {"slaying", 100}, -- RING_SLAYING {"protection", 67}, -- RING_PROTECTION {"evasion", 75}, -- RING_EVASION {"strength", 50}, -- RING_STRENGTH {"intelligence", 0}, -- RING_INTELLIGENCE -- TODO: revisit {"dexterity", 40}, -- RING_DEXTERITY {"reflection", 40}, } -- AMU_REFLECTION -- get the base value (prop value) for an armour item -- this compares it.name() only, so it should work with any ident flags (including unidentified items), -- this does not check egos or pluses. handling those depends on ident flags, so they are handled separately by get_prop_value() -- this function is only intended to work with items of type it.class(true) == "armour" function get_armour_base_value(it) local value = 0 local it_name = it.name() for _, armour_type_name in ipairs(armour_base_def_table) do if it_name:find(armour_type_name[1]) then -- basing this weight logic around AC value is wonky for shields, -- but they're only going to be compared against other shields... -- value = value + (artprop_weight_table[3][2] * 3 * armour_type_name[2]) -- [3][2] should be AC+1 value in the artprop table if it_name:find("troll") or it_name:find("crystal") then if armour_type_name[1]:find("troll") or armour_type_name[1]:find("crystal") then value = value + (67 * armour_type_name[2]) -- base ["AC"] bonus if armour_type_name[4] ~= nil then value = value + (75 * armour_type_name[4]) -- normalized ["EV"] bonus end end -- don't add extra value to CPA or TLA for false it_name:find() matches else value = value + (67 * armour_type_name[2]) -- base ["AC"] bonus if armour_type_name[4] ~= nil then value = value + (75 * armour_type_name[4]) -- normalized ["EV"] bonus end end -- TODO: "proper" AEVP calculations, instead of this dumb normalization approach that ignores the actual character stats? end end -- translate dragon scale and TLA base types into prop values, then add them -- TODO: dynamic dscale resist prop values depending on current character resistances if it_name:find("dragon") or it_name:find("troll") then for _,dscale_type in ipairs(dragon_armour_base_props_table) do if it_name:find(dscale_type[1]) then for dscale_prop_key,dscale_prop_val in pairs(dscale_type[2]) do for _, artprop in ipairs(artprop_weight_table) do if dscale_prop_key == artprop[1] then value = value + (dscale_prop_val * artprop[2]) end end end end end end return value end -- currently, this function is only written to work with armour and jewellery function get_prop_value(it) local value = 0 local it_ego = it.ego(false) -- our armour and jewellery ego tables use terse = false local it_class = it.class(true) local it_plus = it.plus local it_name = it.name() -- apply values for the item's base AC and AEVP, and extra AC from it.plus, if it has them -- -- TODO: weight base armour type more heavily for body armour; -- in a test it grabbed a low-plus randart leather armour with lots of props and rolled with it -- it did the same thing with a rF- +1 randart robe, because it had rElec and rCorr if it_class == "armour" then -- i think it's okay to include shields here; they will only be compared against other shields value = value + get_armour_base_value(it) if it_plus then -- value = value + (artprop_weight_table[3][2] * it_plus) value = value + (67 * it_plus) end end -- apply values for it.ego(false), if it has an ego if it_ego then if it_class == "armour" then for _, armour_ego_type in ipairs(armour_ego_weight_table) do if it_ego == armour_ego_type[1] then -- special case for shield reflection if it_ego == "reflection" and it.subtype() == "shield" and it_plus then value = value + (armour_ego_type[2] * (it.ac + it_plus)) else value = value + armour_ego_type[2] end end end end if it_class == "jewellery" then if it_plus then for _, jewellery_ego_plus_type in ipairs(jewellery_plus_ego_weight_table) do if it_ego == jewellery_ego_plus_type[1] then value = value + (jewellery_ego_plus_type[2] * it_plus) end end else -- most jewellery types do not get pluses for _, jewellery_ego_type in ipairs(jewellery_ego_weight_table) do if it_ego == jewellery_ego_type[1] then value = value + jewellery_ego_type[2] end end end end end -- apply values for the artprop table, if it has one if it.artefact then for prop, val in pairs(it.artprops) do for _, prop_types in ipairs(artprop_weight_table) do if prop == prop_types[1] then value = value + (prop_types[2] * val) end end end end -- apply values for the item's "brand" artprop, if it has one -- TODO: this return value end -- reordering of armour by rough desire = closer to top, -- some of these do not fit neatly into a generalized ordering -- first column, (67 * base AC) + resist vals -- second column, (67 * base AC) + resist vals + (75 * (EVP/10)) normalized to 150 evp, with everything below as bonus -- and everything above as penalty -- {"crystal plate armour", 14, -230, 938, 338}, -- {"gold", 12, -230, 1279, 679}, -- rF, rC, rPois -- {"shadow", 10, -150, 520, 520}, -- {"plate armour", 10, -180, 670, 445}, -- {"chain mail", 8, -150, 536, 536}, -- above -150 is penalty evp in second column calc -- {"pearl", 10, -110, 700, 1000}, -- if i take 75*4 as the 40-pt difference here, it would set the pearl to 1000 -- {"storm", 10, -150, 970, 970}, -- normalizing around 150 evp -- {"fire", 8, -110, 811, 1111}, -- rF rF rC- -- {"quicksilver", 9, -70, 753, 1353}, -- MR -- also, i'm going to cap the EVP bonus here at -70, for 75 * 8 bonus -- {"swamp", 7, -70, 619, 1219}, -- rPois -- anything below that is generally negated by strength and armour -- {"ice", 9, -110, 653, 953}, -- rC rC rF- -- {"acid", 6, -50, 527, 1127}, -- rCorr -- {"scale mail", 6, -100, 402, 777}, -- {"steam", 5, 0, 335, 935}, -- rSteam is not rated, here -- {"ring mail", 5, -70, 335, 935}, -- {"removed troll hide", 0, 0}, -- old v -- {"troll leather armour", 4, -40, 493, 1093}, -- regen is rated here -- {"leather armour", 3, -40, 201, 801}, -- {"animal skin", 2, 0, 134, 734}, -- {"robe", 2, 0, 134, 734}, -- get the best armour we have for a given slot by prop value -- get_prop_value will produce wrong values for unidentified items; -- but we're trying to grab the best known item so that's okay -- slot_type == it.subtype() function get_best_known_armour_slot_item_by_value(slot_type) local best = nil for it in inventory() do if it.fully_identified then if it.subtype() == slot_type then if best == nil or get_prop_value(it) > get_prop_value(best) then best = it end end end end return best end function check_desired_armour_v2(it) if it.is_useless then return false end local subtype = it.subtype() local best = get_best_known_armour_slot_item_by_value(subtype) -- if we don't have anything for this slot, then take it if best == nil then return true end -- if it's art, then if it.artefact then -- if it's unid, take it if not it.fully_identified then return true end -- if it's fully identified, then -- if it's body armour, then if subtype == "body" then -- subtype *should* always be accessible for class(true) == "armour", even if unid -- if its base type value is >= our best slot base type in inventory, AND -- (don't downgrade body armour base type) -- if its prop value is >= our best slot prop value in inventory, then take (/keep) it if get_armour_base_value(it) >= get_armour_base_value(best) and get_prop_value(it) >= get_prop_value(best) then return true end -- also try to hang on to really good artefact or dscale armour, in case the player wants to switch -- (the number here is just "anything >= than plain fire dragon scales") if get_prop_value(it) >= 1111 then return true end -- else if it's a shield, then elseif subtype == "shield" then -- if its base type value is >= our best slot base type in inventory, AND -- (don't downgrade shield base type) -- if its prop value is >= our best slot prop value in inventory, then take (/keep) it if get_armour_base_value(it) >= get_armour_base_value(best) and get_prop_value(it) >= get_prop_value(best) then return true end -- also try to hang on to really good shields, in case the player wants to switch -- (this value "is this shield value >= medium shield of fire resistance"), -- notably, this will keep all artefact tower shields which do not have completely terrible props if get_prop_value(it) >= 736 then return true end -- else (if it's aux armour) then else -- if its prop value is >= our best slot prop value in inventory, then take (/keep) it if get_prop_value(it) >= get_prop_value(best) then return true end -- also try to hang on to really good aux items, in case the player wants to switch -- (the 500 here is entirely arbitrary "is this item >= +5 slaying equivalent", it may need adjustment) if get_prop_value(it) >= 500 then return true end end -- otherwise, leave (/drop) it return false end local it_name = it.name() local it_ego = it.ego() -- if it's ego or dscale and its base type value is >= our best slot base type value in inventory, then if it_name:find("enchanted") or it_name:find("embroidered") or it_name:find("shiny") or it_name:find("dyed") or it_name:find("runed") or it_name:find("glowing") or it_name:find("dragon scales") or it_ego ~= nil then if get_armour_base_value(it) >= get_armour_base_value(best) then -- if it's unid, take it (for now) if not it.fully_identified then return true end -- if it's fully identified, then -- take (/keep) it if its prop value is >= our best slot prop value in inventory if get_prop_value(it) >= get_prop_value(best) then return true end end -- otherwise, leave (/drop) it return false end -- if it is not ego or art or dscale, then -- if our best item for that slot is plain armour, then -- -- note: this could leave us equipped with ego robe or ego leather instead of plain whatever else -- (maybe remove this best. check?) -- if not best.artefact or best.ego() or best.name():find("dragon scales") then -- if it's unid, then if not it.fully_identified then -- if its base type value is > our best slot base type value in inventory, -- (and if we don't already have one), then take it -- a problem here is that (best) only compares against fully identified items: -- if we have, e.g, a +0 robe and 3x unidentified ring mails, (best) will compare the robe and continue to pick ring mail if get_armour_base_value(it) > get_armour_base_value(best) then for stuff in inventory() do if it_name == stuff.name() then -- an exact name match should prevent most duplicate items from being picked.. if it.ininventory == false then return false else return true end -- we need to keep the first such item for autoequip to work with, at the end of the floor end end end else -- if it's fully identified, then -- if its base type value is >= our best slot base type in inventory, AND -- (don't downgrade body armour base type) -- if its prop value is >= our best slot prop value in inventory, then take (/keep) it -- to keep higher plus items, get_prop_value() *should* handle it.plus here -- NOTE: this logic will result in duplicate base items being kept, if they are == to the best available fully-id -- slot item, e.g. multiple +0 boots, +0 cloaks, +0 ring mails, etc. -- if get_armour_base_value(it) >= get_armour_base_value(best) and get_prop_value(it) >= get_prop_value(best) then -- return true -- end if it.ininventory == false then if get_armour_base_value(it) >= get_armour_base_value(best) and get_prop_value(it) > get_prop_value(best) then return true end -- i'm making no assumptions here regarding it.ininventory nil behavior elseif it.ininventory == true then if get_armour_base_value(it) >= get_armour_base_value(best) and get_prop_value(it) >= get_prop_value(best) then local count = 0 for stuff in inventory() do if it_name == stuff.name() then count = count + 1 end end -- try to prevent duplicate base items from being retained if they were picked up while unid and are now equal to -- our current (best) if count > 1 then return false end return true end end end -- end -- otherwise, leave (/drop) it return false end --function check_desired_armour_v2(it) -- if it.is_useless then return false end -- local itname = it.name() -- if it's unid and art or ego or dscale, then take it -- if not it.fully_identified then -- if it.artefact then -- return true -- elseif itname:find("enchanted") or itname:find("embroidered") or itname:find("shiny") or -- itname:find("dyed") or itname:find("runed") or itname:find("glowing") then -- return true -- elseif itname:find("dragon scales") then -- return true -- end -- end -- if it's unid and we don't have that subtype in inventory, then take it -- (if we have 1 or fewer fully identified items of that subtype in inventory, then take it?) -- if it's unid and our equipped version is negative enchant [negative prop value?], then take it -- if it's id, then -- if its prop value is > -- if we have only one of its subtype in inventory, then keep it ? -- bad hack to ensure dragon scales remain in inventory for now, -- TODO: replace this with better logic below after adding dragon scale resist weights -- to the base armour def table --if it.name():find("dragon scales") then return true end --local prop_value = nil --if it.fully_identified then --prop_value = get_prop_value(it) --end --if prop_value then --if prop_value <= 0 then -- reject known bad props like *Tele from inventory --return false --end --end -- if we don't have one equipped, take one -- TODO: check full inventory here, in case we've just temporarily removed some armour? --local equipped = items.equipped_at(it.equip_type) --if not equipped then return true end --local eq_prop_value = nil -- hmm --end -- artefacts are already pickup = true, before this function is ever called, -- we've also already excluded equip slots that are unavailable to the character's species. -- TODO: verify whether is_useless(temp = false) excludes temporarily-melded item slots (?) . -- TODO: rewrite this entire function function check_desired_armour(it) local subtype = it.subtype() local plus = nil if it.fully_identified then -- it.plus returns nil if the item is unidentified; ebering's crawl lua docs are incomplete -- if it.plus ~= nil then plus = it.plus end local ego = it.ego() local equipped = items.equipped_at(it.equip_type) if not equipped then -- if we don't have one, take one return true end -- logic below this point can assume there is a pre-existing equipped item in this slot local eqplus = equipped.plus -- can we assume that we will always know the plus of the equipped item? local eqego = equipped.ego() -- TODO: verify whether knowing it.plus is guaranteed for equipped items local eqart = equipped.artefact -- here i'm wrapping all the normal checks inside an it.plus check, -- and putting a separate pickup for glowing/whatever unidentified egos at the bottom if plus then -- if it has significantly higher enchantment than what's equipped, take it, -- assuming also that we know what we're doing if we're wearing a badly-enchanted artefact if plus >= (eqplus + 2) and (not eqart) then return true end -- TODO: this is rejecting blank dragon scale armour most of the time, need to fix -- if it's blank and we already have an ego or art, leave it if ego == nil and ( eqego ~= nil or eqart ) then return false end -- if it's an ego aux item with +0 or better, take it if ego and subtype ~= "body" and plus >= 0 then return true end -- heavily enchanted body armour, take it if subtype == "body" and plus >= 4 then return true end -- if it's better than what we have, take it if plus > eqplus then return true end end -- below this point, we're comparing against unidentified items local name = it.name() local eqname = equipped.name() -- don't hang on to bucklers if we have a shield if subtype == "shield" then if name:find("buckler") and eqname:find("shield") then return false end end -- don't hang onto medium shields if we have a large shield if subtype == "shield" then if name:find("kite") and eqname:find("tower") then return false end end -- also try to be smart about picking up extra large shields if subtype == "shield" then if name:find("tower") or name:find("large") then if eqname:find("buckler") or eqname:find("kite") then return true elseif not it.fully_identified then return true elseif it.ego() then return true elseif plus and eqplus and plus > eqplus then return true else return false end end end -- as in item-name.cc, item_def name_aux( , switch case OBJ_ARMOUR: -- enchanted , embroidered , shiny , dyed , runed , glowing if name:find("enchanted") or name:find("embroidered") or name:find("shiny") or name:find("dyed") or name:find("runed") or name:find("glowing") then return true end -- below this point we're dealing with non-artefact, non-ego items -- pickup generic dragon scales, if we don't already have something good if subtype == "body" and name:find("dragon") and (not eqname:find("dragon") and eqplus > 2) then return true end -- pickup a body armour if we're wearing leather or worse if subtype == "body" then if name:find("dragon") or name:find("scale") or name:find("ring") or name:find("plate") then if eqname:find("dragon") or eqname:find("scale") or eqname:find("ring") or eqname:find("plate") then return false else return true -- as written, this will pickup multiple such items until one is actually equipped end end end -- let's assume that by default, we don't want it return false end -- autopick func function check_desired_item_types(it, name) local class = it.class(true) local subtype = it.subtype() local you_xl = you.xl() -- hack to set persistent "do we recognize identify scrolls" variable if class == "scroll" and it.ininventory and subtype == "identify" then c_persist.ID_SCROLLS_RECOGNIZED = true end -- there appear to be some situations where is_useless might return temporarily-wrong values: -- if I'm reading the code correctly, scrolls of teleport will never be picked up inside of a gauntlet ? -- probably crawl's is_useless code has other oversights like this as well. -- -- It should be "usable", at least, if incorrect, since I think default autopick also checks is_useless. -- (the consequences of an erroneous useless check are more severe here, though: Crawl would ignore the item for pickup, -- we will autodrop entire stacks.) -- TODO: go through the is_useless_item(*item, temp=false) function in item-name.cc, -- to verify in what situations items might erroneously tag as useless here. if it.is_useless then if class == "scroll" and subtype == "teleportation" and you.branch() == "Gauntlet" then return true end return false end if class == "weapon" then -- adding custom weapon logic before the artefact check, return check_desired_weapon(it) -- we really don't need to pick unusable randart weapons end if class == "book" then -- apparently each manual gets its own subtype, return true -- it's easiest to just pick all books here end if class == "armour" then return check_desired_armour_v2(it) end if it.artefact then -- if not class == "book" then -- can't check artprops on artefact books; doing so on roxanne's book crashes the game -- if it.artprops then -- I spent a long time hunting down this ["Slay"] bit: it.artprops["thing"] returns if it.artprops and it.artprops["Slay"] then -- nil if the artefact doesn't have that property. You can never assume that if it.artprops["Slay"] < 0 then -- it's possible to compare to a particular prop's value, without first return false -- checking to see if that specific prop exists on the item. end end -- cloak of the Thief has Slay-2, maybe come up with smarter logic here return true -- end -- moving the book check above the art check due to roxanne, since we're picking up all books anyway end if it.equipped then return true end if class == "missile" then local missile_ego = it.ego(true) -- terse = true for _, mtype in ipairs(missile_types) do for _, mego in ipairs(missile_egos) do if subtype == mtype[1] and missile_ego == mego[1] then return mtype[2] and mego[2] and you_xl <= mtype[3] and you_xl <= mego[3] end end end end if class == "wand" then for _, wtype in ipairs(wand_types) do if subtype == wtype[1] then return you_xl <= wtype[3] end end end -- -- moving the v2 armour check above the it.artefact check, so we can drop less-useful artefact armour if necessary -- if class == "armour" then -- return check_desired_armour(it) -- end for _,which_type in ipairs(item_types) do -- subtype comparison tables, wherever we can get away with it, if class == which_type[1] then -- for everything that doesn't need fancier pickup logic for _,value in ipairs(which_type[2]) do if subtype == value[1] then return value[2] end end -- return true -- fallthrough pickup == true for unmatched subtypes end end -- return true -- fallthrough pickup == true for unmatched base types end function check_for_junk_to_drop() for it in inventory() do if (not it.equipped and check_desired_item_types(it, it.name()) == false) then return it end end return nil end -- TODO: weapons, this requires adding weapon checks to get_prop_value() function check_bad_equips_by_value() for it in inventory() do if it.equipped then local it_class = it.class(true) local it_value = nil if it_class == "armour" or it_class == "jewellery" then it_value = get_prop_value(it) end if it_value ~= nil and it_value < 0 then return it end end end return nil end function check_bad_wields() local best_skill = get_best_weapon_skill() local it = items.equipped_at("Weapon") if it ~= nil then local it_class = it.class(true) -- if it_class == "magical staff" or it_class == "weapon" or it_class == "food" then if it.weap_skill ~= best_skill then return it end -- end -- TODO: prop value, brand, and base type comparisons.. -- TODO: some kind of exemption for breadswinging (will this need a c_persist to track resting-state?) -- TODO: some kind of exemption for temporary staff-wielding (as in staff of air or staff or energy) -- TODO: some kind of exemption for temporary launcher wielding end return nil end -- any fn registered with chk_lua_save is supposed to return a valid string of Lua, to be executed on savefile load, -- in this case, the intent is to persist ID_SCROLL_RECOGNIZED through saving without using c_persist -- hope this works --function id_recognized_save(current) -- local res = "" -- local val = nil -- if current == true then -- i'm sure there's a better way to write this but my brain is fried right now -- val = "true" -- elseif current == false then -- TODO: make this less ugly -- val = "false" -- end -- tostring(current) ? -- -- if val then -- res = res .. "ID_SCROLL_RECOGNIZED = " .. val .. "\n" -- end -- return res --end -- spoiler: it didn't work -- "identify", "remove curse", "enchant armour", etc. function check_for_known_scroll(subtype_name) for it in inventory() do if it.class(true) == "scroll" and it.subtype() == subtype_name then return it end end return nil end -- "experience", "heal wounds", "resistance", "berserk rage", etc. function check_for_known_potion(subtype_name) for it in inventory() do if it.class(true) == "potion" and it.subtype() == subtype_name then return it end end return nil end function check_for_amulet_id_target() for it in inventory() do if it.class(true) == "jewellery" and not it.fully_identified and it.name():find("amulet") then return it end end return nil end function check_for_armour_id_target() for it in inventory() do if it.class(true) == "armour" and not it.fully_identified then return it end end return nil end function check_for_ring_id_target() for it in inventory() do if it.class(true) == "jewellery" and not it.fully_identified and it.name():find("ring") then return it end end return nil end function check_for_staff_id_target() for it in inventory() do if it.class(true) == "magical staff" and not it.fully_identified then return it end end return nil end function check_for_largest_unid_stack(classname) local stack = nil for it in inventory() do if it.class(true) == classname and not it.fully_identified then if not stack then stack = it else if it.quantity > stack.quantity then stack = it end end end end return stack -- if nil, all stacks of this classtype are (should be?) fully identified end function good_amulet_equipped() amu = items.equipped_at("Amulet") if amu then local subtype = amu.subtype() if amu.artefact or subtype == "amulet of regeneration" or subtype == "amulet of guardian spirit" or subtype == "amulet of reflection" or subtype == "amulet of faith" or subtype == "amulet of the acrobat" or subtype == "amulet of rage" or subtype == "amulet of conservation" then return true end end return false end -- amulets first if we aren't wearing a good one, -- then the largest stack of either scrolls or potions in that order, -- lastly, id the rest of the amulets function check_for_scroll_id_target() local amulet = check_for_amulet_id_target() local scroll = check_for_largest_unid_stack("scroll") local potion = check_for_largest_unid_stack("potion") if not good_amulet_equipped() then if amulet then return amulet end end if scroll and potion then if scroll.quantity >= potion.quantity then return scroll else return potion end end if scroll then return scroll end if potion then return potion end if amulet then return amulet end return nil end function can_we_scroll_id_something() if not c_persist.ID_SCROLLS_RECOGNIZED then return nil end local id_scroll = nil id_scroll = check_for_known_scroll("identify") local id_target = nil id_target = check_for_scroll_id_target() if id_scroll and id_target then return id_scroll, id_target end return nil, nil end function check_for_wear_id_target() local armour = check_for_armour_id_target() local ring = check_for_ring_id_target() if armour then return armour end if ring then return ring end return nil end function check_for_wield_id_target() local staff = check_for_staff_id_target() if staff then return staff end return nil end function can_we_wear_id_something() local equip_target = nil local rc_scroll = nil equip_target = check_for_wear_id_target() rc_scroll = check_for_known_scroll("remove curse") if rc_scroll and equip_target then return equip_target end return nil end function can_we_wield_id_something() local wield_target = nil local rc_scroll = nil wield_target = check_for_wield_id_target() rc_scroll = check_for_known_scroll("remove curse") if rc_scroll and wield_target then return wield_target end return nil end function is_armour_type_enchantable(it) if it == nil then return nil end if it.class(true) ~= "armour" then return false end if it.artefact then return false end local it_name = it.name() if it_name:find("quicksilver") or it_name:find("scarf") then return false end return true end --/** -- * Return the enchantment limit of a piece of armour. -- * -- * @param item The item being considered. -- * @return The maximum enchantment the item can hold. -- */ --int armour_max_enchant(const item_def &item) --{ -- ASSERT(item.base_type == OBJ_ARMOUR); -- -- // Unenchantables. -- if (!armour_is_enchantable(item)) -- return 0; -- -- const int eq_slot = get_armour_slot(item); -- -- int max_plus = MAX_SEC_ENCHANT; -- if (eq_slot == EQ_BODY_ARMOUR -- || item.sub_type == ARM_CENTAUR_BARDING -- || item.sub_type == ARM_NAGA_BARDING) -- { -- max_plus = property(item, PARM_AC); -- } -- else if (eq_slot == EQ_SHIELD) -- // 3 / 5 / 8 for bucklers/shields/lg. shields -- max_plus = (property(item, PARM_AC) - 3)/2 + 3; -- -- return max_plus; -- thanks, crawlcode, for not exposing this value through lua -- heck with it, let's reimplement the function! function get_armour_max_plus(it) if it == nil then return nil end if it.class(true) ~= "armour" then return 0 end if it.artefact then return 0 end local it_name = it.name() if it_name:find("quicksilver") or it_name:find("scarf") then return 0 end local it_subtype = it.subtype() local max_plus = 2 -- MAX_SEC_ENCHANT = 2 -- we can't use it.ac here because item identification flags exist (and are irritating) if it_subtype == "body" or it_name:find("barding") then for _,base_type in ipairs(armour_base_def_table) do if it_name:find(base_type[1]) then if it_name:find("troll") or it_name:find("crystal") then if base_type[1]:find("troll") or base_type[1]:find("crystal") then max_plus = base_type[2] end -- don't overwrite false leather and plate matches else max_plus = base_type[2] end end end end if it_subtype == "shield" then if it_name:find("buckler") then max_plus = 3 elseif it_name:find("kite shield") then max_plus = 5 elseif it_name:find("tower shield") then max_plus = 8 end end return max_plus end -- helms vs hats is so dumb, why is this still a thing, just bikeshed it all to hats and get rid of all the hatcode function get_best_helm_type() local helm = "helmet" local species = you.race() if you.get_base_mutation_level("horns") > 0 or you.get_base_mutation_level("antennae") > 0 or you.get_base_mutation_level("beak") > 0 or species == "Octopode" or species == "Ogre" or species == "Troll" or species == "Spriggan" or species:find("Draconian") then helm = "hat" end if species == "Felid" or you.get_base_mutation_level("horns") > 2 then helm = nil end -- i'm not sure if this covers everything return helm end function check_for_ea_target() for _,subtable in ipairs(enchant_armour_targets_table) do for _,slot in ipairs(subtable) do local it = nil it = items.equipped_at(slot) if it ~= nil then it_name = it.name() it_subtype = it.subtype() for _,armour_type in ipairs(armour_base_def_table) do -- we don't differentiate crystal plate or troll leather here, both matches are ench true and ench false if it_name:find(armour_type[1]) and armour_type[5] == true then if it.plus < get_armour_max_plus(it) and is_armour_type_enchantable(it) then if get_prop_value(it) >= 0 then -- don't want to enchant bad egos or negative plus if it_subtype == "helmet" then local helmtype = get_best_helm_type() if helmtype ~= nil and it_name:find(helmtype) then return it end -- saving ea scrolls for helmet over hat is probably not OpTiMaL bEhAvIoR else return it end end end end end end end end return nil end function should_we_enchant_something() local ench_target = nil local ench_scroll = nil -- TODO: enchant weapon, brand weapon ench_target = check_for_ea_target() ench_scroll = check_for_known_scroll("enchant armour") if ench_scroll and ench_target then return ench_scroll, ench_target end return nil, nil end -- this function doesn't handle rings, call is_slot_cursed() function is_slot_type_cursed(slot_type) local it = nil it = items.equipped_at(slot_type) if it and it.cursed then return true end return false end function is_slot_cursed(it) if it.equip_type == 101 then local ring_slot_tables = get_you_ring_slot_tables() for _,subtable in ipairs(ring_slot_tables) do for _,slot_name in ipairs(subtable) do if is_slot_type_cursed(slot_name) then return true end end end return false else return is_slot_type_cursed(it.equip_type) end end function you_are_cursed() -- return true -- as your bandages flutter in the wind, you feel only an idle sense of loss local it = nil for _, slot_type in ipairs(equip_slot_names_table) do it = items.equipped_at(slot_type) if it and it.cursed then return true end end return false end local skill_list = { -- i actually don't see where the strings corresponding to these enums are defined, i'm going off of memory "Fighting", "Short Blades", "Long Blades", "Axes & Hammers", "Whips & Flails", "Polearms", "Maces & Staves", "Slings", "Bows", "Crossbows", "Throwing", "Armour", "Dodging", "Stealth", -- "Stabbing", -- old ver "Shields", -- "Traps & Doors", -- dang how old is this code "Unarmed Combat", "Spellcasting", "Conjurations", "Hexes", "Charms", "Summonings", "Translocations", "Transmutations", "Fire Magic", "Ice Magic", "Air Magic", "Earth Magic", "Poison Magic", "Invocations", "Evocations", } local fighting_skill = { {"Fighting", true}, } local offense_skills = { -- {"Unarmed Combat", true}, {"melee", true}, } -- {"Axes", true}, } -- etc, TODO: fix this local defensive_skills = { {"Dodging", true}, {"Armour", true}, {"Shields", true}, } local utility_skills = { {"Invocations", true}, {"Throwing", true}, } -- {"Evocations", true}, } local primary_skill_table = { fighting_skill, offense_skills, defensive_skills, utility_skills, } local melee_skills = { -- "Fighting", -- "Short Blades", "Long Blades", "Axes & Hammers", "Whips & Flails", "Polearms", "Maces & Staves", -- "Stabbing", -- old ver "Unarmed Combat", } local ranged_skills = { "Slings", "Bows", "Crossbows", "Throwing", } local magic_skills = { "Conjurations", "Hexes", "Charms", "Summonings", "Translocations", "Transmutations", "Fire Magic", "Ice Magic", "Air Magic", "Earth Magic", "Poison Magic", } -- "Invocations", -- "Evocations", } local melee_range_magic = { melee_skills, ranged_skills, magic_skills, } -- skill cost table from skills.cc -- can use this to help with autoskilling cost comparisons --static const int MAX_SKILL_COST_LEVEL = 27; -- --// skill_cost_level makes skills more expensive for more experienced characters --int calc_skill_cost(int skill_cost_level) -- -- const int cost[] = { 1, 2, 3, 4, 5, // 1-5 -- 7, 8, 9, 13, 22, // 6-10 -- 37, 48, 73, 98, 125, // 11-15 -- 145, 170, 190, 212, 225, // 16-20 -- 240, 255, 260, 265, 265, // 21-25 -- 265, 265 }; -- the above indicates (5, 5, 8, 8, 10, 10) might be helpful earlygame order for scripted chars? local skilling_goals = { -- initial attempt at skilling order modelled after the postgame skilling dump of a sample MiMo I ran --{"Unarmed Combat", 7}, -- {offense, 7} -- figure out how to write this, it's late and i'm tired {"melee", 7}, {"Fighting", 7}, --{"Unarmed Combat", 10}, -- {offense, 10} {"melee", 10}, --{"Fighting", 10}, {"Fighting", 12}, {"Invocations", 8}, --{"Unarmed Combat", 14}, {"melee", 14}, {"Dodging", 4}, {"Armour", 7}, {"Shields", 4}, --{"Fighting", 12}, --{"Unarmed Combat", 15}, -- {offense, 15} --{"Fighting", 15}, {"Fighting", 17}, --{"Unarmed Combat", 17}, {"melee", 17}, --{"Shields", 7}, --{"Dodging", 15}, {"Armour", 11}, {"Shields", 10}, {"Dodging", 12}, --{"Fighting", 17}, --{"Unarmed Combat", 22}, -- {offense, 22} -- shouldn't this be earlier? {"melee", 22}, {"Fighting", 22}, {"Armour", 22}, {"Dodging", 22}, {"Shields", 20}, {"Invocations", 20}, } -- adding this at the end for now to delay onset of empty skill table user prompts in Zot -- yeah it's late and i'm tired, TODO: fixup this logic local skilling_goals_test = { {"melee", 5}, {"Fighting", 5}, {"melee", 8}, {"Fighting", 8}, {"Dodging", 5}, {"melee", 9}, {"Fighting", 9}, {"Dodging", 8}, {"melee", 10}, {"Fighting", 10}, {"Invocations", 8}, {"Shields", 4}, {"Armour", 8}, {"melee", 12}, {"Fighting", 12}, {"melee", 14}, {"Fighting", 14}, {"Dodging", 12}, {"Armour", 10}, {"melee", 16}, {"Fighting", 16}, {"Shields", 10}, {"Dodging", 16}, {"melee", 20}, {"Fighting", 20}, {"Dodging", 20}, {"Armour", 16}, {"melee", 22}, {"Fighting", 22}, {"Shields", 16}, {"Dodging", 22}, {"Armour", 22}, {"Shields", 20}, {"Invocations", 20}, } function get_best_damage_skill() local damage_skill = "Fighting" -- i think? every species can train Fighting? this logic needs rewrite if not, for _, which_type in ipairs(melee_range_magic) do -- the return here needs to be trainable for _, skill in ipairs(which_type) do if you.can_train_skill(skill) then if you.base_skill(skill) > you.base_skill(damage_skill) then damage_skill = skill end end end end return damage_skill end -- TODO: factor in the cost of training; things like claws? factor in available weapons? -- TODO: factor in cross-training !! (this needs to happen) function get_best_melee_skill() local melee_skill = "Unarmed Combat" for _, skill in ipairs(melee_skills) do if you.can_train_skill(skill) then if you.base_skill(skill) > you.base_skill(melee_skill) then melee_skill = skill end end end return melee_skill end -- TODO: Felids function get_best_ranged_skill() local ranged_skill = "Throwing" for _, skill in ipairs(ranged_skills) do if you.can_train_skill(skill) then if you.base_skill(skill) > you.base_skill(ranged_skill) then ranged_skill = skill end end end return ranged_skill end function get_best_weapon_skill() local melee = get_best_melee_skill() local ranged = get_best_ranged_skill() if you.base_skill(melee) > you.base_skill(ranged) then return melee end return ranged end --function get_best_skill_target() -- for _, sktars in ipairs(skilling_goals) do -- if you.base_skill(sktars[1]) < sktars[2] then -- if you.can_train_skill(sktars[1]) then -- return sktars[1], sktars[2] -- end -- end -- end -- -- return nil, nil --end -- skillname wrapper to ease skill fn table substitutions function skwrap(skill) if skill == "melee" then return get_best_melee_skill() elseif skill == "launcher" then return get_best_ranged_skill() else return skill end end -- v2, melee substitutions function get_best_skill_target() -- for _, sktars in ipairs(skilling_goals) do for _, sktars in ipairs(skilling_goals_test) do if you.base_skill(skwrap(sktars[1])) < sktars[2] then if you.can_train_skill(skwrap(sktars[1])) then return skwrap(sktars[1]), sktars[2] end end end return nil, nil end function clear_skill_training() -- note: when all skill training is disabled, crawl automatically opens the skill window, for _,skill in ipairs(skill_list) do -- asking the player for input if you.can_train_skill(skill) then you.train_skill(skill, 0) -- this can result in the skill window opening superfluously, end -- don't call this fn unless you really want to end end -- autoskill function -- compares current skills against a target skill table, and assigns training as needed -- also assigns secondary skills of opportunity if doing so is particularly inexpensive -- -- returns true if successful, false if it can't find a primary skill target -- -- TODO: setup a can_train table at the beginning of this function, instead of -- calling you.can_train() 150 times each time autoskill is called. function autoskill(zero) if zero then clear_skill_training() end local sk = nil local tar = nil local success = nil sk, tar = get_best_skill_target() -- primary autoskill target, e.g. "get 7 weaponskill" -- early out if our target skilling table has been exhausted, as at endgame if sk == nil then return false end -- in this case, crawl will prompt the user if sk and tar then success = you.set_training_target(sk, tar) end if success then crawl.mpr("Changed primary autoskill target: " .. sk .. "(" .. tar .. ")") end local subsk = nil local subsuc = nil -- wherever we intend to call you.skill_cost(sk) , we MUST first ensure sk is currently trainable: -- you.skill_cost() will return nil if scaled_skill_cost() in skills.cc returns 0, this will occur if -- the skill is already at 27 or if it is tagged 'useless', e.g. : species restriction, Ru sacrifice, some mutations local can_train = nil for _,subtable in ipairs(primary_skill_table) do -- secondary autoskill targets of opportunity, like species apts and manuals for _,skill in ipairs(subtable) do -- e.g. "is it very inexpensive to skill Dodging right now?" -- at present, subsk = skwrap(skill[1]) -- this only trains skills on our secondary subtables, mostly defensive subsuc = nil can_train = you.can_train_skill(subsk) if can_train then -- if you.skill_cost(subsk) * 4 < you.skill_cost(you.best_skill()) then if you.skill_cost(subsk) * 4 < you.skill_cost(sk) then -- compare this against the currently-training skill, not the highest skill subsuc = you.set_training_target(subsk, you.base_skill(subsk) + 1) if subsuc then crawl.mpr("Added secondary autoskill target: " .. subsk .. "(" .. subsuc .. ")") end end end end -- TODO: add logic here to handle magic schools / stealth / conditional Shields / conditional launchers / etc end -- here we activate training for the last skill in skill_list, before running the loops that update skill training status -- this ensures that skill training will not be disabled for all skills unless we have run out of targets; -- this is necessary to prevent extraneous skill window popups during play with our autoskill() function enabled -- -- this should run through the skill table, switching each skill on and off in turn, leaving the last skill enabled local prev = nil can_train = nil for _,skill in ipairs(skill_list) do can_train = you.can_train_skill(skill) if can_train then --if you.can_train_skill(skill) then you.train_skill(skill, 2) if prev then you.train_skill(prev, 0) end prev = skill end end can_train = nil for _,skill in ipairs(skill_list) do can_train = you.can_train_skill(skill) if can_train then --if you.can_train_skill(skill) then local base = you.base_skill(skill) local target = you.get_training_target(skill) you.train_skill(skill, 2) if base >= target then you.train_skill(skill, 0) -- TODO: verify if this is going to cause problems on endgame characters with no remaining scripted targets end -- i'm setting skills in this order because of previous problems with unwanted skill window popups, but.. end -- TODO: this logic needs a path for when it runs out of targets, i think end -- for _,skill in ipairs(skill_list) do -- old logic, single pass, resulted in empty skill window popups during play -- if you.can_train_skill(skill) then -- you.train_skill(skill, 0) -- end -- if you.base_skill(skill) < you.get_training_target(skill) then -- you.train_skill(skill, 2) -- end -- end return true end -- the crawl Lua api reference sure could use any kind of information on shop iterator structure -- the function def in l-item.cc was about as clear as mud -- -- If, dear traveller, you should ever chance upon a comment like: -- -- @treturn array|nil An array of @{Item} objects or nil if not on a shop -- -- Please understand its truth to be of the sort the fae folk might tell. -- -- items.shop_inventory() does not return a simple array of @{Item} objects, as one might naively hope, -- it appears to return a table where the key is an off-by-one int representing the item's slot index, -- and the values are [{Item} object, int price, bool is_on_shopping_list]. -- -- at no point does lua_push_shop_items_at() call lua_pushnil , it seems to assume that a c return 0; is good enough -- (maybe it is? i need a stronger drink.) -- autoshop() function -- adds shop items to the shopping list if they are desired by autopick -- returns true if our shopping list has been updated, returns false if no update or if the local kfeat is not a shop function autoshop() local did_we_update_the_shopping_list = false if (view.feature_at(0,0) == "enter_shop") then for off_by_index, v in ipairs(items.shop_inventory()) do local it = v[1] local it_price = v[2] local it_is_on_list = v[3] local itname = it.name() local it_letter = items.index_to_letter(off_by_index-1) -- thank you, dark souls -- TODO: Fix this up, it currently results in wrong behavior for duplicate item names. -- When adding an item to the shopping list, Crawl's shop window adds ALL such items (matching by subtype? name? other?) -- to your list if they are present in the shop. This means that as this loop cycles through, -- it currently *removes* the item from the list on the second pass, adds it again at the third, etc. -- -- you would think that it_is_on_list should handle this, but that bool appears to be set for each item in the array -- when calling shop_inventory(), and does not update down here in my iterator. -- (This might be because items.shop_inventory() doesn't pass references, instead passing duplicate temp items / values?) -- I need to fixup this function to ensure proper shopping list behavior with several of the same item. if not it_is_on_list and check_desired_item_types(it, itname) then crawl.mpr("Desired shop item: " .. it.name()) crawl.sendkeys('>' .. string.upper(it_letter) .. string.char(27)) did_we_update_the_shopping_list = true end end if did_we_update_the_shopping_list then crawl.mpr("Auto-Shop!") end --crawl.sendkeys(string.char(27)) end return did_we_update_the_shopping_list end -- if we've found a new interesting kfeat, try to insert it into the found kfeat table, -- hopefully ignoring duplicates -- returns true if we've inserted a new entry into the table, -- returns false if we have not updated the table because of a duplicate entry function update_found_kfeats_table(kfeat, where) --local where = you.where() local new_entry = { kfeat , where } for _,entry in ipairs(c_persist.FOUND_KFEATS_TABLE) do if entry[1] == new_entry[1] and entry[2] == new_entry[2] then return false end end table.insert(c_persist.FOUND_KFEATS_TABLE, new_entry) return true end -- check the kfeats table for a matching { kfeat, you.where() } entry -- returns true if match, false if no match function check_found_kfeats_table(kfeat, where) for _,entry in ipairs(c_persist.FOUND_KFEATS_TABLE) do if entry[1] == kfeat and entry[2] == where then return true end end return false end function echo_kfeats_table() -- debug function, print found kfeats to mpr if c_persist.FOUND_KFEATS_TABLE ~= nil then for _,kfeat in ipairs(c_persist.FOUND_KFEATS_TABLE) do crawl.mpr("Found kfeat: " .. kfeat[1] .. ", at location : " .. kfeat[2]) end else crawl.mpr("kfeat table == nil, maybe something went wrong?") end end ---------------------------------------------------- ----- wizmode realtime action tracking ---------------------------------------------------- -- similar to getkey() from /dat/clua/automagic.lua, -- but we don't bother with the string.char conversion or limit to alphanumeric keycodes function get_input_keycode() local key while true do key = crawl.getch() return key end end -- numeric keycode mappings pulled from /source/webserver/static/scripts/key_conversion.js -- apparently that file was mapped from cio.h somehow? -- CK_WHATEVER to CMD_WHATEVER mappings pulled from /source/cmd-keys.h -- "CMD_WHATEVER" name strings pulled from /source/cmd-name.h -- crawl keycode / scancode handling is ugly as heck local crawl_getch_keymap_table = { -- val = -255 {CK_DELETE, -255, nil}, {CK_UP, -254, "CMD_MOVE_UP"}, {CK_DOWN, -253, "CMD_MOVE_DOWN"}, {CK_LEFT, -252, "CMD_MOVE_LEFT"}, {CK_RIGHT, -251, "CMD_MOVE_RIGHT"}, {CK_INSERT, -250, "CMD_REPEAT_CMD"}, {CK_HOME, -249, "CMD_MOVE_UP_LEFT"}, {CK_END, -248, "CMD_MOVE_DOWN_LEFT"}, {CK_CLEAR, -247, "CMD_WAIT"}, {CK_PGUP, -246, "CMD_MOVE_UP_RIGHT"}, {CK_PGDN, -245, "CMD_MOVE_DOWN_RIGHT"}, {CK_TAB_TILE, -244, nil}, -- // unused {CK_SHIFT_UP, -243, "CMD_RUN_UP"}, {CK_SHIFT_DOWN, -242, "CMD_RUN_DOWN"}, {CK_SHIFT_LEFT, -241, "CMD_RUN_LEFT"}, {CK_SHIFT_RIGHT, -240, "CMD_RUN_RIGHT"}, {CK_SHIFT_INSERT, -239, nil}, {CK_SHIFT_HOME, -238, "CMD_RUN_UP_LEFT"}, {CK_SHIFT_END, -237, "CMD_RUN_DOWN_LEFT"}, {CK_SHIFT_CLEAR, -236, "CMD_REST"}, {CK_SHIFT_PGUP, -235, "CMD_RUN_UP_RIGHT"}, {CK_SHIFT_PGDN, -234, "CMD_RUN_DOWN_RIGHT"}, {CK_SHIFT_TAB, -233, "CMD_AUTOFIGHT_NOMOVE"}, {CK_CTRL_UP, -232, "CMD_ATTACK_UP"}, {CK_CTRL_DOWN, -231, "CMD_ATTACK_DOWN"}, {CK_CTRL_LEFT, -230, "CMD_ATTACK_LEFT"}, {CK_CTRL_RIGHT, -229, "CMD_ATTACK_RIGHT"}, {CK_CTRL_INSERT, -228, nil}, {CK_CTRL_HOME, -227, "CMD_ATTACK_UP_LEFT"}, {CK_CTRL_END, -226, "CMD_ATTACK_DOWN_LEFT"}, {CK_CTRL_CLEAR, -225, "CMD_REST"}, {CK_CTRL_PGUP, -224, "CMD_ATTACK_UP_RIGHT"}, {CK_CTRL_PGDN, -223, "CMD_ATTACK_DOWN_RIGHT"}, {CK_CTRL_TAB, -222, nil}, -- // Mouse codes. -- val = -10009 -- in key_conversion.js this is listed as -10009, but in cio.h this is listed as -9999 -- (possible error in key_conversion.js?) (is this val defined differently, elsewhere, for webtiles builds?) {CK_MOUSE_MOVE, -10009, "CMD_MOUSE_MOVE"}, {CK_MOUSE_CMD, -10008, nil}, {CK_MOUSE_B1, -10007, nil}, {CK_MOUSE_B2, -10006, nil}, {CK_MOUSE_B3, -10005, nil}, {CK_MOUSE_B4, -10004, "CMD_MAP_SCROLL_UP"}, {CK_MOUSE_B5, -10003, "CMD_MAP_SCROLL_DOWN"}, {CK_MOUSE_CLICK, -10002, "CMD_MOUSE_CLICK"}, -- i'm just going to duplicate this section with cio.h keycode values {CK_MOUSE_MOVE, -9999, "CMD_MOUSE_MOVE"}, {CK_MOUSE_CMD, -9998, nil}, {CK_MOUSE_B1, -9997, nil}, {CK_MOUSE_B2, -9996, nil}, {CK_MOUSE_B3, -9995, nil}, {CK_MOUSE_B4, -9994, "CMD_MAP_SCROLL_UP"}, {CK_MOUSE_B5, -9993, "CMD_MAP_SCROLL_DOWN"}, {CK_MOUSE_CLICK, -9992, "CMD_MOUSE_CLICK"}, -- // Function keys -- As far as I can tell, function keys do not have CK_WHATEVER definitions in crawlcode. -- I'm just calling them F1 through F10 here. {F1, -1011, nil}, -- // F1 {F2, -1012, nil}, {F3, -1013, nil}, {F4, -1014, nil}, {F5, -1015, nil}, {F6, -1016, nil}, {F7, -1017, nil}, {F8, -1018, nil}, {F9, -1019, nil}, {F10, -1020, nil}, --{F11, -1021, 122}, -- // Don't occupy F11, it's used for fullscreen --{F12, -1022, 123}, } -- // used for chat} -- SDL produces different keycodes for local tiles builds: Included here are the function keys. -- As far as I can tell, these keycodes do not have CK_WHATEVER crawlcode definitions. -- I'm using SDLK_F1 through SDLK_F10 here, as their SDL_Keycode names, found at: https://wiki.libsdl.org/SDLKeycodeLookup {SDLK_F1, -1073741882, nil}, -- // F1 {SDLK_F2, -1073741883, nil}, {SDLK_F3, -1073741884, nil}, {SDLK_F4, -1073741885, nil}, {SDLK_F5, -1073741886, nil}, {SDLK_F6, -1073741887, nil}, {SDLK_F7, -1073741888, nil}, {SDLK_F8, -1073741889, nil}, {SDLK_F9, -1073741890, nil}, {SDLK_F10, -1073741891, nil}, } --{SDLK_F11, -1073741892, 122}, -- // Don't occupy F11, it's used for fullscreen --{SDLK_F12, -1073741893, 123}, } -- TODO: use crawl.get_command(which_code[3]) to allow crawl to map its internal CK_KEY for most of these? function get_mapped_keychar(keycode) if keycode >= 0 then local c = string.char(keycode) return c else for _,which_code in ipairs(crawl_getch_keymap_table) do if which_code[2] == keycode then local c = which_code[3] return c end end end return nil end local keybind_macros_table = { {9, "maybe_auto"}, } -- {keycode, function_name, function_args} local keybind_macros_table_redo = { {9, maybe_auto, nil}, {121, maybe_repeat_swing, "CMD_MOVE_UP_LEFT"}, -- y {117, maybe_repeat_swing, "CMD_MOVE_UP_RIGHT"}, -- u {98, maybe_repeat_swing, "CMD_MOVE_DOWN_LEFT"}, -- b {110, maybe_repeat_swing, "CMD_MOVE_DOWN_RIGHT"}, -- n {-254, maybe_repeat_swing, "CMD_MOVE_UP"}, -- up arrow {-253, maybe_repeat_swing, "CMD_MOVE_DOWN"}, -- down arrow {-252, maybe_repeat_swing, "CMD_MOVE_LEFT"}, -- left arrow {-251, maybe_repeat_swing, "CMD_MOVE_RIGHT"}, -- right arrow {-1073741888, set_local_waypoint, 1}, -- F7 {-1073741889, vector_to_waypoint, 1}, } -- F8 --{CK_UP, -254, "CMD_MOVE_UP"}, --{CK_DOWN, -253, "CMD_MOVE_DOWN"}, --{CK_LEFT, -252, "CMD_MOVE_LEFT"}, --{CK_RIGHT, -251, "CMD_MOVE_RIGHT"}, local REALTIME_MARKER = nil local PREVIOUS_REALTIME_MARKER = nil local REALTIME_DELTA = nil local IDLE_TIME_CLAMP = 30000 -- at present, this only works in wizmode function get_realtime() if you.wizard() then return crawl.call_dlua("return crawl.millis()") end end function init_realtime() PREVIOUS_REALTIME_MARKER = get_realtime() REALTIME_MARKER = PREVIOUS_REALTIME_MARKER end function update_realtime() PREVIOUS_REALTIME_MARKER = REALTIME_MARKER REALTIME_MARKER = get_realtime() REALTIME_DELTA = REALTIME_MARKER - PREVIOUS_REALTIME_MARKER -- mimic crawl's per-turn realtime clamp value (IDLE_TIME_CLAMP) if REALTIME_DELTA > IDLE_TIME_CLAMP then REALTIME_DELTA = IDLE_TIME_CLAMP end end function get_rt_marker() return REALTIME_MARKER end function get_prev_rt_marker() return PREVIOUS_REALTIME_MARKER end function get_rt_delta() return REALTIME_DELTA end -- ["key"] = crawl keycode, -- ["prev_key"] = previous action crawl keycode, -- ["chara"] = string.char(key) return OR CMD_WHATEVER equivalent, as returned from get_mapped_keychar(key) -- ["prev_chara"] = string.char(prev_key) return OR CMD_WHATEVER equivalent, as returned from get_mapped_keychar(prev_key) -- ["real_ms"] = delta of crawl.millis() since previous action -- (realtime in ms, that was taken by the player to generate this turn's input) -- Crawl clamps this value at 30000 ms, so we do too. -- ["count"] = Amount of times the player has input this key combo this game. local action_realtime_entry = {["key"] = nil, ["prev_key"] = nil, ["chara"] = nil, ["prev_chara"] = nil, ["real_ms"] = nil, ["count"] = nil} local prev_action_realtime_entry = {["key"] = nil, ["prev_key"] = nil, ["chara"] = nil, ["prev_chara"] = nil, ["real_ms"] = nil, ["count"] = nil} function sort_rt_ms(a, b) return a["real_ms"] > b["real_ms"] end function wrap_player_input() crawl.flush_input() crawl.redraw_screen() local key = nil local chara = nil local did_macro = false key = get_input_keycode() crawl.mpr("get_input_keycode() == " .. tostring(key)) chara = get_mapped_keychar(key) for _,which_macro in ipairs(keybind_macros_table_redo) do if key == which_macro[1] then did_macro = true -- i can't use crawl.runmacro here if i want to pass args to the macro function -- TODO: call the keycode's function table entry directly here -- crawl.runmacro(which_macro[2]) if which_macro[3] == nil then which_macro[2]() else which_macro[2](which_macro[3]) end end end if chara ~= nil and not did_macro then if key >=0 then crawl.sendkeys(chara) else -- TODO: use crawl.get_command(which_code[3]) to allow crawl to map its internal CK_KEY here? crawl.do_commands({chara}) -- this ends up mangling something when used with CTRL + direction for swings, -- it's probably fixable but do_commands works okay, so... -- crawl.sendkeys(crawl.get_command(chara)) -- hmm, will this work? end end -- this is going to be super spammy, but i'm temporarily adding it for testing feedback -- crawl.mpr("Last input key #: " .. key .. -- " , Last input chara equivalent: " .. chara) crawl.flush_input() crawl.redraw_screen() end -- This function logs the player's total realtime spent per unique action input, saves it to a c_persist table, -- and sorts that table by total realtime. -- -- The intent is to help identify where the player spends the most realtime on input decisions, -- so that the script can be further improved. -- -- At present this only works in wizmode, because it requires the functionality of crawl.millis() (which is dlua only). -- -- Unique table entries follow the above action_realtime_entry format. -- Duplicate entries increment "real_ms" and "count", instead of generating new entries. function wizmode_log_action_ms() crawl.flush_input() crawl.redraw_screen() local key = nil local chara = nil local did_macro = false -- get_input_keycode() invokes a while(true) until player input happens ( through crawl.getch() ) -- ((will this cause issues with scripted CPU usage?)) -- TODO: figure this out? key = get_input_keycode() update_realtime() chara = get_mapped_keychar(key) for _,which_macro in ipairs(keybind_macros_table) do if key == which_macro[1] then did_macro = true crawl.runmacro(which_macro[2]) end end if chara ~= nil and not did_macro then if key >=0 then crawl.sendkeys(chara) else -- As far as I can tell, Crawl doesn't have any kind of good lua keybind handling for those keys for which it internally -- produces negative keycodes: I'm resorting to manually mapping keycodes to command names here, and piping it through -- do_commands instead. This is not ideal and it's likely to break things in non-default CMD_CONTEXT maps. -- Only a few of these keys are commonly used in Crawl (arrow keys, maybe pgup/pgdn), so mapping each context -- manually *might* be feasible, but it would obviously be better to have Crawl handle its own mapping. -- Trouble is, I just don't see how to do it. crawl.do_commands({chara}) end end -- action_realtime_entry = {c_persist.LAST_ACTION_MARKER, tostring(key), chara, get_rt_delta()} -- naive per-turn realtime table is not useful without further parsing -- table.insert(c_persist.PLAYER_ACTION_REALTIME_TABLE, action_realtime_entry) prev_action_realtime_entry = action_realtime_entry action_realtime_entry = {["key"] = key, ["prev_key"] = prev_action_realtime_entry["key"], ["chara"] = chara, ["prev_chara"] = prev_action_realtime_entry["chara"], ["real_ms"] = get_rt_delta(), ["count"] = 1} local match = false for _,which_entry in ipairs(c_persist.PLAYER_ACTION_REALTIME_TABLE) do if action_realtime_entry["key"] == which_entry["key"] and action_realtime_entry["prev_key"] == which_entry["prev_key"] then match = true which_entry["real_ms"] = which_entry["real_ms"] + action_realtime_entry["real_ms"] which_entry["count"] = which_entry["count"] + 1 end end if match == false then table.insert(c_persist.PLAYER_ACTION_REALTIME_TABLE, action_realtime_entry) end table.sort(c_persist.PLAYER_ACTION_REALTIME_TABLE, sort_rt_ms) -- this is going to be super spammy, but i'm temporarily adding it for testing feedback crawl.mpr("Last input key #: " .. action_realtime_entry["key"] .. " , Last input chara equivalent: " .. action_realtime_entry["chara"] .. " , Last input delta (ms): " .. tostring(action_realtime_entry["real_ms"])) crawl.flush_input() crawl.redraw_screen() end ---------------------------------------------------- ----- crawl Lua hooks and hook overrides below ----- ---------------------------------------------------- clear_autopickup_funcs() add_autopickup_func(check_desired_item_types) --table.insert(chk_lua_save, id_recognized_save(ID_SCROLL_RECOGNIZED)) if c_persist.LAST_ACTION_MARKER == nil then c_persist.LAST_ACTION_MARKER = 0 end if c_persist.ID_SCROLLS_RECOGNIZED == nil then c_persist.ID_SCROLLS_RECOGNIZED = false end if c_persist.FOUND_KFEATS_TABLE == nil then c_persist.FOUND_KFEATS_TABLE = { } end if c_persist.PLAYER_ACTION_REALTIME_TABLE == nil then c_persist.PLAYER_ACTION_REALTIME_TABLE = { } end -- if c_persist.CLEARED_WHATEVER flags are nil, initialize them initialize_strategic_goal_flags() function choose_stat_gain() return "s" end -- skill_training_needed() MUST return true, else its calling fn in skills.cc will always open a user prompt function skill_training_needed() return autoskill() -- we are now handling the return from within autoskill() end -- if we return true and have failed to set any training, check_selected_skills() will prompt the user anyway function auto_experience() -- hook to auto-distribute potions of experience return autoskill() end function c_answer_prompt(prompt) if prompt:find("Really abort") then -- for convenience, try to ensure the player doesn't quit from a read-id scroll prompt return false -- TODO: test this to make sure it's working properly end if prompt:find("Really quaff the potion of lignification") then -- it's a good potion, this prompt is dumb, never show it return true end if prompt:find("Really attack barehanded") then -- today is a special sale day, i don't have time for this prompt return true end -- try to prevent the game interrupting auto-shop with suggested shoplist alterations. -- returning false here in case we later track shop items; it shouldn't hurt to have extra list items in the meantime if prompt:find("Shopping list: replace") then return false end if prompt:find("Shopping list: remove") then return false end -- if any of our autoequip logic is bugged, this could cause problems, -- but it *should* only be doing this if the cursed piece is actually better than what's currently worn, -- so we'll allow it for now. if prompt:find("Really wear") and prompt:find("cursed") then return true end -- if we're in wizmode testing the script, let characters die normally if you.wizard() and prompt:find("Die?") then return true end end -- This TravelDelay interrupt allows us to bypass the hardcoded EXPLORE_GREEDY shop entry behavior, -- so that we can add shop items to our shopping list before the player sees the shop window contents. chk_interrupt_activity["travel"] = function (iname, cause, extra) if view.feature_at(0,0) == "enter_shop" then crawl.mpr("chk_interrupt_activity found a shop") return true end -- during the orbrun, we also interrupt inter-level travel on upstairs at low HP, so the script can try to rest safely if c_persist.HAVE_ORB then if view.feature_at(0,0):find("up") or view.feature_at(0,0):find("exit") then if orbrun_rest_check() then return true end end end end -- autoshop, as a post-runrest hook, to be run automatically after "travel" delays like CMD_EXPLORE -- For this to work properly, it needs a counterpart custom TravelDelay interrupt, because EXPLORE_GREEDY natively sends -- a *hardcoded* CMD_GO_UPSTAIRS at shops. (EXPLORE_GREEDY travel does not execute post-runrest hooks at shops.) -- By cancelling our TravelDelay early with our own interrupt, we preempt this problem and runrest hooks execute as expected. -- See: travel.cc , line 891, command_type travel() function ch_stop_running(runrest_type) -- if runrest_type == "explore_greedy" then if (view.feature_at(0,0) == "enter_shop") then if autoshop() then -- only open the shop window if something was added to our shopping list -- crawl.mpr("ch_stop_running found a shop") kickback("Found a Shop!") crawl.sendkeys(">") crawl.flush_input() end -- this shouldn't take any turns (?) -- TODO: verify this end -- end return true end function ready() -- TODO: Try again to get chk_lua_save working. if you.turns() == 0 then -- c_persist *persists*, across games. Resetting it at T:0 like this won't c_persist.ID_SCROLLS_RECOGNIZED = false -- work properly, if you save/load between games already in progress. c_persist.LAST_ACTION_MARKER = 0 -- I'm only doing it this way because I couldn't get chk_lua_save working. c_persist.FOUND_KFEATS_TABLE = { } autoskill(true) reset_strategic_goal_flags() set_current_goal("goal_god") c_persist.PLAYER_ACTION_REALTIME_TABLE = { } if you.wizard() then init_realtime() end end -- if we've saved and reloaded, re-initialize the realtime markers if you.wizard() then if get_rt_marker() == nil or get_prev_rt_marker() == nil then init_realtime() end end if auto then check_kickbacks() end c_persist.LAST_ACTION_MARKER = (1 + c_persist.LAST_ACTION_MARKER) crawl.mpr("ACTIONMARKER" .. tostring(c_persist.LAST_ACTION_MARKER)) -- ^^ this has to be visible for the messages() find to work... doesn't look like I can use crawl.dpr ... -- if not auto and view.feature_at(0,0) == "enter_shop" then -- autoshop() -- handle automatic shopping list during manual control -- end -- can't do this here without a check to only do it once per game turn, it loops the script if autoshop errors if auto == false and auto_swing() == false then if you.wizard() then wizmode_log_action_ms() else wrap_player_input() end end if auto_swing() then repeat_swing() elseif auto then if ONLINE_PLAY then crawl.delay(ONLINE_DELAY_MS) end crawl.flush_input() crawl.more_autoclear(true) crawl.redraw_screen() if c_persist.HAVE_ORB then fn_goal_orbrun() elseif monster_in_view() then fight() elseif hungry() then try_to_eat() elseif hp_mp_not_full() or you.transform() ~= "" then -- slot meld status seems to not be exposed through Lua: we need to rest rest() -- if transformed, or we risk breaking autoequip further down else local id_scroll = nil local id_thing = nil id_scroll, id_thing = can_we_scroll_id_something() -- this returns nil, nil if we don't have both -- i'm wary of putting the bad_equip check so late in the script. -- *tele gear could cause problems while trying to rest. -- however, if we've been transformed, we need to rest it off before trying to drop equipment, -- due to the above slot-meld issues. local bad_equip = nil bad_equip = check_bad_equips_by_value() local bad_wield = nil bad_wield = check_bad_wields() local junk = nil junk = check_for_junk_to_drop() local rc_scroll = nil rc_scroll = check_for_known_scroll("remove curse") local acq_scroll = nil acq_scroll = check_for_known_scroll("acquirement") local exp_potion = nil exp_potion = check_for_known_potion("experience") if bad_equip and is_slot_cursed(bad_equip) and rc_scroll then read(rc_scroll) elseif bad_wield and is_slot_cursed(bad_wield) and rc_scroll then read(rc_scroll) elseif bad_equip and not is_slot_cursed(bad_equip) then drop(bad_equip) elseif bad_wield and not is_slot_cursed(bad_wield) then unwield(bad_wield) elseif acq_scroll then acquire(acq_scroll) elseif exp_potion then quaff(exp_potion) elseif id_thing then scroll_identify(id_scroll, id_thing) elseif junk then drop(junk) else -- TODO: I need to add an "are we currently shafted" check here so that I can change this behavior -- to seek upstairs if in local LOS, when appropriate -- Setting the "shafted" variable is easy enough: I can either use the "shafted" kickback message trigger, -- or I can check you.depth() at every given action marker, and if it has changed without us trying to go downstairs, -- then we've been shafted explore() -- wear-id and read-id are handled from within explore(), they are just before "G>" for safety end end -- coroutine.yield(true) if you.wizard() then update_realtime() -- prevent the script from accruing REALTIME_DELTA during auto turns end end end } # i'm glad it's ogre