# This is designed w/intent to play with a userland script in browser for custom message positioning, sizing and opacity to make it more like an RPG of old and recapture screen real estate. # This has some highly opinionated implementations of LUA scripting for for spell casting options... Which might {will} really annoy vanilla players. # This is hardcoded for UltraWide users. # # WHAT THIS DOES # # 1. Spells and management # 1. Spells are grouped and cast by 1-5 # 2. Adjust groupings globally via 0 # 3. Pin one spell to one slot via ctrl+1-5 # 4. 6 lists every spell # 5. 7 picks and casts the best spell for the situation # 6. Casts autotarget the biggest threat; 8 turns that off # 7. 9 toggles the "too injured to fight recklessly" guard # 8. = wipes the learned state, as if a new character # # 2. Message changes # 1. Suppresses flavour and other junk messages # 2. Highlights and halts on key threats # 3. Highlights and halts on Ashenzari offering a curse # 4. Highlights and halts on artefacts in view, with their properties # 5. Grades threat, estimates spell and melee damage, and says how # many turns the fight and the room will take # 6. Shows enemy gear, resistances and hp, grouping duplicates # 7. Names who hit you and the worst hit each has landed # 8. ctrl+6 shows the working behind the threat score # # 3. Map and pickup changes # 1. Colours monsters by what they do to you, not what they are # 2. Colours artefacts and items of interest # 3. Picks up every randart # 4. Picks up the first of each useful item, and skips heavy armour # unless it is an artefact # # Designed to be used with a userland script that overlays this # information over the map. Without it, everything still works, it just # renders in the ordinary message pane. # Two things that will bite you if you edit this: # 1. A line that TRIMS to } ends the lua block. Indentation does not # save you. That is why table closers here carry trailing text. # 2. Inline # comments are not comments. Only a leading # is. # # tools/ has the linters that catch both, plus the test suites. small_more = true show_more = false clear_messages = false messages_at_top = true easy_confirm = all warn_hatches = true fail_severity_to_confirm = 4 travel_avoid_terrain = deep water explore_auto_rest = true use_animations -= player show_paged_inventory = true easy_unequip = true launcher_autoquiver = false action_panel_show = true always_show_gems = true more_gem_info = true warn_contam_cost = true runrest_ignore_monster += ^butterfly:1 force_spell_targeter += Arcjolt, Conjure Ball Lightning, Death Channel force_spell_targeter += Death's Door, Detonation Catalyst, Dragon's Call force_spell_targeter += Eringya's Noxious Bog, Fireball, Fire Storm force_spell_targeter += Freezing Cloud, Hoarfrost Cannonade, Ignite Poison force_spell_targeter += Irradiate, Iskenderun's Mystic Blast force_spell_targeter += Leda's Liquefaction, Malign Gateway, Manifold Assault force_spell_targeter += Olgreb's Toxic Radiance, Ozocubu's Refrigeration force_spell_targeter += Permafrost Eruption, Plasma Beam, Polar Vortex force_spell_targeter += Scorch, Silence, Starburst, Static Discharge force_spell_targeter += Summon Forest, Blink force_scroll_targeter += immolation, noise, silence, vulnerability force_scroll_targeter += butterflies always_show_zot = true always_show_doom_contam = true cloud_status = true default_show_all_skills = true auto_hide_spells = false rest_wait_percent = 90 autofight_fires = false autofight_nomove_fires = false explore_greedy_visit += glowing_items, artefacts explore_greedy_visit -= stacks { MSG_LINES = 10 if crawl.is_webtiles() then crawl.setopt("msg_min_height = 7") crawl.setopt("msg_webtiles_height = " .. MSG_LINES) else crawl.setopt("msg_min_height = 10") end } explore_wall_bias = 5 travel_delay = -1 explore_delay = 25 rest_delay = -1 travel_open_doors = open rest_wait_both = true show_travel_trail = false explore_stop = stairs,shops,altars,portals,branches,runed_doors explore_stop += greedy_visited_item_stack explore_stop += artefacts, glowing_items, runes explore_stop += greedy_items explore_item_greed = -5 auto_exclude += oklob plant, statue, curse skull, curse toe, Roxanne auto_exclude += moth of wrath, lurking horror, guardian mummy mons := mon_glyph mons += efreet : lightred mons += fire dragon : lightred mons += fire giant : lightred mons += molten gargoyle : lightred mons += salamander : lightred mons += draconian scorcher : lightred mons += hellion : lightred mons += balrug : lightred mons += orb of fire : lightred mons += ice dragon : lightblue mons += frost giant : lightblue mons += ice devil : lightblue mons += blizzard demon : lightblue mons += ice beast : lightblue mons += ironbound frostheart : lightblue mons += azure jelly : lightblue mons += storm dragon : yellow mons += electric eel : yellow mons += spark wasp : yellow mons += raiju : yellow mons += electric golem : yellow mons += ironbound thunderhulk : yellow mons += titan : yellow mons += swamp dragon : green mons += swamp drake : green mons += cane toad : green mons += redback : green mons += jumping spider : green mons += wolf spider : green mons += emperor scorpion : green mons += black mamba : green mons += nagaraja : green mons += shadow dragon : darkgrey mons += wight : darkgrey mons += phantasmal warrior : darkgrey mons += ghost moth : darkgrey mons += profane servitor : darkgrey mons += deep elf death mage : darkgrey mons += deep elf demonologist : brown mons += necromancer : brown mons += ironbound convoker : brown mons += guardian serpent : brown mons += shadow demon : brown mons += spriggan druid : brown mons += floating eye : lightmagenta mons += great orb of eyes : lightmagenta mons += shining eye : lightmagenta mons += wretched star : lightmagenta mons += cacodemon : lightmagenta mons += neqoxec : lightmagenta mons += orc sorcerer : lightmagenta mons += ogre mage : lightmagenta mons += deep elf sorcerer : lightmagenta mons += sphinx : lightmagenta mons += merfolk avatar : lightmagenta mons += merfolk siren : lightmagenta mons += tarantella : lightmagenta mons += torpor snail : lightmagenta mons += catoblepas : lightmagenta mons += basilisk : lightmagenta mons += vault sentinel : lightmagenta mons += entropy weaver : lightmagenta mons += moth of wrath : lightmagenta mons += starcursed mass : lightmagenta mons ^= dancing weapon : cyan mons ^= spectral weapon : lightcyan unusual_monster_items = wand unusual_monster_items += disto, chaos, antimagic, pain, drain, vamp unusual_monster_items += elec, freez, flame, venom, holy, spect, reap unusual_monster_items += curare, atropa, datura, dispersal unusual_monster_items += throwing net, silver, javelin unusual_monster_items += protection, holy wrath unusual_highlight = hi:white stab_highlight = hi:blue may_stab_highlight = hi:brown tile_show_threat_levels = tough, nasty, unusual monster_list_colour = monster_list_colour += friendly:green, neutral:brown, good_neutral:brown monster_list_colour += trivial:darkgrey, easy:lightgrey monster_list_colour += tough:yellow, nasty:lightred monster_list_colour += unusual:white more := force_more_message stop := runrest_stop_message ignore := runrest_ignore_message flash := flash_screen_message stop += (paralys|petrif|turn(ing|ed) to stone) stop += You are (confused|vexed|marked|mesmeris) stop += You feel (sluggish|weakened|frail|less protected) stop += (You are|A profound) silence stop += You feel yourself slow down stop += (caught in|entangled in|held in place|constricts you) stop += Your surroundings flicker stop += You cannot teleport right now stop += A sentinel's mark forms upon you stop += You feel firmly anchored in space stop += Space (warps|bends|twists)[^.!]* around you more += banishment:.* stop += banishment:.* more += Ouch! That really hurt! flash += Ouch! That really hurt! more += Your body is wracked with pain flash += Your body is wracked with pain more += LOW HITPOINT WARNING flash += LOW HITPOINT WARNING stop += You are blasted by holy energy! stop += You are (blasted|electrocuted)! stop += dispelling energy hits you stop += calcifying dust hits stop += (blundered into a|invokes the power of) Zot stop += enter a teleport trap more += lose consciousness more += You are too injured to fight blindly more += hell_effect: stop += The mighty Pandemonium lord .* resides here more += HP DROP flash += HP DROP stop += HP DROP stop += You don't .* that spell stop += You fail to use your ability stop += You miscast.*(Blink|Borgnjor|Door|Invisibility) stop += You can't (read|drink|do) stop += You cannot .* (while unable to breathe|in your current state) stop += when .*silenced stop += too confused stop += You are too berserk stop += no means to grasp stop += That item cannot be evoked stop += You are held in a net stop += You can't unwield stop += enough magic points stop += You are unable to access your magic stop += You have reached level stop += You rejoin the land of the living stop += You pass through the gate : if you.god() == "Ashenzari" then stop += You have a vision of.*gates? : end : if you.god() == "Xom" then stop += god: : end stop += You (partially )?resist stop += You shrug off stop += Your willpower stop += ((glass|shining) eye|dream sheep|death drake).*into view stop += (wretched star|apocalypse crab|starcursed mass).*into view stop += (entropy weaver|torpor snail|spriggan druid).*into view stop += (vault (warden|sentinel)|merfolk (avatar|siren)).*into view stop += (guardian serpent|draconian shifter|convoker).*into view stop += (phantasmal warrior|floating eye|catoblepas).*into view stop += (orc sorcerer|wizard|ogre mage|great orb of eyes|sphinx).*into view stop += (vampire knight|basilisk|deep elf (sorcerer|demonologist)).*into view stop += (flayed ghost|royal mummy|mummy priest|fiend|tzitzimitl).*into view stop += (tormentor|curse toe|curse skull|hell sentinel|hellion).*into view stop += (deep elf (sorcerer|high priest)|draconian scorcher).*into view stop += ((deep elf|draconian) annihilator|tengu reaver).*into view stop += (lich|orb of fire|executioner|juggernaut|shrike).*into view stop += (ghost moth|emperor scorpion|moth of wrath).*into view stop += (?-i)[A-Z].* comes? into view stop += You are starting to lose your buoyancy stop += Your hearing returns stop += time is quickly running out stop += life is in your own hands stop += You start to feel a little slower stop += is no longer charmed stop += You feel your power leaking away stop += You feel drained stop += (Zot (is|has)|The Orb) stop += wrath finds you stop += doesn't seem very happy stop += (You see here|Items here).*(runed|glowing|enchanted|embroidered|shiny|dyed) stop += (You see here|Items here).*\{.*\} msc += lightcyan:(You see here|Items here).*(runed|glowing|enchanted|embroidered|shiny|dyed) msc += mute:This spell is (mildly|quite|extremely|astonishingly )?dangerous to cast ignore += contamination has completely ignore += your breath back ignore += talk: ignore += talk_visual: ignore += friend_spell: ignore += friend_enchant: ignore += friend_action: ignore += sound: : if you.god() ~= "Xom" then ignore += god: : end channel.multiturn = mute channel.monster_warning = lightred channel.god = lightmagenta channel.timed_portal = lightcyan channel.duration = brown channel.sound = darkgrey msc := message_colour msc += lightmagenta:invites you to partake stop += invites you to partake msc += lightred:(Ouch! That really hurt|LOW HITPOINT WARNING|HP DROP) msc += lightred:Your body is wracked with pain msc += lightmagenta:(paralys|petrif|mesmeris|turn(ing|ed) to stone) msc += lightmagenta:You are (confused|vexed|marked) msc += lightmagenta:(caught in|entangled in|held in place|constricts you) msc += lightmagenta:(mutat|contaminat) msc += lightmagenta:space (warps|bends|twists) msc += lightmagenta:(profound silence|silence descends) msc += lightcyan:(Zot (is|has)|The Orb) msc += lightcyan:You feel (your power leaking away|drained) msc += yellow:lodged in your body msc += yellow:covered in liquid flames msc += yellow:goes berserk msc += mute:Your .* begins to (drip with poison|ooze corrosive slime) msc += mute:Your .* bursts into flame msc += mute:Your .* is covered in frost msc += mute:Your .* glows (with a cold blue light|with a divine radiance|horrifically) msc += mute:You (hear the crackle of electricity|see sparks fly) msc += mute:Your .* exudes an aura of protection msc += mute:Your .* hums with potential msc += mute:Your .* tingle msc += mute:Your .* quivers in your msc += mute:Your .* gleams with (eagerness|a vicious edge) msc += mute:Your .* radiates an overwhelming force msc += mute:Your .* is briefly surrounded by (a scintillating aura|shifting shadows) msc += mute:(Pain shudders through|A searing pain shoots up) your msc += mute:You feel (a sense of dread|a bond with|a baleful cunning) msc += mute:Your .* stops (flaming|glowing|crackling|quivering) msc += mute:Your .* stops (dripping with poison|oozing corrosive slime|radiating force) msc += mute:Your .* goes (still|dull) msc += mute:You feel the dreadful sensation subside msc += mute:You feel magic returning to you msc += mute:You feel (very meek|guileless) msc += mute:The vines retreat back into msc += mute:Vines begin sprouting from msc += mute:You sense an unholy aura msc += mute:A chill wind blows around you msc += mute:An electric hum fills the air msc += mute:Was it this warm in here before msc += mute:The flames dance msc += mute:Moving in this stuff is going to be slow msc += mute:You reach to attack msc += mute:dissolves into shadows msc += mute:Your foxfire dissipates msc += mute:(is honoured by|accepts) your kill msc += mute:appreciates the change of pace msc += mute:returns to your side msc += mute:a demon appears msc += mute:puff of smoke msc += mute:carefully avoids msc += mute:is recalled msc += mute:dissolves? into (sparkling lights|shadows) msc += mute:(crumbles|melts) away\. msc += mute:(merges|forms) itself .* the air msc += mute:you swap places msc += mute:your.*(looks stronger|shudders|resists) msc += mute:your.*(stumbles backwards|holds.*ground) msc += mute:your.*(blinks|safely over) msc += mute:(phases out.*|misses) (your|something).* msc += mute:your.*(picks up|drops) msc += mute:your.*basks in the mutagenic energy msc += mute:your.*(struggles|tears|pulls away).*(web|net) msc += mute:You start (resting|waiting) msc += mute:Done exploring msc += mute:You now have [0-9]+ gold view_delay = 300 hp_colour = 100:green, 99:lightgray, 75:yellow, 50:lightred, 25:red mp_colour = 100:green, 99:lightgray, 75:yellow, 50:lightred, 25:red stat_colour = 5:lightred, 3:red hp_warning = 50 mp_warning = 15 equip_bar = true tile_full_screen = false default_manual_training = true equip_unequip = true sort_menus = true:equipped,identified,basename,art,ego,glowing,qualname,charged,slot ability_menu = true spell_menu = false autofight_stop = 70 autofight_caught = true autofight_wait = false bindkey = [~] CMD_LUA_CONSOLE item := item_glyph item += potion:lightgrey item += scroll:lightgrey item += wand:lightgrey item += dangerous_item:blue item += useless_item:darkgrey item += potions? of.*curing:lightgrey item += potions? of.*heal wounds:white item += potions? of.*berserk:brown item += potions? of.*might:yellow item += potions? of.*(cancellation|enlightenment):cyan item += potions? of.*mutation:lightcyan item += potions? of.*(lignification|ambrosia|attraction):blue item += potions? of.*experience:lightred item += potions? of.*resistance:green item += potions? of.*(haste|invisibility):lightgreen item += potions? of.*brilliance:magenta item += potions? of.*magic:lightmagenta : if you.race() == "Vampire" or you.race() == "Mummy" or you.race() == "Ghoul" then item += scroll.*torment:brown : else item += scroll.*torment:darkgrey : end item += scroll.*acquirement:white item += scroll.*summoning:yellow item += scroll.*identify:cyan item += scroll.*revelation:lightcyan item += scroll.*(silence|vulnerability|immolation):blue item += scroll.*(fog|butterflies|teleport):green item += scroll.*(fear|blink):lightgreen item += scroll.*enchant:magenta item += scroll.*brand weapon:lightmagenta item += wand of.*flame:brown item += wand of.*mindburst:yellow item += wand of.*acid:cyan item += wand of.*iceblast:lightcyan item += wand of.*random effects:blue item += wand of.*polymorph:green item += wand of.*digging:lightgreen item += wand of.*paralysis:magenta item += wand of.*charming:lightmagenta item += (identified|known).*(jewellery|magical staff):brown item += unidentified.*(potion|scroll|jewellery|wand|magical staff).*:lightblue item += manual:lightcyan item += identified.*artefact.*(jewellery):white item += unidentified.*artefact.*(jewellery).*:lightmagenta item += (a )?stones?$:lightgray item += useless:darkgrey item += forbidden.*(potion|scroll):red menu := menu_colour menu = menu += notes:white:Reached XP level menu += darkgrey:(melded) menu += red:forbidden menu += darkgrey:.*useless.* menu += lightred:.*equipped.* cursed menu += inventory:lightgreen:.*equipped.* menu += pickup:lightred:god gift menu += inventory:white:\w \+\s menu += inventory:white:\w \#\s menu += lightgrey:potions? of.*curing menu += white:potions? of.*heal wounds menu += brown:potions? of.*berserk menu += yellow:potions? of.*might menu += cyan:potions? of.*(cancellation|enlightenment) menu += lightcyan:potions? of.*mutation menu += blue:potions? of.*(lignification|ambrosia|attraction) menu += lightred:potions? of.*experience menu += green:potions? of.*resistance menu += lightgreen:potions? of.*(haste|invisibility) menu += magenta:potions? of.*brilliance menu += lightmagenta:potions? of.*magic : if you.race() == "Vampire" or you.race() == "Mummy" or you.race() == "Ghoul" then menu += brown:scroll.*torment : else menu += darkgrey:scroll.*torment : end menu += yellow:scroll.*(summoning) menu += white:scroll.*acquirement menu += cyan:scroll.*identify menu += lightcyan:scroll.*revelation menu += blue:scroll.*(noise|silence|vulnerability|immolation) menu += green:scroll.*(fog|butterflies|teleport) menu += lightgreen:scroll.*(fear|blink) menu += magenta:scroll.*enchant menu += lightmagenta:scroll.*brand weapon menu += brown:wand of.*flame menu += yellow:wand of.*mindburst menu += cyan:wand of.*acid menu += lightcyan:wand of.*iceblast menu += blue:wand of.*random effects menu += green:wand of.*polymorph menu += lightgreen:wand of.*digging menu += magenta:wand of.*paralysis menu += lightmagenta:wand of.*charming menu += lightcyan:manual menu += lightmagenta:unidentified.*artefact.* menu += white:.*artefact.* menu += lightblue:unidentified .*(potion|scroll|wand|jewellery|spellbook|rod|magical staff) menu += inventory:lightgray:.*(jewellery|magical staff) menu += lightblue:unidentified.*weapon.*(runed|glowing|enchanted) menu += lightblue:unidentified.*armour.*(runed|glowing|embroidered|shiny|dyed) autopickup = $?!+=/}| ae := autopickup_exceptions ae += > note_items += experience,of resistance, Archmagi note_items += crystal plate armour,pearl dragon scales note_items += artefact note_items += talisman note_items += wand, potions?, scrolls? dump_item_origins = all note_skill_levels = 3,6,9,12,15,18,21,24,27 note_messages += You feel monstrous note_messages += You pass through the gate note_messages += cast .* Abyss note_monsters += ancient lich,curse skull,curse toe,royal mummy note_monsters += caustic shrike note_monsters += killer klown,electric golem,orb of fire dump_message_count = 30 dump_order = header,hiscore,stats,misc,mutations,skills,spells,inventory dump_order += overview,screenshot,monlist,messages,skill_gains,action_counts dump_order += vaults,notes,kills,turns_by_place,xp_by_level dump_item_origins = artefacts dump_skill_gains = true dump_vaults = true dump_turns_by_place = true fsim_rounds = 10000 macros += M 1 ===spellclass_1 macros += M 2 ===spellclass_2 macros += M 3 ===spellclass_3 macros += M 4 ===spellclass_4 macros += M 5 ===spellclass_5 macros += M \{NP5} ===smart_wait macros += M 6 Z*! macros += M 0 ===spellclass_manage macros += M \{-15} ===spellclass_slot_1 macros += M \{-14} ===spellclass_slot_2 macros += M \{-13} ===spellclass_slot_3 macros += M \{-12} ===spellclass_slot_4 macros += M \{-11} ===spellclass_slot_5 macros += M 7 ===cast_best macros += M 8 ===toggle_autotarget macros += M \{61} ===reset_rc_state macros += M \{-10} ===risk_explain macros += M 9 ===toggle_reckless macros += K3 \{Enter} y macros += K3 \{NPEnter} y { AP_DEFAULTS = { artefacts = true, -- any artefact, any class, always armour_slot = true, -- armour for a slot you have NOTHING for yet armour_ego = true, -- armour whose ego is on the wanted list below armour_better = true, -- branded armour when what you wear is not weapons = false, -- weapons are noted, not hoarded (artefacts still land) jewellery = true, -- also needs '=' in the autopickup string debug = false, -- print the reason for every decision } -- end AP_DEFAULTS <-- trailing comment REQUIRED (a bare } ends the rc lua block) AP_ORDER = { "artefacts", "armour_slot", "armour_ego", "armour_better", "weapons", "jewellery", "debug" } AP_LABEL = { artefacts = "Artefacts (any class)", armour_slot = "Armour for a slot you have nothing for", armour_ego = "Armour with a wanted ego", armour_better = "Branded armour beating an unbranded one", weapons = "Weapons", jewellery = "Jewellery", debug = "Debug: explain every decision", } -- end AP_LABEL <-- trailing comment REQUIRED (a bare } ends the rc lua block) AP_WANT_EGO = { "resistance", "positive energy", "willpower", "stealth", "reflection", "protection", "invisibility", "rampaging", "spirit shield", "archmagi", "shadows", "guile", "energy", "infusion", "repulsion", "flight", "harm", "mayhem", "regeneration", } -- end AP_WANT_EGO <-- trailing comment REQUIRED (a bare } ends the rc lua block) AP_SKIP_BASE = { "troll leather armour", "dragon scales", "crystal plate armour", "plate armour", "pearl dragon", "gold dragon", "storm dragon", "shadow dragon", "quicksilver dragon", "swamp dragon", "fire dragon", "ice dragon", "acid dragon", "steam dragon", } -- end AP_SKIP_BASE <-- trailing comment REQUIRED (a bare } ends the rc lua block) function ap_flags() if not c_persist.ap then c_persist.ap = {} for k, v in pairs(AP_DEFAULTS) do c_persist.ap[k] = v end end return c_persist.ap end local function safe(fn, ...) local ok, v = pcall(fn, ...) if ok then return v end return nil end local function iclass(it) return safe(function() return it.class(true) end) end local function iname(it) return string.lower(safe(function() return it.name("plain") end) or "") end local function ibase(it) return string.lower(safe(function() return it.name("base") end) or "") end local function islot(it) return safe(function() return it.subtype() end) end local function matches_any(name, list) for _, frag in ipairs(list) do if string.find(name, frag, 1, true) then return true end end return false end local function gear_snapshot() local have = { slot = {}, worn = {} } local function note(it, is_worn) if not it then return end if iclass(it) ~= "armour" then return end local slot = islot(it) if slot then have.slot[slot] = true if is_worn then have.worn[slot] = it end end end for _, inv in ipairs(items.inventory() or {}) do note(inv, safe(function() return inv.equipped end) == true) end for i = 0, 20 do note(safe(items.equipped_at, i), true) end return have end ap_claim_turn, ap_claimed = -1, {} local function ap_say(name, verdict, why) if not ap_flags().debug then return end crawl.mpr("[ap] " .. (verdict and "TAKE " or "skip ") .. name .. " -- " .. why .. "") end function ch_force_autopickup(it, name) local ap = ap_flags() local nm = iname(it) if it.artefact then if ap.artefacts then ap_say(nm, true, "artefact") return true end ap_say(nm, false, "artefact, but artefacts toggle is off") return end if it.is_useless then ap_say(nm, false, "useless to you") return end local cls = iclass(it) if cls == "weapon" or cls == "staff" then if ap.weapons then ap_say(nm, true, "weapons toggle on") return true end ap_say(nm, false, "non-artefact weapon") return end if cls ~= "armour" then return end local slot = islot(it) local base = ibase(it) local have = gear_snapshot() local worn = slot and have.worn[slot] or nil local own = slot and have.slot[slot] or false if you.turns() ~= ap_claim_turn then ap_claim_turn, ap_claimed = you.turns(), {} end if matches_any(base, AP_SKIP_BASE) then ap_say(nm, false, "heavy base type, artefacts only") return end if ap.armour_ego and matches_any(nm, AP_WANT_EGO) then ap_say(nm, true, "wanted ego") return true end if ap.armour_better and it.branded and not (worn and safe(function() return worn.branded end) == true) then ap_say(nm, true, "branded, and what you have for that slot is not") return true end if ap.armour_slot and not own and not ap_claimed[slot] then ap_claimed[slot] = true ap_say(nm, true, "nothing for the " .. tostring(slot) .. " slot yet") return true end if own then ap_say(nm, false, "already have something for the " .. tostring(slot) .. " slot") elseif ap_claimed[slot] then ap_say(nm, false, "already took a " .. tostring(slot) .. " item from this pile") else ap_say(nm, false, "no rule matched") end return end AUTO_YES_PROMPTS = { } -- end AUTO_YES_PROMPTS function c_answer_prompt(prompt) for _, frag in ipairs(AUTO_YES_PROMPTS) do if string.find(prompt, frag, 1, true) then return true end end return nil end function ap_menu() local ap = ap_flags() crawl.mpr("Autopickup categories") for i, key in ipairs(AP_ORDER) do local on = ap[key] crawl.mpr(string.format("%d) %s %s", i, on and "[on ]" or "[off]", AP_LABEL[key])) end crawl.mpr("R) reset to defaults") crawl.flush_prev_message() local ch = crawl.getch() local key = (ch >= 32 and ch < 127) and string.char(ch) or "" if key == "R" or key == "r" then c_persist.ap = nil ap_flags() crawl.mpr("Autopickup categories reset.") return end local pick = ch - 48 if pick >= 1 and pick <= #AP_ORDER then local k = AP_ORDER[pick] ap[k] = not ap[k] crawl.mpr((ap[k] and "On: " or "Off: ") .. AP_LABEL[k] .. "") else crawl.mpr("Cancelled.") end end HP_DROP_PCT = 20 local prev_hp local prev_mhp = nil BANNER_PATTERNS = { "invites you to partake", "Ashenzari invites", } -- end BANNER_PATTERNS local BANNER = "#####################################" function banner_mpr(text, colour) colour = colour or "lightmagenta" local w = function(t) return "<" .. colour .. ">" .. t .. "" end emit_block({ w(BANNER), w(text), w(BANNER) }) crawl.flush_prev_message() you.stop_activity() crawl.redraw_screen() crawl.more() end local function check_banner(text) local clean = text:gsub("<[^>]->", "") if clean:find("#####", 1, true) then return end -- our own banner for _, pat in ipairs(BANNER_PATTERNS) do if clean:find(pat, 1, true) then banner_mpr(clean) return true end end end ATTACK_LOG = {} local ATTACKER_PATTERNS = { "^The ([%l%s'%-]+) [%a]+s you", "^([A-Z][%a%s'%-]-) [%a]+s you", "^The ([%l%s'%-]+) closely misses you", "^The ([%l%s'%-]+) casts", "^([A-Z][%a%s'%-]-) casts", } -- end ATTACKER_PATTERNS local function note_attacker(text) local clean = text:gsub("<[^>]->", "") for _, pat in ipairs(ATTACKER_PATTERNS) do local who = clean:match(pat) if who then who = who:gsub("%s+$", "") if #who > 0 and #who < 40 then for _, seen in ipairs(ATTACK_LOG) do if seen == who then return end end ATTACK_LOG[#ATTACK_LOG + 1] = who end return end end end function c_message(text, channel) if channel == "god" or channel == "plain" then if check_banner(text) then return end end if channel == "plain" or channel == "monster_spell" or channel == "monster_enchant" or channel == "danger" or channel == "monster_warning" then pcall(note_attacker, text) end end function attacker_str(lost) if #ATTACK_LOG == 0 then return "" end c_persist.dmg_by = c_persist.dmg_by or {} local parts = {} for _, who in ipairs(ATTACK_LOG) do local prev = c_persist.dmg_by[who] or 0 if lost > prev then c_persist.dmg_by[who] = lost end local worst = c_persist.dmg_by[who] if #ATTACK_LOG == 1 then parts[#parts + 1] = who .. " (worst from one: " .. worst .. ")" else parts[#parts + 1] = who .. " (" .. worst .. ")" end end local lead = #ATTACK_LOG == 1 and " from " or " from " return lead .. table.concat(parts, ", ") .. "" end RECKLESS_OFF = false function toggle_reckless() if RECKLESS_OFF then AUTOFIGHT_STOP = RECKLESS_SAVED or 70 RECKLESS_OFF = false crawl.mpr("Autofight guard back ON at " .. AUTOFIGHT_STOP .. "% HP.") else RECKLESS_SAVED = AUTOFIGHT_STOP or 70 AUTOFIGHT_STOP = 0 RECKLESS_OFF = true crawl.mpr("Autofight guard OFF. Tab will swing at any HP." .. "") end crawl.flush_prev_message() end -- ---- artefact spotter --------------------------------------------- -- Artefacts on the floor are easy to walk past: explore_stop halts for -- them, but the message is one ordinary line among many. This announces -- them in gold with a banner, once each, and says what the thing -- actually is rather than just naming it. -- -- items.get_items_at(dx, dy) reads any square in view, so a pile is -- reported from across the room without visiting it. ARTE_WATCH = true ARTE_BANNER = true -- false for a plain gold line, no forced more ARTE_ALSO_BRANDED = false -- also call out ego items local function arte_seen() c_persist.arte_seen = c_persist.arte_seen or {} return c_persist.arte_seen end -- esc() is a LOCAL declared further down the block, so a reference to it -- from up here resolves to a nil global instead. The whole scan runs -- inside pcall, which swallowed the error and made this look like "no -- artefacts found". Own copy, no ordering dependency. local function arte_esc(x) return (tostring(x):gsub("[|~\n<>]", " ")) end -- Compact stat line: what you would want before deciding to walk over. local function item_note(it) local bits = {} local ok = pcall(function() if it.is_weapon then if it.damage then bits[#bits+1] = "dam " .. it.damage end if it.accuracy then bits[#bits+1] = "acc " .. it.accuracy end if it.delay then bits[#bits+1] = "dly " .. it.delay end if it.hands and it.hands > 1 then bits[#bits+1] = "2h" end else if it.ac and it.ac > 0 then bits[#bits+1] = "AC " .. it.ac end if it.encumbrance and it.encumbrance > 0 then bits[#bits+1] = "encumb " .. it.encumbrance end end -- artefact properties, the reason to care at all if it.artprops then local props = {} for k, v in pairs(it.artprops) do props[#props+1] = k .. (v > 0 and ("+" .. v) or (v < 0 and tostring(v) or "")) end table.sort(props) if #props > 0 then bits[#bits+1] = table.concat(props, " ") end end end) if not ok then return "" end return table.concat(bits, ", ") end function floor_watch() if not ARTE_WATCH then return end local r = you.los() local mem = arte_seen() for x = -r, r do for y = -r, r do pcall(function() if not you.see_cell(x, y) then return end for _, it in ipairs(items.get_items_at(x, y) or {}) do local want = it.artefact or (ARTE_ALSO_BRANDED and it.branded) if want and not it.is_useless then local nm = it.name("plain") if nm and not mem[nm] then mem[nm] = true local dist = math.max(math.abs(x), math.abs(y)) local note = item_note(it) local line = "* " .. arte_esc(nm) .. " *" .. " " .. dist .. " away" .. (note ~= "" and (" -- " .. note) or "") .. "" if ARTE_BANNER then banner_mpr(line:gsub("<[^>]->", ""), "yellow") else crawl.mpr(line) you.stop_activity() end end end end end) end end end function hp_watch() local hp, mhp = you.hp() if prev_mhp and mhp ~= prev_mhp then prev_hp, prev_mhp = hp, mhp return -- not a real hit, skip the turn end if c_persist.worst_hit and mhp > 0 and c_persist.worst_hit > mhp then c_persist.worst_hit = nil -- stale, drop it end if prev_hp and hp < prev_hp and mhp > 0 then local lost = math.min(prev_hp - hp, mhp) local pct = math.floor(lost * 100 / mhp) if pct >= HP_DROP_PCT then crawl.mpr("*** HP DROP -- " .. lost .. " damage" .. attacker_str(lost) .. ", " .. pct .. "% of max, " .. hp .. "/" .. mhp .. " left ***") crawl.flush_prev_message() end if not c_persist.worst_hit or lost > c_persist.worst_hit then c_persist.worst_hit = lost end if c_persist.worst_hit and hp > 0 and hp <= c_persist.worst_hit then crawl.mpr("*** " .. hp .. " HP LEFT -- you have " .. "already taken " .. c_persist.worst_hit .. " in one turn this game ***") crawl.flush_prev_message() end end prev_hp, prev_mhp = hp, mhp end fm_patterns = { {name = "30mhp", cond = "maxhp", cutoff = 30, pattern = "adder|hound"}, {name = "40mhp", cond = "maxhp", cutoff = 40, pattern = "orc priest|electric eel"}, {name = "60mhp", cond = "maxhp", cutoff = 60, pattern = "acid dragon|steam dragon|manticore"}, {name = "70mhp", cond = "maxhp", cutoff = 70, pattern = "centaur(?! warrior)|meliai|yaktaur(?! captain)"}, {name = "80mhp", cond = "maxhp", cutoff = 80, pattern = "gargoyle|orc (warlord|knight)"}, {name = "90mhp", cond = "maxhp", cutoff = 90, pattern = {"centaur warrior", "deep elf archer", "efreet", "molten gargoyle", "tengu conjurer"} }, {name = "110mhp", cond = "maxhp", cutoff = 110, pattern = {"centaur warrior", "deep elf (mage|knight)", "cyclops", "efreet", "molten gargoyle", "tengu conjurer", "yaktaur captain", "necromancer", "deep troll earth mage", "hell knight", "stone giant"} }, {name = "160mhp", cond = "maxhp", cutoff = 160, pattern = {"(fire|ice|quicksilver|shadow|storm) dragon", "(fire|frost) giant", "war gargoyle", "draconian (knight|stormcaller)"} }, } -- end fm_patterns <-- trailing comment is REQUIRED, see note active_fm = {} notify_fm = false if not mpr then mpr = function (msg, color) if not color then color = "white" end crawl.mpr("<" .. color .. ">" .. msg .. "") end end function init_force_mores() for i,v in ipairs(fm_patterns) do active_fm[#active_fm + 1] = false end end function update_force_mores() local activated = {} local deactivated = {} local hp, maxhp = you.hp() for i,v in ipairs(fm_patterns) do local msg = nil if type(v.pattern) == "table" then for j, p in ipairs(v.pattern) do if msg == nil then msg = p else msg = msg .. "|" .. p end end else msg = v.pattern end msg = "(?= v.cutoff then action = "-" elseif not active_fm[i] and you.xl() < v.cutoff then action = "+" end elseif v.cond == "maxhp" then if active_fm[i] and maxhp >= v.cutoff then action = "-" elseif not active_fm[i] and maxhp < v.cutoff then action = "+" end end if action == "+" then activated[#activated + 1] = fm_name elseif action == "-" then deactivated[#deactivated + 1] = fm_name end if action ~= nil then crawl.setopt("force_more_message " .. action .. "= " .. msg) crawl.setopt("runrest_stop_message " .. action .. "= " .. msg) active_fm[i] = not active_fm[i] end end if #activated > 0 and notify_fm then mpr("Activating force_mores: " .. table.concat(activated, ", ")) end if #deactivated > 0 and notify_fm then mpr("Deactivating force_mores: " .. table.concat(deactivated, ", ")) end end local last_fm_turn = nil function force_mores() if last_fm_turn ~= you.turns() then update_force_mores() last_fm_turn = you.turns() end end init_force_mores() message_color = "white" function save_with_message() if you.turns() == 0 then crawl.sendkeys("S") return end crawl.formatted_mpr("Save game and exit?", "prompt") local res = crawl.getch() if not (string.char(res) == "y" or string.char(res) == "Y") then crawl.formatted_mpr("Okay, then.", "prompt") return end crawl.formatted_mpr("Leave a message: ", "prompt") local res = crawl.c_input_line() c_persist.message = res crawl.sendkeys(control("s")) end function load_message() if c_persist.message and c_persist.message ~= "nil" and c_persist.message ~= "" then mpr("MESSAGE: " .. c_persist.message, message_color) c_persist.message = nil end end local function generic_cast_spell(cmd) crawl.mpr('Cast which spell?') crawl.flush_prev_message() crawl.process_keys(cmd) end function cast_spell() generic_cast_spell('z') end function force_cast_spell() generic_cast_spell('Z') end SMART_WAIT_ALLOW_REST = true -- false = always one turn, never rest local ATT_HOSTILE = 0 local function hostile_in_view() if you.feel_safe() then return false end local r = you.los() for x = -r, r do for y = -r, r do if not (x == 0 and y == 0) then local ok, hostile = pcall(function() local m = monster.get_monster_at(x, y) return m and m:attitude() == ATT_HOSTILE and not m:is_firewood() end) if ok and hostile then return true end end end end return false end function smart_wait() if not SMART_WAIT_ALLOW_REST or hostile_in_view() then crawl.sendkeys("s") -- CMD_WAIT: exactly one turn else crawl.sendkeys("5") -- CMD_REST end end SPELL_CLASSES = { ["1"] = { label = "Attack", spells = { "Foxfire", "Freeze", "Kiss of Death", "Magic Dart", "Poisonous Vapours", "Sandblast", "Shock", "Grave Claw", "Mercury Arrow", "Momentum Strike", "Scorch", "Searing Ray", "Static Discharge", "Throw Flame", "Thunderbolt", "Tremorstone", "Hailstorm", "Maxwell's Portable Piledriver", "Stone Arrow", "Unleash Destruction", "Vampiric Draining", "Airstrike", "Brom's Barrelling Boulder", "Cigotuvi's Putrefaction", "Dispel Undead", "Flame Wave", "Fulminant Prism", "Hurl Torchlight", "Ignite Poison", "Iskenderun's Mystic Blast", "Olgreb's Toxic Radiance", "Smiting", "Steam Breath", "Sticky Flame", "Vhi's Electric Charge", "Arcjolt", "Caustic Breath", "Combustion Breath", "Fireball", "Galvanic Breath", "Glacial Breath", "Golden Breath", "Iceblast", "Irradiate", "Lee's Rapid Deconstruction", "Mud Breath", "Noxious Breath", "Nullifying Breath", "Quicksilver Bolt", "Rust Breath", "Bolt of Light", "Bombard", "Conjure Ball Lightning", "Corrosive Bolt", "Permafrost Eruption", "Plasma Beam", "Starburst", "Magnavolt", "Manifold Assault", "Orb of Destruction", "Ozocubu's Refrigeration", "Rimeblight", "Fulsome Fusillade", "Ignition", "Infestation", "Lehudib's Crystal Spear", "Maxwell's Capacitive Coupling", "Chain Lightning", "Fire Storm", "Hurl Damnation", "Polar Vortex", "Shatter", } }, ["2"] = { label = "Control & terrain", spells = { "Slow", "Soul Splinter", "Ensorcelled Hibernation", "Frozen Ramparts", "Gell's Gravitas", "Gloom", "Inner Flame", "Mephitic Cloud", "Sigil of Binding", "Teleport Other", "Tukima's Dance", "Volatile Blastmotes", "Anguish", "Cause Fear", "Charm", "Dig", "Dimensional Bullseye", "Leda's Liquefaction", "Paralyse", "Petrify", "Polymorph", "Borgnjor's Vile Clutch", "Curse of Agony", "Fastroot", "Freezing Cloud", "Metabolic Englaciation", "Phantom Mirror", "Silence", "Yara's Violent Unravelling", "Eringya's Noxious Bog", "Mindburst", "Sculpt Simulacrum", "Enfeeble", "Discord", } }, ["3"] = { label = "Defence & buff", spells = { "Apportation", "Jinxbite", "Sublimation of Blood", "Confusing Touch", "Fugue of the Fallen", "Ozocubu's Armour", "Animate Dead", "Awaken Armour", "Alistair's Intoxication", "Detonation Catalyst", "Death Channel", "Borgnjor's Revivification", "Death's Door", } }, ["4"] = { label = "Escape", no_menu = true, spells = { "Blink", "Lesser Beckoning", "Passwall", "Swiftness", "Passage of Golubria", "Warp Space", "Dispersal", "Gell's Gavotte", "Disjunction", } }, ["5"] = { label = "Summons & constructs", spells = { "Kinetic Grapnel", "Summon Small Mammal", "Call Imp", "Construct Spike Launcher", "Call Canine Familiar", "Launch Clockwork Bee", "Summon Ice Beast", "Eringya's Surprising Crocodile", "Forge Blazeheart Golem", "Forge Lightning Spire", "Iskenderun's Battlesphere", "Martyr's Knell", "Rending Blade", "Summon Seismosaurus Egg", "Alistair's Walking Alembic", "Hoarfrost Cannonade", "Nazja's Percussive Tempering", "Summon Forest", "Summon Mana Viper", "Forge Monarch Bomb", "Forge Phalanx Beetle", "Fortress Blast", "Summon Cactus Giant", "Diamond Sawblades", "Haunt", "Hellfire Mortar", "Malign Gateway", "Spellspark Servitor", "Sphinx Sisters", "Splinterfrost Shell", "Summon Hydra", "Summon Horrible Things", "Dragon's Call", "Platinum Paragon", } }, } -- end SPELL_CLASSES <-- trailing comment REQUIRED (a bare } ends the rc lua block) <-- trailing comment REQUIRED (bare } ends the rc lua block) local real_cache = {} local function spell_is_real(name) if real_cache[name] == nil then real_cache[name] = pcall(spells.describe, name) end return real_cache[name] end local function known_set() local set = {} for _, title in ipairs(you.spells()) do set[string.lower(title)] = true end return set end local function is_known(name, set) return set[string.lower(name)] == true end for _, class in pairs(SPELL_CLASSES) do for i, e in ipairs(class.spells) do if type(e) == "string" then class.spells[i] = { spell = e } end end end local function assign_set() if not c_persist.spell_assign then c_persist.spell_assign = {} end local a = c_persist.spell_assign for name, key in pairs(a) do if not SPELL_CLASSES[key] then a[name] = nil end end return a end local function builtin_class(name) for key, class in pairs(SPELL_CLASSES) do for _, e in ipairs(class.spells) do if e.spell == name then return key end end end return nil end local function entries_for(key) local class = SPELL_CLASSES[key] if not class then return nil, {} end local assign, out = assign_set(), {} for _, e in ipairs(class.spells) do local a = assign[e.spell] if a == nil or a == key then out[#out + 1] = e end end local moved = {} for name, k in pairs(assign) do if k == key and builtin_class(name) ~= key then moved[#moved + 1] = name end end table.sort(moved) -- pairs() order is not stable for _, name in ipairs(moved) do out[#out + 1] = { spell = name, moved = true } end return class, out end local function order_sync() if not c_persist.spell_order then c_persist.spell_order = {} end local ord = c_persist.spell_order local known = you.spells() if #known == 0 then return ord end local live = {} for _, n in ipairs(known) do live[string.lower(n)] = true end for k in pairs(ord) do if not live[k] then ord[k] = nil end end local next_seq = 0 for _, v in pairs(ord) do if v >= next_seq then next_seq = v + 1 end end table.sort(known) for _, n in ipairs(known) do local k = string.lower(n) if not ord[k] then ord[k] = next_seq; next_seq = next_seq + 1 end end return ord end local function seq_of(ord, name) return ord[string.lower(name)] or math.huge end local function order_to_front(name) local ord = order_sync() local lo = 0 for _, v in pairs(ord) do if v < lo then lo = v end end ord[string.lower(name)] = lo - 1 end local function state_set() if not c_persist.spell_state then c_persist.spell_state = {} if c_persist.spell_hidden then for name, on in pairs(c_persist.spell_hidden) do if on then c_persist.spell_state[name] = "hidden" end end c_persist.spell_hidden = nil end end return c_persist.spell_state end local function retired(entry, set) if entry.retire_xl and you.xl() >= entry.retire_xl then return "XL " .. entry.retire_xl end if entry.retire_if_known then for _, other in ipairs(entry.retire_if_known) do if is_known(other, set) then return other end end end return nil end local function live_spells(key) local class, entries = entries_for(key) if not class then return nil, {} end local st = state_set() local set = known_set() local out, suppressed = {}, 0 for _, entry in ipairs(entries) do if is_known(entry.spell, set) then if st[entry.spell] == "hidden" then suppressed = suppressed + 1 else out[#out + 1] = entry.spell end end end local ord = order_sync() table.sort(out, function(a, b) return seq_of(ord, a) < seq_of(ord, b) end) return class, out, suppressed end local function castable(name) local mp = you.mp() return mp >= spells.mana_cost(name) end local function menu_line(i, name) local cost = spells.mana_cost(name) local fail = spells.fail(name) local ok = castable(name) local col = ok and "lightgrey" or "brown" local mark = ok and " " or "-" return string.format("<%s>%s%d) %-28s %2d MP %2d%% fail", col, mark, i, name, cost, fail, col) end SPELLCLASS_CAST_MODE = "keys" -- "keys" | "api" SPELLCLASS_CAST_KEY = "Z" -- "Z" (force) | "z" (normal) local function letter_ok(letter) return type(letter) == "string" and #letter == 1 and letter:match("^%a$") ~= nil end local pending_unmutes = {} function single_turn_mute(pattern) crawl.setopt("message_colour ^= mute:" .. pattern) pending_unmutes[#pending_unmutes + 1] = pattern end function clear_single_turn_mutes() for _, p in ipairs(pending_unmutes) do crawl.setopt("message_colour -= mute:" .. p) end pending_unmutes = {} end local function do_cast(name) if SPELLCLASS_CAST_MODE ~= "api" then local ok, letter = pcall(spells.letter, name) if ok and letter_ok(letter) then single_turn_mute("Cast which spell") crawl.sendkeys(SPELLCLASS_CAST_KEY .. letter) return end end local ok, err = pcall(spells.cast, name, 0, 0) if not ok then crawl.mpr("Could not cast " .. name .. ": " .. tostring(err) .. "") end end local intel_cache = {} local function dice_avg(s) if not s then return 0 end local total, n = 0, 0 local mult, rest = s:match("^(%d+)x(.+)$") local body = rest or s local reps = tonumber(mult) or 1 for a, b in body:gmatch("(%d+)d(%d+)") do total = total + tonumber(a) * (tonumber(b) + 1) / 2 n = n + 1 end if n == 0 then local lo, hi = body:match("(%d+)%s*%-%s*(%d+)") if lo then return (tonumber(lo) + tonumber(hi)) / 2 * reps end local flat = body:match("^(%d+)") if flat then return tonumber(flat) * reps end return 0 end return total * reps end function spell_intel(name) local pp = 0 local ok, v = pcall(spells.power_perc, name) if ok and type(v) == "number" then pp = v end local key = name .. "|" .. math.floor(pp / 10) local hit = intel_cache[key] if hit then return hit end local t = { pp = pp, dmg = nil, avg = 0, sev = 0, range = 0, hated = false } local ok2, body = pcall(spells.describe, name) if ok2 and body then t.dmg = body:match("Damage:%s*([^\n%(]+)") if t.dmg then t.dmg = t.dmg:gsub("%s+$", "") end t.avg = dice_avg(t.dmg) end local ok3, sev = pcall(spells.fail_severity, name) if ok3 and type(sev) == "number" then t.sev = sev end local ok4, r = pcall(spells.max_range, name) if ok4 and type(r) == "number" then t.range = r end local ok5, h = pcall(spells.god_hates, name) if ok5 then t.hated = h and true or false end intel_cache[key] = t return t end function spell_intel_note(name) local t = spell_intel(name) local bits = {} if t.dmg then bits[#bits + 1] = "" .. t.dmg .. "" end if t.pp > 0 and t.pp < 100 then local c = t.pp < 40 and "brown" or t.pp < 75 and "yellow" or "lightgrey" bits[#bits + 1] = "<" .. c .. ">" .. t.pp .. "%pow" end if t.sev >= 2 then local c = t.sev >= 4 and "lightred" or t.sev == 3 and "red" or "brown" bits[#bits + 1] = "<" .. c .. ">sev" .. t.sev .. "" end if t.range > 0 then bits[#bits + 1] = "r" .. t.range .. "" end if t.hated then bits[#bits + 1] = "godhates" end if #bits == 0 then return "" end return " " .. table.concat(bits, " ") end TARGET_TIER_NAMES = { [4] = "banisher", [3] = "summoner", [2] = "caster", [1] = "damage" } local SUMMON_WORDS = { "Summon", "Call ", "Conjure ", "Forge ", "Malign Gateway", "Spellspark Servitor" } local function target_tier(m) local tier = 1 local ok = pcall(function() for _, sp in ipairs(m:spells() or {}) do if sp == "Banishment" then tier = 4 return end for _, w in ipairs(SUMMON_WORDS) do if string.find(sp, w, 1, true) and tier < 3 then tier = 3 end end if tier < 2 then tier = 2 end end end) if not ok then return 1 end return tier end function best_target() if you.feel_safe() then return nil end local r = you.los() local bx, by, bname, btier, bscore = nil, nil, nil, 0, -1 for x = -r, r do for y = -r, r do if not (x == 0 and y == 0) then pcall(function() local m = monster.get_monster_at(x, y) if not m or m:attitude() ~= 0 or m:is_firewood() then return end local tier = target_tier(m) local dist = math.max(math.abs(x), math.abs(y)) local score = tier * 1000 + (m:threat() or 0) * 10 - dist if score > bscore then bscore, bx, by, bname, btier = score, x, y, m:name(), tier end end) end end end if not bx then return nil end return bx, by, bname, btier end function score_spell(name, ctx, tx, ty) local t = spell_intel(name) local score = t.avg > 0 and t.avg or 4 -- non-damaging spells get a floor local key = SPELL_DMG[name] if key and ctx then local v = ctx.resists[key] or 0 if v >= 3 then return -1 end -- immune: never pick it if v > 0 then score = score / (v + 1) end end if tx and t.range > 0 then local dist = math.max(math.abs(tx), math.abs(ty)) if dist > t.range then return -1 end end local mp = select(1, you.mp()) local cost = spells.mana_cost(name) or 0 if cost > mp then return -1 end score = score * (1 - 0.15 * math.max(0, t.sev - 1)) if t.hated then score = score * 0.5 end if ctx and ctx.abjurer and builtin_class and builtin_class(name) == "5" then return -1 end return score end function cast_best() local live = {} for _, k in ipairs({"1", "2", "3", "5"}) do local _, names = live_spells(k) for _, n in ipairs(names or {}) do live[#live + 1] = n end end if #live == 0 then crawl.mpr("No live attack spells.") return end local tx, ty, tname, tier = best_target() local ctx = CTX and ctx_scan() or nil local best, bs = nil, 0 for _, n in ipairs(live) do local s = score_spell(n, ctx, tx, ty) if s > bs then best, bs = n, s end end if not best then crawl.mpr("Nothing worth casting at that.") return end if tname then crawl.mpr("" .. best .. " -> " .. tname .. " (" .. (TARGET_TIER_NAMES[tier] or "?") .. ")") end do_cast_at(best, tx, ty) end function do_cast_at(name, dx, dy) if not dx then return do_cast(name) end local ok = pcall(spells.cast, name, dx, dy) if not ok then return do_cast(name) end end SPELLCLASS_SORT_BY_CONTEXT = true SPELLCLASS_ALWAYS_MENU = false SPELLCLASS_SMART_TARGET = true function autotarget_on() if c_persist.smart_target == nil then return SPELLCLASS_SMART_TARGET end return c_persist.smart_target end function toggle_autotarget() c_persist.smart_target = not autotarget_on() if c_persist.smart_target then crawl.mpr("Auto-target ON. Class keys aim at the biggest" .. " threat.") else crawl.mpr("Auto-target OFF. Class keys use the normal targeter," .. " so you pick.") end crawl.flush_prev_message() end function smart_cast(name) if not autotarget_on() then return do_cast(name) end local tx, ty, tname, tier = best_target() if not tx then return do_cast(name) end crawl.mpr("-> " .. tname .. " (" .. (TARGET_TIER_NAMES[tier] or "?") .. ")") return do_cast_at(name, tx, ty) end function spellclass_cast(key) local anchored, stale = anchor_of(key) if anchored then return smart_cast(anchored) end if stale then crawl.mpr("Slot " .. key .. " is anchored to " .. stale .. ", which is not memorised. Falling back to the class." .. "") end local class, live, suppressed = live_spells(key) if SPELLCLASS_SORT_BY_CONTEXT and #live > 1 and not you.feel_safe() then local ctx = CTX and ctx_scan() or nil local tx, ty = best_target() local sc = {} for _, n in ipairs(live) do sc[n] = score_spell(n, ctx, tx, ty) end table.sort(live, function(a, b) return (sc[a] or 0) > (sc[b] or 0) end) end if not class then crawl.mpr("No spell class configured for '" .. key .. "'.") return end if #live == 0 then if suppressed and suppressed > 0 then crawl.mpr("" .. class.label .. ": " .. suppressed .. " memorised but hidden. Press 0 to review.") else crawl.mpr("" .. class.label .. ": nothing memorised.") end return end if (#live == 1 and not SPELLCLASS_ALWAYS_MENU) or class.no_menu then smart_cast(live[1]) return end if #live > 9 then crawl.mpr("" .. class.label .. ": " .. (#live - 9) .. " past the ninth are unreachable -- hide some with 0.") while #live > 9 do table.remove(live) end end crawl.mpr("" .. class.label .. "") local ctx = CTX and ctx_scan() or nil for i, name in ipairs(live) do crawl.mpr(menu_line(i, name) .. (ctx and ctx_spell_note(name, ctx) or "") .. spell_intel_note(name)) end crawl.flush_prev_message() local ch = crawl.getch() local pick = ch - 48 -- ASCII '1' == 49 if pick >= 1 and pick <= #live then smart_cast(live[pick]) else crawl.mpr("Cancelled.") end end function spellclass_hide(key) local class = SPELL_CLASSES[key] if not class then crawl.mpr("No spell class configured for '" .. key .. "'.") return end local _, entries = entries_for(key) local st = state_set() local set = known_set() local known = {} for _, entry in ipairs(entries) do if is_known(entry.spell, set) then known[#known + 1] = entry end end local ord = order_sync() table.sort(known, function(a, b) return seq_of(ord, a.spell) < seq_of(ord, b.spell) end) if #known == 0 then crawl.mpr("" .. class.label .. ": no memorised spells.") return end crawl.mpr("" .. class.label .. " -- pick one to show or hide") for i, entry in ipairs(known) do local name = entry.spell local s = st[name] local why = retired(entry, set) local mark, note if s == "hidden" then mark, note = "[hidden]", "" else mark = "[shown] " note = why and " (superseded by " .. why .. ")" or "" end if entry.moved then note = note .. " (moved here)" end crawl.mpr(string.format("%d) %s %s%s", i, mark, name, note) .. spell_intel_note(name)) end crawl.flush_prev_message() local pick = crawl.getch() - 48 if pick < 1 or pick > #known then crawl.mpr("Cancelled.") return end local name = known[pick].spell if st[name] == "hidden" then st[name] = nil crawl.mpr("Shown: " .. name .. "") else st[name] = "hidden" crawl.mpr("Hidden: " .. name .. "") end end function spellclass_move() local assign = assign_set() local known = you.spells() if #known == 0 then crawl.mpr("You don't know any spells yet.") return end table.sort(known) crawl.mpr("Move a spell -- pick one") for i, name in ipairs(known) do if i > 9 then break end local b, a = builtin_class(name), assign[name] local where if a then where = "[" .. a .. "]" elseif b then where = "[" .. b .. "]" else where = "[ -- unclassified ]" end crawl.mpr(string.format("%d) %s %s", i, where, name)) end if #known > 9 then crawl.mpr("(" .. (#known - 9) .. " more; only the first nine are reachable here)") end crawl.flush_prev_message() local pick = crawl.getch() - 48 if pick < 1 or pick > math.min(#known, 9) then crawl.mpr("Cancelled.") return end local name = known[pick] crawl.mpr("Move " .. name .. " to which class?") for _, k in ipairs({"1","2","3","4","5"}) do local c = SPELL_CLASSES[k] if c then crawl.mpr(k .. ") " .. c.label) end end crawl.mpr("0) clear the override, use the built-in class") crawl.mpr("F) leave the class alone, move it to position 1") crawl.flush_prev_message() local ch = crawl.getch() local key = (ch >= 32 and ch < 127) and string.char(ch) or "" if key == "F" or key == "f" then order_to_front(name) crawl.mpr("" .. name .. " moved to position 1 of its class.") elseif key == "0" then assign[name] = nil local b = builtin_class(name) crawl.mpr("" .. name .. " back to " .. (b and ("class " .. b) or "unclassified") .. ".") elseif SPELL_CLASSES[key] then assign[name] = key crawl.mpr("" .. name .. " -> " .. SPELL_CLASSES[key].label .. " (press " .. key .. ")") else crawl.mpr("Cancelled.") end end function spellclass_diagnose() local assign = assign_set() local st = state_set() local ord = order_sync() local known = you.spells() if #known == 0 then crawl.mpr("you.spells() returned nothing -- no spells memorised.") return end table.sort(known) crawl.mpr("spell / built-in class / override / state / order") for _, name in ipairs(known) do crawl.mpr(string.format("%-26s %-3s %-3s %-8s %s", name, builtin_class(name) or "--", assign[name] or "-", st[name] or "shown", tostring(ord[string.lower(name)] or "-"))) end end function spellclass_reset() c_persist.spell_assign = nil c_persist.spell_state = nil c_persist.spell_order = nil c_persist.spell_hidden = nil crawl.mpr("Spell overrides cleared: classes, hide/pin, order.") end function spellclass_lint(quiet_if_clean) local bad = 0 for key, class in pairs(SPELL_CLASSES) do for _, entry in ipairs(class.spells) do local names = { entry.spell } for _, o in ipairs(entry.retire_if_known or {}) do names[#names + 1] = o end for _, n in ipairs(names) do if not spell_is_real(n) then crawl.mpr("[" .. key .. "] unknown spell name: " .. n .. "") bad = bad + 1 end end end end local seen, dupes = {}, 0 for key, class in pairs(SPELL_CLASSES) do for _, entry in ipairs(class.spells) do if seen[entry.spell] then crawl.mpr("spellclass: " .. entry.spell .. " is in both " .. seen[entry.spell] .. " and " .. key .. " -- hide/pin applies to both.") dupes = dupes + 1 else seen[entry.spell] = key end end end if bad == 0 and dupes == 0 and not quiet_if_clean then crawl.mpr("spellclass: all configured names valid.") end return bad end RC_STATE_KEYS = { "worst_hit", "dmg_by", "aut_hist", "spell_order", "spell_state", "spell_assign", "anchor", "smart_target", "ap", } -- end RC_STATE_KEYS local function char_sig() local ok, sig = pcall(function() return (you.name() or "?") .. "/" .. (you.race() or "?") .. "/" .. (you.class() or "?") end) return ok and sig or "?" end function reset_rc_state(quiet) for _, k in ipairs(RC_STATE_KEYS) do c_persist[k] = nil end c_persist.rc_sig = char_sig() c_persist.rc_last_turn = you.turns() if not quiet then crawl.mpr("New character: rc counters and spell layout reset." .. "") end end function new_game_check() local sig = char_sig() local turns = you.turns() local fresh = (c_persist.rc_sig ~= sig) or (c_persist.rc_last_turn and turns < c_persist.rc_last_turn) if fresh then reset_rc_state(c_persist.rc_sig == nil) else c_persist.rc_last_turn = turns end end local function anchors() c_persist.anchor = c_persist.anchor or {} return c_persist.anchor end function anchor_of(key) local a = anchors()[key] if not a then return nil end for _, n in ipairs(you.spells() or {}) do if string.lower(n) == string.lower(a) then return n end end return nil, a end function spellclass_slot(key) local known = you.spells() or {} local live, stale = anchor_of(key) local class = SPELL_CLASSES[key] crawl.mpr("Slot " .. key .. " -- currently " .. (live and ("" .. live .. "") or stale and ("" .. stale .. " (not memorised)") or ("class " .. (class and class.label or "?") .. "")) .. "") if #known == 0 then crawl.mpr("Nothing memorised.") return end for i, name in ipairs(known) do if i > 9 then break end crawl.mpr(string.format("%d) %s%s", i, name, spell_intel_note(name))) end crawl.mpr("0) back to the class" .. (class and (" (" .. class.label .. ")") or "") .. " -) show/hide list") crawl.flush_prev_message() local ch = crawl.getch() if ch == 45 then -- '-' return spellclass_hide(key) end local pick = ch - 48 if pick == 0 then anchors()[key] = nil crawl.mpr("Slot " .. key .. " back to its class.") elseif pick >= 1 and pick <= math.min(#known, 9) then anchors()[key] = known[pick] crawl.mpr("Slot " .. key .. " anchored to " .. known[pick] .. ".") else crawl.mpr("Cancelled.") end end function spellclass_slot_1() spellclass_slot("1") end function spellclass_slot_2() spellclass_slot("2") end function spellclass_slot_3() spellclass_slot("3") end function spellclass_slot_4() spellclass_slot("4") end function spellclass_slot_5() spellclass_slot("5") end function spellclass_1() spellclass_cast("1") end function spellclass_2() spellclass_cast("2") end function spellclass_3() spellclass_cast("3") end function spellclass_4() spellclass_cast("4") end function spellclass_5() spellclass_cast("5") end function spellclass_manage() crawl.mpr("Spell classes -- pick one to show/hide spells in") for _, k in ipairs({"1","2","3","4","5"}) do local c = SPELL_CLASSES[k] if c then crawl.mpr(k .. ") " .. c.label) end end crawl.mpr("L) check all configured spell names") crawl.mpr("M) move a spell to another class") crawl.mpr("D) diagnose: where is each spell and why") crawl.mpr("R) reset all spell overrides") crawl.mpr("A) autopickup categories") crawl.flush_prev_message() local ch = crawl.getch() local key = (ch >= 32 and ch < 127) and string.char(ch) or "" if key == "L" or key == "l" then spellclass_lint() elseif key == "M" or key == "m" then spellclass_move() elseif key == "D" or key == "d" then spellclass_diagnose() elseif key == "R" then spellclass_reset() elseif key == "A" or key == "a" then ap_menu() elseif SPELL_CLASSES[key] then spellclass_hide(key) else crawl.mpr("Cancelled.") end end PANEL_EXPORT = true -- false disables all of this PANEL_EXPORT_SPELLS = false -- the companion window dropped the spells local last_spell_sig = nil local last_mons_sig = nil local function esc(s) return (tostring(s):gsub("[|~\n]", " ")) end local function spell_payload() local parts = {} for _, key in ipairs({"1","2","3","4","5"}) do local class, live = live_spells(key) if class then local names = {} for _, n in ipairs(live) do names[#names+1] = esc(n) end parts[#parts+1] = key .. "=" .. esc(class.label) .. "=" .. table.concat(names, ",") end end return table.concat(parts, "|") end MONS_SPELL_AGENCY = { ["Banishment"] = true, ["Symbol of Torment"] = true, ["Paralyse"] = true, ["Petrify"] = true, ["Malmutate"] = true, ["Mesmerise"] = true, ["Sleep"] = true, ["Confuse"] = true, ["Mass Confusion"] = true, ["Polymorph"] = true, ["Vitrify"] = true, ["Slow"] = true, ["Metabolic Englaciation"] = true, ["Ensorcelled Hibernation"] = true, ["Cause Fear"] = true, ["Discord"] = true, ["Enfeeble"] = true, ["Sap Magic"] = true, ["Dimension Anchor"] = true, ["Sentinel\'s Mark"] = true, ["Corrupting Pulse"] = true, ["Word of Recall"] = true, } -- end MONS_SPELL_AGENCY local function worst_spell(m) local ok, list = pcall(function() return m:spells() end) if not ok or not list or #list == 0 then return nil end local best, best_rank, best_agency = nil, -1, false for _, name in ipairs(list) do local lvl = 0 local ok2, l = pcall(spells.level, name) if ok2 and type(l) == "number" then lvl = l end local agency = MONS_SPELL_AGENCY[name] == true local rank = lvl + (agency and 100 or 0) if rank > best_rank then best, best_rank, best_agency = name, rank, agency end end if not best then return nil end local col = best_agency and "lightmagenta" or (best_rank >= 8 and "lightred" or "brown") return "<" .. col .. ">{" .. esc(best) .. "}", best_agency or best_rank >= 8 end local ATT_HOSTILE_P = 0 local function hp_number(s) return tonumber(tostring(s or ""):match("(%d+)")) or 0 end local function mons_payload() local r = you.los() local rows = {} for x = -r, r do for y = -r, r do if not (x == 0 and y == 0) then local ok, row = pcall(function() local m = monster.get_monster_at(x, y) if not m then return nil end if m:attitude() ~= ATT_HOSTILE_P then return nil end if m:is_firewood() then return nil end local gear = {} for _, it in ipairs(m:items() or {}) do local nm = it.name and it.name("plain") or "?" local tag = "." if it.artefact then tag = "*" elseif it.branded then tag = "+" end gear[#gear+1] = tag .. esc(nm) end local hp = m:max_hp() -- string local hurt = m:damage_desc() -- "" when unhurt if hurt == nil or hurt == "" then hurt = "unhurt" end local function pipstr(n, cap) if not n or n == 0 then return nil end if n < 0 then return "-" end if cap and n >= cap then return "*" end return tostring(n) end local RES = { { "rF", "lightred", m:res_fire() }, { "rC", "lightblue", m:res_cold() }, { "rE", "yellow", m:res_shock() }, { "rP", "lightgreen", m:res_poison() }, { "rX", "brown", m:res_corr() }, { "rN", "lightmagenta", m:res_draining() }, } -- end RES <-- trailing text REQUIRED: a line that TRIMS to } ends the rc lua block local res = {} local scary = false for _, r in ipairs(RES) do local v = pipstr(r[3], 3) if v then if r[3] and r[3] >= 3 then scary = true end res[#res+1] = "<" .. r[2] .. ">" .. r[1] .. v .. "" end end local defs = {} local ac, ev, wl = m:ac(), m:ev(), m:wl() if ac and ac > 0 then local c = ac >= 4 and "white" or "lightgrey" if ac >= 4 then scary = true end defs[#defs+1] = "<" .. c .. ">AC" .. ac .. "" end if ev and ev > 0 then defs[#defs+1] = "EV" .. ev .. "" end if wl and wl >= 125 then scary = true defs[#defs+1] = "WL*" elseif wl and wl > 0 then defs[#defs+1] = "WL" .. math.min(wl, 5) .. "" end local sbase = select(1, mons_rates(m)) local spd = nil if sbase >= 200 then spd = ">>>>" elseif sbase >= 150 then spd = ">>>" elseif sbase > 100 then spd = ">>" elseif sbase <= 60 then spd = "<<<" elseif sbase < 100 then spd = "<<" end local TC = { [0]="darkgrey", [1]="lightgrey", [2]="yellow", [3]="lightred" } local TN = { [0]="triv", [1]="easy", [2]="TOUGH", [3]="NASTY" } local th = m:threat() or 0 local threat = "<" .. TC[th] .. ">[" .. TN[th] .. "]" local sp, sp_scary = worst_spell(m) if sp_scary then scary = true end -- Split into a STATIC half (everything determined by the -- species and its gear) and a DYNAMIC half (how hurt this one -- is). Two monsters merge only when their static halves are -- identical, so "goblin x2" can never hide one carrying a -- distortion weapon while the other holds a club. local pre = { threat } if m:is_unique() then pre[#pre+1] = "*" end pre[#pre+1] = (scary and "! " or "") .. "" .. esc(m:name()) .. "" if spd then pre[#pre+1] = spd end local post = {} if sp then post[#post+1] = sp end for _, d in ipairs(defs) do post[#post+1] = d end for _, d in ipairs(res) do post[#post+1] = d end for _, g in ipairs(gear) do local tag, nm2 = g:sub(1,1), g:sub(2) local c = tag == "*" and "yellow" or tag == "+" and "lightcyan" or "darkgrey" post[#post+1] = "<" .. c .. ">" .. nm2 .. "" end return { pre = table.concat(pre, " "), post = table.concat(post, " "), hp = esc(hp), hpn = hp_number(hp), band = (m:damage_level() or 0), hurt = hurt, } -- end row table end) if ok and row then rows[#rows+1] = row end end end end -- group identical static halves local order, group = {}, {} for _, r in ipairs(rows) do local key = r.pre .. "\1" .. r.post if not group[key] then group[key] = { n = 0, pre = r.pre, post = r.post, hpsum = 0, hps = {}, worst = -1, hurt = "unhurt" } -- new group order[#order+1] = key end local g = group[key] g.n = g.n + 1 g.hpsum = g.hpsum + r.hpn g.hps[r.hp] = true if r.band > g.worst then g.worst, g.hurt = r.band, r.hurt end end local out = {} for _, key in ipairs(order) do local g = group[key] local bits = { g.pre } if g.n > 1 then bits[#bits+1] = "x" .. g.n .. "" end -- one distinct hp string across the group prints as is; several -- print the average, marked with ~ so it is not read as exact local distinct = 0 local only = nil for k in pairs(g.hps) do distinct = distinct + 1; only = k end if distinct == 1 then bits[#bits+1] = "" .. only .. "" else bits[#bits+1] = "~" .. math.floor(g.hpsum / g.n + 0.5) .. " avg" end if g.hurt ~= "unhurt" then bits[#bits+1] = "" .. esc(g.hurt) .. (g.n > 1 and " (worst)" or "") .. "" end if g.post ~= "" then bits[#bits+1] = g.post end out[#out+1] = table.concat(bits, " ") end table.sort(out) return table.concat(out, " | ") end local function resist_divisor(name, m) local key = SPELL_DMG[name] if not key then return 1 end local ok, v = pcall(function() if key == "rF" then return m:res_fire() end if key == "rC" then return m:res_cold() end if key == "rElec" then return m:res_shock() end if key == "rPois" then return m:res_poison() end if key == "rN" then return m:res_draining() end return 0 end) local v2 = (ok and v) or 0 if v2 >= 3 then return nil end -- immune if v2 > 0 then return v2 + 1 end return 1 end local last_time = nil function move_aut_watch() local ok, t = pcall(you.time) if not ok or not t then return end if last_time and t > last_time then local d = t - last_time if d >= 6 and d <= 40 then c_persist.aut_hist = c_persist.aut_hist or {} c_persist.aut_hist[d] = (c_persist.aut_hist[d] or 0) + 1 end end last_time = t end function move_aut() local h = c_persist.aut_hist if not h then return 10 end local best, bn = 10, 0 for d, n in pairs(h) do if n > bn then best, bn = d, n end end return best end function mons_rates(m) local s = "" local ok, v = pcall(function() return m:speed_description() end) if ok and v then s = v end local base = tonumber(s:match("Speed:%s*(%d+)%%")) or 100 local atk = tonumber(s:match("attack:%s*(%d+)%%")) or base local trav = tonumber(s:match("travel:%s*(%d+)%%")) or base local rel = s:match("normally travels (%a+) than you") return base, atk, trav, rel end function hit_chance(name, m) local body = "" local ok, v = pcall(spells.describe, name) if ok and v then body = v end local bar = body:match("Accuracy:%s*([%+%s]+)") if not bar then return 1.0 end -- no to-hit roll local acc = 0 for _ in bar:gmatch("%+") do acc = acc + 1 end local ev = 0 local ok2, e = pcall(function() return m:ev() end) if ok2 and type(e) == "number" then ev = e end local p = 0.55 + 0.13 * acc - 0.14 * ev if p > 0.95 then p = 0.95 end if p < 0.15 then p = 0.15 end return p end local BAND_LO = { [0]=1.00, [1]=0.80, [2]=0.60, [3]=0.40, [4]=0.20, [5]=0.00 } local BAND_HI = { [0]=1.00, [1]=1.00, [2]=0.80, [3]=0.60, [4]=0.40, [5]=0.20 } DMG_TRACK = {} function band_hp(m) local hp = hp_number(m:max_hp()) if hp <= 0 then return nil end local ok, b = pcall(function() return m:damage_level() end) if not ok or type(b) ~= "number" then return nil end if b < 0 then b = 0 end if b > 5 then b = 5 end return math.floor(hp * (BAND_LO[b] or 0)), math.ceil(hp * (BAND_HI[b] or 1)), hp, b end function hp_estimate(m) local lo, hi, max = band_hp(m) if not lo then return nil end local key = m:name() local prev = DMG_TRACK[key] DMG_TRACK[key] = { lo = lo, hi = hi } local s = "~" .. lo .. "-" .. hi .. " of " .. max .. "" if prev and prev.lo > lo then local dmin = math.max(0, prev.lo - hi) local dmax = math.max(0, prev.hi - lo) if dmax > 0 then s = s .. " (-" .. dmin .. " to -" .. dmax .. " last turn)" end end return s end BLOCK_ORDER = "down" function emit_block(lines) if not lines then return end if BLOCK_ORDER == "up" then for i = #lines, 1, -1 do crawl.mpr(lines[i]) end else for i = 1, #lines do crawl.mpr(lines[i]) end end end -- One line while things are ordinary, the full block once the risk bar -- reads SEVERE or worse. A 9 line message pane cannot afford six lines -- of detail on every goblin. FOCUS_COMPACT = true FOCUS_EXPAND_AT = 12 -- ---- melee estimate ------------------------------------------------ -- Rough on purpose, and marked as such wherever it prints. Crawl's melee -- pipeline is stat mods, skill mods, brand procs, AC reduction and a -- non-uniform damage roll; none of that is exposed. What IS exposed on -- an item is damage, accuracy, plus, delay and weap_skill, and -- you.skill() gives the training. That is enough for "is swinging -- better than casting", which is the only question being asked. function weapon_estimate(m) local w = items.equipped_at("weapon") if not w or not w.is_weapon then return nil end local base = (w.damage or 0) + (w.plus or 0) if base <= 0 then return nil end -- delay drops with skill to a floor, the same shape as the real one local sk = 0 local ok, v = pcall(function() return you.skill(w.weap_skill) end) if ok and type(v) == "number" then sk = v end local delay = math.max(3, (w.delay or 10) - sk / 2) / 10 -- to hit, against EV in pips local ev = 0 local ok2, e = pcall(function() return m:ev() end) if ok2 and type(e) == "number" then ev = e end local acc = (w.accuracy or 0) + (w.plus or 0) local p = 0.60 + 0.04 * acc - 0.14 * ev if p > 0.95 then p = 0.95 end if p < 0.15 then p = 0.15 end -- average roll is below the maximum; 0.55 is a fit, not a formula local per_turn = base * 0.55 * p / delay return per_turn, w.name("plain"), p, delay end -- Total hostile HP in view, for "how long does this whole room take". function room_hp() local r, total, n = you.los(), 0, 0 for x = -r, r do for y = -r, r do if not (x == 0 and y == 0) then pcall(function() local m = monster.get_monster_at(x, y) if not m or m:attitude() ~= 0 or m:is_firewood() then return end local _, hi = band_hp(m) total = total + (hi or hp_number(m:max_hp())) n = n + 1 end) end end end return total, n end function focus_report() if not PANEL_EXPORT then return nil end local tx, ty, tname, tier = best_target() if not tx then return nil end local m = monster.get_monster_at(tx, ty) if not m then return nil end local ctx = CTX and ctx_scan() or nil local dist = math.max(math.abs(tx), math.abs(ty)) local live = {} for _, k in ipairs({"1", "2", "3", "5"}) do local _, names = live_spells(k) for _, n in ipairs(names or {}) do live[#live + 1] = n end end local best, bexp, bhit, bdiv = nil, 0, 1, 1 for _, n in ipairs(live) do local div = resist_divisor(n, m) if div then local t = spell_intel(n) local hit = hit_chance(n, m) local expected = (t.avg / div) * hit local cost = spells.mana_cost(n) or 1 local rank = expected + expected / math.max(1, cost) * 0.25 if expected > 0 and rank > bexp then best, bexp, bhit, bdiv = n, rank, hit, div end end end local lines = {} local tcol = ({ [0]="darkgrey", [1]="lightgrey", [2]="yellow", [3]="lightred" }) [m:threat() or 0] if FOCUS_COMPACT and (LAST_RISK or 0) < FOCUS_EXPAND_AT then local lo, hi = band_hp(m) local best2, per2, cost2 = nil, 0, 0 for _, k in ipairs({"1", "2", "3", "5"}) do local _, names = live_spells(k) for _, n in ipairs(names or {}) do local div = resist_divisor(n, m) if div then local e = (spell_intel(n).avg / div) * hit_chance(n, m) if e > per2 then best2, per2 = n, e; cost2 = spells.mana_cost(n) or 1 end end end end local turns = (per2 > 0 and hi and hi > 0) and math.ceil(hi / per2) or nil local one = " >> " .. esc(tname) .. " <" .. tcol .. ">[" .. (TARGET_TIER_NAMES[tier] or "?") .. "]" .. " " .. (hi or "?") .. "hp, " .. dist .. " away" if best2 then one = one .. " | " .. best2 .. "" .. " ~" .. string.format("%.0f", per2) .. "" .. (turns and (" | " .. turns .. " turn" .. (turns == 1 and "" or "s") .. "") or "") else one = one .. " | nothing lands" end return { one } end local hurt = m:damage_desc() lines[#lines + 1] = " >> " .. esc(tname) .. " <" .. tcol .. ">[" .. (TARGET_TIER_NAMES[tier] or "?") .. "] " .. (hurt and hurt ~= "" and (esc(hurt) .. ", ") or "") .. dist .. " away" local est = hp_estimate(m) if est then lines[#lines + 1] = " hp: " .. est end local base, atk, trav, rel = mons_rates(m) local mine = move_aut() local speed_bits = {} if rel then local c = rel == "faster" and "lightred" or "lightgreen" speed_bits[#speed_bits + 1] = "<" .. c .. ">moves " .. rel .. " than you" else speed_bits[#speed_bits + 1] = "moves as you do" end local acol = atk > 100 and "lightred" or atk < 100 and "lightgreen" or "lightgrey" speed_bits[#speed_bits + 1] = "<" .. acol .. ">acts at " .. atk .. "%" lines[#lines + 1] = " speed: " .. table.concat(speed_bits, " | ") if dist <= 1 then local hits = atk / 100 * mine / 10 local c = hits >= 1.5 and "lightred" or hits > 1 and "yellow" or "lightgreen" lines[#lines + 1] = " <" .. c .. ">stepping away gives it ~" .. string.format("%.1f", hits) .. " attacks" .. " (your move " .. mine .. " aut)" end if not best then lines[#lines + 1] = " best: nothing that will land" return lines end local t = spell_intel(best) local per = (t.avg / bdiv) * bhit local cost = spells.mana_cost(best) or 1 local reach = (t.range > 0 and dist > t.range) and " (out of range, close " .. (dist - t.range) .. ")" or "" lines[#lines + 1] = " best: " .. best .. " ~" .. string.format("%.0f", per) .. " dmg/cast " .. cost .. " MP, " .. string.format("%.0f", bhit * 100) .. "% to hit" .. reach local _, hi = band_hp(m) -- melee, and whether it beats the spell local wdmg, wname, wp = weapon_estimate(m) if wdmg and wdmg > 0 then local better = wdmg > per local c = better and "lightgreen" or "darkgrey" lines[#lines + 1] = " <" .. c .. ">melee: " .. esc(wname or "?") .. " ~" .. string.format("%.0f", wdmg) .. "/turn " .. string.format("%.0f", (wp or 0) * 100) .. "% to hit" .. (better and " (better than casting)" or " (casting is better)") end local best_per = math.max(per, wdmg or 0) local hp = hi or hp_number(m:max_hp()) if per > 0 and hp > 0 then local turns = math.ceil(hp / best_per) local mp = select(1, you.mp()) local afford = cost > 0 and math.floor(mp / cost) or turns local col = turns <= 2 and "lightgreen" or turns <= 5 and "yellow" or "lightred" local note = "" if afford == 0 then note = " (not enough MP for one cast)" elseif afford < turns then note = " (MP for " .. afford .. (afford == 1 and " cast" or " casts") .. ")" end lines[#lines + 1] = " <" .. col .. ">Estimated Turns of Combat: " .. turns .. "" .. note -- and the whole room, since one target at a time is not the fight local rtotal, rn = room_hp() if rn > 1 and best_per > 0 then local rturns = math.ceil(rtotal / best_per) local rc = rturns <= 6 and "lightgreen" or rturns <= 15 and "yellow" or "lightred" lines[#lines + 1] = " all " .. rn .. " in view: ~" .. rtotal .. " hp, <" .. rc .. ">" .. rturns .. " turns" end end return lines end function panel_export() if not PANEL_EXPORT then return end if PANEL_EXPORT_SPELLS then local ok, sp = pcall(spell_payload) if ok and sp ~= last_spell_sig then last_spell_sig = sp crawl.mpr("##DCP-S|" .. sp) end end local ok2, mp = pcall(mons_payload) if ok2 and mp ~= last_mons_sig then last_mons_sig = mp local header = (mp ~= "") and mp or nil if BLOCK_ORDER == "up" then emit_block(focus_report()) if header then crawl.mpr(header) end else if header then crawl.mpr(header) end emit_block(focus_report()) end end end SPELL_DMG = { ["Acid Ball"]="rPois", ["Agonising Touch"]="rN", ["Agony"]="rN", ["Airstrike"]="rElec", ["Alistair's Intoxication"]="rPois", ["Anguish"]="rN", ["Arcjolt"]="rElec", ["Awaken Flesh"]="rN", ["Beckoning Gale"]="rElec", ["Blinkbolt"]="rElec", ["Bolt of Cold"]="rC", ["Bolt of Draining"]="rN", ["Bolt of Fire"]="rF", ["Bolt of Magma"]="rF", ["Borgnjor's Revivification"]="rN", ["Borgnjor's Vile Clutch"]="rN", ["Brain Bite"]="rN", ["Call Down Lightning"]="rElec", ["Caustic Breath"]="rPois", ["Chain Lightning"]="rElec", ["Cold Breath"]="rC", ["Combustion Breath"]="rF", ["Conjure Ball Lightning"]="rElec", ["Corrosive Bolt"]="rPois", ["Corrupting Pulse"]="rPois", ["Creeping Frost"]="rC", ["Creeping Shadow"]="rC", ["Curse of Agony"]="rN", ["Death's Door"]="rN", ["Dispel Undead"]="rN", ["Dispel Undead Range"]="rN", ["Dominate Undead"]="rN", ["Drain Life"]="rN", ["Draining Gaze"]="rN", ["Electrical Bolt"]="rElec", ["Ensorcelled Hibernation"]="rC", ["Eringya's Noxious Bog"]="rPois", ["Eruption"]="rF", ["Fire Breath"]="rF", ["Fire Storm"]="rF", ["Fireball"]="rF", ["Flame Wave"]="rF", ["Flaming Cloud"]="rF", ["Flash Freeze"]="rC", ["Flay"]="rN", ["Foxfire"]="rF", ["Freeze"]="rC", ["Frozen Ramparts"]="rC", ["Fulminant Prism"]="rPois", ["Fulsome Fusillade"]="rPois", ["Funeral Dirge"]="rN", ["Galvanic Breath"]="rElec", ["Ghostly Fireball"]="rN", ["Ghostly Sacrifice"]="rN", ["Glacial Breath"]="rC", ["Glaciate"]="rC", ["Gloom"]="rN", ["Grave Claw"]="rN", ["Hailstorm"]="rC", ["Hoarfrost Bullet"]="rC", ["Hurl Sludge"]="rPois", ["Hurl Torchlight"]="rN", ["Iceblast"]="rC", ["Ignition"]="rF", ["Infestation"]="rN", ["Inner Flame"]="rF", ["Irradiate"]="rPois", ["Kiss of Death"]="rN", ["Leda's Liquefaction"]="rPois", ["Lightning Bolt"]="rElec", ["Magma Barrage"]="rF", ["Magnavolt"]="rElec", ["Malign Offering"]="rN", ["Malmutate"]="rPois", ["Marshlight"]="rF", ["Maxwell's Capacitive Coupling"]="rElec", ["Mercury Arrow"]="rPois", ["Metabolic Englaciation"]="rC", ["Mourning Wail"]="rN", ["Noxious Breath"]="rElec", ["Olgreb's Toxic Radiance"]="rPois", ["Orb of Electricity"]="rElec", ["Ozocubu's Armour"]="rC", ["Ozocubu's Refrigeration"]="rC", ["Pain"]="rN", ["Permafrost Eruption"]="rC", ["Petrify"]="rPois", ["Petrifying Cloud"]="rElec", ["Poison Arrow"]="rPois", ["Polar Vortex"]="rC", ["Polymorph"]="rPois", ["Porkalator"]="rPois", ["Primal Wave"]="rC", ["Pyre Arrow"]="rF", ["Pyroclastic Surge"]="rF", ["Ravenous Swarm"]="rN", ["Rebounding Blaze"]="rF", ["Rebounding Chill"]="rC", ["Scorch"]="rF", ["Searing Breath"]="rF", ["Seracfall"]="rC", ["Shadow Draining"]="rN", ["Shadow Prism"]="rPois", ["Shadow Tempest"]="rElec", ["Shadowball"]="rF", ["Shock"]="rElec", ["Sign of Ruin"]="rN", ["Silence"]="rElec", ["Siphon Essence"]="rN", ["Soul Splinter"]="rN", ["Spectral Cloud"]="rN", ["Spit Acid"]="rPois", ["Spit Lava"]="rF", ["Spit Poison"]="rPois", ["Starburst"]="rF", ["Static Discharge"]="rElec", ["Sting"]="rPois", ["Stoke Flames"]="rF", ["Stunning Burst"]="rElec", ["Sublimation of Blood"]="rN", ["Symbol of Torment"]="rN", ["Throw Flame"]="rF", ["Throw Frost"]="rC", ["Throw Icicle"]="rC", ["Thunderbolt"]="rElec", ["Vampiric Draining"]="rN", ["Venom Bolt"]="rPois", ["Vhi's Electric Charge"]="rElec", ["Vhi's Electrolunge"]="rElec", ["Virulence"]="rPois", ["Volatile Blastmotes"]="rF", ["Waterstrike"]="rC", ["Wind Blast"]="rElec", ["Yara's Violent Unravelling"]="rPois", } -- end SPELL_DMG CTX = true CTX_INCOMING = { { "fire damage", "rF" }, { "cold damage", "rC" }, { "electric damage", "rElec" }, { "strong poison", "rPois" }, { "poison", "rPois" }, { "drain life", "rN" }, { "acid damage", "rPois" }, } -- end CTX_INCOMING local function my_res(key) local ok, v = pcall(function() if key == "rF" then return you.res_fire() end if key == "rC" then return you.res_cold() end if key == "rElec" then return you.res_shock() end if key == "rPois" then return you.res_poison() end if key == "rN" then return you.res_draining() end return 0 end) return (ok and v) or 0 end local function mons_res(m, key) local ok, v = pcall(function() if key == "rF" then return m:res_fire() end if key == "rC" then return m:res_cold() end if key == "rElec" then return m:res_shock() end if key == "rPois" then return m:res_poison() end if key == "rN" then return m:res_draining() end return 0 end) return (ok and v) or 0 end local incoming_cache = {} local function incoming_types(m) local name = m:name() local hit = incoming_cache[name] local types = hit or {} if not hit then local ok, body = pcall(function() return m:desc(true) end) if ok and body then body = strip_negations(string.lower(body)) for _, c in ipairs(CTX_INCOMING) do if string.find(body, c[1], 1, true) then types[c[2]] = true end end end incoming_cache[name] = types end local out = {} for k in pairs(types) do out[k] = true end for _, sp in ipairs(m:spells() or {}) do local r = SPELL_DMG[sp] if r then out[r] = true end end return out end local function has_abjuration(m) for _, sp in ipairs(m:spells() or {}) do if sp == "Abjuration" or string.find(sp, "Abjur") then return true end end return false end function ctx_scan() local r = you.los() local ctx = { unresisted = {}, resists = {}, abjurer = nil, high_wl = false } for x = -r, r do for y = -r, r do if not (x == 0 and y == 0) then pcall(function() local m = monster.get_monster_at(x, y) if not m or m:attitude() ~= 0 or m:is_firewood() then return end for key in pairs(incoming_types(m)) do local mine = my_res(key) if mine <= 0 then ctx.unresisted[key] = (ctx.unresisted[key] or 0) + (mine < 0 and 2 or 1) end end for _, key in ipairs({"rF","rC","rElec","rPois","rN"}) do local v = mons_res(m, key) if v > (ctx.resists[key] or 0) then ctx.resists[key] = v end end if has_abjuration(m) then ctx.abjurer = m:name() end if (m:wl() or 0) >= 125 then ctx.high_wl = true end end) end end end return ctx end function ctx_labels(ctx) local out = {} for key, n in pairs(ctx.unresisted) do local mine = my_res(key) out[#out + 1] = key .. (mine < 0 and "!" or "-") .. (n > 1 and ("x" .. n) or "") end table.sort(out) if ctx.abjurer then out[#out + 1] = "abjurer" end if ctx.high_wl then out[#out + 1] = "WL-immune" end return out end function ctx_multiplier(ctx) local n = 0 for _, v in pairs(ctx.unresisted) do n = n + v end return 1.0 + math.min(1.0, n * 0.15) end function ctx_spell_note(name, ctx) local key = SPELL_DMG[name] if key then local v = ctx.resists[key] or 0 if v >= 3 then return " x" .. key .. "*" elseif v > 0 then return " x" .. key .. string.rep("+", v) .. "" end end if ctx.abjurer then local cls = builtin_class and builtin_class(name) if cls == "5" then return " abjurer" end end return "" end RISK_BAR = true RISK_CELLS = 10 -- width of the bar RISK_FULL = 20 -- score that fills the bar RISK_HAZARDS = { { "petrif", "petrify", 7 }, { "paralys", "paralyse", 7 }, { "banish", "banish", 7 }, { "torment", "torment", 6 }, { "malmutat", "mutate", 5 }, { "distortion", "distort", 5 }, { "curare", "curare", 4 }, { "drain magic", "antimagic", 3 }, { "reduce willpower", "will-", 3 }, { "ensnare", "web", 3 }, { "constrict", "constrict", 3 }, { "drain life", "drain", 3 }, { "drive defenders berserk", "berserk", 3 }, { "cause confusion", "confuse", 3 }, { "charm", "charm", 4 }, { "engulf", "engulf", 2 }, { "steal items", "steal", 2 }, { "strong poison", "poison+", 2 }, { "drain speed", "slow", 2 }, } -- end RISK_HAZARDS ESCAPE_ITEMS = { "teleportation", "blinking", "fog", "haste", "heal wounds", "curing", "resistance", "might", "invisibility", "cancellation", } -- end ESCAPE_ITEMS local THREAT_W = { [0] = 0, [1] = 1, [2] = 3, [3] = 6 } function speed_weight(m) local base = select(1, mons_rates(m)) if base >= 200 then return 3 end if base >= 150 then return 2 end if base > 100 then return 1 end return 0 end local hazard_cache = {} -- The description is prose, and prose negates. A drude's entry reads -- "It is immune to torment; and resistant to steam and miasma", which a -- plain substring scan turns into "this thing torments you". Drop whole -- sentences that grant or deny a property before looking for hazards. local NEGATED = { "immune to", "resistant to", "unaffected by", "cannot be", "protects against", "does not", } -- end NEGATED function strip_negations(body) for _, phrase in ipairs(NEGATED) do -- a sentence is everything up to the next full stop body = body:gsub("[^%.]*" .. phrase .. "[^%.]*%.", " ") end return body end local function hazards_of(m) local name = m:name() local hit = hazard_cache[name] if hit then return hit end local ok, body = pcall(function() return m:desc(true) end) local found = {} if ok and body then body = strip_negations(string.lower(body)) for _, h in ipairs(RISK_HAZARDS) do if string.find(body, h[1]) then found[#found + 1] = h end end end hazard_cache[name] = found return found end local function risk_score() local r = you.los() local total, labels, worst, worst_s = 0, {}, nil, -1 for x = -r, r do for y = -r, r do if not (x == 0 and y == 0) then pcall(function() local m = monster.get_monster_at(x, y) if not m or m:attitude() ~= 0 or m:is_firewood() then return end local s = THREAT_W[m:threat() or 0] or 0 if m:is_unique() then s = s + 2 end s = s + speed_weight(m) for _, h in ipairs(hazards_of(m)) do s = s + h[3] labels[h[2]] = true end for _, sp in ipairs(m:spells() or {}) do if MONS_SPELL_AGENCY[sp] then s = s + 5 labels[string.lower(sp)] = true end end total = total + s if s > worst_s then worst_s, worst = s, m:name() end end) end end end local list = {} for k in pairs(labels) do list[#list + 1] = k end table.sort(list) return total, list, worst end local RISK_LEVELS = { { 0, "clear", "darkgrey" }, { 1, "low", "green" }, { 3, "moderate", "yellow" }, { 6, "high", "brown" }, { 12, "SEVERE", "lightred" }, { 20, "LETHAL", "lightmagenta"}, } -- end RISK_LEVELS local function level_of(score) local best = RISK_LEVELS[1] for _, l in ipairs(RISK_LEVELS) do if score >= l[1] then best = l end end return best end local last_risk = nil LAST_RISK = 0 -- The bar shows one number built from threat, hazards, speed and the -- context multiplier. This shows the working, because the weights are -- judgement calls and you cannot retune what you cannot see. function risk_explain() if you.feel_safe() then crawl.mpr("Nothing hostile in view.") return end local r = you.los() local total = 0 crawl.mpr("Risk breakdown") for x = -r, r do for y = -r, r do if not (x == 0 and y == 0) then pcall(function() local m = monster.get_monster_at(x, y) if not m or m:attitude() ~= 0 or m:is_firewood() then return end local parts = {} local th = m:threat() or 0 local s2 = THREAT_W[th] or 0 parts[#parts+1] = "threat " .. s2 if m:is_unique() then s2 = s2 + 2; parts[#parts+1] = "unique +2" end local sw = speed_weight(m) if sw > 0 then s2 = s2 + sw; parts[#parts+1] = "speed +" .. sw end for _, h in ipairs(hazards_of(m)) do s2 = s2 + h[3] parts[#parts+1] = h[2] .. " +" .. h[3] end for _, sp in ipairs(m:spells() or {}) do if MONS_SPELL_AGENCY[sp] then s2 = s2 + 5 parts[#parts+1] = string.lower(sp) .. " +5" end end total = total + s2 crawl.mpr(" " .. esc(m:name()) .. " " .. s2 .. " = " .. table.concat(parts, ", ") .. "") end) end end end local ctx = CTX and ctx_scan() or nil local mult = ctx and ctx_multiplier(ctx) or 1 local labels = ctx and ctx_labels(ctx) or {} crawl.mpr(" raw " .. total .. " x context " .. string.format("%.2f", mult) .. (#labels > 0 and (" (" .. table.concat(labels, " ") .. ")") or "") .. " = " .. math.floor(total * mult) .. "") crawl.flush_prev_message() end function risk_bar(force) if not RISK_BAR then return end if you.feel_safe() then if last_risk ~= nil then last_risk = nil end return end local score, labels, worst = risk_score() local ctx = CTX and ctx_scan() or nil if ctx then score = math.floor(score * ctx_multiplier(ctx)) for _, l in ipairs(ctx_labels(ctx)) do labels[#labels + 1] = l end end LAST_RISK = score local lvl = level_of(score) local sig = lvl[2] .. "|" .. table.concat(labels, ",") if not force and sig == last_risk then return end last_risk = sig if score == 0 then return end local filled = math.min(RISK_CELLS, math.ceil(score * RISK_CELLS / RISK_FULL)) local bar = string.rep("#", filled) .. string.rep(".", RISK_CELLS - filled) local msg = "<" .. lvl[3] .. ">RISK [" .. bar .. "] " .. lvl[2] .. "" if #labels > 0 then msg = msg .. " " .. table.concat(labels, " ") .. "" end if worst then msg = msg .. " (" .. worst .. ")" end crawl.mpr(msg) if score >= 12 then you.stop_activity() end if score >= 20 then crawl.redraw_screen() crawl.more() end if score >= 12 then local outs = {} for _, inv in ipairs(items.inventory() or {}) do local ok, nm = pcall(function() return string.lower(inv.name("plain")) end) if ok and nm then local q = pcall(function() return inv.quantity end) and inv.quantity or 1 for _, e in ipairs(ESCAPE_ITEMS) do if string.find(nm, e, 1, true) then outs[#outs + 1] = (q or 1) .. "x " .. e break end end end end if #outs > 0 then crawl.mpr("outs: " .. table.concat(outs, ", ") .. "") else crawl.mpr("outs: none carried") end end end function ready() clear_single_turn_mutes() new_game_check() if not spellclass_linted then spellclass_linted = true spellclass_lint(true) end if not skills_set and you.turns() == 0 then crawl.sendkeys("m") skills_set = true end force_mores() hp_watch() move_aut_watch() ATTACK_LOG = {} risk_bar() floor_watch() panel_export() load_message() end }