#################################### Begin lua/core/_header.lua ################################### ############## https://github.com/brianfaires/crawl-rc/blob/main/lua/core/_header.lua ############# { BRC = {} BRC.Config = {} -- Specify a config by name, or "ask" to prompt at start of each new game BRC.Config.to_use = "Custom" } ##################################### End lua/core/_header.lua #################################### ################################################################################################### ################################### Begin lua/config/custom.lua ################################### ############# https://github.com/brianfaires/crawl-rc/blob/main/lua/config/custom.lua ############# { --- Custom Main Config: Personalized settings -- Aims to list the most commonly adjusted config settings. -- See feature config sections, or config/explicit.lua for more settings. brc_config_custom = { BRC_CONFIG_NAME = "Custom", mouse_input = true, emojis = true, ["misc-alerts"] = { alert_low_hp_threshold = 35, -- % max HP to alert; 0 to disable preferred_god = nil, -- Stop on first altar with this text (Ex. "Wu Jian", "Ash"); nil disables }, ["announce-hp-mp"] = { dmg_flash_threshold = 0.20, -- Flash screen when losing this % of max HP dmg_fm_threshold = 0.30, -- Force more for losing this % of max HP always_on_bottom = false, -- Rewrite HP/MP meters after each turn with messages }, ["color-inscribe"] = { disabled = true, -- Worth trying this out, but inconsistently supported on webtiles (CAO) }, ["fm-messages"] = { force_more_threshold = 6, -- How many force_more_messages; 1=many; 10=none }, ["inscribe-stats"] = { inscribe_weapons = true, -- Inscribe weapon stats on pickup and keep updated inscribe_armour = true, -- Inscribe armour stats on pickup and keep updated }, ["remind-id"] = { stop_on_scrolls_count = 2, -- Stop when largest un-ID'd scroll stack increases and is >= this stop_on_pots_count = 3, -- Stop when largest un-ID'd potion stack increases and is >= this }, ["runrest-features"] = { ignore_portal_exits = true, -- don't stop explore on portal exits temple_search = true, -- on enter or explore, auto-search altars gauntlet_search = true, -- on enter or explore, auto-search gauntlet with filters }, ["startup"] = { show_skills_menu = false, -- Open skills menu on startup auto_set_skill_targets = { { "Stealth", 2.0 }, -- First, focus stealth to 2.0 { "Fighting", 2.0 }, -- If already have stealth, focus fighting to 2.0 }, }, ["pickup-alert"] = { Pickup = { armour = true, staves = true, weapons = true, weapons_pure_upgrades_only = true, -- Only pick up better versions of same exact weapon }, Alert = { armour_sensitivity = 1, -- Adjust all armour alerts; range [0.5-2.0]; 0 to disable weapon_sensitivity = 1, -- Adjust all weapon alerts; range [0.5-2.0]; 0 to disable orbs = true, -- Unique orbs staff_resists = true, -- When a staff gives a missing resistance talismans = true, -- Alert talismans, if their min skill <= Shapeshifting + talisman_lvl_diff talisman_lvl_diff = you.class() == "Shapeshifter" and 27 or 6, -- Alert the first time each item is found. Can require training with OTA_require_skill. one_time = { "wand of digging", "buckler", "kite shield", "tower shield", "crystal plate armour", "gold dragon scales", "pearl dragon scales", "storm dragon scales", "shadow dragon scales", "quick blade", "demon blade", "eudemon blade", "double sword", "triple sword", "broad axe", "executioner's axe", "demon whip", "eveningstar", "giant spiked club", "morningstar", "sacred scourge", "lajatang", "bardiche", "demon trident", "partisan", "trishula", "hand cannon", "triple crossbow", }, OTA_require_skill = { weapon = 2, armour = 2.5, shield = 0 }, -- No alert if skill < this More = { -- Which alerts generate a force_more_message (some categories overlap) early_weap = false, -- Good weapons found early upgrade_weap = false, -- Better DPS / weapon_score weap_ego = false, -- New or diff egos body_armour = false, shields = true, aux_armour = false, armour_ego = false, -- New or diff egos high_score_weap = false, -- Highest damage found high_score_armour = true, -- Highest AC found one_time_alerts = true, artefact = false, -- Any artefact trained_artefacts = true, -- Artefacts where you have corresponding skill > 0 orbs = false, talismans = you.class() == "Shapeshifter", -- True for shapeshifter, false for everyone else staff_resists = false, -- When a staff gives a missing resistance }, }, }, } -- brc_config_custom (do not remove this comment) } #################################### End lua/config/custom.lua #################################### ################################################################################################### ################################### Begin lua/config/testing.lua ################################## ############# https://github.com/brianfaires/crawl-rc/blob/main/lua/config/testing.lua ############ { --- Testing Main Config: Isolate and test specific features brc_config_testing = { BRC_CONFIG_NAME = "Testing", emojis = false, mpr = { show_debug_messages = true, logs_to_stderr = true, }, disable_other_features = false, -- only use features explicitly configured below ["pickup-alert"] = { Alert = { armour_sensitivity = 0.5, weapon_sensitivity = 0.5, }, Tuning = { Armour = { diff_body_ego_is_good = false, }, }, }, init = function() if BRC.Config.disable_other_features then for _, v in pairs(_G) do if BRC.is_feature_module(v) and not BRC.Config[v.BRC_FEATURE_NAME] then BRC.Config[v.BRC_FEATURE_NAME] = { disabled = true } end end end end, } -- brc_config_testing (do not remove this comment) } #################################### End lua/config/testing.lua ################################### ################################################################################################### ################################## Begin lua/config/turncount.lua ################################# ############ https://github.com/brianfaires/crawl-rc/blob/main/lua/config/turncount.lua ########### { --- Turncount Main Config: For turncount runs brc_config_turncount = { BRC_CONFIG_NAME = "Turncount", ["alert-monsters"] = { sensitivity = 1.25, -- 0 to disable all; at 2.0, alerts will fire at 1/2 HP }, ["announce-items"] = { disabled = false, }, ["bread-swinger"] = { disabled = false, }, ["drop-inferior"] = { disabled = true, }, ["fm-messages"] = { force_more_threshold = 5, }, ["hotkey"] = { equip_hotkey = false, }, ["inscribe-stats"] = { skip_dps = true, }, ["mute-messages"] = { mute_level = 2, }, ["runrest-features"] = { after_shaft = false }, ["pickup-alert"] = { Pickup = { weapons = false, }, Alert = { hotkey_travel = false, hotkey_pickup = false, one_time = { "distortion", "troll leather armour", "wand of digging", "Apportation", "Passage of Golubria", "Shatter", "Ignition", "Fire Storm", "Polar Vortex", }, More = { armour_ego = false, shields = false, }, }, }, ["safe-stairs"] = { warn_backtracking = false, }, init = function() crawl.setopt("show_game_time = false") crawl.setopt("default_autopickup = false") crawl.setopt("explore_stop += shops") -- Adds an announcement with the shop name crawl.setopt("macros += M o zo") -- Disable autoexplore; cast spell 'o' instead crawl.setopt("autoinscribe += offhand:!T") crawl.setopt("autopickup_exceptions ^= > stones?$") -- Don't highlight autopickup for stones crawl.setopt("explore_auto_rest = false") for _, m in ipairs(f_pickup_alert.Config.Alert.More) do if m ~= "one_time_alerts" then f_pickup_alert.Config.Alert.More[m] = false end end end, } -- brc_config_turncount (do not remove this comment) } ################################### End lua/config/turncount.lua ################################## ################################################################################################### ################################## Begin lua/config/realtime.lua ################################## ############ https://github.com/brianfaires/crawl-rc/blob/main/lua/config/realtime.lua ############ { --- Realtime Main Config: For realtime speedruns brc_config_realtime = { BRC_CONFIG_NAME = "Realtime", emojis = true, ["alert-monsters"] = { disabled = true }, ["manage-consumables"] = { disabled = true }, ["safe-stairs"] = { disabled = true }, ["announce-hp-mp"] = { dmg_flash_threshold = 0.20, -- Flash screen when losing this % of max HP dmg_fm_threshold = 1, -- Force more for losing this % of max HP always_on_bottom = true, -- Rewrite HP/MP meters after each turn with messages }, ["display-realtime"] = { disabled = false, }, ["fm-messages"] = { force_more_threshold = 9, }, ["misc-alerts"] = { alert_low_hp_threshold = 0, -- % max HP to alert; 0 to disable save_with_msg = false, -- Shift-S to save and leave yourself a message }, ["mute-messages"] = { mute_level = 3, }, ["remind-id"] = { stop_on_scrolls_count = 99, -- Stop when largest un-ID'd scroll stack increases and is >= this stop_on_pots_count = 99, -- Stop when largest un-ID'd potion stack increases and is >= this }, ["runrest-features"] = { after_shaft = false }, ["startup"] = { show_skills_on_startup = false, -- Open skills menu on startup }, ["pickup-alert"] = { Pickup = { armour = true, staves = false, weapons = true, weapons_pure_upgrades_only = false, -- Only pick up better versions of same exact weapon }, Alert = { armour_sensitivity = 0.9, -- [0.5-2.0] Adjust all armour alerts; 0 to disable weapon_sensitivity = 0.9, -- [0.5-2.0] Adjust all weapon alerts; 0 to disable orbs = false, staff_resists = false, -- When a staff gives a missing resistance talismans = false, one_time = { -- Alert the first time each item is found "kite shield", "tower shield", "crystal plate armour", "gold dragon scales", "pearl dragon scales", "storm dragon scales", "broad axe", "executioner's axe", "demon whip", "eveningstar", "giant spiked club", "morningstar", "sacred scourge", "lajatang", "bardiche", "demon trident", "partisan", "trishula", "hand cannon", "triple crossbow", }, OTA_require_skill = { weapon = 6, armour = 0, shield = 0 }, -- No one_time if skill < this More = {}, -- All nil / false }, Tuning = { Armour = { encumb_penalty_weight = 0, -- [0-2.0] Penalty to heavy armour when training magic/ranged early_xl = 0, -- Alert any usable runed body armour when XL <= `early_xl` }, }, }, init = function() BRC.Config.startup.auto_set_skill_targets = { { BRC.you.top_wpn_skill(), 8.0 } } crawl.setopt("view_delay = 0") crawl.setopt("autofight_stop = 15") crawl.setopt("fail_severity_to_confirm = 4") crawl.setopt("view_delay = 100") crawl.setopt("enable_more = false") end, } -- brc_config_realtime (do not remove this comment) } ################################### End lua/config/realtime.lua ################################### ################################################################################################### ################################## Begin lua/config/explicit.lua ################################## ############ https://github.com/brianfaires/crawl-rc/blob/main/lua/config/explicit.lua ############ { --- Explicit config: All config values from all features listed explicitly, set to defaults --- Auto-generated by build/generate_explicit_config.py — do not edit manually. --- To regenerate: python3 build/generate_explicit_config.py --- Large feature config sections are at the end --- @warning Since this lives at the top of the RC, it can't reference constants.lua or util/*.lua --- So it must hardcode values like keycodes, where feature configs get to use BRC.KEYS, etc brc_config_explicit = { BRC_CONFIG_NAME = "Explicit", ---- BRC Core values ---- emojis = true, -- Include emojis in alerts unskilled_egos_usable = false, mpr = { show_debug_messages = false, logs_to_stderr = false, take_note_on_error = true, -- Note BRC errors in the character file for debugging with char dump }, -- BRC.Configs.Default.mpr (do not remove this comment) dump = { max_lines_per_table = 200, -- Avoid huge tables (alert_monsters.Config.Alerts) in debug dumps omit_pointers = true, -- Don't dump functions and userdata (they only show a hex address) }, -- BRC.Configs.Default.dump (do not remove this comment) --- How weapon damage is calculated for inscriptions+pickup/alert: (factor * DMG + offset) BrandBonus = { chaos = { factor = 1.15, offset = 2.0 }, -- Approximate weighted average distort = { factor = 1.0, offset = 6.0 }, drain = { factor = 1.25, offset = 2.0 }, elec = { factor = 1.0, offset = 4.5 }, -- 3.5 on avg; fudged up for AC pen entangle = { factor = 1.1, offset = 3 }, flame = { factor = 1.25, offset = 0 }, freeze = { factor = 1.25, offset = 0 }, heavy = { factor = 1.8, offset = 0 }, -- Speed is accounted for elsewhere pain = { factor = 1.0, offset = you.skill("Necromancy") / 2 }, spect = { factor = 1.7, offset = 0 }, -- Fudged down for increased incoming damage sunder = { factor = 1.2, offset = 0 }, valour = { factor = 1.15, offset = 0 }, venom = { factor = 1.0, offset = 5.0 }, -- 5 dmg per poisoning subtle = { -- Values to use for weapon "scores" (not damage) antimagic = { factor = 1.1, offset = 0 }, concuss = { factor = 1.2, offset = 0 }, devious = { factor = 1.1, offset = 0 }, holy = { factor = 1.15, offset = 0 }, penet = { factor = 1.3, offset = 0 }, protect = { factor = 1.15, offset = 0 }, reap = { factor = 1.3, offset = 0 }, rebuke = { factor = 1.2, offset = 0 }, vamp = { factor = 1.2, offset = 0 }, }, }, -- BRC.Configs.Default.BrandBonus (do not remove this comment) hotkey = { key = { keycode = 13, name = "[Enter]" }, skip_keycode = 27, -- ESC keycode equip_hotkey = true, -- Offer to equip after picking up equipment wait_for_safety = true, -- Don't expire the hotkey with monsters in view explore_clears_queue = true, -- Clear the hotkey queue on explore newline_before_hotkey = true, -- Add a newline before the hotkey message move_to_feature = { -- Hotkey for "move to _" when you find these features enter_temple = "Temple", enter_lair = "Lair", altar_ecumenical = "faded altar", enter_bailey = "flagged portal", enter_bazaar = "bazaar", enter_desolation = "crumbling gateway", enter_gauntlet = "gauntlet", enter_ice_cave = "frozen archway", enter_necropolis = "phantasmal passage", enter_ossuary = "sand-covered staircase", enter_sewer = "glowing drain", enter_trove = "trove of treasure", enter_volcano = "dark tunnel", enter_wizlab = "magical portal", enter_ziggurat = "ziggurat", }, }, ---- Feature configs ---- ["announce-hp-mp"] = { disabled = false, dmg_flash_threshold = 0.20, -- Flash screen when losing this % of max HP dmg_fm_threshold = 0.30, -- Force more for losing this % of max HP always_on_bottom = false, -- Rewrite HP/MP meters after each turn with messages meter_length = 10, -- Number of pips in each meter Announce = { hp_loss_limit = 1, -- Announce when HP loss >= this hp_gain_limit = 4, -- Announce when HP gain >= this mp_loss_limit = 1, -- Announce when MP loss >= this mp_gain_limit = 2, -- Announce when MP gain >= this hp_first = false, -- Show HP first in the message same_line = true, -- Show HP/MP on the same line always_both = true, -- If showing one, show both very_low_hp = 0.10, -- At this % of max HP, show all HP changes and mute % HP alerts }, HP_METER = { FULL = "❤️", PART = "❤️‍🩹", EMPTY = "🤍" }, MP_METER = { FULL = "🟦", PART = "🔹", EMPTY = "➖" }, init = function() if not BRC.Config.emojis then f_announce_hp_mp.Config.HP_METER = { BORDER = BRC.txt.white("|"), FULL = BRC.txt.lightgreen("+"), PART = BRC.txt.lightgrey("+"), EMPTY = BRC.txt.darkgrey("-"), } -- HP_METER (do not remove this comment) f_announce_hp_mp.Config.MP_METER = { BORDER = BRC.txt.white("|"), FULL = BRC.txt.lightblue("+"), PART = BRC.txt.lightgrey("+"), EMPTY = BRC.txt.darkgrey("-"), } -- MP_METER (do not remove this comment) end end, }, ["announce-items"] = { disabled = true, -- Disabled by default. Intended only for turncount runs. announce_class = { "book", "gold", "jewellery", "misc", "missile", "potion", "scroll", "wand" }, announce_glowing = true, announce_artefacts = true, max_gold_announcements = 3, -- Stop announcing gold after 3rd pile on screen announce_duplicate_consumables = true, -- Announce when standing on not-id'd duplicates }, ["answer-prompts"] = { disabled = false, }, ["bread-swinger"] = { disabled = true, -- Disable by default allow_plant_damage = false, -- Allow damaging plants to rest walk_delay = 50, -- ms delay between walk commands. Makes visuals less jarring. 0 to disable alert_slow_weap_min = 1.5, -- Alert when finding the slowest weapon yet, starting at this delay set_manual_slot_key = -11, -- Manually set which weapon slot to swing max_heal_perc = 90, -- Stop resting at this percentage of max HP/MP emoji = "🍞", init = function() if not BRC.Config.emojis then f_bread_swinger.Config.emoji = BRC.txt.cyan("---- ") end end, }, ["color-inscribe"] = { disabled = true, }, ["drop-inferior"] = { disabled = false, msg_on_inscribe = true, -- Show a message when an item is marked for drop hotkey_drop = true, -- BRC hotkey drops all items on the drop list }, ["display-realtime"] = { disabled = true, -- Disabled by default interval_s = 60, -- seconds between updates emoji = "🕒", init = function() if not BRC.Config.emojis then f_display_realtime.Config.emoji = BRC.txt.white("--") end end, }, ["exclude-dropped"] = { disabled = false, not_weapon_scrolls = true, -- Don't exclude enchant/brand scrolls if holding enchantable weapon }, ["fully-recover"] = { disabled = false, }, ["go-up-macro"] = { disabled = false, go_up_macro_key = 5, -- Key for "go up closest stairs" macro ignore_mon_on_orb_run = true, -- Ignore monsters on orb run -- %HP thresholds for ignoring monsters during orb run (2-7 tiles away, depending on HP percent) orb_ignore_hp_min = 0.30, -- HP percent to stop ignoring monsters orb_ignore_hp_max = 0.70, -- HP percent to ignore monsters at min distance away (2 tiles) }, ["inscribe-stats"] = { disabled = false, inscribe_weapons = true, -- Inscribe weapon stats on pickup inscribe_armour = true, -- Inscribe armour stats on pickup dmg_type = 1, -- unbranded, plain, branded, scoring skip_dps = false, -- Skip DPS in weapon inscriptions prefix_staff_dmg = true, -- Special prefix for magical staves }, ["misc-alerts"] = { disabled = false, preferred_god = "", -- Stop on first altar with this text (Ex. "Wu Jian"); nil or "" disables force_more_on_pref_altar = true, -- Force more message on first altar for preferred god save_with_msg = true, -- Shift-S to save and leave yourself a message alert_low_hp_threshold = 35, -- % max HP to alert; 0 to disable alert_spell_level_changes = true, -- Alert when you gain additional spell levels alert_remove_faith = true, -- Reminder to remove amulet at max piety remove_faith_hotkey = true, -- Hotkey remove amulet }, ["quiver-reminders"] = { disabled = false, confirm_consumables = true, warn_diff_missile_turns = 10, }, ["remind-id"] = { disabled = false, stop_on_scrolls_count = 2, -- Stop when largest un-ID'd scroll stack increases and is >= this stop_on_pots_count = 3, -- Stop when largest un-ID'd potion stack increases and is >= this read_id_hotkey = true, -- Put read ID on hotkey emoji = "🎁", init = function() if not BRC.Config.emojis then f_remind_id.Config.emoji = BRC.txt.magenta("?") end end, }, ["runrest-features"] = { disabled = false, after_shaft = true, -- stop on stairs after being shafted, until returned to original floor ignore_altars = true, -- when you don't need a god ignore_portal_exits = true, -- don't stop explore on portal exits stop_on_hell_stairs = true, -- stop explore on hell stairs stop_on_pan_gates = true, -- stop explore on pan gates temple_search = true, -- on entering or exploring temple, auto-search gauntlet_search = true, -- on entering or exploring gauntlet, auto-search with filters necropolis_search = true, -- on exploring necropolis, auto-search with filters }, ["manage-consumables"] = { disabled = false, maintain_safe_scrolls = true, maintain_safe_potions = true, scroll_slots = { ["acquirement"] = "A", ["amnesia"] = "x", ["blinking"] = "B", ["brand weapon"] = "W", ["butterflies"] = "s", ["enchant armour"] = "a", ["enchant weapon"] = "w", ["fear"] = "f", ["fog"] = "g", ["identify"] = "i", ["immolation"] = "I", ["noise"] = "N", ["revelation"] = "r", ["poison"] = "p", ["silence"] = "S", ["summoning"] = "s", ["teleportation"] = "t", ["torment"] = "T", ["vulnerability"] = "V", }, potion_slots = { ["ambrosia"] = "a", ["attraction"] = "A", ["berserk rage"] = "B", ["brilliance"] = "b", ["cancellation"] = "C", ["curing"] = "c", ["experience"] = "E", ["enlightenment"] = "e", ["haste"] = "h", ["heal wounds"] = "w", ["invisibility"] = "i", ["lignification"] = "L", ["magic"] = "g", ["might"] = "z", ["resistance"] = "r", ["mutation"] = "M", }, }, ["safe-stairs"] = { disabled = false, warn_backtracking = true, -- Warn if immediately taking stairs twice in a row warn_v5 = true, -- Prompt before entering Vaults:5 }, ["startup"] = { disabled = false, -- Save current training targets and config, for race/class macro_save_key = 20, -- (Cntl-T) Keycode to save training targets and config save_training = true, -- Allow save/load of race/class training targets save_config = true, -- Allow save/load of BRC config prompt_before_load = false, -- Prompt before loading in a new game with same race+class allow_race_only_saves = false, -- Also save for race only (always prompts before loading) allow_class_only_saves = false, -- Also save for class only (always prompts before loading) -- Remaining values only used if no training targets were loaded by race/class show_skills_menu = false, -- Show skills menu on startup -- Settings to set skill targets, regardless of race/class set_all_targets = true, -- Set all targets, even if only focusing one focus_one_skill = true, -- Focus one skill at a time, even if setting all targets auto_set_skill_targets = { { "Stealth", 2.0 }, -- First, focus stealth to 2.0 { "Fighting", 2.0 }, -- If already have stealth, focus fighting to 2.0 }, -- For non-spellcasters, add preferred weapon type as 3rd skill target init = function() if you.skill("Spellcasting") == 0 then local wpn_skill = BRC.you.top_wpn_skill() if wpn_skill then local t = f_startup.Config.auto_set_skill_targets t[#t + 1] = { wpn_skill, 6.0 } end end end, }, ["weapon-slots"] = { disabled = false, }, ---- Large config sections ---- ["alert-monsters"] = { disabled = false, sensitivity = 1.0, -- 0 to disable all; at 2.0, alerts will fire at 1/2 HP pack_timeout = 10, -- turns to wait before repeating a pack alert. 0 to disable disable_alert_monsters_in_zigs = true, -- Disable dynamic force_mores in Ziggurats debug_alert_monsters = false, -- Get a message when alerts toggle off/on --[[ Config.Alerts contains all alerts. Each table in it creates one alert, using the following fields: - `name` is for debugging. - `pattern` is a string or list of monster names, will alert when you encounter one. - `is_pack` (optional) indicates the alert is for a pack of monsters. Packs only fire once every few turns - as defined in Config.pack_timeout (default 15). - `flash_screen` (optional) alert will flash the screen instead of using force_more. - `cutoff` sets the point when the alert is active (usually how much HP you have) - `cond` defines HOW the character stats are compared against `cutoff` (HP/will/etc). Ex: `always` alerts are always on. `hp` alerts are active when you have < `cutoff` HP. `will` alerts are active when you have <= `cutoff` pips of willpower. `int` alerts are active when you have < `cutoff` Int. `xl` alerts are active when your XL is < `cutoff`. `elec` alerts are active when you have no rElec and < `cutoff` HP. `fire`, `cold`, etc active < `cutoff` HP with no resistance. Pips lower cutoff to 50/33/20% --]] Alerts = { { name = "always_fm", pattern = { -- High damage/speed "flayed ghost", "juggernaut", "orbs? of (entropy|fire|winter)", --Summoning "boundless tesseract", "demonspawn corrupter", "draconian stormcaller", "dryad", "guardian serpent", "halazid warlock", "shadow demon", "spriggan druid", "worldbinder", --Dangerous abilities "iron giant", "merfolk aquamancer", "nekomata", "shambling mangrove", "starflower", "torpor snail", "water nymph", "wretched star", "wyrmhole", --Dangerous clouds "apocalypse crab", "catoblepas", } }, { name = "always_flash", flash_screen = true, pattern = { -- Noteworthy abilities "air elemental", "elemental wellspring", "ghost crab", "ironbound convoker", "vault guardian", "vault warden", "wendigo", -- Displacement "deep elf knight", "swamp worm", -- Summoning "deep elf elementalist", -- Agony "death knight", "imperial myrmidon", "necromancer", } }, -- Early game Dungeon problems for chars with low hp. (adder defined below) { name = "30hp", cond = "hp", cutoff = 30, is_pack = true, pattern = { "hound", "gnoll" } }, { name = "mid_game_packs", cutoff = 90, is_pack = true, pattern = { "boggart", "dream sheep" } }, -- Monsters dangerous until a certain point { name = "xl_7", cond = "xl", cutoff = 6, is_pack = true, pattern = { "orc wizard" } }, { name = "xl_12", cond = "xl", cutoff = 12, pattern = { "hydra", "bloated husk" } }, -- Monsters that can hit for ~50% of hp from range with unbranded attacks { name = "40hp", cond = "hp", cutoff = 40, pattern = { "orc priest" } }, { name = "50hp", cond = "hp", cutoff = 50, pattern = { "manticore", "orc high priest" } }, { name = "60hp", cond = "hp", cutoff = 60, pattern = { "centaur(?! warrior)", "cyclops", "orc knight", "yaktaur(?! captain)" } }, { name = "70hp_melai", cond = "hp", cutoff = 70, is_pack = true, pattern = "meliai" }, { name = "80hp", cond = "hp", cutoff = 80, pattern = { "gargoyle" } }, { name = "90hp", cond = "hp", cutoff = 90, pattern = { "deep elf archer", "tengu conjurer" } }, { name = "110hp", cond = "hp", cutoff = 110, pattern = { "cacodemon", "centaur warrior", "deep elf high priest", "deep troll earth mage", "eye of devastation", "hellion", "stone giant", "sun moth", "yaktaur captain" } }, { name = "120hp", cond = "hp", cutoff = 120, pattern = { "magenta draconian", "thorn hunter", "quicksilver (dragon|elemental)" } }, { name = "160hp", cond = "hp", cutoff = 160, pattern = { "brimstone fiend", "deep elf sorcererhell sentinal", "draconian (knight|scorcher)", "war gargoyle" } }, { name = "200hp", cond = "hp", cutoff = 200, pattern = { "(deep elf|draconian) annihilator", "iron (dragon|elemental)" } }, -- Monsters that can crowd-control you without sufficient willpower -- Cutoff ~10% for most spells; lower for more significant spells like banish { name = "willpower2", cond = "will", cutoff = 2, pattern = { "basilisk", "naga ritualist", "vampire(?! (bat|mage|mosquito))", "sphinx marauder" } }, { name = "willpower3", cond = "will", cutoff = 3, pattern = { "cacodemon", "death knight", "deep elf (demonologist|sorcerer|archer)", "draconian shifter", "fenstrider witch", "glowing orange brain", "guardian sphinx", "imperial myrmidon", "iron elemental", "occultist", "merfolk siren", "nagaraja", "ogre mage", "orc sorcerer", "satyr", "vampire knight", "vault sentinel" } }, { name = "willpower3_great_orb_of_eyes", cond = "will", cutoff = 3, is_pack = true, pattern = "great orb of eyes" }, { name = "willpower3_golden_eye", cond = "will", cutoff = 3, is_pack = true, pattern = "golden eye" }, { name = "willpower4", cond = "will", cutoff = 4, pattern = { "merfolk avatar", "tainted leviathan", "nargun" } }, -- Brain feed with low int { name = "brainfeed", cond = "int", cutoff = 6, pattern = { "glowing orange brain", "neqoxec" } }, -- Alert if no resist and HP below cutoff { name = "pois_30", cond = "pois", cutoff = 30, pattern = { "adder" } }, { name = "pois_80", cond = "pois", cutoff = 80, pattern = { "golden dragon", "green draconian", "swamp dragon" } }, { name = "pois_120", cond = "pois", cutoff = 120, pattern = { "fenstrider witch", "green death", "naga mage", "nagaraja" } }, { name = "pois_140", cond = "pois", cutoff = 140, pattern = { "tengu reaver" } }, { name = "elec_40", cond = "elec", cutoff = 40, is_pack = true, pattern = "electric eel" }, { name = "elec_80", cond = "elec", cutoff = 80, pattern = { "raiju", "shock serpent", "spark wasp" } }, { name = "elec_120", cond = "elec", cutoff = 120, pattern = { "black draconian", "blizzard demon", "deep elf zephyrmancer", "storm dragon", "tengu conjurer" } }, { name = "elec_140", cond = "elec", cutoff = 140, pattern = { "electric golem", "servants? of whisper", "spriggan air mage", "tengu reaver", "titan" } }, { name = "elec_140_pack", cond = "elec", cutoff = 140, is_pack = true, pattern = { "ball lightning" } }, { name = "corr_60", cond = "corr", cutoff = 60, pattern = { "acid dragon" } }, { name = "caustic_shrike", cond = "corr", cutoff = 120, is_pack = true, pattern = { "caustic shrike" } }, { name = "corr_140", cond = "corr", cutoff = 140, pattern = { "demonspawn corrupter", "entropy weaver", "moon troll", "tengu reaver" } }, { name = "fire_60_pack", cond = "fire", cutoff = 60, is_pack = true, pattern = { "hell hound", "lava snake", "lindwurm" } }, { name = "fire_60", cond = "fire", cutoff = 60, pattern = { "fire crab", "steam dragon" } }, { name = "fire_100", cond = "fire", cutoff = 100, pattern = { "deep elf pyromancer", "efreet", "smoke demon", "sun moth" } }, { name = "fire_120", cond = "fire", cutoff = 120, pattern = { "demonspawn blood saint", "hell hog", "hell knight", "molten gargoyle", "ogre mage", "orc sorcerer", "red draconian" } }, { name = "fire_140", cond = "fire", cutoff = 140, pattern = { "balrug" } }, { name = "fire_160", cond = "fire", cutoff = 160, pattern = { "fire dragon", "fire giant", "golden dragon", "ophan", "salamander tyrant", "tengu reaver", "will-o-the-wisp" } }, { name = "fire_240", cond = "fire", cutoff = 240, pattern = { "crystal (guardian|echidna)", "draconian scorcher", "hellephant" } }, { name = "cold_80", cond = "cold", cutoff = 80, pattern = { "rime drake" } }, { name = "cold_120", cond = "cold", cutoff = 120, pattern = { "blizzard demon", "bog body", "demonspawn blood saint", "ironbound frostheart", "white draconian" } }, { name = "shard_shrike", cond = "cold", cutoff = 120, is_pack = true, pattern = { "shard shrike" } }, { name = "cold_160", cond = "cold", cutoff = 160, pattern = { "draconian knight", "frost giant", "golden dragon", "ice dragon", "tengu reaver" } }, { name = "cold_180", cond = "cold", cutoff = 180, pattern = { "(?= this value, in weap_school/armour/shield OTA_require_skill = { weapon = 2, armour = 2.5, shield = 0 }, hotkey_travel = true, hotkey_pickup = true, allow_arte_weap_upgrades = true, -- If false, won't alert weapons as upgrades to an artefact -- Only alert a plain talisman if its min_skill <= Shapeshifting + talisman_lvl_diff talisman_lvl_diff = you.class() == "Shapeshifter" and 27 or 6, -- Which alerts generate a force_more More = { early_weap = false, -- Good weapons found early upgrade_weap = false, -- Better DPS / weapon_score weap_ego = false, -- New or diff egos body_armour = false, shields = true, aux_armour = false, armour_ego = true, -- New or diff egos high_score_weap = false, -- Highest damage found high_score_armour = true, -- Highest AC found one_time_alerts = true, artefact = false, -- Any artefact trained_artefacts = true, -- Artefacts where you have corresponding skill > 0 orbs = false, -- Unique orbs talismans = you.class() == "Shapeshifter", -- True for shapeshifter, false for everyone else staff_resists = false, -- When a staff gives a missing resistance autopickup_disabled = true, -- Alerts for autopickup items, when autopickup is disabled }, }, -- Alert ---- Heuristics for tuning the pickup/alert system. Advanced behavior customization. Tuning = { --[[ f_pickup_alert.Config.Tuning.Armour: Magic numbers for the armour pickup/alert system. For armour with different encumbrance, alert when ratio of gain/loss (AC|EV) is > value Lower values mean more alerts. gain/diff/same/lose refers to egos. min_gain/max_loss block alerts for new egos, when AC or EV delta is outside limits ignore_small: if abs(AC+EV) <= this, ignore ratios and alert any gain/diff ego --]] Armour = { Lighter = { gain_ego = 0.6, new_ego = 0.7, diff_ego = 0.9, same_ego = 1.2, lost_ego = 2.0, min_gain = 3.0, max_loss = 4.0, ignore_small = 3.5, }, Heavier = { gain_ego = 0.4, new_ego = 0.5, diff_ego = 0.6, same_ego = 0.7, lost_ego = 2.0, min_gain = 3.0, max_loss = 8.0, ignore_small = 5, }, encumb_penalty_weight = 0.7, -- [0-2.0] Penalty to heavy armour when training magic/ranged early_xl = 6, -- Alert any usable runed body armour when XL <= `early_xl` diff_body_ego_is_good = false, -- More body_armour alerts for diff_ego (no min_gain check) }, -- Armour --[[ f_pickup_alert.Config.Tuning.Weap: Magic numbers for the weapon pickup/alert system, namely: 1. Cutoffs for pickup/alert weapons (when DPS ratio exceeds a value) 2. Cutoffs for when alerts are active (XL, skill_level) Pickup/alert system will try to upgrade ANY weapon in your inventory. "DPS ratio" is (new_weapon_score / inventory_weapon_score). Score considers DPS/brand/accuracy. --]] Weap = { Pickup = { add_ego = 1.0, -- Pickup weapon that gains a brand if DPS ratio > add_ego same_type_melee = 1.2, -- Pickup melee weap of same school if DPS ratio > same_type_melee same_type_ranged = 1.1, -- Pickup ranged weap if DPS ratio > same_type_ranged accuracy_weight = 0.25, -- Treat +1 Accuracy as +accuracy_weight DPS }, -- Pickup Alert = { -- Alerts for weapons not requiring an extra hand pure_dps = 1.0, -- Alert if DPS ratio > pure_dps gain_ego = 0.8, -- Gaining ego; Alert if DPS ratio > gain_ego new_ego = 0.8, -- Get ego not in inventory; Alert if DPS ratio > new_ego low_skill_penalty_damping = 8, -- [0-20] Reduce penalty to lower-trained weapons -- Alerts for 2-handed weapons, when carrying 1-handed AddHand = { ignore_sh_lvl = 4.0, -- Treat offhand as empty if shield_skill < ignore_sh_lvl add_ego_lose_sh = 0.8, -- Alert 1h -> 2h (using shield) if DPS ratio > add_ego_lose_sh not_using = 1.0, -- Alert 1h -> 2h (not using 2nd hand) if DPS ratio > not_using }, -- Alerts for good early weapons of all types Early = { xl = 7, -- Alert early weapons if XL <= xl skill = { factor = 1.5, offset = 2.0 }, -- Ignore weapons w skill_diff > XL*fact+offset branded_min_plus = 4, -- Alert branded weapons with plus >= branded_min_plus }, -- Alerts for particularly strong ranged weapons EarlyRanged = { xl = 14, -- Alert strong ranged weapons if XL <= xl min_plus = 7, -- Alert ranged weapons with plus >= min_plus branded_min_plus = 4, -- Alert branded ranged weapons with plus >= branded_min_plus max_shields = 8.0, -- Require max_shields skill to block 2h ranged alerts }, }, -- Alert }, -- Weap }, -- Tuning AlertColor = { weapon = { desc = "magenta", item = "yellow", stats = "lightgrey" }, body_arm = { desc = "lightblue", item = "lightcyan", stats = "lightgrey" }, aux_arm = { desc = "lightblue", item = "yellow" }, orb = { desc = "green", item = "lightgreen" }, talisman = { desc = "green", item = "lightgreen" }, misc = { desc = "brown", item = "white" }, }, -- AlertColor Emoji = { RARE_ITEM = "💎", ARTEFACT = "💠", ORB = "🔮", TALISMAN = "🧬", STAFF_RES = "🔥", WEAPON = "⚔️", RANGED = "🏹", POLEARM = "🔱", TWO_HAND = "✋🤚", EGO = "✨", ACCURACY = "🎯", STRONGER = "💪", STRONGEST = "💪💪", LIGHTER = "⏬", HEAVIER = "⏫", AUTOPICKUP_ITEM = "👍", }, -- Emoji init = function() if not BRC.Config.emojis then f_pickup_alert.Config.Emoji = {} end end, }, } -- brc_config_explicit (do not remove this comment) } ################################### End lua/config/explicit.lua ################################### ################################################################################################### ### Mostly normal RC options ### ######################################### Begin rc/main.rc ######################################## ################### https://github.com/brianfaires/crawl-rc/blob/main/rc/main.rc ################## ####### Main options ####### easy_confirm = all default_manual_training = true show_more = false small_more = true mouse_input = true tile_web_mouse_control = false fail_severity_to_confirm = 4 tile_key_repeat_delay = 100 ####### Combat options ####### autofight_stop = 40 hp_warning = 20 autofight_caught = true rest_wait_both = true rest_wait_ancestor = true monster_alert += uniques ####### Display options ####### tile_runrest_rate = 25 item_stack_summary_minimum = 8 sort_menus = true:equipped,art,ego,basename,identified,qualname,>qty drop_filter += useless_item, forbidden fire_order = silver javelin, javelin, silver boomerang, boomerang, curare-tipped dart, poisoned dart, dart, stone ####### Explore options ####### explore_delay = -1 travel_delay = -1 rest_delay = -1 view_delay = 200 show_travel_trail = true explore_stop = altars, branches, portals, runed_doors, greedy_pickup_smart explore_stop_pickup_ignore += scroll, potion, misc, wand, stone, dart, boomerang, javelin ####### Autopickup exceptions ####### ae := autopickup_exceptions ae ^= useless_item ########################################## End rc/main.rc ######################################### ################################################################################################### ######################################## Begin rc/macros.rc ####################################### ################## https://github.com/brianfaires/crawl-rc/blob/main/rc/macros.rc ################# # Remap ~ to Lua interpreter (CMD_GAME_MENU is still on F1) bindkey = [~] CMD_LUA_CONSOLE # Cntl-Tab bindkey = [\{-222}] CMD_AUTOFIGHT_NOMOVE # Cntl-D (Go down closest stairs w/ {Cntl-G, '>'}) macros += M \{4} \{7}> # Spellcasting macros macros += M 1 Za macros += M 2 Zb macros += M 3 Zc macros += M 4 Zd macros += M 6 Zf macros += M 7 Zg macros += M 8 Zh macros += M 9 Zi macros += M 0 Zj # Confirm targeting with same keys as spellcasting macros += K2 \{-1018} \{13} macros += K2 \{-1015} \{13} macros += K2 \{-1012} \{13} macros += K2 1 \{13} macros += K2 2 \{13} macros += K2 3 \{13} macros += K2 4 \{13} macros += K2 6 \{13} macros += K2 7 \{13} macros += K2 8 \{13} macros += K2 9 \{13} macros += K2 0 \{13} # Numpad keymaps; disabled cause I haven't used them in years ## Keycodes: ## NP0 (-1000), NPenter (-1010), NP/ (-1012), NP* (-1015), ## NP+ (-1016), NP- (-1018), NP. (-1019), NP= (-1021), Tab (9) #macros += K \{-1019} f #macros += K \{-1012} 1 #macros += K \{-1015} 2 #macros += K \{-1018} 3 #macros += K \{-1016} \{9} #macros += K \{-1010} o #macros += K \{-1000} . #macros += K \{-247} 5 #macros += K2 \{-1019} . ######################################### End rc/macros.rc ######################################## ################################################################################################### #################################### Begin rc/slot-defaults.rc #################################### ############## https://github.com/brianfaires/crawl-rc/blob/main/rc/slot-defaults.rc ############## ####### Item Slots ######### # See manage-consumables.lua for scrolls/potions. # Wands use crawl defaults # Rings to P/p for easy swapping gear_slot += ring of:Pp consumable_shortcut ^= box of beasts:B consumable_shortcut ^= condenser vane:C consumable_shortcut ^= figurine of a ziggurat:Z consumable_shortcut ^= Gell's gravitambourine:G consumable_shortcut ^= horn of Geryon:H consumable_shortcut ^= lightning rod:L consumable_shortcut ^= phantom mirror:M consumable_shortcut ^= phial of floods:P consumable_shortcut ^= sack of spiders:S consumable_shortcut ^= tin of tremorstones:T ######## Spell Slots ######### spell_slot ^= Apportation:zZ spell_slot ^= Passage of Golubria:zZ spell_slot ^= Alistair's Intoxication:AITX spell_slot ^= Alistair’s Walking Alembic:AWKLMBSTEI spell_slot ^= Anguish:ANGUISH spell_slot ^= Animate Armour:ANMT spell_slot ^= Animate Dead:ADNMT spell_slot ^= Arcjolt:AJCOLT spell_slot ^= Blink:BLNK spell_slot ^= Borgnjor's Revivification:BRVF spell_slot ^= Call Canine Familiar:CFN spell_slot ^= Call Imp:ICMP spell_slot ^= Cause Fear:CFSUR spell_slot ^= Chain Lightning:LCHGNT spell_slot ^= Cigotuvi's Dreadful Rot:CDRVT spell_slot ^= Confusing Touch:CTF spell_slot ^= Conjure Ball Lightning:BLCTN spell_slot ^= Construct Spike Launcher:CSLPKNRAUE spell_slot ^= Corpse Rot:CRPS spell_slot ^= Death Channel:DCNL spell_slot ^= Death's Door:DROTHEA spell_slot ^= Detonation Catalyst:DCTNYTSAEO spell_slot ^= Diamond Sawblades:DSWBLNIAO spell_slot ^= Discord:DISCORD spell_slot ^= Disjunction:DJNIS spell_slot ^= Dispersal:DPLIS spell_slot ^= Dragon's Call:DCRGN spell_slot ^= Forge Monarch Bomb:FMBONRCHGEA spell_slot ^= Forge Phalanx Beetle:FPBENXORG spell_slot ^= Fortress Blast:FBRTSLA spell_slot ^= Foxfire:abcFXOIRE spell_slot ^= Fugue of the Fallen:FUGALNE spell_slot ^= Fulsome Fusillade:FLSOMADE spell_slot ^= Gloom:GLOM spell_slot ^= Hoarfrost Cannonade:HCORFND spell_slot ^= Ice Form:IFORM spell_slot ^= Ignite Poison:IPOSN spell_slot ^= Ignition:IGNTO spell_slot ^= Infestation:INFESTAON spell_slot ^= Irradiate:IRADTE spell_slot ^= Iskenderun's Battlespehere:BSI spell_slot ^= Iskenderun's Mystic Blast:IMB spell_slot ^= Jinxbite:JNXBITE spell_slot ^= Leda's Liquefaction:LQFND spell_slot ^= Malign Gateway:MGWTN spell_slot ^= Manifold Assault:MANFT spell_slot ^= Martyr's Knell:MKNARTYEL spell_slot ^= Maxwell's Capacitive Coupling:MCXW spell_slot ^= Metabolic Englaciation:MENC spell_slot ^= Monstrous Menagerie:MNGR spell_slot ^= Nazja’s Percussive Tempering:NPTZJACUE spell_slot ^= Olgreb's Toxic Radiance:TOR spell_slot ^= Ozocubu's Armour:OAMR spell_slot ^= Ozocubu's Refrigeration:ROZFG spell_slot ^= Permafrost Eruption:PERUTFMAOSTIN spell_slot ^= Platinum Paragon:PLTGNMAO spell_slot ^= Polar Vortex:PVXT spell_slot ^= Rending Blade:RBENDALG spell_slot ^= Scorch:SCORH spell_slot ^= Shatter:SHTR spell_slot ^= Sigil of Binding:SBGILDNG spell_slot ^= Silence:SICL spell_slot ^= Spellspark Servitor:SVFR spell_slot ^= Sphinx Sisters:SPHINXR spell_slot ^= Starburst:SBUT spell_slot ^= Static Discharge:DSCGT spell_slot ^= Sublimation of Blood:SBLM spell_slot ^= Summon Blazeheart Golem:SBGLZOM spell_slot ^= Summon Cactus Giant:CGS spell_slot ^= Summon Forest:FSRTM spell_slot ^= Summon Horrible Things:HST spell_slot ^= Summon Hydra:HYDRA spell_slot ^= Summon Ice Beast:ISB spell_slot ^= Summon Lightning Spire:SMLIRNG spell_slot ^= Summon Mana Viper:MSV spell_slot ^= Summon Seismosaurus Egg:EGSMIEOAU spell_slot ^= Summon Small Mammal:abSMLAUON spell_slot ^= Swiftness:SWIFT spell_slot ^= Volatile Blastmotes:VBOLMASTE :if you.class() == "Summoner" then spell_slot ^= (summon|call):abcdefgh :end ##################################### End rc/slot-defaults.rc ##################################### ################################################################################################### ##################################### Begin rc/autoinscribe.rc #################################### ############### https://github.com/brianfaires/crawl-rc/blob/main/rc/autoinscribe.rc ############## # See manage-consumables.lua for consumable inscriptions ai := autoinscribe # General ai += of cold resistance:rC+ ai += of fire resistance:rF+ ai += of poison resistance:rPois ai += of corrosion resistance:rCorr ai += of invulnerability:rInv ai += of magic regeneration:MRegen+ ai += of positive energy:rN+ ai += of regeneration:Regen+ ai += (?!" BRC.EMOJI.EXCLAMATION = "!" BRC.EMOJI.EXCLAMATION_2 = "!!" BRC.EMOJI.SUCCESS = nil end end ---- Items ---- BRC.MISC_ITEMS = { "box of beasts", "condenser vane", "figurine of a ziggurat", "Gell's gravitambourine", "horn of Geryon", "lightning rod", "phantom mirror", "phial of floods", "sack of spiders", "tin of tremorstones", } -- BRC.MISC_ITEMS (do not remove this comment) BRC.MISSILES = { "poisoned dart", "atropa-tipped dart", "curare-tipped dart", "datura-tipped dart", "darts? of disjunction", "darts? of dispersal", " stone", "boomerang", "silver javelin", "javelin", "large rock", "throwing net", } -- BRC.MISSILES (do not remove this comment) -- Could be removed after https://github.com/crawl/crawl/issues/4606 is addressed BRC.SPELLBOOKS = { "parchment of", "book of", "Necronomicon", "Grand Grimoire", "tome of obsoleteness", "Everburning Encyclopedia", "Ozocubu's Autobiography", "Maxwell's Memoranda", "Young Poisoner's Handbook", "Fen Folio", "Inescapable Atlas", "There-And-Back Book", "Great Wizards, Vol. II", "Great Wizards, Vol. VII", "Trismegistus Codex", "the Unrestrained Analects", "Compendium of Siegecraft", "Codex of Conductivity", "Handbook of Applied Construction", "Treatise on Traps", "My Sojourn through Swampland", "Akashic Record", -- Include prefixes for randart books "Almanac", "Anthology", "Atlas", "Book", "Catalogue", "Codex", "Compendium", "Compilation", "Cyclopedia", "Directory", "Elucidation", "Encyclopedia", "Folio", "Grimoire", "Handbook", "Incunable", "Incunabulum", "Octavo", "Omnibus", "Papyrus", "Parchment", "Precepts", "Quarto", "Secrets", "Spellbook", "Tome", "Vellum", "Volume", } -- BRC.SPELLBOOKS (do not remove this comment) ---- Races ---- BRC.UNDEAD_RACES = { "Demonspawn", "Mummy", "Poltergeist", "Revenant" } BRC.NONLIVING_RACES = { "Djinni", "Gargoyle" } BRC.POIS_RES_RACES = { "Djinni", "Gargoyle", "Mummy", "Naga", "Poltergeist", "Revenant" } BRC.LITTLE_RACES = { "Spriggan" } BRC.SMALL_RACES = { "Kobold" } BRC.LARGE_RACES = { "Armataur", "Gale Centaur", "Naga", "Oni", "Troll" } ---- Skills ---- BRC.MAGIC_SCHOOLS = { air = "Air Magic", alchemy = "Alchemy", chemistry = "Alchemy", cold = "Ice Magic", conjuration = "Conjurations", death = "Necromancy", earth = "Earth Magic", fire = "Fire Magic", necromancy = "Necromancy", } -- BRC.MAGIC_SCHOOLS (do not remove this comment) BRC.TRAINING_SKILLS = { "Air Magic", "Alchemy", "Armour", "Axes", "Conjurations", "Dodging", "Earth Magic", "Evocations", "Fighting", "Fire Magic", "Forgecraft", "Hexes", "Ice Magic", "Invocations", "Long Blades", "Maces & Flails", "Necromancy", "Polearms", "Ranged Weapons", "Shapeshifting", "Shields", "Short Blades", "Spellcasting", "Staves", "Stealth", "Summonings", "Translocations", "Unarmed Combat", "Throwing", } -- BRC.TRAINING_SKILLS (do not remove this comment) BRC.WEAP_SCHOOLS = { "axes", "maces & flails", "polearms", "long blades", "short blades", "staves", "unarmed combat", "ranged weapons", } -- BRC.WEAP_SCHOOLS (do not remove this comment) ---- Branches ---- BRC.HELL_BRANCHES = { "Coc", "Dis", "Geh", "Hell", "Tar" } BRC.PORTAL_FEATURE_NAMES = { "Bailey", "Bazaar", "Desolation", "Gauntlet", "IceCv", "Necropolis", "Ossuary", "Sewer", "Trove", "Volcano", "Wizlab", "Zig", } -- BRC.PORTAL_FEATURE_NAMES (do not remove this comment) ---- Egos ---- BRC.RISKY_EGOS = { "antimagic", "chaos", "distort", "harm", "heavy", "Infuse", "Ponderous" } BRC.NON_ELEMENTAL_DMG_EGOS = { "distort", "heavy", "spect" } BRC.ADJECTIVE_EGOS = { -- Egos whose English modifier comes before item name antimagic = "antimagic", heavy = "heavy", spectralising = "spectral", vampirism = "vampiric" } -- BRC.ADJECTIVE_EGOS (do not remove this comment) ---- Artefact properties ---- BRC.ARTPROPS_BAD = { "Bane", "-Cast", "-Move", "-Tele", "^Contam", "^Drain", "^Fragile", "*Corrode", "*Noise", "*Rage", "*Silence", "*Slow", "*Tele", } -- BRC.ARTPROPS_BAD (do not remove this comment) BRC.ARTPROPS_EGO = { -- Corresponding ego rF = "fire resistance", rC = "cold resistance", rPois = "poison resistance", rN = "positive energy", rCorr = "corrosion resistance", Archmagi = "the Archmagi", Rampage = "rampaging", Will = "willpower", Air = "air", Earth = "earth", Fire = "fire", Ice = "ice", Necro = "death", Summ = "command", } -- BRC.ARTPROPS_EGO (do not remove this comment) ---- Other ---- BRC.KEYS = { ENTER = 13, ESC = 27, ["Cntl-T"] = 20, ["Cntl-E"] = 5, ["Cntl-5"] = crawl.is_webtiles() and -11 or -43 } -- BRC.KEYS (do not remove this comment) BRC.COL = { black = "0", blue = "1", green = "2", cyan = "3", red = "4", magenta = "5", brown = "6", lightgrey = "7", darkgrey = "8", lightblue = "9", lightgreen = "10", lightcyan = "11", lightred = "12", lightmagenta = "13", yellow = "14", white = "15", } -- BRC.COL (do not remove this comment) BRC.DMG_TYPE = { unbranded = 1, -- No brand plain = 2, -- Include brand dmg for non-elemental brands branded = 3, -- Include full brand dmg scoring = 4, -- Include boosts for non-damaging brands } -- BRC.DMG_TYPE (do not remove this comment) BRC.SIZE_PENALTY = { LITTLE = -2, SMALL = -1, NORMAL = 0, LARGE = 1, GIANT = 2 } BRC.BAD_DURATIONS = { "berserk", "blind", "confused", "corroded", "diminished spells", "marked", "magic-sapped", "short of breath", "sign of ruin", "slowed", "sluggish", "tree-form", "vertiginous", "vulnerable", "weakened", } -- BRC.BAD_DURATIONS (do not remove this comment) } #################################### End lua/core/constants.lua ################################### ################################################################################################### ##################################### Begin lua/util/util.lua ##################################### ############### https://github.com/brianfaires/crawl-rc/blob/main/lua/util/util.lua ############### { --------------------------------------------------------------------------------------------------- -- BRC utility module -- @module BRC.util -- General utility functions. --------------------------------------------------------------------------------------------------- BRC.util = {} --- Get the keycode for Cntl+char function BRC.util.cntl(c) if c >= '0' and c <= '9' or c >= 'a' and c <= 'z' then -- Idk why webtiles numeric keycodes are different than local tiles on Mac if crawl.is_webtiles() and c >= '0' and c <= '9' then return string.byte(c) - 64 end return string.byte(c) - 96 elseif c >= 'A' and c <= 'Z' then return string.byte(c) - 64 end BRC.mpr.error("Unsupported character sent to BRC.util.cntl: %s", c) return nil end --- Get key assigned to a crawl command (e.g. "CMD_EXPLORE") function BRC.util.get_cmd_key(cmd) local key = crawl.get_command(cmd) if not key or key == "NULL" then return nil end -- get_command returns things like "Uppercase Ctrl-S"; we just want 'S' local char_key = key:sub(-1) return key:contains("Ctrl") and BRC.util.cntl(char_key) or char_key end --- Tries sendkeys() first, fallback to do_commands() (which isn't always immediate) -- @param cmd (string) The command to execute like "CMD_EXPLORE" function BRC.util.do_cmd(cmd) local key = BRC.util.get_cmd_key(cmd) if key then crawl.sendkeys({ key }) crawl.flush_input() else crawl.do_commands({ cmd }) end end ---- Lua table helpers ---- --- Add or remove an item from a list function BRC.util.add_or_remove(list, item, add) if add then list[#list + 1] = item else util.remove(list, item) end end --- Sorts the keys of a dictionary/map: vars before tables, then alphabetically by key -- If a list is passed, will assume it's a list of global variable names function BRC.util.get_sorted_keys(map_or_list) local keys_vars = {} local keys_tables = {} if BRC.util.is_map(map_or_list) then for key, v in pairs(map_or_list) do table.insert(type(v) == "table" and keys_tables or keys_vars, key) end else for _, key in ipairs(map_or_list) do table.insert(type(_G[key]) == "table" and keys_tables or keys_vars, key) end end util.sort(keys_vars) util.sort(keys_tables) util.append(keys_vars, keys_tables) return keys_vars end function BRC.util.is_list(value) return value and type(value) == "table" and #value > 0 end function BRC.util.is_map(value) return value and type(value) == "table" and next(value) ~= nil and #value == 0 end --- Compare version (x.y) to crawl version. Return true if v1 <= crawl version. function BRC.util.version_is_valid(v1) local crawl_v = crawl.version("major") local cv_parts = { string.match(crawl_v, "([0-9]+)%.([0-9]+)" ) } local v1_parts = { string.match(v1, "([0-9]+)%.([0-9]+)" ) } return v1_parts[1] < cv_parts[1] or (v1_parts[1] == cv_parts[1] and v1_parts[2] <= cv_parts[2]) end } ###################################### End lua/util/util.lua ###################################### ################################################################################################### ##################################### Begin lua/util/text.lua ##################################### ############### https://github.com/brianfaires/crawl-rc/blob/main/lua/util/text.lua ############### { --------------------------------------------------------------------------------------------------- -- BRC utility module -- @module BRC.txt -- Text and string functions. -- Creates string:contains() for all strings --------------------------------------------------------------------------------------------------- BRC.txt = {} ---- Text parsing ---- --- Search for text within a string, without Lua pattern matching. -- @return (bool) True if text is found, false otherwise. function BRC.txt.contains(self, text) return self:find(text, 1, true) ~= nil end --- Connect string:contains() to BRC.txt.contains() getmetatable("").__index.contains = BRC.txt.contains --- Parse the slot and item name from an item pickup message (e.g. "w - a +0 short sword") -- @return (string, int) The item name and slot index function BRC.txt.get_pickup_info(text) local cleaned = BRC.txt.clean(text) if cleaned:sub(2, 4) ~= " - " then return nil end return cleaned:sub(5, #cleaned), items.letter_to_index(cleaned:sub(1, 1)) end ---- Color functions - Usage: BRC.txt.white("Hello"), or BRC.txt["15"]("Hello") ---- for k, color in pairs(BRC.COL) do BRC.txt[k] = function(text) return string.format("<%s>%s", color, tostring(text), color) end BRC.txt[color] = BRC.txt[k] end ---- String manipulation ---- function BRC.txt.capitalize(s) if not s or s == "" then return s end return string.upper(string.sub(s, 1, 1)) .. string.lower(string.sub(s, 2)) end --- Remove newlines and tags from text function BRC.txt.clean(text) if type(text) ~= "string" then return text end return text:gsub("\n", ""):gsub("<[^>]*>", "") end function BRC.txt.wrap(text, wrapper, no_space) if not wrapper then return text end return table.concat({ wrapper, text, wrapper }, no_space and "" or " ") end ---- Conversion to string ---- function BRC.txt.int2char(num) return string.char(string.byte("a") + num) end function BRC.txt.serialize_chk_lua_save() local tokens = { BRC.txt.lightblue("\n---CHK_LUA_SAVE---") } for _, func in ipairs(chk_lua_save) do local result = func() if result and #result > 0 then tokens[#tokens + 1] = util.trim(result) end end return table.concat(tokens, "\n") end function BRC.txt.serialize_inventory() local tokens = { BRC.txt.lightcyan("\n---INVENTORY---\n") } for _, inv in ipairs(items.inventory()) do local base = inv.name("base") or "N/A" local cls = inv.class(true) or "N/A" local st = inv.subtype() or "N/A" tokens[#tokens + 1] = string.format("%s: (%s) Qual: %s", inv.slot, inv.quantity, inv.name()) tokens[#tokens + 1] = string.format(" Base: %s Class: %s, Subtype: %s\n", base, cls, st) end return table.concat(tokens) end ---- BRC.txt.tostr() local helper functions ---- local function limit_lines(str) if not str or str == "" then return str end if BRC.Config.dump.max_lines_per_table and BRC.Config.dump.max_lines_per_table > 0 then local lines = 1 str:gsub("\n", function() lines = lines + 1 end) if lines > BRC.Config.dump.max_lines_per_table then return string.format("{ %s lines... }", lines) end end return str end local function tostr_string(var, pretty) local s if var:contains("\n") then s = string.format("[[\n%s]]", var) else s = '"' .. var:gsub('"', "") .. '"' end if not pretty then return s end -- Replace > and < to display the color tags instead of colored text return s:gsub(">", "TempGT"):gsub("<", "TempLT"):gsub("TempGT", ""):gsub("TempLT", "") end local function tostr_list(var, pretty, indents) local tokens = {} for _, v in ipairs(var) do tokens[#tokens + 1] = limit_lines(BRC.txt.tostr(v, pretty, indents + 1)) end if #tokens < 4 and not util.exists(var, function(t) return type(t) == "table" end) then return "{ " .. table.concat(tokens, ", ") .. " }" else local INDENT = string.rep(" ", indents) local CHILD_INDENT = string.rep(" ", indents + 1) local LIST_SEP = ",\n" .. CHILD_INDENT return "{\n" .. CHILD_INDENT .. table.concat(tokens, LIST_SEP) .. "\n" .. INDENT .. "}" end end local function tostr_map(var, pretty, indents) local tokens = {} if pretty then local keys = BRC.util.get_sorted_keys(var) local contains_table = false for i = 1, #keys do local v = limit_lines(BRC.txt.tostr(var[keys[i]], true, indents + 1)) if v then if type(var[keys[i]]) == "table" then contains_table = true tokens[#tokens + 1] = string.format('["%s"] = %s', keys[i], v) else tokens[#tokens + 1] = string.format("%s = %s", keys[i], v) end end end if #tokens <= 2 and not contains_table then return "{ " .. table.concat(tokens, ", ") .. " }" end else for k, v in pairs(var) do local val_str = BRC.txt.tostr(v, pretty, indents + 1) if val_str then tokens[#tokens + 1] = '["' .. k .. '"] = ' .. val_str end end end local INDENT = string.rep(" ", indents) local CHILD_INDENT = string.rep(" ", indents + 1) local LIST_SEP = ",\n" .. CHILD_INDENT return "{\n" .. CHILD_INDENT .. table.concat(tokens, LIST_SEP) .. "\n" .. INDENT .. "}" end --- Serializes a variable to a string, for chk_lua_save or data dumps. -- @param pretty (optional bool) format for human readability -- @param _indents (optional int) Used internally to format multi-line tables function BRC.txt.tostr(var, pretty, _indents) local var_type = type(var) if var_type == "string" then return tostr_string(var, pretty) elseif var_type == "table" then _indents = _indents or 0 if BRC.util.is_list(var) then return tostr_list(var, pretty, _indents) elseif BRC.util.is_map(var) then return tostr_map(var, pretty, _indents) else return "{}" end end if BRC.Config.dump.omit_pointers and (var_type == "function" or var_type == "userdata") then return nil end return tostring(var) -- fallback to tostring() end } ###################################### End lua/util/text.lua ###################################### ################################################################################################### ###################################### Begin lua/util/mpr.lua ##################################### ################ https://github.com/brianfaires/crawl-rc/blob/main/lua/util/mpr.lua ############### { --------------------------------------------------------------------------------------------------- -- BRC utility module -- @module BRC.mpr -- Wrappers around crawl.mpr for message printing : Colors, formatted messages, message queue, etc. --------------------------------------------------------------------------------------------------- BRC.mpr = {} BRC.mpr.brc_prefix = BRC.txt.darkgrey("[BRC] ") ---- mpr queue (displayed after all other messages for the turn) ---- local _mpr_queue = {} --- Queue a message to dispay at the end of ready() function BRC.mpr.que(msg, color, channel) BRC.mpr.que_optmore(false, msg, color, channel) end --- Queue msg w/ conditional force_more_message -- send with empty msg for a delayed force_more_message function BRC.mpr.que_optmore(show_more, msg, msg_color, channel) for _, q in ipairs(_mpr_queue) do if q.m == msg and q.ch == channel and q.more == show_more then return end end msg_color = msg_color or BRC.COL.lightgrey if not msg or #msg == 0 then msg = "" else msg = BRC.txt[msg_color](msg) end _mpr_queue[#_mpr_queue + 1] = { m = msg, ch = channel, more = show_more } end --- Display queued messages and clear the queue function BRC.mpr.consume_queue() local do_more = util.exists(_mpr_queue, function(q) return q.more end) -- stop_activity() can generate more autopickups, and thus more queue'd messages if do_more then you.stop_activity() end for _, msg in ipairs(_mpr_queue) do if msg.m and #msg.m > 0 then crawl.mpr(tostring(msg.m), msg.ch) crawl.flush_prev_message() end end _mpr_queue = {} if do_more then crawl.redraw_screen() crawl.more() end end ---- Color functions - Usage: BRC.mpr.white("Hello"), or BRC.mpr["15"]("Hello") ---- for k, color in pairs(BRC.COL) do BRC.mpr[k] = function(msg, channel) crawl.mpr(BRC.txt[color](msg), channel) crawl.flush_prev_message() end BRC.mpr[color] = BRC.mpr[k] end ---- Pre-formatted logging functions ---- local function log_message(message, context, msg_color) -- Avoid referencing BRC, to stay robust during startup msg_color = msg_color or "lightgrey" local msg = BRC.mpr.brc_prefix .. tostring(message) if context then msg = string.format("%s (%s)", msg, tostring(context)) end crawl.mpr(string.format("<%s>%s", msg_color, msg, msg_color)) crawl.flush_prev_message() end --- Primary function for displaying errors. Includes a force_more_message by default. -- @param context (optional) Additional context. No context if params are (string, bool). function BRC.mpr.error(message, context, skip_more) if type(context) == "boolean" and skip_more == nil then skip_more = context context = nil end -- Stop and clean up state before displaying the error BRC.opt.clear_single_turn_mutes() crawl.flush_input() you.stop_activity() log_message("(Error) " .. message, context, BRC.COL.lightred) if context then message = message .. " (" .. context .. ")" end print(message) if not skip_more then crawl.redraw_screen() crawl.more() end if BRC.Config.mpr.take_note_on_error then crawl.take_note("[BRC] (Error) " .. tostring(message)) end if BRC.Config.mpr.logs_to_stderr then crawl.stderr(BRC.mpr.brc_prefix .. "(Error) " .. message) end end function BRC.mpr.warning(message, context) log_message(message, context, BRC.COL.yellow) you.stop_activity() if BRC.Config.mpr.logs_to_stderr then crawl.stderr(BRC.mpr.brc_prefix .. "(Warning) " .. message) end end function BRC.mpr.info(message, context) log_message(message, context, BRC.COL.darkgrey) end function BRC.mpr.debug(message, context) if BRC.Config.mpr.show_debug_messages then log_message(message, context, BRC.COL.lightblue) end if BRC.Config.mpr.logs_to_stderr then crawl.stderr(BRC.mpr.brc_prefix .. "(Debug) " .. message) end end function BRC.mpr.okay(suffix) BRC.mpr.darkgrey("Okay, then." .. (suffix and " " .. suffix or "")) end --- Print a variable's stringified value function BRC.mpr.tostr(v) crawl.mpr(BRC.txt.tostr(v, true)) end ---- Messages with stop or force_more ---- --- Message plus stop travel/activity function BRC.mpr.stop(msg, color, channel) BRC.mpr[color or BRC.COL.lightgrey](msg, channel) you.stop_activity() end --- Message as a force_more_message function BRC.mpr.more(msg, color, channel) BRC.mpr[color or BRC.COL.lightgrey](msg, channel) you.stop_activity() crawl.redraw_screen() crawl.more() end --- Conditional force_more_message function BRC.mpr.optmore(show_more, msg, color, channel) if show_more then BRC.mpr.more(msg, color, channel) else BRC.mpr[color or BRC.COL.lightgrey](msg, channel) end end ---- Prompts for user input ---- --- Get a selection from the user, from a list of options function BRC.mpr.select(msg, options, color) if not (type(options) == "table" and #options > 0) then BRC.mpr.error("No options provided for BRC.mpr.select") return false end msg = msg .. ":\n" for i, option in ipairs(options) do msg = msg .. string.format("%s: %s\n", i, BRC.txt.white(option)) end BRC.mpr[color or BRC.COL.lightcyan](msg, "prompt") for _ = 1, 10 do local res = crawl.getch() if res then local num = res - string.byte("0") if num > 0 and num <= #options then return options[num] end end BRC.mpr.magenta("Invalid option, try again.") end BRC.mpr.lightmagenta("Fine then. Using option 1: " .. options[1]) return options[1] end --- Get a yes/no response function BRC.mpr.yesno(msg, color, capital_only) msg = string.format("%s (%s)", msg, capital_only and "Y/N" or "y/n") for i = 1, 10 do BRC.mpr[color or BRC.COL.lightgrey](msg, "prompt") local res = crawl.getch() if res and res >= 0 and res <= 255 then local c = string.char(res) if c == "Y" or c == "y" and not capital_only then return true end if c == "N" or c == "n" and not capital_only then return false end end if i == 1 and capital_only then msg = "[CAPS ONLY] " .. msg end end BRC.mpr.lightmagenta("Feels like a no.") return false end } ####################################### End lua/util/mpr.lua ###################################### ################################################################################################### ##################################### Begin lua/util/item.lua ##################################### ############### https://github.com/brianfaires/crawl-rc/blob/main/lua/util/item.lua ############### { --------------------------------------------------------------------------------------------------- -- BRC utility module -- @module BRC.it -- Utilities for checking item types/attributes, and retrieving item-related information. --------------------------------------------------------------------------------------------------- BRC.it = {} function BRC.it.get_xy(name, radius) local r = radius or you.los() for dx = -r, r do for dy = -r, r do for _, fl in ipairs(items.get_items_at(dx, dy) or {}) do if fl.name() == name then return dx, dy end end end end end function BRC.it.get_staff_school(it) for k, v in pairs(BRC.MAGIC_SCHOOLS) do if it.subtype() == k then return v end end end function BRC.it.get_talisman_min_level(it) if it.name() == "protean talisman" then return 6 end -- Parse the item description local tokens = crawl.split(it.description, "\n") for _, v in ipairs(tokens) do if v:sub(1, 4) == "Min " then local start_pos = v:find("%d", 4) if start_pos then local end_pos = v:find("[^%d]", start_pos) return tonumber(v:sub(start_pos, end_pos - 1)) end end end BRC.mpr.error("Failed to find skill required for: " .. it.name()) return -1 end --- @return table All items whose slot is idx function BRC.it.all_inslot(idx) return util.filter(function(i) return i.slot == idx end, items.inventory()) end ---- Simple boolean checks ---- function BRC.it.is_amulet(it) return it and it.name("base") == "amulet" end function BRC.it.is_armour(it, include_orbs) return it and it.class(true) == "armour" and (include_orbs or not BRC.it.is_orb(it)) end function BRC.it.is_aux_armour(it) return BRC.it.is_armour(it) and not (BRC.it.is_body_armour(it) or BRC.it.is_shield(it)) end function BRC.it.is_body_armour(it) return it and it.subtype() == "body" end function BRC.it.is_jewellery(it) return it and it.class(true) == "jewellery" end function BRC.it.is_magic_staff(it) return it and it.class and it.class(true) == "magical staff" end function BRC.it.is_ring(it) return it and it.name("base") == "ring" end function BRC.it.is_scarf(it) return BRC.it.is_armour(it) and it.subtype() == "cloak" and it.name():contains("scarf") end function BRC.it.is_shield(it) return it and it.is_shield() end function BRC.it.is_talisman(it) return it and it.class(true) == "talisman" end function BRC.it.is_orb(it) return it and it.subtype() == "offhand" and not it.is_shield() end function BRC.it.is_polearm(it) return it and it.weap_skill:contains("Polearms") end } ###################################### End lua/util/item.lua ###################################### ################################################################################################### ###################################### Begin lua/util/you.lua ##################################### ################ https://github.com/brianfaires/crawl-rc/blob/main/lua/util/you.lua ############### { --------------------------------------------------------------------------------------------------- -- BRC utility module -- @module BRC.you -- Utilities for checking character attributes and state --------------------------------------------------------------------------------------------------- BRC.you = {} --- Get mutation level, explicitly specifying crawl's optional params. -- @param innate_only boolean (optional) True to count only innate mutations, else count all. function BRC.you.mut_lvl(mutation, innate_only) return you.get_base_mutation_level(mutation, true, not innate_only, not innate_only) end ---- Boolean attributes ---- function BRC.you.by_slimy_wall() for x = -1, 1 do for y = -1, 1 do if view.feature_at(x, y) == "slimy_wall" then return true end end end return false end function BRC.you.free_offhand() if BRC.you.mut_lvl("missing a hand") > 0 then return true end return not items.equipped_at("offhand") end function BRC.you.have_shield() return BRC.it.is_shield(items.equipped_at("offhand")) end function BRC.you.in_hell(exclude_vestibule) local branch = you.branch() if exclude_vestibule and branch == "Hell" then return false end return util.contains(BRC.HELL_BRANCHES, branch) end function BRC.you.miasma_immune() if util.contains(BRC.UNDEAD_RACES, you.race()) then return true end if util.contains(BRC.NONLIVING_RACES, you.race()) then return true end return false end function BRC.you.mutation_immune() return util.contains(BRC.UNDEAD_RACES, you.race()) end function BRC.you.shapeshifting_skill() local skill = you.skill("Shapeshifting") local AMU = "amulet of wildshape" if util.exists(items.inventory(), function(i) return i.name("qual") == AMU end) then return skill + 5 end return skill end function BRC.you.size_penalty() if util.contains(BRC.LITTLE_RACES, you.race()) then return BRC.SIZE_PENALTY.LITTLE elseif util.contains(BRC.SMALL_RACES, you.race()) then return BRC.SIZE_PENALTY.SMALL elseif util.contains(BRC.LARGE_RACES, you.race()) then return BRC.SIZE_PENALTY.LARGE else return BRC.SIZE_PENALTY.NORMAL end end function BRC.you.unarmed_attack_delay() return 1 - you.skill("Unarmed Combat") / 54 end function BRC.you.zero_stat() return you.strength() <= 0 or you.dexterity() <= 0 or you.intelligence() <= 0 end ---- Equipment slot functions ---- --- The number of equipment slots available for the item (usually 1) function BRC.you.num_eq_slots(it) local player_race = you.race() if it.is_weapon then return player_race == "Coglin" and 2 or 1 end if BRC.it.is_aux_armour(it) then if player_race == "Formicid" then return it.subtype() == "gloves" and 2 or 1 end return player_race == "Poltergeist" and 6 or 1 end return 1 end --- Get all equipped items in the slot type for the item -- @return (table, int) - items, and num_slots (max size the list can be) -- This is usually a list of length 1, with num_slots==1 function BRC.you.equipped_at(it) local all_aux = {} local num_slots = BRC.you.num_eq_slots(it) local slot_name = it.is_weapon and "weapon" or BRC.it.is_body_armour(it) and "armour" or it.subtype() for i = 1, num_slots do local eq = items.equipped_at(slot_name, i) all_aux[#all_aux + 1] = eq end return all_aux, num_slots end ---- Skill attributes ---- function BRC.you.top_wpn_skill() local max_weap_skill = 0 local pref = nil for _, v in ipairs(BRC.WEAP_SCHOOLS) do if BRC.you.skill(v) > max_weap_skill then max_weap_skill = BRC.you.skill(v) pref = v end end return pref end --- Get the max skill for a comma-separated list of skills function BRC.you.skill(skill) if skill and not skill:contains(",") then return you.skill(skill) end local skills = crawl.split(skill, ",") local max = 0 for _, s in ipairs(skills) do if you.skill(s) > max then max = you.skill(s) end end return max end function BRC.you.skill_with(it) if BRC.it.is_magic_staff(it) then return math.max(BRC.you.skill(BRC.it.get_staff_school(it)), BRC.you.skill("Staves")) end if it.is_weapon then return BRC.you.skill(it.weap_skill) end if BRC.it.is_body_armour(it) then return BRC.you.skill("Armour") end if BRC.it.is_shield(it) then return BRC.you.skill("Shields") end if BRC.it.is_talisman(it) then return BRC.you.shapeshifting_skill() end return nil end } ####################################### End lua/util/you.lua ###################################### ################################################################################################### ################################### Begin lua/util/equipment.lua ################################## ############# https://github.com/brianfaires/crawl-rc/blob/main/lua/util/equipment.lua ############ { --------------------------------------------------------------------------------------------------- -- BRC utility module -- @module BRC.eq -- This module contains 2 main types of functions: -- 1. Mirroring crawl calculations, like weapon damage, armour penalty, etc. -- 2. Design choices that aren't as generalizable as other util functions. -- Ex: Dragon scales are always considered branded, DPS calculation is an approximation, etc. -- Ex: is_risky(), is_useless_ego(), get_ego(), get_hands(), etc. --------------------------------------------------------------------------------------------------- BRC.eq = {} ---- Local functions (Mostly mirroring crawl calculations) ---- -- Last verified against: current crawl master branch (0.34-a0-786-ge5b59a6c5f) local function get_unadjusted_armour_pen(encumb) local pen = encumb - 2 * BRC.you.mut_lvl("sturdy frame") if pen > 0 then return pen end return 0 end local function get_adjusted_armour_pen(encumb, str) local base_pen = get_unadjusted_armour_pen(encumb) return 2 * base_pen * base_pen * (45 - you.skill("Armour")) / 45 / (5 * (str + 3)) end local function get_adjusted_dodge_bonus(encumb, str, dex) local size_factor = -2 * BRC.you.size_penalty() local dodge_bonus = 8 * (10 + you.skill("Dodging") * dex) / (20 - size_factor) / 10 local armour_dodge_penalty = get_unadjusted_armour_pen(encumb) - 3 if armour_dodge_penalty <= 0 then return dodge_bonus end if armour_dodge_penalty >= str then return dodge_bonus * str / (armour_dodge_penalty * 2) end return dodge_bonus - dodge_bonus * armour_dodge_penalty / (str * 2) end local function get_shield_penalty(sh) return 2 * sh.encumbrance * sh.encumbrance * (27 - you.skill("Shields")) / 27 / (25 + 5 * you.strength()) end local function get_branded_delay(delay, ego) if not ego then return delay end if ego == "speed" then return delay * 2 / 3 elseif ego == "heavy" then return delay * 1.5 end return delay end local function get_weap_min_delay(it) -- This is an abbreviated version of the actual calculation. -- Doesn't check brand or delay >=3, which are covered in get_weap_delay() if it.artefact and it.name("qual"):contains("woodcutter's axe") then return it.delay end local min_delay = math.floor(it.delay / 2) if it.weap_skill == "Short Blades" then return 5 end if it.is_ranged then local basename = it.name("base") local is_2h_ranged = basename:contains("crossbow") or basename:contains("arbalest") if is_2h_ranged then return math.max(min_delay, 10) end end return math.min(min_delay, 7) end local function get_slay_bonuses() local sum = 0 -- Slots can go as high as 18 afaict for i = 0, 20 do local inv = items.equipped_at(i) if inv then if BRC.it.is_ring(inv) then if inv.artefact then local name = inv.name() local idx = name:find("Slay", 1, true) if idx then local slay = tonumber(name:sub(idx + 5, idx + 5)) if slay == 1 then local next_digit = tonumber(name:sub(idx + 6, idx + 6)) if next_digit then slay = 10 + next_digit end end if name:sub(idx + 4, idx + 4) == "+" then sum = sum + slay else sum = sum - slay end end elseif BRC.eq.get_ego(inv) == "Slay" then sum = sum + inv.plus end elseif inv.artefact and (BRC.it.is_armour(inv, true) or BRC.it.is_amulet(inv)) then local slay = inv.artprops["Slay"] if slay then sum = sum + slay end end end end if you.race() == "Demonspawn" then sum = sum + 3 * BRC.you.mut_lvl("augmentation") sum = sum + BRC.you.mut_lvl("sharp scales") end return sum end --- Gets the updated stats after equipping an item -- @param stats (table) Keys are artefact properties, values are the current values -- If multiple equip slots available, assumes no item is removed. -- @return (table) New stats table with updated values (original table is not modified) local function get_stats_with_item(it, stats) local new_stats = util.copy_table(stats) local cur = items.equipped_at(it.equip_type) if not cur or it.equipped then return new_stats end if cur.artefact and BRC.you.num_eq_slots(it) == 1 then for k, _ in pairs(new_stats) do new_stats[k] = new_stats[k] - (cur.artprops[k] or 0) end end if it.artefact then for k, _ in pairs(new_stats) do new_stats[k] = new_stats[k] + (it.artprops[k] or 0) end end return new_stats end --- Get change in SH and EV when switching to shield local function get_delta_sh_ev(it) local it_sh = BRC.eq.get_sh(it) local it_ev = -get_shield_penalty(it) local cur = items.equipped_at(it.equip_type) if not cur or it.equipped then return it_sh, it_ev else return it_sh - BRC.eq.get_sh(cur), it_ev + get_shield_penalty(cur) end end --- Get change in AC and EV when switching to armour local function get_delta_ac_ev(it) local it_ac = BRC.eq.get_ac(it) local it_ev = BRC.eq.get_armour_ev(it) local cur = items.equipped_at(it.equip_type) if not cur or it.equipped or BRC.you.num_eq_slots(it) > 1 then return it_ac, it_ev else return it_ac - BRC.eq.get_ac(cur), it_ev - BRC.eq.get_armour_ev(cur) end end --- Calculate weapon damage using the brand bonuses in BRC.Config.BrandBonus -- @param dmg_type int Matches a damage type defined in BRC.DMG_TYPE: -- (1) unbranded: Only "heavy" is included -- (2) plain: Include non-elemental damaging brands -- (3) branded: Include all damaging brands -- (4) scoring: Include heuristics from 'subtle' brands local function get_dmg_with_brand_bonus(ego, base_dmg, it_plus, dmg_type) if not ego then return base_dmg + it_plus end -- Check if brand should apply based on damage type local should_apply = ( dmg_type == BRC.DMG_TYPE.unbranded and ego == "heavy" or dmg_type == BRC.DMG_TYPE.plain and util.contains(BRC.NON_ELEMENTAL_DMG_EGOS, ego) or dmg_type >= BRC.DMG_TYPE.branded and BRC.Config.BrandBonus[ego] or dmg_type == BRC.DMG_TYPE.scoring and BRC.Config.BrandBonus.subtle[ego] ) if should_apply then local bonus = BRC.Config.BrandBonus[ego] or BRC.Config.BrandBonus.subtle[ego] return bonus.factor * base_dmg + it_plus + bonus.offset else return base_dmg + it_plus end end ---- Stat formatting functions ---- --- Format damage values for consistent display width (4 characters) local function format_dmg(dmg) if dmg < 10 then return string.format("%.2f", dmg) end if dmg < 100 then return string.format("%.1f", dmg) end return string.format("%.0f ", dmg) end --- Format stat string for display or inscription local function format_stat(abbr, val, is_worn) local stat_str = string.format("%.1f", val) if val < 0 then return string.format("%s%s", abbr, stat_str) elseif is_worn then return string.format("%s:%s", abbr, stat_str) else return string.format("%s+%s", abbr, stat_str) end end --- Get armour stats as strings -- @return (string, string) AC or SH, and EV. If not worn, returns deltas from the worn item stats function BRC.eq.arm_stats(it) if not BRC.it.is_armour(it) then return "", "" end if BRC.it.is_shield(it) then local sh_delta, ev_delta = get_delta_sh_ev(it) local sh_str = format_stat("SH", sh_delta, it.equipped) local ev_str = format_stat("EV", ev_delta, it.equipped) return sh_str, ev_str else local ac_delta, ev_delta = get_delta_ac_ev(it) local ac_str = format_stat("AC", ac_delta, it.equipped) if not BRC.it.is_body_armour(it) then return ac_str end local ev_str = format_stat("EV", ev_delta, it.equipped) return ac_str, ev_str end end --- Get weapon stats as a string -- @return (string) DPS, damage, delay, and accuracy function BRC.eq.wpn_stats(it, dmg_type, skip_dps) if not it.is_weapon then return end if not dmg_type then -- Default to pulling from inscribe-stats config, if it exists. Else use plain. if f_inscribe_stats and f_inscribe_stats.Config then if type(f_inscribe_stats.Config.dmg_type) == "string" then dmg_type = BRC.DMG_TYPE[f_inscribe_stats.Config.dmg_type] else dmg_type = f_inscribe_stats.Config.dmg_type end else dmg_type = BRC.DMG_TYPE.plain end end local dmg = format_dmg(BRC.eq.get_avg_dmg(it, dmg_type)) local delay = BRC.eq.get_weap_delay(it) local delay_str = string.format("%.1f", delay) if delay < 1 then delay_str = string.format("%.2f", delay) delay_str = delay_str:sub(2, #delay_str) end local dps = format_dmg(dmg / delay) local acc = it.accuracy + (it.plus or 0) if acc >= 0 then acc = "+" .. acc end --TODO: This would be nice if it worked in all UIs --return string.format("DPS=%s (%s/%s) Ac%s", dps, dmg, delay_str, acc) if skip_dps then return string.format("Dmg=%s/%s A%s", dmg, delay_str, acc) end return string.format("DPS=%s (%s/%s) A%s", dps, dmg, delay_str, acc) end ---- Armour stats ---- function BRC.eq.get_ac(it) local it_plus = it.plus or 0 if it.artefact then local art_ac = it.artprops["AC"] if art_ac then it_plus = it_plus + art_ac end end local ac = it.ac * (1 + you.skill("Armour") / 22) + it_plus if not BRC.it.is_body_armour(it) then return ac end if BRC.you.mut_lvl("deformed body") + BRC.you.mut_lvl("pseudopods") > 0 then ac = ac * 0.6 end return ac end --- Compute an armour's impact on EV, including stat changes from wearing/removing artefacts function BRC.eq.get_armour_ev(it) local cur = { Str = you.strength(), Dex = you.dexterity(), EV = 0 } local worn = get_stats_with_item(it, cur) if worn.Str <= 0 then worn.Str = 1 end local bonus = get_adjusted_dodge_bonus(it.encumbrance, worn.Str, worn.Dex) if cur.Str <= 0 then cur.Str = 1 end local naked_bonus = get_adjusted_dodge_bonus(0, cur.Str, cur.Dex) return bonus - naked_bonus + worn.EV - get_adjusted_armour_pen(it.encumbrance, worn.Str) end function BRC.eq.get_sh(it) local stats = get_stats_with_item(it, { Dex = you.dexterity() }) local it_plus = it.plus or 0 local sh_skill = you.skill("Shields") local base_sh = it.ac * 2 local shield = base_sh * (50 + sh_skill * 5 / 2) shield = shield + 200 * it_plus shield = shield + 38 * (sh_skill + 3 + stats.Dex * (base_sh + 13) / 26) return shield / 200 end ---- Weapon stats ---- function BRC.eq.get_weap_delay(it) local delay = it.delay - BRC.you.skill(it.weap_skill) / 2 delay = math.max(delay, get_weap_min_delay(it)) delay = get_branded_delay(delay, BRC.eq.get_ego(it)) delay = math.max(delay, 3) local sh = items.equipped_at("offhand") if BRC.it.is_shield(sh) then delay = delay + get_shield_penalty(sh) end if it.is_ranged then local worn = items.equipped_at("armour") if worn then local str = you.strength() local cur = items.equipped_at("weapon") if cur and cur ~= it and cur.artefact then if it.artefact and it.artprops["Str"] then str = str + it.artprops["Str"] end if cur.artefact and cur.artprops["Str"] then str = str - cur.artprops["Str"] end end delay = delay + get_adjusted_armour_pen(worn.encumbrance, str) end end return delay / 10 end --- Get weapon damage (average), including stat/slay changes when swapping from current weapon. -- Aux attacks not included function BRC.eq.get_avg_dmg(it, dmg_type) dmg_type = dmg_type or BRC.DMG_TYPE.scoring local it_plus = (it.plus or 0) + get_slay_bonuses() local stats = { Str = you.strength(), Dex = you.dexterity(), Slay = it_plus } stats = get_stats_with_item(it, stats) local stat = (it.is_ranged or it.weap_skill:contains("Blades")) and stats.Dex or stats.Str local stat_mod = 0.75 + 0.025 * stat local skill_mod = (1 + BRC.you.skill(it.weap_skill) / 50) * (1 + you.skill("Fighting") / 60) local base_dmg = it.damage * stat_mod * skill_mod if BRC.it.is_magic_staff(it) then return base_dmg + stats.Slay + BRC.eq.get_staff_bonus_dmg(it, dmg_type) else return get_dmg_with_brand_bonus(BRC.eq.get_ego(it), base_dmg, stats.Slay, dmg_type) end end function BRC.eq.get_dps(it, dmg_type) if not dmg_type then dmg_type = BRC.DMG_TYPE.scoring end return BRC.eq.get_avg_dmg(it, dmg_type) / BRC.eq.get_weap_delay(it) end --- Calculate expected damage output from a staff -- @return (number) Damage * chance, (number) damage on proc, (number) chance to proc function BRC.eq.get_staff_bonus_dmg(it, dmg_type) local result = nil if dmg_type == BRC.DMG_TYPE.unbranded then result = 0 end if dmg_type == BRC.DMG_TYPE.plain then local basename = it.name("base") if basename ~= "staff of earth" and basename ~= "staff of conjuration" then result = 0 end end local spell_skill = BRC.you.skill(BRC.it.get_staff_school(it)) local evo_skill = you.skill("Evocations") local chance = (2 * evo_skill + spell_skill) / 30 if chance > 1 then chance = 1 end -- 0.75 is an acceptable approximation; most commonly 63/80 -- Varies by staff type in sometimes complex ways local dmg = 3 / 4 * (evo_skill / 2 + spell_skill) result = result or dmg * chance return result, dmg, chance end ---- Item properties ---- --- Get the ego of an item, with custom logic: -- Treat unusable egos as no ego. Always lowercase ego in return value. -- Include armours with innate effects (except steam dragon scales) -- Artefacts return their normal ego if they have one, else their name -- @param no_stat_only_egos (optional bool) Exclude egos that only affect speed/damage function BRC.eq.get_ego(it, no_stat_only_egos) local ego = it.ego(true) if ego then ego = ego:lower() if BRC.eq.is_useless_ego(ego) or (no_stat_only_egos and (ego == "speed" or ego == "heavy")) then return it.artefact and it.name() or nil end return ego end if BRC.it.is_body_armour(it) then local name = it.name("qual") local good_scales = name:contains("dragon scales") and not name:contains("steam") if name:contains("troll leather") or good_scales then return name end end return it.artefact and it.name() or nil end function BRC.eq.get_hands(it) if you.race() ~= "Formicid" then return it.hands end local st = it.subtype() if st == "giant club" or st == "giant spiked club" then return 2 end return 1 end function BRC.eq.is_risky(it) if it.artefact then for k, v in pairs(it.artprops) do if util.contains(BRC.ARTPROPS_BAD, k) or v < 0 then return true end end end local ego_name = BRC.eq.get_ego(it) return ego_name and util.contains(BRC.RISKY_EGOS, ego_name) end function BRC.eq.is_useless_ego(ego) if BRC.MAGIC_SCHOOLS[ego] then return BRC.Config.unskilled_egos_usable or you.skill(BRC.MAGIC_SCHOOLS[ego]) > 0 end local race = you.race() return ego == "holy" and util.contains(BRC.UNDEAD_RACES, race) or (ego == "rPois" or ego == "rpois" or ego == "poison resistance") and util.contains(BRC.POIS_RES_RACES, race) or ego == "pain" and you.skill("Necromancy") == 0 end } #################################### End lua/util/equipment.lua ################################### ################################################################################################### #################################### Begin lua/util/options.lua ################################### ############## https://github.com/brianfaires/crawl-rc/blob/main/lua/util/options.lua ############# { --------------------------------------------------------------------------------------------------- -- BRC utility module -- @module BRC.opt -- Functions for setting crawl options and macros. --------------------------------------------------------------------------------------------------- BRC.opt = {} ---- Single turn mutes: Mute a message for the current turn only ---- local _single_turn_mutes = {} local _claimed_macro_keys = {} function BRC.opt.single_turn_mute(pattern) BRC.opt.message_mute(pattern, true) _single_turn_mutes[#_single_turn_mutes + 1] = pattern end function BRC.opt.clear_single_turn_mutes() util.foreach(_single_turn_mutes, function(m) BRC.opt.message_mute(m, false) end) _single_turn_mutes = {} end ---- crawl.setopt() wrappers ---- function BRC.opt.autopickup_exceptions(pattern, create) local op = create and "^=" or "-=" crawl.setopt(string.format("autopickup_exceptions %s %s", op, pattern)) end function BRC.opt.explore_stop(pattern, create) local op = create and "+=" or "-=" crawl.setopt(string.format("explore_stop %s %s", op, pattern)) end function BRC.opt.explore_stop_pickup_ignore(pattern, create) local op = create and "+=" or "-=" crawl.setopt(string.format("explore_stop_pickup_ignore %s %s", op, pattern)) end function BRC.opt.flash_screen_message(pattern, create) local op = create and "+=" or "-=" crawl.setopt(string.format("flash_screen_message %s %s", op, pattern)) end function BRC.opt.force_more_message(pattern, create) local op = create and "+=" or "-=" crawl.setopt(string.format("force_more_message %s %s", op, pattern)) end --- Bind a macro to a key. Function must be global and not a member of a module. -- If key is a number, it is converted to a keycode string. -- Passing an invalid function name will clear the macro for that key. function BRC.opt.macro(key, function_name, overwrite_existing) -- Format msg for debugging and keycode for crawl.setopt() local key_str = nil if type(key) == "number" then -- Try to convert to key name for better debug msg for k, v in pairs(BRC.KEYS) do if v == key then key_str = "<<< " .. k .. " >>" break end end -- Format keycode string for crawl.setopt() key = "\\{" .. key .. "}" if key_str == nil then key_str = "<<< \\" .. key .. " >>" end end -- The << >> formatting protects against crawl thinking '<' is a tag if key_str == nil then key_str = "<<< '" .. key .. "' >>" end if type(_G[function_name]) == "function" then if _claimed_macro_keys[key] and not overwrite_existing then BRC.mpr.debug("Macro key %s is already assigned to %s", key_str, _claimed_macro_keys[key]) return end crawl.setopt(string.format("macros += M %s ===%s", key, function_name)) _claimed_macro_keys[key] = function_name BRC.mpr.debug( string.format( "Assigned macro %s to key: %s", BRC.txt.magenta(function_name .. "()"), BRC.txt.lightred(key_str) ) ) else function_name = _claimed_macro_keys[key] if not function_name then crawl.mpr("no function name found for key: " .. key) return end crawl.setopt(string.format("macros += M %s %s", key, key)) _claimed_macro_keys[key] = nil BRC.mpr.debug( string.format( "Cleared macro %s from key: %s", BRC.txt.magenta(function_name .. "()"), BRC.txt.lightred(key_str) ) ) end end function BRC.opt.clear_macros() for key, _ in pairs(_claimed_macro_keys) do BRC.opt.macro(key, nil, true) end _claimed_macro_keys = {} end function BRC.opt.message_mute(pattern, create) local op = create and "^=" or "-=" crawl.setopt(string.format("message_colour %s mute:%s", op, pattern)) end function BRC.opt.runrest_ignore_message(pattern, create) local op = create and "+=" or "-=" crawl.setopt(string.format("runrest_ignore_message %s %s", op, pattern)) end function BRC.opt.runrest_ignore_monster(pattern, create) local op = create and "+=" or "-=" crawl.setopt(string.format("runrest_ignore_monster %s %s", op, pattern)) end function BRC.opt.runrest_stop_message(pattern, create) local op = create and "+=" or "-=" crawl.setopt(string.format("runrest_stop_message %s %s", op, pattern)) end } ##################################### End lua/util/options.lua #################################### ################################################################################################### ##################################### Begin lua/core/data.lua ##################################### ############### https://github.com/brianfaires/crawl-rc/blob/main/lua/core/data.lua ############### { --------------------------------------------------------------------------------------------------- -- BRC core feature: data-manager -- @module BRC.Data -- Provides functions and maintenance for persistent variables that survive game restarts. -- Handles backup/restore functionality and error handling. --------------------------------------------------------------------------------------------------- BRC.Data = {} BRC.Data.BRC_FEATURE_NAME = "data-manager" -- Included as a feature for Config override ---- Local constants ---- local RESTORE_TABLE = "_brc_persist_restore_table" local MAX_RESTORE_RETRIES = 5 -- Maximum number of retry prompts for data restoration ---- Local variables ---- -- Init tables in declaration, so persist() can be called before init() local _failures = {} local _persist_names = {} local _default_values = {} local pushed_restore_table_creation = false -- Set this on file load, not on init() local cur_location ---- Initialization ---- function BRC.Data.init() cur_location = you.where() if type(c_persist.BRC) ~= "table" then c_persist.BRC = {} end end ---- Local functions ---- local function is_usable_backup() if type(c_persist.BRC) ~= "table" or type(c_persist.BRC.Backup) ~= "table" or c_persist.BRC.Backup.backup_name ~= you.name() or c_persist.BRC.Backup.backup_race ~= you.race() or c_persist.BRC.Backup.backup_class ~= you.class() then return false end local turn_diff = you.turns() - c_persist.BRC.Backup.backup_turn if turn_diff == 0 then return true end for _ = 1, MAX_RESTORE_RETRIES do if BRC.mpr.yesno("Use backup from " .. turn_diff .. " turns ago?") then return true end if BRC.mpr.yesno("Are you sure? Data will reset to defaults.") then return false end end return true end local function try_restore(failed_vars) if not is_usable_backup() then BRC.mpr.error("Unable to restore from backup. Persistent data reset to defaults.", true) BRC.mpr.info("For detailed startup info, set BRC.Config.mpr.show_debug_messages=True.") return false end for _, name in ipairs(failed_vars) do _default_values[name] = nil -- Avoid re-init warnings _G[name] = BRC.Data.persist(name, c_persist.BRC.Backup[name]) end BRC.mpr.green(BRC.mpr.brc_prefix .. "Restored data from backup.") return true end ---- Public API ---- --- Creates a persistent global variable or table, that retains its value through restarts. -- @usage `var = BRC.Data.persist("var", value)` -- @warning After restarting, the variable/table will not exist until this is called. -- @return any The current value (whether default or persisted) function BRC.Data.persist(name, default_value) local t = type(default_value) if not util.contains({ "table", "string", "number", "boolean", "nil" }, t) then BRC.mpr.error(string.format("Cannot persist %s. Default value is of type %s", name, t)) return default_value end -- Keep default value for re-init if _default_values[name] then BRC.mpr.warning("Multiple calls to BRC.Data.persist(" .. name .. ", ...)") end if type(default_value) == "table" then -- Preserve the user's original table (may be in a config, etc) default_value = util.copy_table(default_value) _default_values[name] = util.copy_table(default_value) else _default_values[name] = default_value end -- Try to restore from persistent restore table if you.turns() == 0 then _G[name] = default_value elseif _G[RESTORE_TABLE] and _G[RESTORE_TABLE][name] ~= nil then _G[name] = _G[RESTORE_TABLE][name] _G[RESTORE_TABLE][name] = nil elseif default_value ~= nil and not util.contains(_failures, name) then -- avoid inf loop _G[name] = default_value _failures[#_failures + 1] = name BRC.mpr.debug(BRC.txt.red(name .. " failed to restore from chk_lua_save.")) end -- Create persistent restore table on next startup if not pushed_restore_table_creation then table.insert(chk_lua_save, function() return RESTORE_TABLE .. " = {}\n" end) pushed_restore_table_creation = true end -- Set up persist on next startup if not util.contains(_persist_names, name) then _persist_names[#_persist_names + 1] = name table.insert(chk_lua_save, function() if _G[name] == nil then return "" end return RESTORE_TABLE .. "." .. name .. " = " .. BRC.txt.tostr(_G[name]) .. "\n" end) end return _G[name] end function BRC.Data.serialize() local tokens = { BRC.txt.lightmagenta("\n---PERSISTENT VARIABLES---\n") } local sorted_keys = BRC.util.get_sorted_keys(_persist_names) for _, key in ipairs(sorted_keys) do tokens[#tokens + 1] = string.format("%s = %s\n", key, BRC.txt.tostr(_G[key], true)) end return table.concat(tokens) end function BRC.Data.reset() if _persist_names then for _, name in ipairs(_persist_names) do if type(_default_values[name]) == "table" then _G[name] = util.copy_table(_default_values[name]) else _G[name] = _default_values[name] end end end BRC.mpr.warning("Reset all persistent data to default values.") end --- @return boolean|nil true if no persist errors, false if failed restore, nil if handled errors function BRC.Data.handle_persist_errors() if #_failures == 0 then return true end local msg = "%s persistent variables did not restore: (%s)" BRC.mpr.error(msg:format(#_failures, table.concat(_failures, ", ")), true) for _ = 1, MAX_RESTORE_RETRIES do if BRC.mpr.yesno("Try restoring from backup?") then break end if BRC.mpr.yesno("Are you sure? Data will reset to defaults.") then return nil end end -- Whether restore works or not, we should reset _failures local failed_vars = _failures _failures = {} if try_restore(failed_vars) then return nil end return false end -- Backup and Restore from c_persist.BRC.Backup function BRC.Data.backup() if type(c_persist.BRC) ~= "table" then c_persist.BRC = {} end c_persist.BRC.Backup = {} c_persist.BRC.Backup.backup_name = you.name() c_persist.BRC.Backup.backup_race = you.race() c_persist.BRC.Backup.backup_class = you.class() c_persist.BRC.Backup.backup_turn = you.turns() for _, name in ipairs(_persist_names) do c_persist.BRC.Backup[name] = _G[name] end end ---- Crawl hook functions ---- function BRC.Data.ready() if you.where() ~= cur_location and not you.have_orb() then cur_location = you.where() BRC.Data.backup() end end } ###################################### End lua/core/data.lua ###################################### ################################################################################################### #################################### Begin lua/core/config.lua #################################### ############## https://github.com/brianfaires/crawl-rc/blob/main/lua/core/config.lua ############## { --------------------------------------------------------------------------------------------------- -- BRC core module -- @module BRC.Configs -- Manages user-defined configs and feature config overrides. -- -- TL;DR: Each feature has its own config, with default values for every field. -- To change those values, define the same fields in a user-defined config. -- -- When a user-defined config is loaded, it inherits values from BRC.Configs.Default. -- If a user-defined config defines a field, that value is used. -- If not defined in the config, the value in BRC.Configs.Default is used. -- If not defined in either, the default value in the feature config is used. -- @warning BRC.Configs.Default defines all fields not in a feature config. Do not remove them. --------------------------------------------------------------------------------------------------- BRC.Configs = {} ---- Persistent variables ---- brc_full_persistant_config = BRC.Data.persist("brc_full_persistant_config", nil) brc_config_name = BRC.Data.persist("brc_config_name", nil) ---- BRC Default Config - Every user-defined config inherits these -- Define config fields that aren't feature-specific, and set their default values BRC.Configs.Default = util.copy_table(BRC.Config) -- Include values from BRC.Config in _header.lua BRC.Configs.Default.BRC_CONFIG_NAME = "Default" BRC.Configs.Default.emojis = true -- Include emojis in alerts -- Does "Armour of " have an ego when skill is 0? BRC.Configs.Default.unskilled_egos_usable = false BRC.Configs.Default.mpr = { show_debug_messages = false, logs_to_stderr = false, take_note_on_error = true, -- Note BRC errors in the character file for debugging with char dump } -- BRC.Configs.Default.mpr (do not remove this comment) BRC.Configs.Default.dump = { max_lines_per_table = 200, -- Avoid huge tables (alert_monsters.Config.Alerts) in debug dumps omit_pointers = true, -- Don't dump functions and userdata (they only show a hex address) } -- BRC.Configs.Default.dump (do not remove this comment) --- How weapon damage is calculated for inscriptions+pickup/alert: (factor * DMG + offset) BRC.Configs.Default.BrandBonus = { chaos = { factor = 1.15, offset = 2.0 }, -- Approximate weighted average distort = { factor = 1.0, offset = 6.0 }, drain = { factor = 1.25, offset = 2.0 }, elec = { factor = 1.0, offset = 4.5 }, -- 3.5 on avg; fudged up for AC pen entangle = { factor = 1.1, offset = 3 }, flame = { factor = 1.25, offset = 0 }, freeze = { factor = 1.25, offset = 0 }, heavy = { factor = 1.8, offset = 0 }, -- Speed is accounted for elsewhere pain = { factor = 1.0, offset = you.skill("Necromancy") / 2 }, spect = { factor = 1.7, offset = 0 }, -- Fudged down for increased incoming damage sunder = { factor = 1.2, offset = 0 }, valour = { factor = 1.15, offset = 0 }, venom = { factor = 1.0, offset = 5.0 }, -- 5 dmg per poisoning subtle = { -- Values to use for weapon "scores" (not damage) antimagic = { factor = 1.1, offset = 0 }, concuss = { factor = 1.2, offset = 0 }, devious = { factor = 1.1, offset = 0 }, holy = { factor = 1.15, offset = 0 }, penet = { factor = 1.3, offset = 0 }, protect = { factor = 1.15, offset = 0 }, reap = { factor = 1.3, offset = 0 }, rebuke = { factor = 1.2, offset = 0 }, vamp = { factor = 1.2, offset = 0 }, }, } -- BRC.Configs.Default.BrandBonus (do not remove this comment) ---- Local functions ---- local function is_config_module(p) return p and type(p) == "table" and p.BRC_CONFIG_NAME and type(p.BRC_CONFIG_NAME) == "string" and #p.BRC_CONFIG_NAME > 0 end local function find_config_modules() for _, c in pairs(_G) do if is_config_module(c) then BRC.Configs[c.BRC_CONFIG_NAME] = c end end end --- @param input_name string "ask" or a config name -- @return string The valid name of a config local function get_valid_config_name(input_name) if #BRC.Configs == 1 then return util.keys(BRC.Configs)[1] end if type(input_name) ~= "string" then BRC.mpr.warning("Non-string config name: " .. tostring(input_name)) else local config_name = input_name:lower() if config_name == "ask" then -- If game has started, restore from the previously saved config name if you.turns() > 0 and brc_config_name then return get_valid_config_name(brc_config_name) end else -- Find by name in BRC.Configs, or display warning for k, _ in pairs(BRC.Configs) do if config_name == k:lower() then return k end end BRC.mpr.warning("Could not load config: " .. tostring(input_name)) end end local config_names = util.keys(BRC.Configs) util.sort(config_names) return BRC.mpr.select("Select a config", config_names) end local function execute_config_init(config) if type(config) ~= "table" then return end if type(config.init) == "function" then config.init() end end --- Override values in dest, with values from source. Take care not to clear existing tables. -- Does not override "init" local function override_table(dest, source) if type(source) ~= "table" then return end for key, value in pairs(source) do if BRC.util.is_map(value) then if not dest[key] then dest[key] = {} end override_table(dest[key], value) else dest[key] = value end end end --- Warn about unknown keys in the user config that don't match any feature or core config key. -- Catches typos like ["pickup-alerts"] (plural) or misspelled option names. local function validate_config_keys() local feature_modules = BRC.get_all_feature_modules() -- Collect known core keys from BRC.Configs.Default local core_keys = {} for key, _ in pairs(BRC.Configs.Default) do core_keys[key] = true end -- Check each key in BRC.Config for key, value in pairs(BRC.Config) do -- Skip known core keys, init functions, and non-string keys if core_keys[key] or key == "init" or type(key) ~= "string" then -- luacheck: ignore 542 elseif feature_modules[key] then -- Known feature name — validate its top-level sub-keys against defaults local defaults = feature_modules[key].ConfigDefaults or feature_modules[key].Config if type(value) == "table" and type(defaults) == "table" then for sub_key, _ in pairs(value) do if sub_key ~= "disabled" and sub_key ~= "init" and defaults[sub_key] == nil then BRC.mpr.debug(string.format( "Unknown config key: %s.%s", BRC.txt.lightcyan(key), BRC.txt.yellow(sub_key) )) end end end elseif type(value) == "table" and key:find("-") then -- Looks like a feature name (contains hyphen) but no such module exists BRC.mpr.warning(string.format( "Unknown feature in config: %s (not registered — possible typo?)", BRC.txt.yellow(key) )) end end end ---- Public API ---- --- Main config loading entry point -- @param config_name string name of a config function BRC.init_config(config_name) find_config_modules() BRC.Config = util.copy_table(BRC.Configs.Default) local name = get_valid_config_name(config_name or BRC.Config.to_use) if BRC.Configs[brc_config_name] and (name ~= brc_config_name) and you.turns() > 0 then if not BRC.mpr.yesno(string.format( "Switch config from %s to %s?", BRC.txt.lightcyan(brc_config_name), BRC.txt.lightcyan(name) )) then name = brc_config_name end end override_table(BRC.Config, BRC.Configs[name]) execute_config_init(BRC.Config) for _ , value in pairs(BRC.get_registered_features()) do BRC.process_feature_config(value) end brc_config_name = name local m = BRC.mpr.brc_prefix .. "Using config: " .. BRC.txt.lightcyan(BRC.Config.BRC_CONFIG_NAME) BRC.mpr.white(m) BRC.init_emojis() -- Updates constant values based on BRC.Config.emojis -- validate_config_keys() runs from BRC.init() after register_all_features(); running here -- would warn on every hyphenated feature key because _features is still empty. end --- Exposed for testing; not part of public API. BRC._validate_config_keys = validate_config_keys --- Process a feature config: Load defaults, then override w BRC.Config function BRC.process_feature_config(feature) if type(feature.ConfigDefaults) == "table" then feature.Config = util.copy_table(feature.ConfigDefaults) else -- Save the defaults after default init(), so they can be used later w a diff config feature.Config = feature.Config or {} local preinit_defaults = util.copy_table(feature.Config) execute_config_init(feature.Config) feature.ConfigDefaults = util.copy_table(feature.Config) -- If init() is overridden, restore to the pre-init defaults and only apply the new init() if type(BRC.Config[feature.BRC_FEATURE_NAME].init) == "function" then feature.Config = util.copy_table(preinit_defaults) end end override_table(feature.Config, BRC.Config[feature.BRC_FEATURE_NAME]) execute_config_init(BRC.Config[feature.BRC_FEATURE_NAME]) end --- Stringify BRC.Config and each feature config, with headers -- @return table of strings, one for each config section (1 big string will overflow crawl.mpr()) function BRC.serialize_config() local tokens = {} tokens[#tokens + 1] = BRC.txt.lightcyan("\n---BRC Config---\n") .. BRC.txt.tostr(BRC.Config, true) local all_features = BRC.get_registered_features() local keys = util.keys(all_features) util.sort(keys) for i = 1, #keys do local name = keys[i] local feature = all_features[name] if feature.Config then local header = BRC.txt.cyan("\n\n---Feature Config: " .. name .. "---\n") tokens[#tokens + 1] = header .. BRC.txt.tostr(feature.Config, true) end end return tokens end ---- Initialize BRC.Config for debugging during startup + data.persist() calls ---- override_table(BRC.Config, BRC.Configs.Default) } ##################################### End lua/core/config.lua ##################################### ################################################################################################### #################################### Begin lua/core/hotkey.lua #################################### ############## https://github.com/brianfaires/crawl-rc/blob/main/lua/core/hotkey.lua ############## { --------------------------------------------------------------------------------------------------- -- BRC core feature: hotkey -- @module BRC.Hotkey -- Manages the BRC hotkey, and provides functions to add actions to it. -- Adding an action to the hotkey will prompt the user after performing any specified checks. -- If user accepts, the defined action is performed. (Equip, pick up, read, move to, etc) --------------------------------------------------------------------------------------------------- BRC.Hotkey = {} BRC.Hotkey.BRC_FEATURE_NAME = "hotkey" BRC.Hotkey.Config = { key = { keycode = BRC.KEYS.ENTER, name = "[Enter]" }, skip_keycode = BRC.KEYS.ESC, -- ESC keycode equip_hotkey = true, -- Offer to equip after picking up equipment wait_for_safety = true, -- Don't expire the hotkey with monsters in view explore_clears_queue = true, -- Clear the hotkey queue on explore newline_before_hotkey = true, -- Add a newline before the hotkey message move_to_feature = { -- Hotkey for "move to _" when you find these features enter_temple = "Temple", enter_lair = "Lair", altar_ecumenical = "faded altar", enter_bailey = "flagged portal", enter_bazaar = "bazaar", enter_desolation = "crumbling gateway", enter_gauntlet = "gauntlet", enter_ice_cave = "frozen archway", enter_necropolis = "phantasmal passage", enter_ossuary = "sand-covered staircase", enter_sewer = "glowing drain", enter_trove = "trove of treasure", enter_volcano = "dark tunnel", enter_wizlab = "magical portal", enter_ziggurat = "ziggurat", }, } -- BRC.Hotkey.Config (do not remove this comment) ---- Local constants ---- local WAYPOINT_MUTES = { "Assign waypoint to what number", "Existing waypoints", "Delete which waypoint", "\\(\\d\\) ", "All waypoints deleted", "You're already here!", "Okay\\, then\\.", "Unknown command", "Waypoint \\d (re-)?assigned", "Waypoints will disappear", } -- WAYPOINT_MUTES (do not remove this comment) ---- Local variables ---- local action_queue local cur_action local delay_expire local cur_floor ---- Initialization ---- function BRC.Hotkey.init() action_queue = {} cur_action = nil delay_expire = false cur_floor = you.where() BRC.opt.macro(BRC.Hotkey.Config.key.keycode, "macro_brc_hotkey") BRC.opt.macro(BRC.Hotkey.Config.skip_keycode, "macro_brc_skip_hotkey") end ---- Local functions ---- local function display_cur_message() if BRC.Hotkey.Config.wait_for_safety and not you.feel_safe() then return end local msg = string.format("[BRC] Press %s to %s.", BRC.Hotkey.Config.key.name, cur_action.msg) BRC.mpr.que(msg, BRC.COL.darkgrey) end local function load_next_action() if #action_queue == 0 then return end cur_action = table.remove(action_queue, 1) if cur_action.condition() then cur_action.turn = you.turns() + 1 display_cur_message() delay_expire = false else cur_action = nil load_next_action() end end local function expire_cur_action() if cur_action then cur_action.cleanup() end cur_action = nil load_next_action() end --- Get the highest available waypoint slot local function get_avail_waypoint() for i = 9, 0, -1 do if not travel.waypoint_delta(i) then return i end end BRC.mpr.debug("No available waypoint slots. Clearing them all.") util.foreach(WAYPOINT_MUTES, function(m) BRC.opt.single_turn_mute(m) end) crawl.sendkeys({ BRC.util.cntl("w"), "d", "*" }) crawl.flush_input() end ---- Macro function: On BRC hotkey press ---- function macro_brc_hotkey() if cur_action then cur_action.action() else BRC.mpr.info("Unknown command (no action assigned to hotkey).") end end function macro_brc_skip_hotkey() if cur_action and (you.feel_safe() or not BRC.Hotkey.Config.wait_for_safety) then expire_cur_action() if not cur_action then BRC.mpr.info("Hotkey cleared.") end else crawl.sendkeys({ BRC.Hotkey.Config.skip_keycode }) crawl.flush_input() end end ---- Public API ---- --- Assign an action to the BRC hotkey -- @param prefix string - The action (equip/pickup/read/etc) -- @param suffix string - Printed after the action. Usually an item name -- @param push_front boolean - Push the action to the front of the queue -- @param f_action function - The function to call when the hotkey is pressed -- @param f_condition (optional function) return bool - If the action is still valid -- @param f_cleanup (optional function) - Function to call after hotkey pressed or skipped -- @return nil function BRC.Hotkey.set(prefix, suffix, push_front, f_action, f_condition, f_cleanup) local act = { msg = BRC.txt.lightgreen(prefix) .. (suffix and (" " .. BRC.txt.white(suffix)) or ""), action = f_action, condition = f_condition or function() return true end, cleanup = f_cleanup or function() end, } -- act (do not remove this comment) if push_front then table.insert(action_queue, 1, act) else table.insert(action_queue, act) end end function BRC.Hotkey.equip(it, push_front) if not (it.is_weapon or BRC.it.is_armour(it, true) or BRC.it.is_jewellery(it)) then return end local name = it.name():gsub(" {.*}", "") local condition = function() local inv_items = util.filter(function(i) return i.name():gsub(" {.*}", "") == name end, items.inventory()) return util.exists(inv_items, function(i) return not i.equipped end) end local do_equip = function() local inv_items = util.filter(function(i) return i.name():gsub(" {.*}", "") == name end, items.inventory()) local already_eq = false for i = 1, #inv_items do if inv_items[i].equipped then already_eq = true else inv_items[i]:equip() return end end if already_eq then BRC.mpr.info("Already equipped.") else BRC.mpr.error("Could not find unequipped item '" .. name .. "' in inventory.") end end BRC.Hotkey.set("equip", it.name(), push_front, do_equip, condition) end --- Pick up an item by name (Must use name, since item goes stale when called from equip hotkey) function BRC.Hotkey.pickup(name, push_front) local condition = function() return util.exists(you.floor_items(), function(fl) return fl.name():contains(name) end) end local do_pickup = function() for _, fl in ipairs(you.floor_items()) do -- Check with contains() in case ID'ing it appends to the name if fl.name():contains(name) then items.pickup(fl) return end end BRC.mpr.info(name .. " isn't here!") end BRC.Hotkey.set("pickup", name, push_front, do_pickup, condition) end --- Set hotkey as 'move to ', if it's in LOS -- If target is a table, moves to that xy coordinate -- If target is a string, moves to that feature, otherwise searches for the item by name -- @param queue_pickup (optional boolean) - Queue pickup after moving to item (default true) local function set_waypoint_hotkey(name, push_front, target, queue_pickup) if util.contains(BRC.PORTAL_FEATURE_NAMES, you.branch()) then return -- Can't auto-travel end local x, y if type(target) == "table" then x, y = target.dx, target.dy elseif type(target) == "string" then local r = you.los() for dx = -r, r do for dy = -r, r do if view.feature_at(dx, dy):contains(target) then x, y = dx, dy break end end end else x, y = BRC.it.get_xy(name) end if x == nil then return BRC.mpr.debug(name .. " not found in LOS") end local waynum = get_avail_waypoint() if not waynum then return end util.foreach(WAYPOINT_MUTES, function(m) BRC.opt.single_turn_mute(m) end) travel.set_waypoint(waynum, x, y) local is_valid = function() local dx, dy = travel.waypoint_delta(waynum) return dx and not(dx == 0 and dy == 0) end local move_to_waypoint = function() f_pickup_alert.pause_alerts() -- Don't interrupt hotkey travel with new alerts crawl.sendkeys({ BRC.util.get_cmd_key("CMD_INTERLEVEL_TRAVEL"), tostring(waynum) }) crawl.flush_input() end local clear_waypoint = function() local keys = { BRC.util.cntl("w"), "d", tostring(waynum) } -- If other waypoints exist, need to send ESC to exit the prompt for i = 0, 9 do if i ~= waynum and travel.waypoint_delta(i) then keys[#keys + 1] = BRC.KEYS.ESC end end util.foreach(WAYPOINT_MUTES, function(m) BRC.opt.single_turn_mute(m) end) crawl.sendkeys(keys) if not target and queue_pickup ~= false then BRC.Hotkey.pickup(name, true) end end BRC.Hotkey.set("move to", name, push_front, move_to_waypoint, is_valid, clear_waypoint) end function BRC.Hotkey.move_to_item(name, push_front, queue_pickup) set_waypoint_hotkey(name, push_front, nil, queue_pickup) end function BRC.Hotkey.move_to_feature(name, push_front, feature_name) set_waypoint_hotkey(name, push_front, feature_name, false) end function BRC.Hotkey.move_to_xy(name, push_front, x, y) set_waypoint_hotkey(name, push_front, {dx=x, dy=y}, false) end ---- Crawl hook functions ---- function BRC.Hotkey.c_assign_invletter(it) if BRC.Hotkey.Config.equip_hotkey then BRC.Hotkey.equip(it, true) end end function BRC.Hotkey.ch_start_running(kind) if BRC.Hotkey.Config.explore_clears_queue and kind:contains("explore") then action_queue = {} cur_action = nil end end function BRC.Hotkey.c_message(text, channel) if channel ~= "plain" then return end if type(BRC.Hotkey.Config.move_to_feature) ~= "table" then return end if not text:contains("Found") then return end for k, v in pairs(BRC.Hotkey.Config.move_to_feature) do if text:contains(v) then BRC.Hotkey.move_to_feature(v, true, k) end end for k, v in pairs(BRC.PORTAL_FEATURE_NAMES) do if text:contains(v) then BRC.Hotkey.move_to_feature(v, true, k) end end end function BRC.Hotkey.ready() if you.where() ~= cur_floor then -- Clear the queue when changing floors cur_floor = you.where() action_queue = {} cur_action = nil elseif cur_action == nil then load_next_action() elseif cur_action.turn > you.turns() then return elseif BRC.Hotkey.Config.wait_for_safety and not you.feel_safe() and cur_action.condition() then delay_expire = true elseif delay_expire and you.feel_safe() then delay_expire = false display_cur_message() else expire_cur_action() end end } ##################################### End lua/core/hotkey.lua ##################################### ################################################################################################### ###################################### Begin lua/core/brc.lua ##################################### ################ https://github.com/brianfaires/crawl-rc/blob/main/lua/core/brc.lua ############### { --------------------------------------------------------------------------------------------------- -- BRC core module -- @module BRC -- @author buehler -- Serves as the central coordinator for all feature modules. -- Automatically loads any global module/table that defines `BRC_FEATURE_NAME` -- and manages the feature's lifecycle and hook dispatching. --------------------------------------------------------------------------------------------------- ---- Local constants ---- BRC.VERSION = "1.3.0" BRC.MIN_CRAWL_VERSION = "0.34" local HOOK_FUNCTIONS = { autopickup = "autopickup", c_answer_prompt = "c_answer_prompt", c_assign_invletter = "c_assign_invletter", c_message = "c_message", ch_start_running = "ch_start_running", init = "init", ready = "ready", multiready = "multiready", } -- HOOK_FUNCTIONS (do not remove this comment) ---- Local variables ---- local _features local _hooks local turn_count = -1 -- Do not reset this in init() ---- Local functions ---- local function char_dump(add_debug_info) if add_debug_info then crawl.take_note(BRC.dump(true, true)) BRC.mpr.info("Debug info added to character dump.") else BRC.mpr.info("No debug info added.") end BRC.util.do_cmd("CMD_CHARACTER_DUMP") end local function feature_is_disabled(f) local main = BRC.Config[f.BRC_FEATURE_NAME] if main and main.disabled == false then return false end -- catch override not yet applied return (f.Config and f.Config.disabled) or (main and main.disabled) end local function handle_feature_error(feature_name, hook_name, result) BRC.mpr.error(string.format("Failure in %s.%s", feature_name, hook_name), result, true) if BRC.mpr.yesno(string.format("Deactivate %s?", feature_name), BRC.COL.yellow) then BRC.unregister(feature_name) else BRC.mpr.okay() end end local function handle_core_error(hook_name, result, ...) local params = { hook_name } for i = 1, select("#", ...) do local param = select(i, ...) if param and type(param.name) == "function" then params[#params + 1] = "[" .. param.name() .. "]" else params[#params + 1] = BRC.txt.tostr(param, true) end end local msg = "BRC failure in safe_call_all_hooks(" .. table.concat(params, ", ") .. ")" BRC.mpr.error(msg, result, true) if BRC.mpr.yesno("Deactivate BRC." .. hook_name .. "?", BRC.COL.yellow) then _hooks[hook_name] = nil BRC.mpr.brown("Unregistered hook: " .. tostring(hook_name)) else BRC.mpr.okay("Returning nil to " .. hook_name .. ".") end end -- Hook management local function call_all_hooks(hook_name, ...) local last_return_value = nil local returning_feature = nil for i = #_hooks[hook_name], 1, -1 do local hook_info = _hooks[hook_name][i] if not feature_is_disabled(_features[hook_info.feature_name]) then if hook_name == HOOK_FUNCTIONS.init then BRC.mpr.debug(string.format("Initialize %s...", BRC.txt.lightcyan(hook_info.feature_name))) end local success, result = pcall(hook_info.func, ...) if not success then handle_feature_error(hook_info.feature_name, hook_name, result) elseif result ~= nil and last_return_value ~= result then -- Only track non-nil return values. This actually matters for autopickup if hook_name == HOOK_FUNCTIONS.autopickup then -- Unique case. One false will block autopickup. if result == false or last_return_value == nil then last_return_value = result returning_feature = hook_info.feature_name end else if last_return_value ~= nil then BRC.mpr.warning( string.format( "Return value mismatch in %s:\n (first) %s -> %s\n (final) %s -> %s", hook_name, returning_feature, BRC.txt.tostr(last_return_value, true), hook_info.feature_name, BRC.txt.tostr(result, true) ) ) end last_return_value = result returning_feature = hook_info.feature_name end end end end return last_return_value end --- Errors in this function won't show up in crawl, so it's kept simple and safe. local function safe_call_all_hooks(hook_name, ...) if not (_hooks and _hooks[hook_name] and #_hooks[hook_name] > 0) then return end local success, result = pcall(call_all_hooks, hook_name, ...) if success then return result end success, result = pcall(handle_core_error, hook_name, result, ...) if success then return end -- This is a serious error. Failed in the hook, and when we tried to report it. BRC.mpr.error("Failed to handle BRC core error!", result, true) if BRC.mpr.yesno("Dump char and deactivate BRC?", BRC.COL.yellow) then BRC.active = false BRC.mpr.brown("BRC deactivated.", "Error in hook: " .. tostring(hook_name)) pcall(char_dump, true) else BRC.mpr.okay() end end -- Register all features in the global namespace local function register_all_features() -- Find all feature modules local feature_names = {} for name, value in pairs(_G) do if BRC.is_feature_module(value) then feature_names[#feature_names + 1] = name end end -- Sort alphabetically (for reproducable behavior) util.sort(feature_names) -- Register features local loaded_count = 0 for _, name in ipairs(feature_names) do local success = BRC.register(_G[name]) if success then loaded_count = loaded_count + 1 elseif success == false then BRC.mpr.error("Failed to register feature: " .. name .. ". Aborting bulk registration.") return loaded_count end end return loaded_count end ---- Public API ---- function BRC.get_registered_features() return _features end function BRC.is_feature_module(f) return f and type(f) == "table" and f.BRC_FEATURE_NAME and type(f.BRC_FEATURE_NAME) == "string" and #f.BRC_FEATURE_NAME > 0 end --- Feature module tables for config validation: active registrations plus any module present in --- the environment but skipped by BRC.register (disabled-by-default) and tables nested under --- BRC (e.g. BRC.Data), which are not top-level globals. Different from get_registered_features(). function BRC.get_all_feature_modules() local modules = {} for name, mod in pairs(BRC.get_registered_features()) do modules[name] = mod end for _, value in pairs(_G) do if BRC.is_feature_module(value) then modules[value.BRC_FEATURE_NAME] = value end end if type(BRC) == "table" then for _, value in pairs(BRC) do if BRC.is_feature_module(value) then modules[value.BRC_FEATURE_NAME] = value end end end return modules end -- BRC.register(): Return true if success, false if error, nil if feature is disabled function BRC.register(f) if not BRC.is_feature_module(f) then BRC.mpr.error("Tried to register a non-feature module! Module contents:\n" .. BRC.txt.tostr(f)) return false elseif _features[f.BRC_FEATURE_NAME] then BRC.mpr.warning(BRC.txt.lightcyan(f.BRC_FEATURE_NAME) .. " already registered! Repeating...") BRC.unregister(f.BRC_FEATURE_NAME) end if feature_is_disabled(f) then BRC.mpr.debug(BRC.txt.lightcyan(f.BRC_FEATURE_NAME) .. " is disabled. Skipped registration.") return nil else if not BRC.Config[f.BRC_FEATURE_NAME] then BRC.Config[f.BRC_FEATURE_NAME] = {} end if not f.Config then f.Config = {} end end BRC.mpr.debug(string.format("Registering %s...", BRC.txt.lightcyan(f.BRC_FEATURE_NAME))) _features[f.BRC_FEATURE_NAME] = f -- Register hooks for _, hook_name in pairs(HOOK_FUNCTIONS) do if f[hook_name] then if not _hooks[hook_name] then _hooks[hook_name] = {} end table.insert(_hooks[hook_name], { feature_name = f.BRC_FEATURE_NAME, hook_name = hook_name, func = f[hook_name], }) end end BRC.process_feature_config(f) return true end function BRC.unregister(name) if not _features[name] then BRC.mpr.error(BRC.txt.yellow(name) .. " is not registered. Cannot unregister.") return false end _features[name] = nil local removed = {} for hook_name, hooks in pairs(_hooks) do for i = #hooks, 1, -1 do if hooks[i].feature_name == name then table.remove(hooks, i) removed[#removed + 1] = hook_name end end end BRC.mpr.info(string.format("Unregistered %s.", name)) BRC.mpr.debug(string.format("Unregistered %s hooks: (%s)", name, table.concat(removed, ", "))) return true end -- @param config_name (optional string) name of a config function BRC.reset(config_name) BRC.active = false BRC.Data.reset() BRC.opt.clear_macros() BRC.init(config_name) end -- @param config_name string name of a config function BRC.init(config_name) BRC.active = false _features = {} _hooks = {} if not BRC.util.version_is_valid(BRC.MIN_CRAWL_VERSION) then BRC.mpr.error(string.format( "BRC v%s requires crawl v%s or higher. You are running %s.", BRC.VERSION, BRC.txt.yellow(BRC.MIN_CRAWL_VERSION), BRC.txt.yellow(crawl.version("major")) )) if not BRC.mpr.yesno("Continue loading BRC anyway?", BRC.COL.yellow) then BRC.mpr.brown("BRC deactivated.") return false end end BRC.init_config(config_name) BRC.mpr.debug("Config loaded.") BRC.mpr.debug("Register core features...") BRC.register(BRC.Data) BRC.register(BRC.Hotkey) BRC.mpr.debug("Register features...") register_all_features() BRC._validate_config_keys() BRC.mpr.debug("Initialize features...") safe_call_all_hooks(HOOK_FUNCTIONS.init) local suffix = BRC.txt.blue(string.format(" (%s features)", #util.keys(_features))) BRC.mpr.debug("Add non-feature hooks...") add_autopickup_func(BRC.autopickup) BRC.opt.macro(BRC.util.get_cmd_key("CMD_CHARACTER_DUMP") or "#", "macro_brc_dump_character") BRC.mpr.debug("Verify persistent data reload...") local success = BRC.Data.handle_persist_errors() if success then BRC.Data.backup() -- Only backup after a clean startup local msg = string.format("Successfully initialized BRC v%s!%s", BRC.VERSION, suffix) BRC.mpr.lightgreen("\n" .. BRC.txt.wrap(msg, BRC.EMOJI.SUCCESS) .. "\n") else -- success == nil if errors were resolved, false if tried restore but failed if success == false and BRC.mpr.yesno("Deactivate BRC?" .. suffix, BRC.COL.yellow) then BRC.active = false BRC.mpr.lightred("\nBRC is off.\n") return false end BRC.mpr.magenta(string.format("\nInitialized BRC v%s with warnings!%s\n", BRC.VERSION, suffix)) end -- Avoid weird effects from autopickup before first turn BRC.active = you.turns() > 0 return true end --- Pull debug info. Print to mpr() and return as string -- @param skip_mpr (optional bool) Used in char_dump to just return the string function BRC.dump(verbose, skip_mpr) local tokens = {} tokens[#tokens + 1] = BRC.Data.serialize() if verbose then tokens[#tokens + 1] = BRC.txt.serialize_chk_lua_save() tokens[#tokens + 1] = BRC.txt.serialize_inventory() util.append(tokens, BRC.serialize_config()) end if not skip_mpr then for _, token in ipairs(tokens) do BRC.mpr.white(token) end end return table.concat(tokens, "\n") end ---- Macros ---- function macro_brc_dump_character() if not BRC.active then BRC.util.do_cmd("CMD_CHARACTER_DUMP") end char_dump(BRC.mpr.yesno("Add BRC debug info to character dump?", BRC.COL.lightcyan)) end ---- Crawl hooks ---- function BRC.autopickup(it, _) return safe_call_all_hooks(HOOK_FUNCTIONS.autopickup, it) end function BRC.c_answer_prompt(prompt) if not BRC.active then return end if not prompt then return end -- This fires from crawl, e.g. Shop purchase confirmation return safe_call_all_hooks(HOOK_FUNCTIONS.c_answer_prompt, prompt) end function BRC.c_assign_invletter(it) if not BRC.active then return end return safe_call_all_hooks(HOOK_FUNCTIONS.c_assign_invletter, it) end function BRC.c_message(text, channel) if not BRC.active then return end safe_call_all_hooks(HOOK_FUNCTIONS.c_message, text, channel) end function BRC.ch_start_running(kind) if not BRC.active then return end safe_call_all_hooks(HOOK_FUNCTIONS.ch_start_running, kind) end function BRC.ready() if you.turns() <= 1 then BRC.active = true end -- webtiles skips ready() on turn 0 if not BRC.active then return end BRC.opt.clear_single_turn_mutes() if you.turns() > turn_count then turn_count = you.turns() safe_call_all_hooks(HOOK_FUNCTIONS.ready) end safe_call_all_hooks(HOOK_FUNCTIONS.multiready) -- Always display messages, even if same turn BRC.mpr.consume_queue() crawl.redraw_screen() end } ####################################### End lua/core/brc.lua ###################################### ################################################################################################### ### Lua feature files ### ############################## Begin lua/features/alert-monsters.lua ############################## ######## https://github.com/brianfaires/crawl-rc/blob/main/lua/features/alert-monsters.lua ######## { --------------------------------------------------------------------------------------------------- -- BRC feature module: alert-monsters -- @module f_alert_monsters -- @author gammafunk, buehler -- Dynamic force_more and flash_screen messages for monsters. -- Alerts are active/inactive based on player HP, XL, willpower, resistances, etc. -- -- @warning Never put a '}' on a line by itself. This breaks crawl's RC parser. --------------------------------------------------------------------------------------------------- f_alert_monsters = {} f_alert_monsters.BRC_FEATURE_NAME = "alert-monsters" f_alert_monsters.Config = { sensitivity = 1.0, -- 0 to disable all; at 2.0, alerts will fire at 1/2 HP pack_timeout = 10, -- turns to wait before repeating a pack alert. 0 to disable disable_alert_monsters_in_zigs = true, -- Disable dynamic force_mores in Ziggurats debug_alert_monsters = false, -- Get a message when alerts toggle off/on } -- f_alert_monsters.Config (do not remove this comment) --[[ Config.Alerts contains all alerts. Each table in it creates one alert, using the following fields: - `name` is for debugging. - `pattern` is a string or list of monster names, will alert when you encounter one. - `is_pack` (optional) indicates the alert is for a pack of monsters. Packs only fire once every few turns - as defined in Config.pack_timeout (default 15). - `flash_screen` (optional) alert will flash the screen instead of using force_more. - `cutoff` sets the point when the alert is active (usually how much HP you have) - `cond` defines HOW the character stats are compared against `cutoff` (HP/will/etc). Ex: `always` alerts are always on. `hp` alerts are active when you have < `cutoff` HP. `will` alerts are active when you have <= `cutoff` pips of willpower. `int` alerts are active when you have < `cutoff` Int. `xl` alerts are active when your XL is < `cutoff`. `elec` alerts are active when you have no rElec and < `cutoff` HP. `fire`, `cold`, etc active < `cutoff` HP with no resistance. Pips lower cutoff to 50/33/20% --]] f_alert_monsters.Config.Alerts = { { name = "always_fm", pattern = { -- High damage/speed "flayed ghost", "juggernaut", "orbs? of (entropy|fire|winter)", --Summoning "boundless tesseract", "demonspawn corrupter", "draconian stormcaller", "dryad", "guardian serpent", "halazid warlock", "shadow demon", "spriggan druid", "worldbinder", --Dangerous abilities "iron giant", "merfolk aquamancer", "nekomata", "shambling mangrove", "starflower", "torpor snail", "water nymph", "wretched star", "wyrmhole", --Dangerous clouds "apocalypse crab", "catoblepas", } }, { name = "always_flash", flash_screen = true, pattern = { -- Noteworthy abilities "air elemental", "elemental wellspring", "ghost crab", "ironbound convoker", "vault guardian", "vault warden", "wendigo", -- Displacement "deep elf knight", "swamp worm", -- Summoning "deep elf elementalist", -- Agony "death knight", "imperial myrmidon", "necromancer", } }, -- Early game Dungeon problems for chars with low hp. (adder defined below) { name = "30hp", cond = "hp", cutoff = 30, is_pack = true, pattern = { "hound", "gnoll" } }, { name = "mid_game_packs", cutoff = 90, is_pack = true, pattern = { "boggart", "dream sheep" } }, -- Monsters dangerous until a certain point { name = "xl_7", cond = "xl", cutoff = 6, is_pack = true, pattern = { "orc wizard" } }, { name = "xl_12", cond = "xl", cutoff = 12, pattern = { "hydra", "bloated husk" } }, -- Monsters that can hit for ~50% of hp from range with unbranded attacks { name = "40hp", cond = "hp", cutoff = 40, pattern = { "orc priest" } }, { name = "50hp", cond = "hp", cutoff = 50, pattern = { "manticore", "orc high priest" } }, { name = "60hp", cond = "hp", cutoff = 60, pattern = { "centaur(?! warrior)", "cyclops", "orc knight", "yaktaur(?! captain)" } }, { name = "70hp_melai", cond = "hp", cutoff = 70, is_pack = true, pattern = "meliai" }, { name = "80hp", cond = "hp", cutoff = 80, pattern = { "gargoyle" } }, { name = "90hp", cond = "hp", cutoff = 90, pattern = { "deep elf archer", "tengu conjurer" } }, { name = "110hp", cond = "hp", cutoff = 110, pattern = { "cacodemon", "centaur warrior", "deep elf high priest", "deep troll earth mage", "eye of devastation", "hellion", "stone giant", "sun moth", "yaktaur captain" } }, { name = "120hp", cond = "hp", cutoff = 120, pattern = { "magenta draconian", "thorn hunter", "quicksilver (dragon|elemental)" } }, { name = "160hp", cond = "hp", cutoff = 160, pattern = { "brimstone fiend", "deep elf sorcererhell sentinal", "draconian (knight|scorcher)", "war gargoyle" } }, { name = "200hp", cond = "hp", cutoff = 200, pattern = { "(deep elf|draconian) annihilator", "iron (dragon|elemental)" } }, -- Monsters that can crowd-control you without sufficient willpower -- Cutoff ~10% for most spells; lower for more significant spells like banish { name = "willpower2", cond = "will", cutoff = 2, pattern = { "basilisk", "naga ritualist", "vampire(?! (bat|mage|mosquito))", "sphinx marauder" } }, { name = "willpower3", cond = "will", cutoff = 3, pattern = { "cacodemon", "death knight", "deep elf (demonologist|sorcerer|archer)", "draconian shifter", "fenstrider witch", "glowing orange brain", "guardian sphinx", "imperial myrmidon", "iron elemental", "occultist", "merfolk siren", "nagaraja", "ogre mage", "orc sorcerer", "satyr", "vampire knight", "vault sentinel" } }, { name = "willpower3_great_orb_of_eyes", cond = "will", cutoff = 3, is_pack = true, pattern = "great orb of eyes" }, { name = "willpower3_golden_eye", cond = "will", cutoff = 3, is_pack = true, pattern = "golden eye" }, { name = "willpower4", cond = "will", cutoff = 4, pattern = { "merfolk avatar", "tainted leviathan", "nargun" } }, -- Brain feed with low int { name = "brainfeed", cond = "int", cutoff = 6, pattern = { "glowing orange brain", "neqoxec" } }, -- Alert if no resist and HP below cutoff { name = "pois_30", cond = "pois", cutoff = 30, pattern = { "adder" } }, { name = "pois_80", cond = "pois", cutoff = 80, pattern = { "golden dragon", "green draconian", "swamp dragon" } }, { name = "pois_120", cond = "pois", cutoff = 120, pattern = { "fenstrider witch", "green death", "naga mage", "nagaraja" } }, { name = "pois_140", cond = "pois", cutoff = 140, pattern = { "tengu reaver" } }, { name = "elec_40", cond = "elec", cutoff = 40, is_pack = true, pattern = "electric eel" }, { name = "elec_80", cond = "elec", cutoff = 80, pattern = { "raiju", "shock serpent", "spark wasp" } }, { name = "elec_120", cond = "elec", cutoff = 120, pattern = { "black draconian", "blizzard demon", "deep elf zephyrmancer", "storm dragon", "tengu conjurer" } }, { name = "elec_140", cond = "elec", cutoff = 140, pattern = { "electric golem", "servants? of whisper", "spriggan air mage", "tengu reaver", "titan" } }, { name = "elec_140_pack", cond = "elec", cutoff = 140, is_pack = true, pattern = { "ball lightning" } }, { name = "corr_60", cond = "corr", cutoff = 60, pattern = { "acid dragon" } }, { name = "caustic_shrike", cond = "corr", cutoff = 120, is_pack = true, pattern = { "caustic shrike" } }, { name = "corr_140", cond = "corr", cutoff = 140, pattern = { "demonspawn corrupter", "entropy weaver", "moon troll", "tengu reaver" } }, { name = "fire_60_pack", cond = "fire", cutoff = 60, is_pack = true, pattern = { "hell hound", "lava snake", "lindwurm" } }, { name = "fire_60", cond = "fire", cutoff = 60, pattern = { "fire crab", "steam dragon" } }, { name = "fire_100", cond = "fire", cutoff = 100, pattern = { "deep elf pyromancer", "efreet", "smoke demon", "sun moth" } }, { name = "fire_120", cond = "fire", cutoff = 120, pattern = { "demonspawn blood saint", "hell hog", "hell knight", "molten gargoyle", "ogre mage", "orc sorcerer", "red draconian" } }, { name = "fire_140", cond = "fire", cutoff = 140, pattern = { "balrug" } }, { name = "fire_160", cond = "fire", cutoff = 160, pattern = { "fire dragon", "fire giant", "golden dragon", "ophan", "salamander tyrant", "tengu reaver", "will-o-the-wisp" } }, { name = "fire_240", cond = "fire", cutoff = 240, pattern = { "crystal (guardian|echidna)", "draconian scorcher", "hellephant" } }, { name = "cold_80", cond = "cold", cutoff = 80, pattern = { "rime drake" } }, { name = "cold_120", cond = "cold", cutoff = 120, pattern = { "blizzard demon", "bog body", "demonspawn blood saint", "ironbound frostheart", "white draconian" } }, { name = "shard_shrike", cond = "cold", cutoff = 120, is_pack = true, pattern = { "shard shrike" } }, { name = "cold_160", cond = "cold", cutoff = 160, pattern = { "draconian knight", "frost giant", "golden dragon", "ice dragon", "tengu reaver" } }, { name = "cold_180", cond = "cold", cutoff = 180, pattern = { "(? 1 then remove_patterns[#remove_patterns + 1] = v for _, m in ipairs(v.pattern) do local to_add = util.copy_table(v) to_add.name = (to_add.name or "") .. "_" .. m:gsub(" ", "_") to_add.pattern = m add_patterns[#add_patterns + 1] = to_add end end end util.append(C.Alerts, add_patterns) for _, v in ipairs(remove_patterns) do util.remove(C.Alerts, v) end -- Convert patterns to regex for _, v in ipairs(C.Alerts) do v.active_alert = false v.last_fm_turn = -1 if type(v.pattern) == "table" then v.pattern = table.concat(v.pattern, "|") end v.pattern = WARN_PREFIX .. v.pattern .. WARN_SUFFIX v.regex = crawl.regex(v.pattern:gsub("monster_warning:", "")) end end ---- Local functions ---- --- Check if player HP is below threshold, accounting for 0-3 pips of resistance. -- @return true if player HP is below threshold, and alert should be active. local function is_active_3pip(hp, dmg_threshold, resistance) -- Dmg taken is 1/1; 1/2; 1/3; 1/5 (for 0; 1; 2; 3 resistance) if resistance >= 3 then return hp < dmg_threshold / 5 end return hp < dmg_threshold / (resistance + 1) end --- Dispatch table for condition handler functions; checking if alerts should be active or not. -- Each handler takes (alert, stats) and returns true if the alert should be active. local CONDITION_HANDLERS = { xl = function(a, s) return s.xl < a.cutoff * C.sensitivity end, hp = function(a, s) return s.hp < a.cutoff * C.sensitivity end, int = function(a, s) return s.int < a.cutoff * C.sensitivity end, will = function(a, s) return s.will < a.cutoff * C.sensitivity end, mut = function(_, s) return s.rMut == 0 end, pois = function(a, s) return s.rPois == 0 and s.hp < a.cutoff * C.sensitivity end, elec = function(a, s) return s.rElec == 0 and s.hp < a.cutoff * C.sensitivity end, corr = function(a, s) return not s.rCorr and s.hp < a.cutoff * C.sensitivity end, fire = function(a, s) return is_active_3pip(s.hp, a.cutoff * C.sensitivity, s.rF) end, cold = function(a, s) return is_active_3pip(s.hp, a.cutoff * C.sensitivity, s.rC) end, drain = function(a, s) return is_active_3pip(s.hp, a.cutoff * C.sensitivity, s.rN) end, } -- CONDITION_HANDLERS (do not remove this comment) local function update_pack_mutes() -- Put pending mutes into effect for _, v in ipairs(patterns_to_mute) do if v.flash_screen then BRC.opt.flash_screen_message(v, false) else BRC.opt.force_more_message(v, false) end if C.debug_alert_monsters then BRC.mpr.blue("Muted pack: " .. v) end end patterns_to_mute = {} -- Remove expired mutes for _, v in ipairs(C.Alerts) do if v.is_pack and v.last_fm_turn ~= -1 and you.turns() >= v.last_fm_turn + C.pack_timeout then v.last_fm_turn = -1 v.active_alert = false -- Let the main logic decide if/when to reactivate it. if C.debug_alert_monsters then BRC.mpr.blue("Unmuted pack: " .. v.pattern) end end end end ---- Crawl hook functions ---- function f_alert_monsters.c_message(text, channel) if channel ~= "monster_warning" or not text:find("encounter") then return end if C.pack_timeout <= 0 then return end -- Identify when a mute should be turned on for _, v in ipairs(C.Alerts) do if v.is_pack and v.regex:matches(text) then if v.last_fm_turn == -1 then patterns_to_mute[#patterns_to_mute + 1] = v.pattern if C.debug_alert_monsters then BRC.mpr.blue("To mute: " .. v.pattern) end else if C.debug_alert_monsters then BRC.mpr.blue("Extending mute: " .. v.pattern) end end v.last_fm_turn = you.turns() end end end function f_alert_monsters.ready() local activated = {} local deactivated = {} -- Load all stats before loop. Most of them are used multiple times. local hp, _ = you.hp() if you.spirit_shield() > 0 then local mp, _ = you.mp() hp = hp + mp end -- Collect stats into a table for handlers local stats = { hp = hp, xl = you.xl(), int = you.intelligence(), will = you.willpower(), rMut = you.res_mutation(), rPois = you.res_poison(), rElec = you.res_shock(), rCorr = you.res_corr(), rF = you.res_fire(), rC = you.res_cold(), rN = you.res_draining(), } -- stats (do not remove this comment) for _, v in ipairs(C.Alerts) do local should_be_active = false if C.disable_alert_monsters_in_zigs and you.branch() == "Zig" then should_be_active = false elseif not v.cond then should_be_active = true else local handler = CONDITION_HANDLERS[v.cond] if handler then should_be_active = handler(v, stats) else BRC.mpr.error("Unknown condition in alert-monsters: " .. v.cond) end end if should_be_active ~= v.active_alert then v.active_alert = should_be_active if v.flash_screen then BRC.opt.flash_screen_message(v.pattern, should_be_active) else BRC.opt.force_more_message(v.pattern, should_be_active) end if C.debug_alert_monsters then if v.active_alert then activated[#activated + 1] = v.name or v.pattern else deactivated[#deactivated + 1] = v.name or v.pattern end end end end if C.debug_alert_monsters then if #activated > 0 then BRC.mpr.blue("Activating f_m: " .. table.concat(activated, ", ")) end if #deactivated > 0 then BRC.mpr.blue("Deactivating f_m: " .. table.concat(deactivated, ", ")) end end if C.pack_timeout > 0 then update_pack_mutes() end end } ############################### End lua/features/alert-monsters.lua ############################### ################################################################################################### ############################## Begin lua/features/announce-hp-mp.lua ############################## ######## https://github.com/brianfaires/crawl-rc/blob/main/lua/features/announce-hp-mp.lua ######## { --------------------------------------------------------------------------------------------------- -- BRC feature module: announce-hp-mp -- @module f_announce_hp_mp -- @author magus, buehler -- Announce changes in HP/MP, with visual meters and additional warnings for severe damage. --------------------------------------------------------------------------------------------------- f_announce_hp_mp = {} f_announce_hp_mp.BRC_FEATURE_NAME = "announce-hp-mp" f_announce_hp_mp.Config = { dmg_flash_threshold = 0.20, -- Flash screen when losing this % of max HP dmg_fm_threshold = 0.30, -- Force more for losing this % of max HP always_on_bottom = false, -- Rewrite HP/MP meters after each turn with messages meter_length = 10, -- Number of pips in each meter Announce = { hp_loss_limit = 1, -- Announce when HP loss >= this hp_gain_limit = 4, -- Announce when HP gain >= this mp_loss_limit = 1, -- Announce when MP loss >= this mp_gain_limit = 2, -- Announce when MP gain >= this hp_first = false, -- Show HP first in the message same_line = true, -- Show HP/MP on the same line always_both = true, -- If showing one, show both very_low_hp = 0.10, -- At this % of max HP, show all HP changes and mute % HP alerts }, HP_METER = { FULL = "❤️", PART = "❤️‍🩹", EMPTY = "🤍" }, MP_METER = { FULL = "🟦", PART = "🔹", EMPTY = "➖" }, init = function() if not BRC.Config.emojis then f_announce_hp_mp.Config.HP_METER = { BORDER = BRC.txt.white("|"), FULL = BRC.txt.lightgreen("+"), PART = BRC.txt.lightgrey("+"), EMPTY = BRC.txt.darkgrey("-"), } -- HP_METER (do not remove this comment) f_announce_hp_mp.Config.MP_METER = { BORDER = BRC.txt.white("|"), FULL = BRC.txt.lightblue("+"), PART = BRC.txt.lightgrey("+"), EMPTY = BRC.txt.darkgrey("-"), } -- MP_METER (do not remove this comment) end end, } -- f_announce_hp_mp.Config (do not remove this comment) ---- Persistent variables ---- ad_prev = BRC.Data.persist("ad_prev", { hp = 0, mhp = 0, mp = 0, mmp = 0 }) ---- Local constants ---- local ALWAYS_BOTTOM_SETTINGS = { hp_loss_limit = 0, hp_gain_limit = 0, mp_loss_limit = 0, mp_gain_limit = 0, hp_first = true, same_line = true, always_both = true, very_low_hp = 0, } -- ALWAYS_BOTTOM_SETTINGS (do not remove this comment) ---- Local variables ---- local C -- config alias local pause_announcements ---- Initialization ---- function f_announce_hp_mp.init() C = f_announce_hp_mp.Config pause_announcements = false ad_prev.hp = 0 ad_prev.mhp = 0 ad_prev.mp = 0 ad_prev.mmp = 0 if C.always_on_bottom then C.Announce = ALWAYS_BOTTOM_SETTINGS end if C.dmg_fm_threshold > 0 and C.dmg_fm_threshold <= 0.5 then BRC.opt.message_mute("Ouch! That really hurt!", true) end end ---- Local functions ---- local function create_meter(perc, emojis) perc = math.max(0, math.min(1, perc)) -- Clamp between 0 and 1 local num_halfpips = math.floor(perc * C.meter_length * 2) local num_full_emojis = math.floor(num_halfpips / 2) local num_part_emojis = num_halfpips % 2 local num_empty_emojis = C.meter_length - num_full_emojis - num_part_emojis return table.concat({ emojis.BORDER or "", string.rep(emojis.FULL, num_full_emojis), string.rep(emojis.PART, num_part_emojis), string.rep(emojis.EMPTY, num_empty_emojis), emojis.BORDER or "", }) end local function format_delta(delta) if delta > 0 then return BRC.txt.green("+" .. delta) elseif delta < 0 then return BRC.txt.red(delta) else return BRC.txt.darkgrey("+0") end end local function format_ratio(cur, max) local ratio_color if cur <= (max * 0.25) then ratio_color = BRC.COL.lightred elseif cur <= (max * 0.50) then ratio_color = BRC.COL.red elseif cur <= (max * 0.75) then ratio_color = BRC.COL.yellow elseif cur < max then ratio_color = BRC.COL.white else ratio_color = BRC.COL.green end return BRC.txt[ratio_color](string.format(" -> %s/%s", cur, max)) end local function get_hp_message(hp_delta, mhp_delta) local hp, mhp = you.hp() local msg_tokens = {} msg_tokens[#msg_tokens + 1] = create_meter(hp / mhp, C.HP_METER) msg_tokens[#msg_tokens + 1] = BRC.txt.white(string.format(" HP[%s]", format_delta(hp_delta))) msg_tokens[#msg_tokens + 1] = format_ratio(hp, mhp) if mhp_delta ~= 0 then local text = string.format(" (%s max HP)", format_delta(mhp_delta)) msg_tokens[#msg_tokens + 1] = BRC.txt.lightgrey(text) end if not C.Announce.same_line and hp == mhp then msg_tokens[#msg_tokens + 1] = BRC.txt.white(" (Full HP)") end return table.concat(msg_tokens) end local function get_mp_message(mp_delta, mmp_delta) local mp, mmp = you.mp() local msg_tokens = {} msg_tokens[#msg_tokens + 1] = create_meter(mp / mmp, C.MP_METER) msg_tokens[#msg_tokens + 1] = BRC.txt.lightcyan(string.format(" MP[%s]", format_delta(mp_delta))) msg_tokens[#msg_tokens + 1] = format_ratio(mp, mmp) if mmp_delta ~= 0 then local tok = string.format(" (%s max MP)", format_delta(mmp_delta)) msg_tokens[#msg_tokens + 1] = BRC.txt.cyan(tok) end if not C.Announce.same_line and mp == mmp then msg_tokens[#msg_tokens + 1] = BRC.txt.lightcyan(" (Full MP)") end return table.concat(msg_tokens) end local function last_msg_is_meter() local last_msg = crawl.messages(1) return f_announce_hp_mp.msg_is_meter(last_msg) end ---- Public API ---- function f_announce_hp_mp.msg_is_meter(msg) -- Might be better to check more rigorously, but this seems robust and quick. msg = BRC.txt.clean(msg) return msg:contains("] -> ") and (msg:contains(" HP[") or msg:contains(" MP[")) end function f_announce_hp_mp.single_turn_mute() pause_announcements = true end ---- Crawl hook functions ---- function f_announce_hp_mp.ready() if pause_announcements then pause_announcements = false return end -- Update prev state first, so we can safely return early below local hp, mhp = you.hp() local mp, mmp = you.mp() local is_startup = ad_prev.hp == 0 local hp_delta = hp - ad_prev.hp local mp_delta = mp - ad_prev.mp local mhp_delta = mhp - ad_prev.mhp local mmp_delta = mmp - ad_prev.mmp -- Calc damage taken: scale prev HP to current max HP, then compare to actual HP. -- Guard against startup (ad_prev.mhp == 0) by falling back to hp so damage_taken = 0. local expected_hp = ad_prev.mhp > 0 and mhp * (ad_prev.hp / ad_prev.mhp) or hp local damage_taken = expected_hp - hp ad_prev.hp = hp ad_prev.mhp = mhp ad_prev.mp = mp ad_prev.mmp = mmp if is_startup then return end if hp_delta == 0 and mp_delta == 0 and last_msg_is_meter() then return end local is_very_low_hp = hp <= C.Announce.very_low_hp * mhp -- Determine which messages to show local do_hp = true local do_mp = true if hp_delta <= 0 and hp_delta > -C.Announce.hp_loss_limit then do_hp = false end if hp_delta >= 0 and hp_delta < C.Announce.hp_gain_limit then do_hp = false end if mp_delta <= 0 and mp_delta > -C.Announce.mp_loss_limit then do_mp = false end if mp_delta >= 0 and mp_delta < C.Announce.mp_gain_limit then do_mp = false end if not do_hp and is_very_low_hp and hp_delta ~= 0 then do_hp = true end if not do_hp and not do_mp then return end if C.Announce.always_both then do_hp = true do_mp = true end -- Put messages together local hp_msg = get_hp_message(hp_delta, mhp_delta) local mp_msg = get_mp_message(mp_delta, mmp_delta) local msg_tokens = {} msg_tokens[1] = (C.Announce.hp_first and do_hp) and hp_msg or mp_msg if do_mp and do_hp then msg_tokens[2] = C.Announce.same_line and string.rep(" ", 7) or "\n" msg_tokens[3] = C.Announce.hp_first and mp_msg or hp_msg end if #msg_tokens > 0 then BRC.mpr.que(table.concat(msg_tokens)) end -- Add Damage-related warnings, when damage >= threshold if damage_taken >= mhp * C.dmg_flash_threshold then if is_very_low_hp then return end -- mute % HP alerts if damage_taken >= mhp * C.dmg_fm_threshold then local msg = BRC.txt.lightmagenta("MASSIVE DAMAGE") BRC.mpr.que_optmore(true, BRC.txt.wrap(msg, BRC.EMOJI.EXCLAMATION_2)) else local msg = BRC.txt.magenta("BIG DAMAGE") BRC.mpr.que_optmore(false, BRC.txt.wrap(msg, BRC.EMOJI.EXCLAMATION)) end end end } ############################### End lua/features/announce-hp-mp.lua ############################### ################################################################################################### ############################## Begin lua/features/announce-items.lua ############################## ######## https://github.com/brianfaires/crawl-rc/blob/main/lua/features/announce-items.lua ######## { --------------------------------------------------------------------------------------------------- -- BRC feature module: announce-items -- @module f_announce_items -- Announce when items of certain classes come into view. Off by default. -- Intended and configured for turncount runs, to avoid having to manually check floor items. --------------------------------------------------------------------------------------------------- f_announce_items = {} f_announce_items.BRC_FEATURE_NAME = "announce-items" f_announce_items.Config = { disabled = true, -- Disabled by default. Intended only for turncount runs. announce_class = { "book", "gold", "jewellery", "misc", "missile", "potion", "scroll", "wand" }, announce_glowing = true, announce_artefacts = true, max_gold_announcements = 3, -- Stop announcing gold after 3rd pile on screen announce_duplicate_consumables = true, -- Announce when standing on not-id'd duplicates } -- f_announce_items.Config (do not remove this comment) ---- Local constants ---- local ALERT_COLOR = { gold = BRC.COL.yellow, book = BRC.COL.lightcyan, jewellery = BRC.COL.magenta, misc = BRC.COL.lightcyan, missile = BRC.COL.white, potion = BRC.COL.lightgreen, scroll = BRC.COL.lightgreen, wand = BRC.COL.magenta, default = BRC.COL.lightblue, } -- ALERT_COLOR (do not remove this comment) ---- Local variables ---- local C -- config alias local los_items local prev_item_names local prev_gold_count ---- Initialization ---- function f_announce_items.init() C = f_announce_items.Config los_items = {} prev_item_names = {} prev_gold_count = 0 end ---- Local functions ---- local function should_announce_item(it) if it.is_useless then return false end if not it.is_identified then if it.artefact then return C.announce_artefacts end if it.branded then return C.announce_glowing end elseif crawl.messages(2):contains(it.name()) then -- Avoid duplicating "You see here ..." after floor-id return false end return util.contains(C.announce_class, it.class(true):lower()) end local function announce_item(it) if not should_announce_item(it) then return end local class = it.class(true):lower() if class == "gold" then prev_gold_count = prev_gold_count + 1 if prev_gold_count > C.max_gold_announcements then return end end local item_col = ALERT_COLOR[class] or ALERT_COLOR.default crawl.mpr(BRC.txt.white("Found: ") .. BRC.txt[item_col](it.name())) you.stop_activity() end ---- Crawl hook functions ---- function f_announce_items.ready() los_items = {} local r = you.los() for x = -r, r do for y = -r, r do if you.see_cell(x, y) then local items_xy = items.get_items_at(x, y) if items_xy and #items_xy > 0 then for _, it in ipairs(items_xy) do los_items[#los_items+1] = it if C.announce_duplicate_consumables then if x == 0 and y == 0 and not it.is_identified and (it.class(true) == "scroll" or it.class(true) == "potion") then if util.exists(items.inventory(), function(i) return i.name("qual", false) == it.name("qual", false) end) then crawl.mpr(BRC.txt.magenta("Duplicate: ") .. it.name()) end end end end end end end end for _, it in ipairs(los_items) do if not util.contains(prev_item_names, it.name()) then announce_item(it) end end -- Save history for comparison prev_item_names = {} prev_gold_count = 0 for _, it in ipairs(los_items) do prev_item_names[#prev_item_names+1] = it.name() if it.class(true):lower() == "gold" then prev_gold_count = prev_gold_count + 1 end end end } ############################### End lua/features/announce-items.lua ############################### ################################################################################################### ############################## Begin lua/features/answer-prompts.lua ############################## ######## https://github.com/brianfaires/crawl-rc/blob/main/lua/features/answer-prompts.lua ######## { --------------------------------------------------------------------------------------------------- -- BRC feature module: answer-prompts -- @module f_answer_prompts -- Automatically answer crawl prompts. --------------------------------------------------------------------------------------------------- f_answer_prompts = {} f_answer_prompts.BRC_FEATURE_NAME = "answer-prompts" ---- Local constants ---- local BAD_FOR_TREES = { "deep_water", "lava", "open_sea", "endless_lava" } ---- Crawl hook functions ---- function f_answer_prompts.c_answer_prompt(prompt) if prompt == "Die?" then return false end if prompt:contains("cheaper one?") and you.branch() ~= "Bazaar" then BRC.mpr.yellow("Replacing shopping list items") return true end if prompt:contains("quaff the potion of lignification") then local feat = view.feature_at(0,0) if feat and util.contains(BAD_FOR_TREES, feat) then BRC.mpr.warning("Blocking lignification over " .. feat) return false end end end } ############################### End lua/features/answer-prompts.lua ############################### ################################################################################################### ############################### Begin lua/features/bread-swinger.lua ############################## ######### https://github.com/brianfaires/crawl-rc/blob/main/lua/features/bread-swinger.lua ######## { --------------------------------------------------------------------------------------------------- -- BRC feature module: bread-swinger -- @module f_bread_swinger -- @author gammafunk, buehler -- Efficient resting during turncount runs, by wielding slow weapons, or walking back and forth. -- Based on: https://github.com/gammafunk/dcss-rc/blob/master/speedrun_rest.lua -- -- Press '5' to rest a variable number of turns. Will swap to slowest weapon in inventory. -- Press 'cntl-5' to manually set the weapon slot to swing. --------------------------------------------------------------------------------------------------- f_bread_swinger = {} f_bread_swinger.BRC_FEATURE_NAME = "bread-swinger" f_bread_swinger.Config = { disabled = true, -- Disable by default allow_plant_damage = false, -- Allow damaging plants to rest walk_delay = 50, -- ms delay between walk commands. Makes visuals less jarring. 0 to disable alert_slow_weap_min = 1.5, -- Alert when finding the slowest weapon yet, starting at this delay set_manual_slot_key = BRC.util.cntl("5"), -- Manually set which weapon slot to swing max_heal_perc = 90, -- Stop resting at this percentage of max HP/MP emoji = "🍞", init = function() if not BRC.Config.emojis then f_bread_swinger.Config.emoji = BRC.txt.cyan("---- ") end end, } -- f_bread_swinger.Config (do not remove this comment) ---- Persistent variables ---- bs_manual_swing_slot = BRC.Data.persist("bs_manual_swing_slot", nil) bs_highest_delay = BRC.Data.persist("bs_highest_delay", 0) bs_highest_delay_1h = BRC.Data.persist("bs_highest_delay_1h", 0) ---- Local constants ---- local CUR_WEAP_BIAS = 0.05 -- Borderline config; don't swap weaps unless delay diff > this local DIR_TO_VI = { [-1] = { [-1] = "y", [0] = "h", [1] = "b" }, [0] = { [-1] = "k", [1] = "j" }, [1] = { [-1] = "u", [0] = "l", [1] = "n" }, } -- DIR_TO_VI (do not remove this comment) local SH_PROMPT = "Unequip shield to rest with " local FULLY_RECOVERED_MSG = "Fully recovered" ---- Local variables ---- local C -- config alias local swing_slot local turns_remaining local turns_to_rest local prev_num_turns_to_rest local rest_type local wielding local dir local removed_shield local do_overheal ---- Local functions ---- local function has_bad_duration() local status = you.status() return util.exists(BRC.BAD_DURATIONS, function(x) return status:contains(x) end) end local function reset_rest(msg) if msg then if turns_remaining > 0 and turns_remaining ~= turns_to_rest then local diff = turns_to_rest - turns_remaining msg = string.format("%s (Rested %s/%s turns)", msg, diff, turns_to_rest) end if msg:contains(FULLY_RECOVERED_MSG) then BRC.mpr.lightgreen(msg) else BRC.mpr.warning(msg) end end if removed_shield then BRC.mpr.que(BRC.txt.lightmagenta("Remember to re-equip your shield after resting!")) end swing_slot = nil turns_remaining = 0 turns_to_rest = 0 rest_type = nil wielding = false dir = { x = nil, y = nil } removed_shield = false do_overheal = false end local function get_num_turns() local msg = BRC.txt.white("Enter turns to rest") if prev_num_turns_to_rest > 0 then msg = msg .. " ([Enter] for " .. BRC.txt.white(prev_num_turns_to_rest) .. " turns)" end BRC.mpr.lightgrey(msg .. ": ") local input = crawl.c_input_line() if input == "" then return prev_num_turns_to_rest end local turns = tonumber(input) if not turns or turns <= 0 then return nil end prev_num_turns_to_rest = turns return turns end local function do_alert(msg, it) local tokens = {} tokens[1] = f_bread_swinger.Config.emoji .. " " tokens[#tokens + 1] = msg .. ": " tokens[#tokens + 1] = BRC.txt.cyan(it.name() .. " (") tokens[#tokens + 1] = BRC.txt.lightmagenta(string.format("%.2f", BRC.eq.get_weap_delay(it))) tokens[#tokens + 1] = BRC.txt.cyan(") ") tokens[#tokens + 1] = f_bread_swinger.Config.emoji BRC.mpr.que(table.concat(tokens)) you.stop_activity() end -- Weapon functions local function weapon_can_swap() local weapon = items.equipped_at("Weapon") if not weapon then return true end if weapon.ego() == "distortion" and you.god() ~= "Lugonu" then return false end if weapon.artefact then local artp = weapon.artprops return not (artp["*Contam"] or artp["*Drain"]) end return true end local function get_slowest_slot() local slowest_item = nil local largest_delay = 0 local slowest_item_1h = nil local largest_delay_1h = 0 -- Initialize to wielded weapon + BIAS; so we don't swap for an (approximate) equivalent. local it = items.equipped_at("Weapon") if it and it.class() == "Hand Weapons" then local weap_delay = BRC.eq.get_weap_delay(it) + CUR_WEAP_BIAS largest_delay = weap_delay slowest_item = it if BRC.eq.get_hands(it) == 1 then largest_delay_1h = weap_delay slowest_item_1h = it end end -- Scan inventory for slowest weapon for _, item in ipairs(items.inventory()) do if item.class() == "Hand Weapons" then local weap_delay = BRC.eq.get_weap_delay(item) if weap_delay > largest_delay then largest_delay = weap_delay slowest_item = item end if BRC.eq.get_hands(item) == 1 and weap_delay > largest_delay_1h then largest_delay_1h = weap_delay slowest_item_1h = item end end end if not slowest_item then return nil end -- Often is more efficient to unequip shield and rest with a slower 2-handed weapon. if not BRC.you.free_offhand() and BRC.eq.get_hands(slowest_item) > 1 then local msg = BRC.txt.white(SH_PROMPT .. BRC.txt.lightcyan(slowest_item.name("db")) .. "?") if BRC.mpr.yesno(msg) then items.equipped_at("offhand").remove() removed_shield = true elseif slowest_item_1h then slowest_item = slowest_item_1h else return nil end end return items.index_to_letter(slowest_item.slot) end local function swing_item_wielded() local weapon = items.equipped_at("Weapon") if not weapon and not swing_slot then return true end if not weapon or not swing_slot then return false end return weapon.slot == items.letter_to_index(swing_slot) end local function wield_swing_item() if not swing_slot then return end wielding = true BRC.opt.single_turn_mute("You unwield ") BRC.opt.single_turn_mute(swing_slot .. " - ") crawl.sendkeys({ "w", "*", swing_slot }) crawl.flush_input() end -- Feature checks local function is_water(x, y) local feat = view.feature_at(x, y) return feat and feat:contains("water") and not you.status("flying") end local function is_monster(x, y) local mon = monster.get_monster_at(x, y) return mon and not (C.allow_plant_damage and mon.is_stationary()) end -- Setting direction to move or swing local function is_good_dir_walk(x, y) if x == 0 and y == 0 then return false end return is_water(x, y) == is_water(0, 0) and view.is_safe_square(x, y) and not travel.feature_solid(view.feature_at(x, y)) and not monster.get_monster_at(x, y) end local function is_good_dir_swing(x, y) if x == 0 and y == 0 then return false end if not view.in_known_map_bounds(x, y) then return false end local weapon = items.equipped_at("Weapon") if weapon then if weapon.is_ranged then -- Confirm no monsters in straight line for i = 1, you.los() do local cur_x = i * x local cur_y = i * y if is_monster(cur_x, cur_y) then return false end if travel.feature_solid(view.feature_at(cur_x, cur_y)) then break end end end if weapon.weap_skill:contains("Axes") then -- Confirm no monsters in adjacent squares for cur_x = -1, 1 do for cur_y = -1, 1 do if is_monster(cur_x, cur_y) then return false end end end end end return not (travel.feature_solid(view.feature_at(x, y)) or is_monster(x, y)) end local function get_good_direction() local func_is_good_dir = rest_type == "walk" and is_good_dir_walk or is_good_dir_swing for x = -1, 1 do for y = -1, 1 do if func_is_good_dir(x, y) then return x, y end end end return nil end local function set_good_direction() if rest_type == "walk" and dir.x ~= nil then -- Try to move back and forth by saving next dir if is_good_dir_walk(dir.x, dir.y) then return true end dir.x = nil end if dir.x == nil or not is_good_dir_swing(dir.x, dir.y) then dir.x, dir.y = get_good_direction() if not dir.x then reset_rest("No good direction found!") return false end end return true end -- Resting local function set_rest_type() local inv = swing_slot and items.inslot(items.letter_to_index(swing_slot)) local weap_delay = inv and BRC.eq.get_weap_delay(inv) or BRC.you.unarmed_attack_delay() if (not swing_item_wielded() and not weapon_can_swap()) or you.movement_cost and you.movement_cost() > 10 * weap_delay then rest_type = "walk" elseif weap_delay > 1 then rest_type = "item" else rest_type = "wait" end end local function verify_safe_rest() local hp, mhp = you.hp() local mp, mmp = you.mp() mhp = mhp * C.max_heal_perc / 100 mmp = mmp * C.max_heal_perc / 100 if hp >= mhp and mp >= mmp and not has_bad_duration() and not do_overheal then if turns_remaining == turns_to_rest then if BRC.mpr.yesno("You're healthy enough! Rest anyway?", BRC.COL.yellow) then do_overheal = true return true else BRC.mpr.okay() reset_rest() return false end end reset_rest(FULLY_RECOVERED_MSG) return false end if not you.feel_safe() then reset_rest("Hostile monster in view!") return false elseif rest_type == "walk" then if you.status("barbs") then reset_rest("You must remove the barbs first.") return false end end return true end local function do_resting() if not set_good_direction() then return end if f_announce_hp_mp then f_announce_hp_mp.single_turn_mute() end if rest_type == "wait" then BRC.util.do_cmd("CMD_SAFE_WAIT") elseif rest_type == "item" then BRC.opt.single_turn_mute("You swing at nothing.") BRC.opt.single_turn_mute("You shoot ") BRC.opt.single_turn_mute("unstable footing causes you to fumble your attack") crawl.sendkeys({ BRC.util.cntl(DIR_TO_VI[dir.x][dir.y]) }) crawl.flush_input() else -- Save the return direction as our next direction local cur_x = dir.x local cur_y = dir.y dir.x = -dir.x dir.y = -dir.y crawl.sendkeys({ DIR_TO_VI[cur_x][cur_y] }) crawl.flush_input() if C.walk_delay > 0 then crawl.delay(C.walk_delay) end end turns_remaining = turns_remaining - 1 if turns_remaining <= 0 then BRC.mpr.green("Resting complete. (" .. turns_to_rest .. " turns)") reset_rest() end end --- Checks if weapon has slowest swing speed. Returns 2 if slowest, and 1 for slowest 1-handed. local function is_slowest_weapon(it) if not it or it.is_useless or not it.is_weapon then return nil end local delay = BRC.eq.get_weap_delay(it) if delay < C.alert_slow_weap_min then return nil end if delay > bs_highest_delay then bs_highest_delay = delay if BRC.eq.get_hands(it) == 1 then bs_highest_delay_1h = delay end return 2 elseif delay > bs_highest_delay_1h and BRC.eq.get_hands(it) == 1 then bs_highest_delay_1h = delay return 1 end end ---- Public API ---- function macro_brc_bread_swing(turns) if BRC.active == false or f_bread_swinger.Config.disabled then return BRC.util.do_cmd("CMD_REST") end if not verify_safe_rest() then return end turns_to_rest = turns or get_num_turns() if not turns_to_rest or turns_to_rest <= 0 then return end turns_remaining = turns_to_rest -- Set swing slot swing_slot = bs_manual_swing_slot or get_slowest_slot() if swing_slot then local weap = items.inslot(items.letter_to_index(swing_slot)) if not weap or not weap.is_weapon then BRC.mpr.warning("Swing slot " .. BRC.txt.lightmagenta(swing_slot) .. " is not a weapon!") return end end -- Determine rest type set_rest_type() if rest_type == "walk" and turns_to_rest % 2 == 1 then turns_to_rest = turns_to_rest - 1 turns_remaining = turns_remaining - 1 end f_bread_swinger.ready() end function macro_brc_set_swing_slot() BRC.mpr.info(BRC.txt.white("Enter the inventory slot") .. " for the swing item: ") local letter = crawl.getch() if not letter or letter < string.byte('A') or letter > string.byte('z') then bs_manual_swing_slot = nil BRC.mpr.info(BRC.txt.magenta("Swing slot cleared.") .. " (Must be a letter a-z or A-Z).") return end bs_manual_swing_slot = string.char(letter) BRC.mpr.info(BRC.txt.lightgrey("Set swing slot to " .. BRC.txt.cyan(bs_manual_swing_slot) .. ".")) end ---- Initialization ---- function f_bread_swinger.init() C = f_bread_swinger.Config prev_num_turns_to_rest = 0 reset_rest() BRC.opt.macro(BRC.util.get_cmd_key("CMD_REST") or "5", "macro_brc_bread_swing", true) BRC.opt.macro(C.set_manual_slot_key, "macro_brc_set_swing_slot", true) for _, it in ipairs(items.inventory()) do is_slowest_weapon(it) end end ---- Crawl hook functions ---- function f_bread_swinger.c_message(text, channel) -- Don't stop for expiring effects or removing shield if turns_remaining <= 0 or channel == "recovery" or channel == "duration" or text:find("Your magical contamination .* fade") or removed_shield and text:find("You .* removing your") or text:contains(SH_PROMPT) then return end reset_rest() end function f_bread_swinger.ready() if not turns_remaining or turns_remaining <= 0 then reset_rest() return end if not verify_safe_rest() then return end if wielding and not swing_item_wielded() then -- An error happened with the 'w' command reset_rest("Unable to wield swing item on slot " .. swing_slot .. "!") return end if rest_type == "item" and not swing_item_wielded() then wield_swing_item() else do_resting() end end function f_bread_swinger.autopickup(it) if f_pickup_alert and f_pickup_alert.is_paused() then return end -- follow pause pattern local num_hands = is_slowest_weapon(it) if num_hands == 2 then do_alert("Found slowest weapon", it) elseif num_hands == 1 then do_alert("Found slowest 1-handed weapon", it) end end } ################################ End lua/features/bread-swinger.lua ############################### ################################################################################################### ############################## Begin lua/features/color-inscribe.lua ############################## ######## https://github.com/brianfaires/crawl-rc/blob/main/lua/features/color-inscribe.lua ######## { --------------------------------------------------------------------------------------------------- -- BRC feature module: color-inscribe -- @module f_color_inscribe -- Adds color to key terms in inscriptions (resistances, stats, etc). -- Disabled on main, because webtiles (CAO) doesn't consistently support numeric tags. --------------------------------------------------------------------------------------------------- f_color_inscribe = {} f_color_inscribe.BRC_FEATURE_NAME = "color-inscribe" f_color_inscribe.Config = { disabled = true, } -- f_color_inscribe.Config (do not remove this comment) ---- Local constants ---- local LOSS_COLOR = BRC.COL.brown local GAIN_COLOR = BRC.COL.white local MULTI_PLUS = "%++" local MULTI_MINUS = "%-+" local NEG_NUM = "%-%d+%.?%d*" local POS_NUM = "%+%d+%.?%d*" local POS_WORN = ":%d+%.?%d*" local COLORIZE_TAGS = { { "rF" .. MULTI_PLUS, BRC.COL.lightred }, { "rF" .. MULTI_MINUS, LOSS_COLOR }, { "rC" .. MULTI_PLUS, BRC.COL.lightblue }, { "rC" .. MULTI_MINUS, LOSS_COLOR }, { "rN" .. MULTI_PLUS, BRC.COL.lightmagenta }, { "rN" .. MULTI_MINUS, LOSS_COLOR }, { "rPois", BRC.COL.lightgreen }, { "rElec", BRC.COL.lightcyan }, { "rCorr", BRC.COL.yellow }, { "rMut", BRC.COL.yellow }, { "sInv", BRC.COL.magenta }, { "MRegen" .. MULTI_PLUS, BRC.COL.cyan }, { "^Regen" .. MULTI_PLUS, BRC.COL.green }, -- Avoiding "MRegen" { " Regen" .. MULTI_PLUS, BRC.COL.green }, -- Avoiding "MRegen" { "Stlth" .. MULTI_PLUS, GAIN_COLOR }, { "%+Fly", GAIN_COLOR }, { "RMsl", BRC.COL.yellow }, { "Will" .. MULTI_PLUS, BRC.COL.blue }, { "Will" .. MULTI_MINUS, LOSS_COLOR }, { "Wiz" .. MULTI_PLUS, BRC.COL.cyan }, { "Wiz" .. MULTI_MINUS, LOSS_COLOR }, { "Slay" .. POS_NUM, GAIN_COLOR }, { "Slay" .. NEG_NUM, LOSS_COLOR }, { "Str" .. POS_NUM, GAIN_COLOR }, { "Str" .. NEG_NUM, LOSS_COLOR }, { "Dex" .. POS_NUM, GAIN_COLOR }, { "Dex" .. NEG_NUM, LOSS_COLOR }, { "Int" .. POS_NUM, GAIN_COLOR }, { "Int" .. NEG_NUM, LOSS_COLOR }, { "AC" .. POS_NUM, GAIN_COLOR }, { "AC" .. POS_WORN, GAIN_COLOR }, { "AC" .. NEG_NUM, LOSS_COLOR }, { "EV" .. POS_NUM, GAIN_COLOR }, { "EV" .. POS_WORN, GAIN_COLOR }, { "EV" .. NEG_NUM, LOSS_COLOR }, { "SH" .. POS_NUM, GAIN_COLOR }, { "SH" .. POS_WORN, GAIN_COLOR }, { "SH" .. NEG_NUM, LOSS_COLOR }, { "HP" .. POS_NUM, GAIN_COLOR }, { "HP" .. NEG_NUM, LOSS_COLOR }, { "MP" .. POS_NUM, GAIN_COLOR }, { "MP" .. NEG_NUM, LOSS_COLOR }, } -- COLORIZE_TAGS (do not remove this comment) ---- Local functions ---- local function colorize_subtext(text, subtext, tag) if not text:find(subtext) then return text end -- Remove current color tag if it exists text = text:gsub("<(%d%d?)>(" .. subtext .. ")", "%2") return text:gsub(subtext, string.format("<%s>%%1", tag, tag)) end ---- Public API ---- function f_color_inscribe.colorize(it) local text = it.inscription for _, tag in ipairs(COLORIZE_TAGS) do text = colorize_subtext(text, tag[1], tag[2]) end -- Limit length for % menu: = total width, minus other text, minus length of item name it.inscribe("", false) local max_length = 80 - 3 - (it.is_melded and 32 or 25) - #it.name("plain", true) if max_length < 0 then return end -- Try removing brown, then white, then just remove all if #text > max_length then text = text:gsub("", "") end if #text > max_length then text = text:gsub("", "") end if #text > max_length then text = text:gsub("<.->", "") end it.inscribe(text, false) end ---- Crawl hook functions ---- function f_color_inscribe.c_assign_invletter(it) if it.artefact then return end -- If enabled, call inscribe stats before colorizing if f_inscribe_stats and f_inscribe_stats.Config and not f_inscribe_stats.Config.disabled and f_inscribe_stats.do_stat_inscription and ( it.is_weapon and f_inscribe_stats.Config.inscribe_weapons or BRC.it.is_armour(it) and f_inscribe_stats.Config.inscribe_armour ) then f_inscribe_stats.do_stat_inscription(it) end f_color_inscribe.colorize(it) end --[[ TODO: To colorize more, need a way to: intercept messages before they're displayed (or delete and re-insert) insert tags that affect menus colorize artefact text function f_color_inscribe.c_message(text, _) local orig_text = text text = colorize_subtext(text) if text == orig_text then return end local cleaned = BRC.txt.clean(text) if cleaned:sub(2, 4) == " - " then text = " " .. text end crawl.mpr(text) end --]] } ############################### End lua/features/color-inscribe.lua ############################### ################################################################################################### ############################### Begin lua/features/drop-inferior.lua ############################## ######### https://github.com/brianfaires/crawl-rc/blob/main/lua/features/drop-inferior.lua ######## { --------------------------------------------------------------------------------------------------- -- BRC feature module: drop-inferior -- @module f_drop_inferior -- When picking up an item, inscribes inferior items with "~~DROP_ME" and alerts you. -- Items with "~~DROP_ME" are added to the drop list, and can be quickly selected with `,` --------------------------------------------------------------------------------------------------- f_drop_inferior = {} f_drop_inferior.BRC_FEATURE_NAME = "drop-inferior" f_drop_inferior.Config = { msg_on_inscribe = true, -- Show a message when an item is marked for drop hotkey_drop = true, -- BRC hotkey drops all items on the drop list } -- f_drop_inferior.Config (do not remove this comment) ---- Local constants ---- local DROP_KEY = "~~DROP_ME" ---- Initialization ---- function f_drop_inferior.init() crawl.setopt("drop_filter += " .. DROP_KEY) end ---- Local functions ---- local function inscribe_drop(it) local new_inscr = it.inscription:gsub(DROP_KEY, "") .. DROP_KEY it.inscribe(new_inscr, false) if f_drop_inferior.Config.msg_on_inscribe then local item_name = BRC.txt.yellow(BRC.txt.int2char(it.slot) .. " - " .. it.name()) BRC.mpr.cyan(BRC.txt.wrap("You can drop: " .. item_name, BRC.EMOJI.CAUTION)) end end ---- Crawl hook functions ---- function f_drop_inferior.c_assign_invletter(it) -- Remove any previous DROP_KEY inscriptions it.inscribe(it.inscription:gsub(DROP_KEY, ""), false) if not (it.is_weapon or BRC.it.is_armour(it)) or BRC.eq.is_risky(it) or BRC.you.num_eq_slots(it) > 1 then return end local it_ego = BRC.eq.get_ego(it) local marked_something = false for _, inv in ipairs(items.inventory()) do -- To be a clear upgrade: Not artefact, same subtype, and ego is same or a clear upgrade local inv_ego = BRC.eq.get_ego(inv) local not_worse = inv_ego == it_ego or not inv_ego or BRC.eq.is_risky(inv) if not_worse and not inv.artefact and inv.subtype() == it.subtype() then if it.is_weapon then if inv.plus <= (it.plus or 0) then inscribe_drop(inv) marked_something = true end else local not_more_ac = BRC.eq.get_ac(inv) <= BRC.eq.get_ac(it) if not_more_ac and inv.encumbrance >= it.encumbrance then inscribe_drop(inv) marked_something = true end end end end if marked_something and f_drop_inferior.Config.hotkey_drop and BRC.Hotkey then local condition = function() return util.exists(items.inventory(), function(i) return i.inscription:contains(DROP_KEY) end) end local do_drop = function() crawl.sendkeys(BRC.util.get_cmd_key("CMD_DROP") .. ",\r") crawl.flush_input() end BRC.Hotkey.set("drop", "your useless items", false, do_drop, condition) end end } ################################ End lua/features/drop-inferior.lua ############################### ################################################################################################### ############################# Begin lua/features/display-realtime.lua ############################# ####### https://github.com/brianfaires/crawl-rc/blob/main/lua/features/display-realtime.lua ####### { --------------------------------------------------------------------------------------------------- -- BRC feature module: display-realtime -- @module f_display_realtime -- Display the realtime periodically in the output channel. --------------------------------------------------------------------------------------------------- f_display_realtime = {} f_display_realtime.BRC_FEATURE_NAME = "display-realtime" f_display_realtime.Config = { disabled = true, -- Disabled by default interval_s = 60, -- seconds between updates emoji = "🕒", init = function() if not BRC.Config.emojis then f_display_realtime.Config.emoji = BRC.txt.white("--") end end, } -- f_display_realtime.Config (do not remove this comment) ---- Persistent variables ---- dr_total_time = BRC.Data.persist("dr_total_time", 0) ---- Local variables ---- local last_time local last_cycle ---- Initialization ---- function f_display_realtime.init() last_time = you.real_time() last_cycle = 0 end ---- Crawl hook functions ---- function f_display_realtime.ready() dr_total_time = dr_total_time + you.real_time() - last_time local cycle = dr_total_time // f_display_realtime.Config.interval_s if cycle > last_cycle then local h = dr_total_time // 3600 local remain = dr_total_time % 3600 local m = remain // 60 local s = remain % 60 local time_str if h > 0 then time_str = string.format("Game time: %d:%02d:%02d", h, m, s) else time_str = string.format("Game time: %d:%02d", m, s) end BRC.mpr.white(BRC.txt.wrap(time_str, f_display_realtime.Config.emoji)) last_cycle = cycle end last_time = you.real_time() end } ############################## End lua/features/display-realtime.lua ############################## ################################################################################################### ############################## Begin lua/features/dynamic-options.lua ############################# ######## https://github.com/brianfaires/crawl-rc/blob/main/lua/features/dynamic-options.lua ####### { --------------------------------------------------------------------------------------------------- -- BRC feature module: dynamic-options -- @module f_dynamic_options -- Contains options that change based on game state: xl, class, race, god, skills. --------------------------------------------------------------------------------------------------- f_dynamic_options = {} f_dynamic_options.BRC_FEATURE_NAME = "dynamic-options" f_dynamic_options.Config = { meaningful_spellcasting_skill = 5, -- Skill level to switch on "spellcaster-specific" options --- XL-based force more messages: patterns active when XL <= specified level xl_force_mores = { { pattern = "monster_warning:wielding.*of electrocution", xl = 5 }, { pattern = "You.*re more poisoned", xl = 7 }, { pattern = "^(?!.*Your?).*speeds? up", xl = 10 }, { pattern = "danger:goes berserk", xl = 18 }, { pattern = "monster_warning:carrying a wand of", xl = 15 }, }, --- Call each function for the corresponding race race_options = { Gnoll = function() BRC.opt.message_mute("intrinsic_gain:skill increases to level", true) end, }, --- Call each function for the corresponding class class_options = { Hunter = function() crawl.setopt("view_delay = 30") end, Shapeshifter = function() BRC.opt.autopickup_exceptions(" (4 + you.skill("Armour") / 2) if ignore_advanced_magic ~= encumbered_magic then ignore_advanced_magic = encumbered_magic BRC.opt.autopickup_exceptions(HIGH_LVL_MAGIC_STRING, encumbered_magic) end end -- If spellcaster, add stop for mana drain if spellcasting_skill > C.meaningful_spellcasting_skill and not spellcaster_options_active then spellcaster_options_active = true BRC.opt.force_more_message("You feel your power leaking away", true) end end ---- Crawl hook functions ---- function f_dynamic_options.ready() set_god_options() set_xl_options() set_skill_options() end } ############################### End lua/features/dynamic-options.lua ############################## ################################################################################################### ############################## Begin lua/features/exclude-dropped.lua ############################# ######## https://github.com/brianfaires/crawl-rc/blob/main/lua/features/exclude-dropped.lua ####### { --------------------------------------------------------------------------------------------------- -- BRC feature module: exclude-dropped -- @module f_exclude_dropped -- Excludes dropped items from autopickup, pickup resumes autopickup. -- @todo Can remove when crawl's drop_disables_autopickup setting reaches feature parity. -- (Configurable/optional, dropping partial stack does not exclude, pickup resumes autopickup) --------------------------------------------------------------------------------------------------- f_exclude_dropped = {} f_exclude_dropped.BRC_FEATURE_NAME = "exclude-dropped" f_exclude_dropped.Config = { not_weapon_scrolls = true, -- Don't exclude enchant/brand scrolls if holding enchantable weapon } -- f_exclude_dropped.Config (do not remove this comment) ---- Persistent variables ---- ed_dropped_items = BRC.Data.persist("ed_dropped_items", {}) ---- Local functions ---- local function add_exclusion(item_name) if not util.contains(ed_dropped_items, item_name) then table.insert(ed_dropped_items, item_name) end BRC.opt.autopickup_exceptions(item_name, true) end local function remove_exclusion(item_name) util.remove(ed_dropped_items, item_name) BRC.opt.autopickup_exceptions(item_name, false) end local function enchantable_weap_in_inv() return util.exists(items.inventory(), function(i) return i.is_weapon and not BRC.it.is_magic_staff(i) and i.plus < 9 and (not i.artefact or you.race() == "Mountain Dwarf") end) end local function clean_item_text(text) text = BRC.txt.clean(text) text = text:gsub("{.*}", "") text = text:gsub("[.]", "") text = text:gsub("%(.*%)", "") return util.trim(text) end local function extract_jewellery_or_evoker(text) local idx = text:find("ring of", 1, true) or text:find("amulet of", 1, true) or text:find("wand of", 1, true) if idx then return text:sub(idx, #text) end for _, item_name in ipairs(BRC.MISC_ITEMS) do if text:find(item_name) then return item_name end end end local function extract_missile(text) for _, item_name in ipairs(BRC.MISSILES) do if text:contains(item_name) then return item_name end end end local function extract_potion(text) local idx = text:find("potions? of") if idx then return "potions? of " .. util.trim(text:sub(idx + 10, #text)) end end local function extract_scroll(text) local idx = text:find("scrolls? of") if idx then return "scrolls? of " .. util.trim(text:sub(idx + 10, #text)) end end --[[ get_item_name() - Tries to extract item name from text. Returns name of item, or nil if not recognized as an excludable item. --]] local function get_item_name(text) text = clean_item_text(text) return extract_jewellery_or_evoker(text) or extract_missile(text) or extract_potion(text) or extract_scroll(text) end local function should_exclude(item_name, full_msg) -- Enchant/Brand weapon scrolls continue pickup if they're still useful if f_exclude_dropped.Config.not_weapon_scrolls and (item_name:contains("enchant weapon") or item_name:contains("brand weapon")) and enchantable_weap_in_inv() then return false end -- Don't exclude if we dropped partial stack (except for jewellery) for _, inv in ipairs(items.inventory()) do if inv.name("qual"):contains(item_name) then return BRC.it.is_jewellery(inv) or inv.quantity == 1 or full_msg:contains("ou drop " .. inv.quantity .. " " .. item_name) end end return true end ---- Initialization ---- function f_exclude_dropped.init() for _, v in ipairs(ed_dropped_items) do add_exclusion(v) end end ---- Crawl hook functions ---- function f_exclude_dropped.c_message(text, channel) if channel ~= "plain" then return end local picked_up = BRC.txt.get_pickup_info(text) if not picked_up and not text:contains("ou drop ") then return end local item_name = get_item_name(text) if not item_name then return end if picked_up then remove_exclusion(item_name) elseif should_exclude(item_name, text) then add_exclusion(item_name) end end } ############################### End lua/features/exclude-dropped.lua ############################## ################################################################################################### #lua_file = crawl-rc/lua/features/fast-passage.lua ################################ Begin lua/features/fm-messages.lua ############################### ########## https://github.com/brianfaires/crawl-rc/blob/main/lua/features/fm-messages.lua ######### { --------------------------------------------------------------------------------------------------- -- BRC feature module: fm-messages -- @module f_fm_messages -- Define messages and rate their importance 1-9. -- Configure what level triggers force_more_message and flash_screen_message. -- Also remove default force_more_message patterns. --------------------------------------------------------------------------------------------------- f_fm_messages = {} f_fm_messages.BRC_FEATURE_NAME = "fm-messages" f_fm_messages.Config = { force_more_threshold = 6, -- How many force_more_messages; 1=many; 10=none flash_screen_threshold = 1, --- A list of all messages to respond to. The first value is the message importance. -- Use the above thresholds to adjust how the messages are responded to. -- General guidance on values: -- 8-9: Prevent accidental button press -- 5-7: Make sure you see it -- 3-4: Important to notice -- 1-2: Good to know messages = { -- Significant spells/effects ending {9, "life is in your own"}, -- Death's Door {7, "time is.*running out"}, -- Death's Door {7, "is no longer charmed"}, {7, "You.*re starting to lose your buoyancy"}, {5, "unholy channel is weakening"}, -- Death channel {2, "You feel stable"}, -- Cancelled tele -- Monsters doing things / Dangerous abilities {9, "you stand beside yourself"}, -- Mara {9, "sudden wrenching feeling in your soul"}, -- Mara {8, "monster_warning:wielding.*of distortion"}, {8, "begins to recite a word of recall"}, {7, "The air around.*erupts in flames"}, {7, "The air twists around and violently strikes you in flight"}, {7, "You feel.*(?= f_fm_messages.Config.force_more_threshold then BRC.opt.force_more_message(pattern, true) elseif msg_type >= f_fm_messages.Config.flash_screen_threshold then BRC.opt.flash_screen_message(pattern, true) end end end } ################################# End lua/features/fm-messages.lua ################################ ################################################################################################### ############################### Begin lua/features/fully-recover.lua ############################## ######### https://github.com/brianfaires/crawl-rc/blob/main/lua/features/fully-recover.lua ######## { --------------------------------------------------------------------------------------------------- -- BRC feature module: fully-recover -- @module f_fully_recover -- Rests until no negative duration statuses. Doesn't stop rest on each status expiration. -- @todo Can remove when crawl's explore_auto_rest_status setting reaches feature parity. --------------------------------------------------------------------------------------------------- f_fully_recover = {} f_fully_recover.BRC_FEATURE_NAME = "fully-recover" ---- Persistent variables ---- fr_bad_durations = BRC.Data.persist("fr_bad_durations", util.copy_table(BRC.BAD_DURATIONS)) ---- Local constants ---- local MAX_TURNS_TO_WAIT = 300 ---- Local variables ---- -- recovery_start_turn is a module field (not local) for test observability local M = f_fully_recover local explore_after_recovery ---- Initialization ---- function f_fully_recover.init() M.recovery_start_turn = nil explore_after_recovery = nil BRC.opt.macro(BRC.util.get_cmd_key("CMD_EXPLORE") or "o", "macro_brc_explore", true) BRC.opt.macro(BRC.util.get_cmd_key("CMD_REST") or "5", "macro_brc_rest") BRC.opt.runrest_ignore_message("recovery:.*", true) BRC.opt.runrest_ignore_message("duration:.*", true) BRC.opt.message_mute("^HP restored", true) BRC.opt.message_mute("Magic restored", true) end ---- Local functions ---- local function should_ignore_status(s) if s == "corroded" then return BRC.you.by_slimy_wall() or you.branch() == "Dis" elseif s == "slowed" then return BRC.you.zero_stat() end return false end local function fully_recovered() if you.contamination() > 0 then return false end local hp, mhp = you.hp() local mp, mmp = you.mp() if hp ~= mhp then return false end if mp ~= mmp then return false end local status = you.status() for _, s in ipairs(BRC.BAD_DURATIONS) do if status:find(s) and not should_ignore_status(s) then return false end end return true end local function remove_statuses_from_list() local status = you.status() local to_remove = {} for _, s in ipairs(fr_bad_durations) do if status:find(s) then table.insert(to_remove, s) end end for _, s in ipairs(to_remove) do util.remove(fr_bad_durations, s) BRC.mpr.error(" Removed: " .. s) end end --- If both CMD_EXPLORE macros are enabled, muted_explore is overridden. This function calls it. local function do_cmd_wrapper(cmd) if cmd == "CMD_EXPLORE" and macro_brc_muted_explore then macro_brc_muted_explore() else BRC.util.do_cmd(cmd) end end local function complete_recovery() local turns = you.turns() - M.recovery_start_turn M.recovery_start_turn = nil if turns > 0 then you.stop_activity() BRC.mpr.lightgreen(string.format("Fully recovered (%d turns)", turns)) if explore_after_recovery then do_cmd_wrapper("CMD_EXPLORE") end end end local function start_recovery(cmd) if BRC.active == false or f_fully_recover.Config.disabled or not you.feel_safe() then return do_cmd_wrapper(cmd) end if fully_recovered() then if M.recovery_start_turn ~= nil then complete_recovery() else do_cmd_wrapper(cmd) end else M.recovery_start_turn = you.turns() explore_after_recovery = cmd == "CMD_EXPLORE" do_cmd_wrapper("CMD_REST") end end ---- Macro function: Attach full recovery to auto-explore ---- function macro_brc_explore() start_recovery("CMD_EXPLORE") end ---- Macro function: Attach full recovery to auto-rest ---- function macro_brc_rest() start_recovery("CMD_REST") end ---- Crawl hook functions ---- function f_fully_recover.c_message(text, channel) if M.recovery_start_turn == nil then return end if channel == "plain" and ( text:contains("You start resting.") or text:contains("You start waiting.") or (f_announce_hp_mp and f_announce_hp_mp.msg_is_meter(text)) -- ignore announce-hp-mp messages ) then return end -- Always stop the current recovery, and ready() will re-evaluate with the updated player status -- For any non-duration/recovery message, abort the recovery entirely. you.stop_activity() if channel ~= "duration" and channel ~= "recovery" then M.recovery_start_turn = nil end end function f_fully_recover.ready() if M.recovery_start_turn == nil then return end if fully_recovered() then complete_recovery() elseif not you.feel_safe() then M.recovery_start_turn = nil you.stop_activity() elseif you.turns() - M.recovery_start_turn > MAX_TURNS_TO_WAIT then BRC.mpr.error("fully-recover timed out after " .. MAX_TURNS_TO_WAIT .. " turns.", true) BRC.mpr.error("fr_bad_durations:") remove_statuses_from_list() M.recovery_start_turn = nil you.stop_activity() else do_cmd_wrapper("CMD_REST") end end } ################################ End lua/features/fully-recover.lua ############################### ################################################################################################### ################################ Begin lua/features/go-up-macro.lua ############################### ########## https://github.com/brianfaires/crawl-rc/blob/main/lua/features/go-up-macro.lua ######### { --------------------------------------------------------------------------------------------------- -- BRC feature module: go-up-macro -- @module f_go_up_macro -- Handles orb run mechanics: HP-based monster ignore for cntl-E macro --------------------------------------------------------------------------------------------------- f_go_up_macro = {} f_go_up_macro.BRC_FEATURE_NAME = "go-up-macro" f_go_up_macro.Config = { go_up_macro_key = BRC.util.cntl("e"), -- Key for "go up closest stairs" macro ignore_mon_on_orb_run = true, -- Ignore monsters on orb run -- %HP thresholds for ignoring monsters during orb run (2-7 tiles away, depending on HP percent) orb_ignore_hp_min = 0.30, -- HP percent to stop ignoring monsters orb_ignore_hp_max = 0.70, -- HP percent to ignore monsters at min distance away (2 tiles) } -- f_go_up_macro.Config (do not remove this comment) ---- Local variables ---- local orb_ignore_distance ---- Local functions ---- local function set_orb_ignore_distance(distance) if orb_ignore_distance then BRC.opt.runrest_ignore_monster(".*:" .. orb_ignore_distance, false) orb_ignore_distance = nil end if distance then orb_ignore_distance = distance BRC.opt.runrest_ignore_monster(".*:" .. orb_ignore_distance, true) end end --- Get distance (2 - 7) to ignore monsters based on HP percent local function get_ignore_distance_from_hp() local hp, mhp = you.hp() local hp_pct = hp / mhp local min_pct = f_go_up_macro.Config.orb_ignore_hp_min local max_pct = f_go_up_macro.Config.orb_ignore_hp_max if hp_pct <= min_pct then return nil end if hp_pct >= max_pct then return 2 end -- Linear interpolation between min_pct and max_pct local ratio = (hp_pct - min_pct) / (max_pct - min_pct) return math.floor(2 + ratio * (you.los() - 2)) end ---- Initialization ---- function f_go_up_macro.init() BRC.opt.macro(f_go_up_macro.Config.go_up_macro_key, "macro_brc_go_up") end ---- Macro function ---- --- Go up the closest stairs (Cntl-E) function macro_brc_go_up() if BRC.active == false or f_go_up_macro.Config.disabled then return end if you.have_orb() and f_go_up_macro.Config.ignore_mon_on_orb_run then local distance = get_ignore_distance_from_hp() if distance ~= orb_ignore_distance then set_orb_ignore_distance(distance) end end -- Go up closest stairs; different macro for D:1 and portals local where = you.where() if where == "D:1" and you.have_orb() or where == "Temple" or util.contains(BRC.PORTAL_FEATURE_NAMES, you.branch()) or BRC.you.in_hell(true) then crawl.sendkeys({ "X", "<", "\r", BRC.KEYS.ESC, "<" }) -- {ESC, <} handles standing on stairs else crawl.sendkeys({ BRC.util.cntl("g"), "<" }) end crawl.flush_input() end } ################################# End lua/features/go-up-macro.lua ################################ ################################################################################################### ############################## Begin lua/features/inscribe-stats.lua ############################## ######## https://github.com/brianfaires/crawl-rc/blob/main/lua/features/inscribe-stats.lua ######## { --------------------------------------------------------------------------------------------------- -- BRC feature module: inscribe-stats -- @module f_inscribe_stats -- Inscribes and updates weapon DPS/dmg/delay, and armour AC/EV/SH, for items in inventory. -- For Coglin weapons, evaluates as if swapping out the primary weapon (for artefact stat changes) --------------------------------------------------------------------------------------------------- f_inscribe_stats = {} f_inscribe_stats.BRC_FEATURE_NAME = "inscribe-stats" f_inscribe_stats.Config = { inscribe_weapons = true, -- Inscribe weapon stats on pickup inscribe_armour = true, -- Inscribe armour stats on pickup dmg_type = BRC.DMG_TYPE.unbranded, -- unbranded, plain, branded, scoring skip_dps = false, -- Skip DPS in weapon inscriptions prefix_staff_dmg = true, -- Special prefix for magical staves } -- f_inscribe_stats.Config (do not remove this comment) ---- Local constants ---- local NUM_PATTERN = "[%+%-:]%d+[%.,%,]%d*" -- Matches numbers w/ decimal ---- Local variables ---- local C -- config alias ---- Initialization ---- function f_inscribe_stats.init() C = f_inscribe_stats.Config end ---- Local functions ---- local function inscribe_armour_stats(it) local abbr = BRC.it.is_shield(it) and "SH" or "AC" local ac_or_sh, ev = BRC.eq.arm_stats(it) local sign_change = false local new_insc if it.inscription:find(abbr .. NUM_PATTERN) then new_insc = it.inscription:gsub(abbr .. NUM_PATTERN, ac_or_sh) if not it.inscription:contains(ac_or_sh:sub(1, 3)) then sign_change = true end if ev and #ev > 0 then new_insc = new_insc:gsub("EV" .. NUM_PATTERN, ev) if not it.inscription:contains(ev:sub(1, 3)) then sign_change = true end end else new_insc = ac_or_sh if ev and #ev > 0 then new_insc = string.format("%s, %s", new_insc, ev) end if it.inscription and #it.inscription > 0 then new_insc = string.format("%s; %s", new_insc, it.inscription) end end it.inscribe(new_insc, false) -- If f_color_inscribe is enabled, update the color if sign_change and f_color_inscribe and f_color_inscribe.Config and not f_color_inscribe.Config.disabled and f_color_inscribe.colorize then f_color_inscribe.colorize(it) end end local function get_staff_dmg_str(it) local _, dmg, chance = BRC.eq.get_staff_bonus_dmg(it) if dmg == 0 or chance == 0 then return "(+0)" end if chance >= 1 then return string.format("(+%d)", math.floor(dmg)) end return string.format("(+%.0f|%.0f%%%%)", dmg, chance * 100) end --- Replace the old inscription with the current one, preserving prefix/suffix local function update_inscription(orig, cur) local first = orig:find(cur:sub(1, 4)) -- Handle format migration between DPS= and Dmg= modes (e.g. when skip_dps config changes) if not first then if cur:sub(1, 4) == "Dmg=" then first = orig:find("DPS=") elseif cur:sub(1, 4) == "DPS=" then first = orig:find("Dmg=") end end if not first then return cur .. "; " .. orig end local _, last = orig:find("A%+%d+") if not last then _, last = orig:find("A%-%d+") end if not last then BRC.mpr.error("Missing accuracy in inscription: " .. orig) return cur .. "; " .. orig end local prefix = orig:sub(1, first - 1) if #prefix > 0 and prefix:sub(-2) ~= "; " then prefix = prefix .. "; " end if prefix == "; " then prefix = "" end local suffix = util.trim(orig:sub(last+1)) if #suffix > 0 and suffix:sub(1, 1) ~= ";" then suffix = "; " .. suffix end if suffix == ";" then suffix = "" end return prefix .. cur .. suffix end local function inscribe_weapon_stats(it) local orig_inscr = it.inscription local dmg_type = C.dmg_type if type(dmg_type) == "string" then dmg_type = BRC.DMG_TYPE[dmg_type] end local dps_inscr = BRC.eq.wpn_stats(it, dmg_type, C.skip_dps) if C.prefix_staff_dmg and BRC.it.is_magic_staff(it) then local bonus_str = get_staff_dmg_str(it) dps_inscr = dps_inscr:gsub("/", bonus_str .. "/") -- Recuce weapon damage string from #.## -> # local dmg_index = dps_inscr:find("=%d+%.%d%d") if dmg_index then local dmg_str = dps_inscr:sub(dmg_index + 1, dmg_index + 4) local dmg_int = math.floor(tonumber(dmg_str)+0.5) dps_inscr = dps_inscr:gsub("=" .. dmg_str, "=" .. dmg_int) end end it.inscribe(update_inscription(orig_inscr, dps_inscr), false) end ---- Crawl hook functions ---- function f_inscribe_stats.do_stat_inscription(it) if C.inscribe_weapons and it.is_weapon then inscribe_weapon_stats(it) elseif C.inscribe_armour and BRC.it.is_armour(it) and not BRC.it.is_scarf(it) then inscribe_armour_stats(it) end end function f_inscribe_stats.ready() for _, inv in ipairs(items.inventory()) do f_inscribe_stats.do_stat_inscription(inv) end end } ############################### End lua/features/inscribe-stats.lua ############################### ################################################################################################### ################################ Begin lua/features/misc-alerts.lua ############################### ########## https://github.com/brianfaires/crawl-rc/blob/main/lua/features/misc-alerts.lua ######### { --------------------------------------------------------------------------------------------------- -- BRC feature module: misc-alerts -- @module f_misc_alerts -- @author gammafunk (save game w/ msg), buehler -- Various single-purpose alerts: save game w/ msg, low HP, faith amulet, spell level changes. --------------------------------------------------------------------------------------------------- f_misc_alerts = {} f_misc_alerts.BRC_FEATURE_NAME = "misc-alerts" f_misc_alerts.Config = { preferred_god = "", -- Stop on first altar with this text (Ex. "Wu Jian"); nil or "" disables force_more_on_pref_altar = true, -- Force more message on first altar for preferred god save_with_msg = true, -- Shift-S to save and leave yourself a message alert_low_hp_threshold = 35, -- % max HP to alert; 0 to disable alert_spell_level_changes = true, -- Alert when you gain additional spell levels alert_remove_faith = true, -- Reminder to remove amulet at max piety remove_faith_hotkey = true, -- Hotkey remove amulet } -- f_misc_alerts.Config (do not remove this comment) ---- Persistent variables ---- ma_alerted_max_piety = BRC.Data.persist("ma_alerted_max_piety", false) ma_saved_msg = BRC.Data.persist("ma_saved_msg", "") ma_found_altar = BRC.Data.persist("ma_found_altar", false) ---- Local constants ---- local REMOVE_FAITH_MSG = "6 star piety! Maybe ditch that amulet soon." local GOD_ALTAR_TEXT = "Found.*altar.*" ---- Local variables ---- local C -- config alias local below_hp_threshold local prev_spell_levels local removed_altar_alert ---- Initialization ---- function f_misc_alerts.init() C = f_misc_alerts.Config below_hp_threshold = false prev_spell_levels = you.spell_levels() removed_altar_alert = false if C.save_with_msg then BRC.opt.macro(BRC.util.get_cmd_key("CMD_SAVE_GAME") or "S", "macro_brc_save") if ma_saved_msg and #ma_saved_msg > 0 then BRC.mpr.white("MESSAGE: " .. ma_saved_msg) ma_saved_msg = "" end end if C.preferred_god and not ma_found_altar then if #C.preferred_god == 0 then ma_found_altar = true else GOD_ALTAR_TEXT = GOD_ALTAR_TEXT .. C.preferred_god BRC.opt.flash_screen_message(GOD_ALTAR_TEXT, true) BRC.opt.force_more_message(GOD_ALTAR_TEXT, C.force_more_on_pref_altar) end end end ---- Local functions ---- local function alert_low_hp() local hp, mhp = you.hp() if below_hp_threshold then below_hp_threshold = hp ~= mhp elseif hp <= mhp * C.alert_low_hp_threshold / 100 then below_hp_threshold = true local low_hp_msg = "Dropped below " .. C.alert_low_hp_threshold .. "% HP" BRC.mpr.que_optmore(true, BRC.txt.wrap(BRC.txt.magenta(low_hp_msg), BRC.EMOJI.EXCLAMATION)) end end local function alert_remove_faith() if not ma_alerted_max_piety and you.piety_rank() == 6 then local am = items.equipped_at("amulet") if am and am.subtype() == "amulet of faith" and not am.artefact then if you.god() == "Uskayaw" then return end BRC.mpr.more(REMOVE_FAITH_MSG, BRC.COL.lightcyan) ma_alerted_max_piety = true if C.remove_faith_hotkey and BRC.Hotkey then BRC.Hotkey.set("remove", "amulet of faith", false, function() items.equipped_at("amulet"):remove() end) end end end end local function alert_spell_level_changes() local new_spell_levels = you.spell_levels() if new_spell_levels > prev_spell_levels then local delta = new_spell_levels - prev_spell_levels local msg = string.format("Gained %s spell level%s", delta, delta > 1 and "s" or "") local suffix = string.format(" (%s available)", new_spell_levels) BRC.mpr.lightcyan(msg .. BRC.txt.cyan(suffix)) elseif new_spell_levels < prev_spell_levels then BRC.mpr.magenta(new_spell_levels .. " spell levels remaining") end prev_spell_levels = new_spell_levels end ---- Macro function: Save with message feature ---- function macro_brc_save() if BRC.active == false or f_misc_alerts.Config.disabled or not f_misc_alerts.Config.save_with_msg then return BRC.util.do_cmd("CMD_SAVE_GAME") end if not BRC.mpr.yesno("Save game and exit?", BRC.COL.lightcyan) then BRC.mpr.okay() return end BRC.mpr.white("Leave a message: ", "prompt") ma_saved_msg = crawl.c_input_line() BRC.util.do_cmd("CMD_SAVE_GAME_NOW") end ---- Crawl hook functions ---- function f_misc_alerts.c_message(text, _) if C.preferred_god and not ma_found_altar and text:find(GOD_ALTAR_TEXT) then ma_found_altar = true local feature_name = "altar_" .. (C.preferred_god:match("^%S+") or C.preferred_god):lower() BRC.Hotkey.move_to_feature("altar of " .. C.preferred_god, true, feature_name) end end function f_misc_alerts.ready() if C.alert_remove_faith then alert_remove_faith() end if C.alert_low_hp_threshold > 0 then alert_low_hp() end if C.alert_spell_level_changes then alert_spell_level_changes() end if ma_found_altar and not removed_altar_alert then removed_altar_alert = true BRC.opt.flash_screen_message(GOD_ALTAR_TEXT, false) BRC.opt.force_more_message(GOD_ALTAR_TEXT, false) end end } ################################# End lua/features/misc-alerts.lua ################################ ################################################################################################### ############################### Begin lua/features/mute-messages.lua ############################## ######### https://github.com/brianfaires/crawl-rc/blob/main/lua/features/mute-messages.lua ######## { --------------------------------------------------------------------------------------------------- -- BRC feature module: mute-messages -- @module f_mute_messages -- Mutes various crawl messages, with configurable levels of reduction. f_mute_messages = {} f_mute_messages.BRC_FEATURE_NAME = "mute-messages" f_mute_messages.Config = { do_exploration_mutes = true, -- Mute boring messages while auto-exploring mute_level = 2, messages = { -- Only mute these when auto-exploring explore_only = { "There is a.*(staircase|door|gate|hatch).*here", "You enter the shallow water", "You.*open the door", "You disentangle yourself", "You see here .*", }, -- Light reduction; unnecessary messages [1] = { -- Unnecessary "You now have .* runes", "to see all the runes you have collected", "A chill wind blows around you", "An electric hum fills the air", "You reach to attack", -- Interface "for a list of commands and other information", "Marking area around", "(Reduced|Removed|Placed new) exclusion", "You can access your shopping list by pressing '\\$'", "Shift\\-Dir \\- straight line", "Press: ? .* help", -- Movement "Moving in this stuff is going to be slow", -- Wielding weapons "Your .* begins to (drip with poison|ooze corrosive slime)", "Your .* bursts into flame", "Your .* is covered in frost", "Your .* glows (with a cold blue light|with a divine radiance|horrifically)", "You (hear the crackle of electricity|see sparks fly)", "Your .* exudes an aura of protection", "Your .* hums with potential", "You sense an unholy aura", "Your .* tingle", "Your .* quivers in your", "Space warps around you for a moment", "You feel (a sense of dread|a bond with|a baleful cunning)", "(Pain shudders through|A searing pain shoots up) your", "Your .* is briefly surrounded by (a scintillating aura|shifting shadows)", "Your .* before you manage to get a firm grip on it", "Your .* gleams with (eagerness|a vicious edge)", "Your .* radiates an overwhelming force", "Vines begin sprouting from", -- Unwielding weapons "Your .* stops (flaming|glowing|crackling|quivering)", "Your .* stops (dripping with poison|oozing corrosive slime|radiating force)", "Your .* goes (still|dull)", "You feel the dreadful sensation subside", "You feel magic returning to you", "You feel (very meek|guileless)", "The vines retreat back into", -- Monsters /Allies / Neutrals "dissolves into shadows", "You swap places", "Your spectral weapon disappears", -- Spells "Your foxfire dissipates", -- Religion "accepts your kill", "is honoured by your kill", "appreciates the change of pace", }, -- Moderate reduction; potentially confusing but no info lost [2] = { -- Allies / monsters "Ancestor HP restored", "The (bush|fungus|plant) (looks sick|begins to die|is engulfed|is struck)", "Your.*the (bush|fungus|plant)", "evades? a web", "is (lightly|moderately|heavily|severely) (damaged|wounded)", "is almost (dead|destroyed)", -- Interface "Use which ability\\?", "Evoke which item\\?$", -- Books "You pick up (?!a manual).*and begin reading", "Unfortunately\\, you learn nothing new", -- Ground items / features "There is a.*(door|web).*here", "You see here .*(corpse|skeleton)", "You now have \\d+ gold piece", "You enter the shallow water", -- Religion "Your shadow attacks", }, -- Heavily reduced messages for realtime speedruns [3] = { "No target in view", "You (bite|headbutt|kick)", "You (burn|freeze|drain)", "You block", "but do(es)? no damage", "misses you", }, }, } -- f_mute_messages.Config (do not remove this comment) ---- Macro functions ---- function macro_brc_muted_explore() if BRC.active and not f_mute_messages.Config.disabled and f_mute_messages.Config.do_exploration_mutes then for _, message in ipairs(f_mute_messages.Config.messages.explore_only) do BRC.opt.single_turn_mute(message) end end BRC.util.do_cmd("CMD_EXPLORE") end ---- Initialization ---- function f_mute_messages.init() if f_mute_messages.Config.do_exploration_mutes then BRC.opt.macro(BRC.util.get_cmd_key("CMD_EXPLORE") or "o", "macro_brc_muted_explore") end if f_mute_messages.Config.mute_level and f_mute_messages.Config.mute_level > 0 then for i = 1, f_mute_messages.Config.mute_level do if not f_mute_messages.Config.messages[i] then break end for _, message in ipairs(f_mute_messages.Config.messages[i]) do BRC.opt.message_mute(message, true) end end end end } ################################ End lua/features/mute-messages.lua ############################### ################################################################################################### ############################# Begin lua/features/quiver-reminders.lua ############################# ####### https://github.com/brianfaires/crawl-rc/blob/main/lua/features/quiver-reminders.lua ####### { --------------------------------------------------------------------------------------------------- -- BRC feature module: quiver-reminders -- @module f_quiver_reminders -- A handful of useful quiver-related reminders. (AKA things I often forget.) --------------------------------------------------------------------------------------------------- f_quiver_reminders = {} f_quiver_reminders.BRC_FEATURE_NAME = "quiver-reminders" f_quiver_reminders.Config = { confirm_consumables = true, warn_diff_missile_turns = 10, } -- f_quiver_reminders.Config (do not remove this comment) ---- Local variables ---- -- last_thrown/last_queued state is on module fields (not local) for test observability local M = f_quiver_reminders local C -- config alias ---- Initialization ---- function f_quiver_reminders.init() C = f_quiver_reminders.Config M.last_thrown = nil M.last_thrown_turn = -1 M.last_queued = nil M.last_queued_turn = -1 BRC.opt.macro(BRC.util.get_cmd_key("CMD_FIRE") or "f", "macro_brc_fire") end ---- Local functions ---- --- Generate a string that matches the "Throw: ()" format local function parse_name_from_item(it) local ego = it.ego() if not ego then return it.name("db") end return it.name("db") .. " (" .. ego .. ")" end local function quiver_missile_by_name(name) local slot = nil for _, inv in ipairs(items.inventory()) do if parse_name_from_item(inv) == name then slot = inv.slot break end end if not slot then BRC.mpr.error("Not found in inventory: " .. name) return end crawl.sendkeys(BRC.util.get_cmd_key("CMD_QUIVER_ITEM") .. "*(" .. BRC.txt.int2char(slot)) crawl.flush_input() end ---- Macro function: Fire from quiver ---- function macro_brc_fire() if BRC.active == false or f_quiver_reminders.Config.disabled then return BRC.util.do_cmd("CMD_FIRE") end local quivered = items.fired_item() if not quivered then return end if C.confirm_consumables then local cls = quivered.class(true) if cls == "potion" or cls == "scroll" then local action = cls == "potion" and "drink" or "read" local q = BRC.txt.lightgreen(quivered.name()) local msg = string.format("Really %s %s from quiver?", action, q) if not BRC.mpr.yesno(msg) then return BRC.mpr.okay() end end end local lt = M.last_thrown local ltt = M.last_thrown_turn if lt and (you.turns() - ltt <= C.warn_diff_missile_turns) then local eq_name = items.equipped_at("Weapon") and items.equipped_at("Weapon").name("qual") or nil local quiv_name = parse_name_from_item(quivered) if quiv_name ~= M.last_thrown and quiv_name ~= eq_name then local q = BRC.txt.lightgreen(quiv_name) if not BRC.mpr.yesno("Did you mean to throw " .. q .. "?") then local t = BRC.txt.lightgreen(M.last_thrown) if BRC.mpr.yesno("Quiver and throw " .. t .. " instead?") then quiver_missile_by_name(M.last_thrown) else return BRC.mpr.okay() end end end end BRC.util.do_cmd("CMD_FIRE") end ---- Crawl hook functions ---- function f_quiver_reminders.c_message(text, _) local cleaned = BRC.txt.clean(text) if cleaned:sub(1, 7) == "Throw: " then M.last_queued_turn = you.turns() -- Missile name is shown in message like: "Throw: 23 darts (curare)". Strip prefix. M.last_queued = cleaned:sub(8, #cleaned) -- Remove quantity and pluralization M.last_queued = M.last_queued:gsub("^%d+ ", "") if M.last_queued:sub(-1) == "s" then M.last_queued = M.last_queued:sub(1, -2) else M.last_queued = M.last_queued:gsub("s %(", " (") end elseif cleaned:sub(1, 10) == "You throw " then local lqt = M.last_queued_turn if you.turns() ~= lqt then BRC.mpr.error("quiver-remind turn changed: " .. lqt .. " -> " .. you.turns()) end M.last_thrown = M.last_queued M.last_thrown_turn = M.last_queued_turn end end } ############################## End lua/features/quiver-reminders.lua ############################## ################################################################################################### ################################# Begin lua/features/remind-id.lua ################################ ########### https://github.com/brianfaires/crawl-rc/blob/main/lua/features/remind-id.lua ########## { --------------------------------------------------------------------------------------------------- -- BRC feature module: remind-id -- @module f_remind_id -- Alerts a reminder to read scroll of ID, when carrying unidentified items. -- Before finding scroll of ID, stops explore when largest stack of un-ID'd scrolls/pots increases. --------------------------------------------------------------------------------------------------- f_remind_id = {} f_remind_id.BRC_FEATURE_NAME = "remind-id" f_remind_id.Config = { stop_on_scrolls_count = 2, -- Stop when largest un-ID'd scroll stack increases and is >= this stop_on_pots_count = 3, -- Stop when largest un-ID'd potion stack increases and is >= this read_id_hotkey = true, -- Put read ID on hotkey emoji = "🎁", init = function() if not BRC.Config.emojis then f_remind_id.Config.emoji = BRC.txt.magenta("?") end end, } -- f_remind_id.Config (do not remove this comment) ---- Persistent variables ---- ri_found_scroll_of_id = BRC.Data.persist("ri_found_scroll_of_id", false) ---- Local variables ---- local C -- config alias local do_remind_id_check ---- Initialization ---- function f_remind_id.init() C = f_remind_id.Config do_remind_id_check = true end ---- Local functions ---- local function get_max_stack(class) local max_stack_size = 0 local slot = nil for _, inv in ipairs(items.inventory()) do if inv.class(true) == class and not inv.is_identified then if inv.quantity > max_stack_size then max_stack_size = inv.quantity slot = inv.slot elseif inv.quantity == max_stack_size then slot = nil -- If tied for max, no slot set a new max end end end return max_stack_size, slot end local function have_scroll_of_id() return util.exists(items.inventory(), function(i) return i.name("qual") == "scroll of identify" end) end local function have_unid_item() return util.exists(items.inventory(), function(i) return not i.is_identified end) end ---- Crawl hook functions ---- function f_remind_id.c_assign_invletter(it) if not it.is_identified and have_scroll_of_id() or it.name("qual") == "scroll of identify" and have_unid_item() then you.stop_activity() do_remind_id_check = true end end function f_remind_id.c_message(text, channel) if channel ~= "plain" then return end if text:find("scrolls? of identify") then ri_found_scroll_of_id = true -- Don't re-trigger on dropping or on hotkey notification text = BRC.txt.clean(text) if have_unid_item() and not ( text:contains("ou drop ") or text:contains("to read ") or text:contains("Found") ) then you.stop_activity() do_remind_id_check = true end else local name, slot = BRC.txt.get_pickup_info(text) if not name then return end local is_scroll = name:contains("scroll") local is_potion = name:contains("potion") if not (is_scroll or is_potion) then return end if ri_found_scroll_of_id then -- Check for pickup unidentified consumable if not name:contains(" of ") then do_remind_id_check = true if have_scroll_of_id() then you.stop_activity() end end else -- Check if max stack size increased local num_scrolls, slot_scrolls = get_max_stack("scroll") local num_pots, slot_pots = get_max_stack("potion") if is_scroll and slot_scrolls == slot and num_scrolls >= C.stop_on_scrolls_count or is_potion and slot_pots == slot and num_pots >= C.stop_on_pots_count then you.stop_activity() end end end end function f_remind_id.ready() if do_remind_id_check then do_remind_id_check = false if have_unid_item() and have_scroll_of_id() then local msg = BRC.txt.wrap(BRC.txt.magenta("You have something to identify."), C.emoji) BRC.mpr.stop(msg) if C.read_id_hotkey and BRC.Hotkey then BRC.Hotkey.set("read", "scroll of identify", false, function() for _, inv in ipairs(items.inventory()) do if inv.name("qual") == "scroll of identify" then BRC.util.do_cmd("CMD_READ") crawl.sendkeys(BRC.txt.int2char(inv.slot)) crawl.flush_input() return end end end) end end end end } ################################## End lua/features/remind-id.lua ################################# ################################################################################################### ############################# Begin lua/features/runrest-features.lua ############################# ####### https://github.com/brianfaires/crawl-rc/blob/main/lua/features/runrest-features.lua ####### { --------------------------------------------------------------------------------------------------- -- BRC feature module: runrest-features -- @module f_runrest_features -- Simple features related to auto-explore stops: altars, gauntlets, portals, stairs, etc. --------------------------------------------------------------------------------------------------- f_runrest_features = {} f_runrest_features.BRC_FEATURE_NAME = "runrest-features" f_runrest_features.Config = { after_shaft = true, -- stop on stairs after being shafted, until returned to original floor ignore_altars = true, -- when you don't need a god ignore_portal_exits = true, -- don't stop explore on portal exits stop_on_hell_stairs = true, -- stop explore on hell stairs stop_on_pan_gates = true, -- stop explore on pan gates temple_search = true, -- on entering or exploring temple, auto-search gauntlet_search = true, -- on entering or exploring gauntlet, auto-search with filters necropolis_search = true, -- on exploring necropolis, auto-search with filters } -- f_runrest_features.Config (do not remove this comment) ---- Persistent variables ---- rr_autosearched_temple = BRC.Data.persist("rr_autosearched_temple", false) rr_shaft_location = BRC.Data.persist("rr_shaft_location", nil) ---- Local constants ---- local CONCAT_STRING = " && !!" local SEARCH_FILTERS = table.concat({ "gate leading", "a transporter", "gold piece", " trap", "translucent door", "translucent gate" }, CONCAT_STRING) local SEARCH_STRING = { Gauntlet = "gauntlet" .. CONCAT_STRING .. SEARCH_FILTERS, Necropolis = "necropolis" .. CONCAT_STRING .. SEARCH_FILTERS, } -- SEARCH_STRING (do not remove this comment) ---- Local variables ---- local C -- config alias local stop_on_altars local stop_on_portals local stop_on_stairs local autosearched_gauntlet local autosearched_necropolis ---- Initialization ---- function f_runrest_features.init() C = f_runrest_features.Config stop_on_altars = true stop_on_portals = true stop_on_stairs = false autosearched_gauntlet = false autosearched_necropolis = false if you.turns() == 0 and you.class() == "Delver" then rr_shaft_location = "D:1" end end ---- Local functions ---- local function is_explore_done_msg(text) local cleaned = BRC.txt.clean(text) return cleaned == "Done exploring." or cleaned:find("Partly explored, ", 1, true) == 1 or cleaned:find("Could not explore, unopened runed ", 1, true) == 1 end local function set_stairs_stop_state() local should_be_active = C.stop_on_pan_gates and you.branch() == "Pan" or C.stop_on_hell_stairs and BRC.you.in_hell(true) or C.after_shaft and rr_shaft_location ~= nil if stop_on_stairs and not should_be_active then stop_on_stairs = false BRC.opt.explore_stop("stairs", false) elseif not stop_on_stairs and should_be_active then stop_on_stairs = true BRC.opt.explore_stop("stairs", true) end end -- Altar/Religion functions local function religion_is_handled() if you.race() == "Demigod" then return true end if you.god() == "No God" then return false end if you.good_god() then return you.xl() > 9 end return true end local function ready_ignore_altars() if stop_on_altars and religion_is_handled() then stop_on_altars = false BRC.opt.explore_stop("altars", false) elseif not stop_on_altars and not religion_is_handled() then stop_on_altars = true BRC.opt.explore_stop("altars", true) end end -- Temple functions local function search_altars() local cmd_key = BRC.util.get_cmd_key("CMD_SEARCH_STASHES") or BRC.util.cntl("f") crawl.sendkeys({ cmd_key, "altar", "\r" }) crawl.flush_input() end local function ready_temple_search() if you.branch() == "Temple" and not rr_autosearched_temple then search_altars() rr_autosearched_temple = true end end local function c_message_temple(text, _) if you.branch() == "Temple" then -- Search again after explore if is_explore_done_msg(text) then search_altars() end end end -- Filtered search functions (Gauntlet & Necropolis) local function search_filtered(branch) local cmd_key = BRC.util.get_cmd_key("CMD_SEARCH_STASHES") or BRC.util.cntl("f") crawl.sendkeys({ cmd_key, SEARCH_STRING[branch], "\r" }) crawl.flush_input() end --- Autosearch Gauntlet upon entry local function ready_gauntlet_search() local branch = you.branch() if branch == "Gauntlet" and not autosearched_gauntlet then search_filtered(branch) autosearched_gauntlet = true end end --- Autosearch Necropolis upon entry local function ready_necropolis_search() local branch = you.branch() if branch == "Necropolis" and not autosearched_necropolis then search_filtered(branch) autosearched_necropolis = true end end local function c_message_filtered_search(text, _) -- Search again after explore local branch = you.branch() if is_explore_done_msg(text) and ( C.necropolis_search and branch == "Necropolis" or C.gauntlet_search and branch == "Gauntlet" ) then search_filtered(branch) end end -- Portal exit functions local function ready_ignore_portals() local in_portal = util.contains(BRC.PORTAL_FEATURE_NAMES, you.branch()) if stop_on_portals and in_portal then stop_on_portals = false BRC.opt.explore_stop("portals", false) elseif not stop_on_portals and not in_portal then stop_on_portals = true BRC.opt.explore_stop("portals", true) end end -- After shaft functions local function c_message_after_shaft(text, channel) if channel ~= "plain" or rr_shaft_location then return end if text:find("ou .* into a shaft") and not BRC.you.in_hell(true) then rr_shaft_location = you.where() end end local function ready_after_shaft() if you.where() == rr_shaft_location then rr_shaft_location = nil end end ---- Crawl hook functions ---- function f_runrest_features.c_message(...) if C.temple_search then c_message_temple(...) end if C.gauntlet_search or C.necropolis_search then c_message_filtered_search(...) end if C.after_shaft then c_message_after_shaft(...) end end function f_runrest_features.ready() if C.ignore_altars then ready_ignore_altars() end if C.ignore_portal_exits then ready_ignore_portals() end if C.temple_search then ready_temple_search() end if C.gauntlet_search then ready_gauntlet_search() end if C.necropolis_search then ready_necropolis_search() end if C.after_shaft then ready_after_shaft() end set_stairs_stop_state() end } ############################## End lua/features/runrest-features.lua ############################## ################################################################################################### ############################ Begin lua/features/manage-consumables.lua ############################ ###### https://github.com/brianfaires/crawl-rc/blob/main/lua/features/manage-consumables.lua ###### { --------------------------------------------------------------------------------------------------- -- BRC feature module: manage-consumables -- @module f_manage_consumables -- Features for consumable management. Same as crawl's built-in options, without gaps in coverage. -- safe_scrolls / safe_potions: !r and !q inscriptions. A more consistent version of autoinscribe. -- slots: A more consistent version of crawl's item_slot option. --------------------------------------------------------------------------------------------------- f_manage_consumables = {} f_manage_consumables.BRC_FEATURE_NAME = "manage-consumables" f_manage_consumables.Config = { maintain_safe_scrolls = true, maintain_safe_potions = true, scroll_slots = { ["acquirement"] = "A", ["amnesia"] = "x", ["blinking"] = "B", ["brand weapon"] = "W", ["butterflies"] = "s", ["enchant armour"] = "a", ["enchant weapon"] = "w", ["fear"] = "f", ["fog"] = "g", ["identify"] = "i", ["immolation"] = "I", ["noise"] = "N", ["revelation"] = "r", ["poison"] = "p", ["silence"] = "S", ["summoning"] = "s", ["teleportation"] = "t", ["torment"] = "T", ["vulnerability"] = "V", }, potion_slots = { ["ambrosia"] = "a", ["attraction"] = "A", ["berserk rage"] = "B", ["brilliance"] = "b", ["cancellation"] = "C", ["curing"] = "c", ["experience"] = "E", ["enlightenment"] = "e", ["haste"] = "h", ["heal wounds"] = "w", ["invisibility"] = "i", ["lignification"] = "L", ["magic"] = "g", ["might"] = "z", ["resistance"] = "r", ["mutation"] = "M", }, } -- f_manage_consumables.Config ---- Local constants ---- local NO_INSCRIPTION_NEEDED = { "acquirement", "amnesia", "blinking", "brand weapon", "enchant armour", "enchant weapon", "identify", "immolation", "noise", "vulnerability", "attraction", "lignification", "mutation", } -- NO_INSCRIPTION_NEEDED (do not remove this comment) local SCROLL_CLASS = "scroll" local POTION_CLASS = "potion" local SCROLL_INSCR = "!r" local POTION_INSCR = "!q" local SCROLL_PATT = "%!r" local POTION_PATT = "%!q" ---- Local variables ---- local C -- config alias local found_scroll local found_potion ---- Initialization ---- function f_manage_consumables.init() C = f_manage_consumables.Config C.scroll_slots = C.scroll_slots or {} C.potion_slots = C.potion_slots or {} found_scroll = nil found_potion = nil -- These options must use += to override crawl defaults for st, slot in pairs(C.scroll_slots) do crawl.setopt("consumable_shortcut += scroll of " .. st .. ":" .. slot) end for st, slot in pairs(C.potion_slots) do --crawl.setopt("consumable_shortcut ^= potion of " .. st .. ":" .. slot) crawl.setopt("consumable_shortcut += potion of " .. st .. ":" .. slot) end end ---- Local functions ---- local function potion_needs_inscription(st) return not util.contains(NO_INSCRIPTION_NEEDED, st) end local function scroll_needs_inscription(st) if util.contains(NO_INSCRIPTION_NEEDED, st) then return false end if st == "poison" then return you.res_poison() > 0 end if st == "torment" then return you.torment_immune() end return true end local function change_slot(old_slot, new_slot, name, class) if old_slot == new_slot then return end BRC.mpr.lightgreen(" " .. BRC.txt.lightgrey(old_slot .. " -> ") .. new_slot .. " - " .. name) BRC.opt.single_turn_mute("Adjust") BRC.opt.single_turn_mute(" - ") local class_key = class == SCROLL_CLASS and "r" or "p" crawl.sendkeys("=" .. class_key .. old_slot .. new_slot) end local function maintain_slots() if found_scroll then local new_slot = C.scroll_slots[found_scroll] if new_slot then for _, inv in ipairs(items.inventory()) do if inv.class(true) == SCROLL_CLASS and inv.subtype() == found_scroll then change_slot(items.index_to_letter(inv.slot), new_slot, inv.name(), SCROLL_CLASS) break end end end found_scroll = nil elseif found_potion then local new_slot = C.potion_slots[found_potion] if new_slot then for _, inv in ipairs(items.inventory()) do if inv.class(true) == POTION_CLASS and inv.subtype() == found_potion then change_slot(items.index_to_letter(inv.slot), new_slot, inv.name(), POTION_CLASS) break end end end found_potion = nil end end local function maintain_inscriptions() if not (C.maintain_safe_scrolls or C.maintain_safe_potions) then return end for _, inv in ipairs(items.inventory()) do local inv_class = inv.class(true) if inv_class == SCROLL_CLASS and C.maintain_safe_scrolls then if scroll_needs_inscription(inv.subtype()) then if not inv.inscription:contains(SCROLL_INSCR) then inv.inscribe(SCROLL_INSCR) end elseif inv.inscription:contains(SCROLL_INSCR) then inv.inscribe(inv.inscription:gsub(SCROLL_PATT, ""), false) end elseif inv_class == POTION_CLASS and C.maintain_safe_potions then if potion_needs_inscription(inv.subtype()) then if not inv.inscription:contains(POTION_INSCR) then inv.inscribe(POTION_INSCR) end elseif inv.inscription:contains(POTION_INSCR) then inv.inscribe(inv.inscription:gsub(POTION_PATT, ""), false) end end end end ---- Crawl hook functions ---- function f_manage_consumables.c_message(text, _) if next(C.scroll_slots) then local _, last = text:find(" .[^s]?s a scroll of ") if last then found_scroll = text:sub(last + 1, #text - 1) return end end if next(C.potion_slots) then local _, last = text:find(" .[^s]?s a potion of ") if last then found_potion = text:sub(last + 1, #text - 1) return end end end function f_manage_consumables.ready() maintain_slots() maintain_inscriptions() end } ############################# End lua/features/manage-consumables.lua ############################# ################################################################################################### ################################ Begin lua/features/safe-stairs.lua ############################### ########## https://github.com/brianfaires/crawl-rc/blob/main/lua/features/safe-stairs.lua ######### { --------------------------------------------------------------------------------------------------- -- BRC feature module: safe-stairs -- @module f_safe_stairs -- @author rypofalem (V:5 warning idea), buehler -- Prevent accidental stairs use and warn for Vaults:5 entry. --------------------------------------------------------------------------------------------------- f_safe_stairs = {} f_safe_stairs.BRC_FEATURE_NAME = "safe-stairs" f_safe_stairs.Config = { warn_backtracking = true, -- Warn if immediately taking stairs twice in a row warn_v5 = true, -- Prompt before entering Vaults:5 } -- f_safe_stairs.Config (do not remove this comment) ---- Persistent variables ---- ss_prev_location = BRC.Data.persist("ss_prev_location", you.where()) ss_v5_warned = BRC.Data.persist("ss_v5_warned", false) ---- Local variables ---- local C -- config alias local ss_cur_location ---- Initialization ---- function f_safe_stairs.init() C = f_safe_stairs.Config ss_cur_location = you.where() BRC.opt.macro(BRC.util.get_cmd_key("CMD_GO_DOWNSTAIRS") or ">", "macro_brc_downstairs") BRC.opt.macro(BRC.util.get_cmd_key("CMD_GO_UPSTAIRS") or "<", "macro_brc_upstairs") end ---- Local functions ---- local function check_new_location(cmd) local feature = view.feature_at(0, 0) if C.warn_backtracking and ss_prev_location ~= ss_cur_location then if cmd == "CMD_GO_DOWNSTAIRS" and (feature:contains("down") or feature:contains("shaft")) or cmd == "CMD_GO_UPSTAIRS" and feature:contains("up") then if not BRC.mpr.yesno("Really go right back?") then return BRC.mpr.okay() end end end if C.warn_v5 and not ss_v5_warned and ss_cur_location == "Vaults:4" and cmd == "CMD_GO_DOWNSTAIRS" and (feature:contains("down") or feature:contains("shaft")) then if not BRC.mpr.yesno("Really go to Vaults:5?") then return BRC.mpr.okay() end ss_v5_warned = true end BRC.util.do_cmd(cmd) end ---- Macro functions ---- function macro_brc_downstairs() if BRC.active == false or f_safe_stairs.Config.disabled then BRC.util.do_cmd("CMD_GO_DOWNSTAIRS") else check_new_location("CMD_GO_DOWNSTAIRS") end end function macro_brc_upstairs() if BRC.active == false or f_safe_stairs.Config.disabled then BRC.util.do_cmd("CMD_GO_UPSTAIRS") else check_new_location("CMD_GO_UPSTAIRS") end end ---- Crawl hook functions ---- function f_safe_stairs.ready() ss_prev_location = ss_cur_location ss_cur_location = you.where() end } ################################# End lua/features/safe-stairs.lua ################################ ################################################################################################### ################################## Begin lua/features/startup.lua ################################# ############ https://github.com/brianfaires/crawl-rc/blob/main/lua/features/startup.lua ########### { --------------------------------------------------------------------------------------------------- -- BRC feature module: startup -- @module f_startup -- @author rwbarton (display skills menu), gammafunk (training targets), buehler -- Handles startup actions, like displaying skills menu and auto-setting skill targets. --------------------------------------------------------------------------------------------------- f_startup = {} f_startup.BRC_FEATURE_NAME = "startup" f_startup.Config = { -- Save current training targets and config, for race/class macro_save_key = BRC.util.cntl("t"), -- (Cntl-T) Keycode to save training targets and config save_training = true, -- Allow save/load of race/class training targets save_config = true, -- Allow save/load of BRC config prompt_before_load = false, -- Prompt before loading in a new game with same race+class allow_race_only_saves = false, -- Also save for race only (always prompts before loading) allow_class_only_saves = false, -- Also save for class only (always prompts before loading) -- Remaining values only used if no training targets were loaded by race/class show_skills_menu = false, -- Show skills menu on startup -- Settings to set skill targets, regardless of race/class set_all_targets = true, -- Set all targets, even if only focusing one focus_one_skill = true, -- Focus one skill at a time, even if setting all targets auto_set_skill_targets = { { "Stealth", 2.0 }, -- First, focus stealth to 2.0 { "Fighting", 2.0 }, -- If already have stealth, focus fighting to 2.0 }, -- For non-spellcasters, add preferred weapon type as 3rd skill target init = function() if you.skill("Spellcasting") == 0 then local wpn_skill = BRC.you.top_wpn_skill() if wpn_skill then local t = f_startup.Config.auto_set_skill_targets t[#t + 1] = { wpn_skill, 6.0 } end end end, } -- f_startup.Config (do not remove this comment) ---- Local variables ---- local C -- config alias ---- Initialization ---- function f_startup.init() C = f_startup.Config if C.macro_save_key and (C.save_training or C.save_config) then BRC.opt.macro(C.macro_save_key, "macro_brc_save_skills_and_config") end end ---- Local functions ---- local function ensure_tables_exist() if type(c_persist.BRC) ~= "table" then c_persist.BRC = {} end if type(c_persist.BRC.saved_training) ~= "table" then c_persist.BRC.saved_training = {} end if type(c_persist.BRC.saved_configs) ~= "table" then c_persist.BRC.saved_configs = {} end end local function clear_skill_targets() for _, s in ipairs(BRC.TRAINING_SKILLS) do you.train_skill(s, 0) end end local function create_skill_table() local skill_table = {} for _, skill_name in ipairs(BRC.TRAINING_SKILLS) do local training_level = you.train_skill(skill_name) local target = you.get_training_target(skill_name) if training_level > 0 or target > 0 then skill_table[skill_name] = { training_level = training_level, target = target, } end end return skill_table end local function apply_skill_table(skill_table) clear_skill_targets() for skill_name, data in pairs(skill_table) do you.train_skill(skill_name, data.training_level) you.set_training_target(skill_name, data.target) end end local function load_training_targets(key, require_confirmation) local saved = c_persist.BRC.saved_training[key] if type(saved) ~= "table" then return false end if require_confirmation and not BRC.mpr.yesno("Load training targets for " .. BRC.txt.lightcyan(key) .. "?") then BRC.mpr.okay() return false end apply_skill_table(saved) BRC.mpr.green("Loaded training targets for " .. BRC.txt.lightcyan(key)) return true end local function load_saved_training_targets() ensure_tables_exist() return load_training_targets(you.race() .. " " .. you.class(), C.prompt_before_load) or (C.allow_race_only_saves and load_training_targets(you.race(), true)) or (C.allow_class_only_saves and load_training_targets(you.class(), true)) end local function load_config(key, require_confirmation) local saved = c_persist.BRC.saved_configs[key] if type(saved) ~= "string" then return false end if require_confirmation and not BRC.mpr.yesno("Load config for " .. BRC.txt.lightcyan(key) .. "?") then BRC.mpr.okay() return false end return BRC.init(saved) end local function load_saved_config() ensure_tables_exist() return load_config(you.race() .. " " .. you.class(), C.prompt_before_load) or (C.allow_race_only_saves and load_config(you.race(), true)) or (C.allow_class_only_saves and load_config(you.class(), true)) end --- Save obj to storage_table, under keys: race/class/combo local function save_race_class(desc, parent, child) local keys = { } keys[1] = you.race() .. " " .. you.class() -- Always save combo if C.allow_race_only_saves then keys[#keys + 1] = you.race() end if C.allow_class_only_saves then keys[#keys + 1] = you.class() end for i, key in ipairs(keys) do if i == 1 -- don't prompt for combo or not parent[key] -- don't prompt if empty or BRC.mpr.yesno(string.format("Overwrite saved %s for %s?", desc, BRC.txt.lightcyan(key))) then parent[key] = type(child) == "table" and util.copy_table(child) or child BRC.mpr.green(string.format("Saved %s for %s", desc, BRC.txt.lightcyan(key))) end end end --- Load configured skill targets, not saved by race/class in c_persist local function load_generic_skill_targets() clear_skill_targets() local set_first = false for _, skill_target in ipairs(C.auto_set_skill_targets) do local skill, target = table.unpack(skill_target) if you.skill(skill) < target then you.set_training_target(skill, target) if not set_first or not C.focus_one_skill then you.train_skill(skill, 1) set_first = true end if not C.set_all_targets then break end end end end ---- Macro function: Save current skill targets (training levels and targets) for race/class ---- function macro_brc_save_skills_and_config() if BRC.active == false or f_startup.Config.disabled then BRC.mpr.info("BRC not active, or startup feature is disabled. Training targets not saved.") return end ensure_tables_exist() if f_startup.Config.save_training and you.race() ~= "Gnoll" then local do_save = not f_startup.Config.save_config if not do_save then do_save = BRC.mpr.yesno("Save training + targets?", BRC.COL.magenta) if not do_save then BRC.mpr.okay() end end if do_save then save_race_class("training targets", c_persist.BRC.saved_training, create_skill_table()) end end if f_startup.Config.save_config then local do_save = not f_startup.Config.save_training if not do_save then do_save = BRC.mpr.yesno("Save config?", BRC.COL.magenta) if not do_save then crawl.mpr.okay() end end if do_save then save_race_class("config", c_persist.BRC.saved_configs, brc_config_name) end end end ---- Crawl hook functions ---- function f_startup.ready() if you.turns() ~= 0 then return end -- Check for saved config/targets in c_persist if C.save_config then load_saved_config() end if C.save_training and you.race() ~= "Gnoll" and you.class() ~= "Wanderer" then if load_saved_training_targets() then return end end -- If no saved targets were loaded, use other configured skill targets if C.auto_set_skill_targets and you.race() ~= "Gnoll" then load_generic_skill_targets() end -- Show skills menu: Disable for non-Wanderer Gnolls if C.show_skills_menu and (you.race() ~= "Gnoll" or you.class() == "Wanderer") then BRC.util.do_cmd("CMD_DISPLAY_SKILLS") end end } ################################### End lua/features/startup.lua ################################## ################################################################################################### ############################### Begin lua/features/weapon-slots.lua ############################### ######### https://github.com/brianfaires/crawl-rc/blob/main/lua/features/weapon-slots.lua ######### { --------------------------------------------------------------------------------------------------- -- BRC feature module: weapon-slots -- @module f_weapon_slots -- Automatically keeps weapons in slots a/b/w. Prioritizes slots by weapon type + skill. --------------------------------------------------------------------------------------------------- f_weapon_slots = {} f_weapon_slots.BRC_FEATURE_NAME = "weapon-slots" ---- 0.34/0.35 compatibility ---- local SWAP_SLOTS = items.swap_gear_slots or items.swap_slots ---- Local variables ---- local do_cleanup_weapon_slots local slots_changed local priorities_ab local priorities_w ---- Initialization ---- function f_weapon_slots.init() do_cleanup_weapon_slots = false slots_changed = false priorities_ab = nil priorities_w = nil end ---- Local functions ---- local function get_first_empty_slot() -- First try to avoid same slot as a consumable, then find first empty equipment slot local used_slots = {} for _, inv in ipairs(items.inventory()) do used_slots[inv.slot] = true end for slot = 0, 51 do if not used_slots[slot] then return slot end end for slot = 0, 51 do if not items.inslot(slot) then return slot end end end local function get_priority_ab(it) if not it.is_weapon then return -1 end if it.equipped then return 1 end if BRC.it.is_magic_staff(it) then return 3 end if it.is_ranged then return (you.skill("Ranged Weapons") >= 4) and 2 or 5 end if BRC.it.is_polearm(it) then return (you.skill("Polearms") >= 4) and 2 or 4 end return 2 end local function get_priority_w(it) if not it.is_weapon then return -1 end if it.is_ranged then return 1 end if BRC.it.is_polearm(it) then return 2 end if BRC.it.is_magic_staff(it) then return 3 end return 4 end local function generate_priorities() priorities_ab = { -1, -1, -1, -1, -1 } priorities_w = { -1, -1, -1, -1 } for _, inv in ipairs(items.inventory()) do local p = get_priority_w(inv) if p > 0 then if priorities_w[p] == -1 then priorities_w[p] = inv.slot else priorities_w[p + 1] = inv.slot end end p = get_priority_ab(inv) if p > 0 then if priorities_ab[p] == -1 then priorities_ab[p] = inv.slot else priorities_ab[p + 1] = inv.slot end end end end local function cleanup_ab(slot) local inv = items.inslot(slot) if inv and inv.is_weapon then return end for p = 1, #priorities_ab do if priorities_ab[p] > slot then -- Not from earlier slot SWAP_SLOTS(priorities_ab[p], slot) slots_changed = true priorities_ab[p] = -1 return end end end local function cleanup_w() local slot_w = items.letter_to_index("w") local inv = items.inslot(slot_w) if inv and inv.is_weapon then return end for p = 1, #priorities_w do if priorities_w[p] > 1 then -- Not from slots a or b SWAP_SLOTS(priorities_w[p], slot_w) slots_changed = true return end end end local function cleanup_weapon_slots() generate_priorities() cleanup_ab(0) cleanup_ab(1) cleanup_w() end ---- Crawl hook functions ---- function f_weapon_slots.c_assign_invletter(it) if not it.is_weapon then return end for _, s in ipairs({ "a", "b", "w" }) do local slot = items.letter_to_index(s) local inv = items.inslot(slot) if not inv then return slot end if not inv.is_weapon then SWAP_SLOTS(slot, get_first_empty_slot()) slots_changed = true return slot end end end function f_weapon_slots.c_message(text, channel) do_cleanup_weapon_slots = channel == "plain" and text:contains("ou drop ") end function f_weapon_slots.ready() if do_cleanup_weapon_slots then cleanup_weapon_slots() do_cleanup_weapon_slots = false end if slots_changed then BRC.mpr.debug("Weapon slots updated (ab+w).") slots_changed = false end end } ################################ End lua/features/weapon-slots.lua ################################ ################################################################################################### ### Pickup and alert ### ########################## Begin lua/features/pickup-alert/pa-config.lua ########################## #### https://github.com/brianfaires/crawl-rc/blob/main/lua/features/pickup-alert/pa-config.lua #### { --------------------------------------------------------------------------------------------------- -- BRC feature module: pickup-alert-config -- @submodule f_pickup_alert.Config -- Configuration and in-depth tuning heuristics for the pickup-alert feature. --------------------------------------------------------------------------------------------------- f_pickup_alert = f_pickup_alert or {} f_pickup_alert.Config = {} f_pickup_alert.Config.Pickup = { armour = true, weapons = true, weapons_pure_upgrades_only = true, -- Only pick up better versions of same exact weapon staves = true, } -- f_pickup_alert.Config.Pickup (do not remove this comment) f_pickup_alert.Config.Alert = { armour_sensitivity = 1.0, -- [0.5-2.0] Adjust all armour alerts; 0 to disable all weapon_sensitivity = 1.0, -- [0.5-2.0] Adjust all weapon alerts; 0 to disable all orbs = true, staff_resists = true, talismans = true, first_ranged = true, first_polearm = true, stacked_items = true, -- Special handling for items hidden in stacks, to alert before visiting -- Each usable item is alerted once. one_time = { "wand of digging", "buckler", "kite shield", "tower shield", "crystal plate armour", "gold dragon scales", "pearl dragon scales", "storm dragon scales", "shadow dragon scales", "quick blade", "demon blade", "eudemon blade", "double sword", "triple sword", "broad axe", "executioner's axe", "demon whip", "eveningstar", "giant spiked club", "morningstar", "sacred scourge", "lajatang", "bardiche", "demon trident", "partisan", "trishula", "hand cannon", "triple crossbow", }, -- Only do one-time alerts if your skill >= this value, in weap_school/armour/shield OTA_require_skill = { weapon = 2, armour = 2.5, shield = 0 }, hotkey_travel = true, hotkey_pickup = true, allow_arte_weap_upgrades = true, -- If false, won't alert weapons as upgrades to an artefact -- Only alert a plain talisman if its min_skill <= Shapeshifting + talisman_lvl_diff talisman_lvl_diff = you.class() == "Shapeshifter" and 27 or 6, -- Which alerts generate a force_more More = { early_weap = false, -- Good weapons found early upgrade_weap = false, -- Better DPS / weapon_score weap_ego = false, -- New or diff egos body_armour = false, shields = true, aux_armour = false, armour_ego = true, -- New or diff egos high_score_weap = false, -- Highest damage found high_score_armour = true, -- Highest AC found one_time_alerts = true, artefact = false, -- Any artefact trained_artefacts = true, -- Artefacts where you have corresponding skill > 0 orbs = false, -- Unique orbs talismans = you.class() == "Shapeshifter", -- True for shapeshifter, false for everyone else staff_resists = false, -- When a staff gives a missing resistance autopickup_disabled = true, -- Alerts for autopickup items, when autopickup is disabled }, } -- f_pickup_alert.Config.Alert (do not remove this comment) ---- Heuristics for tuning the pickup/alert system. Advanced behavior customization. f_pickup_alert.Config.Tuning = {} --[[ f_pickup_alert.Config.Tuning.Armour: Magic numbers for the armour pickup/alert system. For armour with different encumbrance, alert when ratio of gain/loss (AC|EV) is > value Lower values mean more alerts. gain/diff/same/lose refers to egos. min_gain/max_loss block alerts for new egos, when AC or EV delta is outside limits ignore_small: if abs(AC+EV) <= this, ignore ratios and alert any gain/diff ego --]] f_pickup_alert.Config.Tuning.Armour = { Lighter = { gain_ego = 0.6, new_ego = 0.7, diff_ego = 0.9, same_ego = 1.2, lost_ego = 2.0, min_gain = 3.0, max_loss = 4.0, ignore_small = 3.5, }, Heavier = { gain_ego = 0.4, new_ego = 0.5, diff_ego = 0.6, same_ego = 0.7, lost_ego = 2.0, min_gain = 3.0, max_loss = 8.0, ignore_small = 5, }, encumb_penalty_weight = 0.7, -- [0-2.0] Penalty to heavy armour when training magic/ranged early_xl = 6, -- Alert any usable runed body armour when XL <= `early_xl` diff_body_ego_is_good = false, -- More body_armour alerts for diff_ego (no min_gain check) } -- f_pickup_alert.Config.Tuning.Armour (do not remove this comment) --[[ f_pickup_alert.Config.Tuning.Weap: Magic numbers for the weapon pickup/alert system, namely: 1. Cutoffs for pickup/alert weapons (when DPS ratio exceeds a value) 2. Cutoffs for when alerts are active (XL, skill_level) Pickup/alert system will try to upgrade ANY weapon in your inventory. "DPS ratio" is (new_weapon_score / inventory_weapon_score). Score considers DPS/brand/accuracy. --]] f_pickup_alert.Config.Tuning.Weap = {} f_pickup_alert.Config.Tuning.Weap.Pickup = { add_ego = 1.0, -- Pickup weapon that gains a brand if DPS ratio > add_ego same_type_melee = 1.2, -- Pickup melee weap of same school if DPS ratio > same_type_melee same_type_ranged = 1.1, -- Pickup ranged weap if DPS ratio > same_type_ranged accuracy_weight = 0.25, -- Treat +1 Accuracy as +accuracy_weight DPS } -- f_pickup_alert.Config.Tuning.Weap.Pickup (do not remove this comment) f_pickup_alert.Config.Tuning.Weap.Alert = { -- Alerts for weapons not requiring an extra hand pure_dps = 1.0, -- Alert if DPS ratio > pure_dps gain_ego = 0.8, -- Gaining ego; Alert if DPS ratio > gain_ego new_ego = 0.8, -- Get ego not in inventory; Alert if DPS ratio > new_ego low_skill_penalty_damping = 8, -- [0-20] Reduce penalty to lower-trained weapons -- Alerts for 2-handed weapons, when carrying 1-handed AddHand = { ignore_sh_lvl = 4.0, -- Treat offhand as empty if shield_skill < ignore_sh_lvl add_ego_lose_sh = 0.8, -- Alert 1h -> 2h (using shield) if DPS ratio > add_ego_lose_sh not_using = 1.0, -- Alert 1h -> 2h (not using 2nd hand) if DPS ratio > not_using }, -- Alerts for good early weapons of all types Early = { xl = 7, -- Alert early weapons if XL <= xl skill = { factor = 1.5, offset = 2.0 }, -- Ignore weapons w skill_diff > XL*fact+offset branded_min_plus = 4, -- Alert branded weapons with plus >= branded_min_plus }, -- Alerts for particularly strong ranged weapons EarlyRanged = { xl = 14, -- Alert strong ranged weapons if XL <= xl min_plus = 7, -- Alert ranged weapons with plus >= min_plus branded_min_plus = 4, -- Alert branded ranged weapons with plus >= branded_min_plus max_shields = 8.0, -- Require max_shields skill to block 2h ranged alerts }, } -- f_pickup_alert.Config.Tuning.Weap.Alert (do not remove this comment) f_pickup_alert.Config.AlertColor = { weapon = { desc = BRC.COL.magenta, item = BRC.COL.yellow, stats = BRC.COL.lightgrey }, body_arm = { desc = BRC.COL.lightblue, item = BRC.COL.lightcyan, stats = BRC.COL.lightgrey }, aux_arm = { desc = BRC.COL.lightblue, item = BRC.COL.yellow }, orb = { desc = BRC.COL.green, item = BRC.COL.lightgreen }, talisman = { desc = BRC.COL.green, item = BRC.COL.lightgreen }, misc = { desc = BRC.COL.brown, item = BRC.COL.white }, } -- f_pickup_alert.Config.AlertColor (do not remove this comment) f_pickup_alert.Config.Emoji = { RARE_ITEM = "💎", ARTEFACT = "💠", ORB = "🔮", TALISMAN = "🧬", STAFF_RES = "🔥", WEAPON = "⚔️", RANGED = "🏹", POLEARM = "🔱", TWO_HAND = "✋🤚", EGO = "✨", ACCURACY = "🎯", STRONGER = "💪", STRONGEST = "💪💪", LIGHTER = "⏬", HEAVIER = "⏫", AUTOPICKUP_ITEM = "👍", } -- f_pickup_alert.Config.Emoji (do not remove this comment) f_pickup_alert.Config.init = function() if not BRC.Config.emojis then f_pickup_alert.Config.Emoji = {} end end } ########################### End lua/features/pickup-alert/pa-config.lua ########################### ################################################################################################### ########################### Begin lua/features/pickup-alert/pa-main.lua ########################### ##### https://github.com/brianfaires/crawl-rc/blob/main/lua/features/pickup-alert/pa-main.lua ##### { --------------------------------------------------------------------------------------------------- -- BRC feature module: pickup-alert -- @module f_pickup_alert -- Comprehensive pickup and alert system for weapons, armour, and miscellaneous items. -- Several submodules: pa-config, pa-data, pa-armour, pa-weapons, pa-misc. --------------------------------------------------------------------------------------------------- f_pickup_alert = f_pickup_alert or {} f_pickup_alert.BRC_FEATURE_NAME = "pickup-alert" ---- Local variables ---- local C -- config alias local A -- alert config alias local M -- more config alias local pause_pa_system local hold_alerts_for_next_turn local pa_last_ready_turn local function_queue -- queue of actions for next ready() local marked_stacks local last_stack_check_turn ---- Initialization ---- function f_pickup_alert.init() C = f_pickup_alert.Config A = f_pickup_alert.Config.Alert M = f_pickup_alert.Config.Alert.More pause_pa_system = false hold_alerts_for_next_turn = false pa_last_ready_turn = you.turns() function_queue = {} marked_stacks = {} last_stack_check_turn = -1 BRC.mpr.debug("Initialize pickup-alert submodules...") if f_pa_data.init then f_pa_data.init() end BRC.mpr.debug(" pa-data loaded") if f_pa_armour then if f_pa_armour.init then f_pa_armour.init() end BRC.mpr.debug(" pa-armour loaded") end if f_pa_weapons then if f_pa_weapons.init then f_pa_weapons.init() end BRC.mpr.debug(" pa-weapons loaded") end if f_pa_misc then if f_pa_misc.init then f_pa_misc.init() end BRC.mpr.debug(" pa-misc loaded") end -- Don't alert for starting items for _, inv in ipairs(items.inventory()) do f_pa_data.remember_alert(inv) f_pa_data.remove_OTA(inv) end end ---- Local functions ---- local function has_configured_force_more(it) if it.artefact then if M.artefact then return true end if M.trained_artefacts then -- Accept artefacts with any relevant training, or no training required local s = BRC.you.skill_with(it) if s == nil or s > 0 then return true end end end return M.armour_ego and BRC.it.is_armour(it) and BRC.eq.get_ego(it) end local function track_unique_egos(it) local ego = BRC.eq.get_ego(it) if ego and not util.contains(pa_egos_alerted, ego) and not (it.artefact and BRC.eq.is_risky(it)) then pa_egos_alerted[#pa_egos_alerted+1] = ego end end local function get_alert_color_for_item(it) if it.is_weapon then return C.AlertColor.weapon end if BRC.it.is_orb(it) then return C.AlertColor.orb end if BRC.it.is_talisman(it) then return C.AlertColor.talisman end if BRC.it.is_body_armour(it) then return C.AlertColor.body_arm end if BRC.it.is_armour(it) then return C.AlertColor.aux_arm end return C.AlertColor.misc end local function should_skip_pickup_check(it) return BRC.active == false or pause_pa_system or you.have_orb() or (not it.is_identified and (it.branded or it.artefact or BRC.it.is_magic_staff(it))) end local function check_and_trigger_alerts(it, unworn_aux_item) if f_pa_data.already_alerted(it) then return true end -- One-time alerts if f_pa_misc and A.one_time and #A.one_time > 0 then if f_pa_misc.alert_OTA(it) then return true end end -- Item-specific alerts if BRC.it.is_magic_staff(it) and f_pa_misc and A.staff_resists then if f_pa_misc.alert_staff(it) then return true end elseif BRC.it.is_orb(it) and f_pa_misc and A.orbs then if f_pa_misc.alert_orb(it) then return true end elseif BRC.it.is_talisman(it) and f_pa_misc and A.talismans then if f_pa_misc.alert_talisman(it) then return true end elseif BRC.it.is_armour(it) and f_pa_armour and A.armour_sensitivity > 0 then if f_pa_armour.alert_armour(it, unworn_aux_item) then return true end elseif it.is_weapon and f_pa_weapons and A.weapon_sensitivity > 0 then if f_pa_weapons.alert_weapon(it) then return true end end return false end --- Run autopickup for all items in view, even those hidden in an item stack. -- Pickup-alert system runs as an autopickup function, which only triggers for stacked items when: -- 1. The stack is visited, 2. autopickup is on. -- This hiding behavior is very annoying when not autoexploring, ie always for turncount runs. -- This function causes alerts to fire without visiting the stack. No impact on autoexplore. -- Also tracks which stacks these are, so we can trick the UI into highlighting them as autopickup. local function mark_stacked_items() marked_stacks = {} local unmarked_item_counts = {} local r = you.los() for x = -r, r do for y = -r, r do if you.see_cell(x, y) then local items_xy = items.get_items_at(x, y) if items_xy then local top_item_name = items_xy[1].name() unmarked_item_counts[top_item_name] = (unmarked_item_counts[top_item_name] or 0) + 1 if #items_xy > 1 then for i, it in ipairs(items_xy) do if i > 1 and f_pickup_alert.autopickup(it) then marked_stacks[#marked_stacks + 1] = {x, y, top_item_name, it.name()} unmarked_item_counts[top_item_name] = unmarked_item_counts[top_item_name] - 1 if not f_pa_data.already_alerted(it) then f_pickup_alert.do_alert( it, "Hidden under stack", C.Emoji.AUTOPICKUP_ITEM, M.autopickup_disabled ) end end end end end end end end -- In autopickup, there's no way to differentiate between items of the same name. -- Can't get its coordinates, can't see what's underneath it, etc. -- Choosing to not mark stacks w duplicated item names, rather than mark all items for autopickup for i = #marked_stacks, 1, -1 do if unmarked_item_counts[marked_stacks[i][3]] > 0 then table.remove(marked_stacks, i) end end end --- This is used to trick the UI into highlighting a stack for autopickup. -- Since this is called from autopickup(), there's no way to differentiate items of the same name. local function is_top_of_marked_stack(it) if last_stack_check_turn < you.turns() then last_stack_check_turn = you.turns() mark_stacked_items() end for _, stack in ipairs(marked_stacks) do local stack_items = items.get_items_at(stack[1], stack[2]) if not stack_items or stack_items[1].name() ~= stack[3] then -- Stack coordinates are stale mark_stacked_items() return is_top_of_marked_stack(it) end if it.name() == stack[3] then for i = 2, #stack_items do if stack_items[i].name() == stack[4] then return true end end end end return false end ---- Public API ---- function f_pickup_alert.pause_alerts() hold_alerts_for_next_turn = true end function f_pickup_alert.is_paused() return pause_pa_system or hold_alerts_for_next_turn end function f_pickup_alert.do_alert(it, alert_type, emoji, force_more) local item_name = f_pa_data.get_keyname(it, true) local alert_col = get_alert_color_for_item(it) -- Handle special formatting for weapons and body armour if it.is_weapon then f_pa_data.update_high_scores(it) local weapon_info = string.format(" (%s)", BRC.eq.wpn_stats(it)) item_name = item_name .. BRC.txt[C.AlertColor.weapon.stats](weapon_info) elseif BRC.it.is_armour(it) then track_unique_egos(it) if BRC.it.is_body_armour(it) then f_pa_data.update_high_scores(it) local ac, ev = BRC.eq.arm_stats(it) local armour_info = string.format(" {%s, %s}", ac, ev) item_name = item_name .. BRC.txt[C.AlertColor.body_arm.stats](armour_info) end end local tokens = {} tokens[1] = emoji and emoji or BRC.txt.cyan("----") tokens[#tokens + 1] = BRC.txt[alert_col.desc](string.format(" %s:", alert_type)) tokens[#tokens + 1] = BRC.txt[alert_col.item](string.format(" %s ", item_name)) tokens[#tokens + 1] = tokens[1] BRC.mpr.que_optmore(force_more or has_configured_force_more(it), table.concat(tokens)) f_pa_data.add_recent_alert(it) f_pa_data.remember_alert(it) if not hold_alerts_for_next_turn then you.stop_activity() end local it_name = it.name() function_queue[#function_queue + 1] = function() -- Set hotkeys (on next turn, so player position is updated before setting waypoint) if util.exists(you.floor_items(), function(fl) return fl.name() == it_name end) then if A.hotkey_pickup and BRC.Hotkey then BRC.Hotkey.pickup(it_name, true) end else if A.hotkey_travel and BRC.Hotkey then BRC.Hotkey.move_to_item(it_name, false, A.hotkey_pickup) end end end return true end ---- Crawl hook functions ---- function f_pickup_alert.autopickup(it, _) if A.stacked_items and is_top_of_marked_stack(it) then -- Fake autopickup to highlight the stack. Don't actually pick it up! local fl = you.floor_items() if not fl or #fl <= 1 or fl[1].name() ~= it.name() then return true end end if should_skip_pickup_check(it) then return end local unworn_aux_item = nil -- Track carried aux armour for mutation scenarios if it.is_useless then -- Allow alerts for useless aux armour, iff you're carrying one (implies a temporary mutation) if not BRC.it.is_aux_armour(it) then return end local st = it.subtype() for _, inv in ipairs(items.inventory()) do if inv.subtype() == st then unworn_aux_item = inv break end end if not unworn_aux_item then return end else if BRC.it.is_armour(it) then if C.Pickup.armour and f_pa_armour.pickup_armour(it) then return true end elseif BRC.it.is_magic_staff(it) then if C.Pickup.staves and f_pa_misc.pickup_staff(it) then return true end elseif it.is_weapon then if C.Pickup.weapons and f_pa_weapons.pickup_weapon(it) then return true end elseif f_pa_misc and f_pa_misc.is_unneeded_ring(it) then return false end end -- Item not picked up - check if it should trigger alerts. -- Autopickup fires many times per turn, and needs to consistently return true for pickup to work -- But, only check for alerts immediately after turncount changes, before ready() is called. if you.turns() ~= pa_last_ready_turn then check_and_trigger_alerts(it, unworn_aux_item) end end function f_pickup_alert.c_assign_invletter(it) f_pa_misc.alert_OTA(it) f_pa_data.remove_recent_alert(it) f_pa_data.remember_alert(it) -- Re-enable the alert, iff we are able to use another one if BRC.you.num_eq_slots(it) > 1 then f_pa_data.forget_alert(it) end -- Ensure we always stop for these autopickup types if it.is_weapon or BRC.it.is_armour(it) then f_pa_data.update_high_scores(it) you.stop_activity() end end function f_pickup_alert.c_message(text, channel) -- Avoid firing alerts when changing armour/weapons if channel == "multiturn" then if not pause_pa_system and text:contains("ou start ") then pause_pa_system = true end elseif channel == "plain" then if pause_pa_system and (text:contains("ou stop ") or text:contains("ou finish ")) then pause_pa_system = false elseif text:contains("one exploring") or text:contains("artly explored") then local tokens = { "Recent alerts:" } for _, v in ipairs(pa_recent_alerts) do tokens[#tokens + 1] = string.format("\n %s", v) end if #tokens > 1 then BRC.mpr.que(table.concat(tokens), BRC.COL.magenta) end pa_recent_alerts = {} end end end function f_pickup_alert.ready() hold_alerts_for_next_turn = false pa_last_ready_turn = you.turns() util.foreach(function_queue, function(f) f() end) function_queue = {} if pause_pa_system then return end f_pa_weapons.ready() f_pa_data.update_high_scores(items.equipped_at("armour")) end } ############################ End lua/features/pickup-alert/pa-main.lua ############################ ################################################################################################### ########################### Begin lua/features/pickup-alert/pa-data.lua ########################### ##### https://github.com/brianfaires/crawl-rc/blob/main/lua/features/pickup-alert/pa-data.lua ##### { --------------------------------------------------------------------------------------------------- -- BRC feature module: pickup-alert-data -- @submodule f_pa_data -- Persistent data management and alert tracking for the pickup-alert feature. --------------------------------------------------------------------------------------------------- f_pa_data = {} ---- Persistent variables ---- pa_items_alerted = BRC.Data.persist("pa_items_alerted", {}) pa_recent_alerts = BRC.Data.persist("pa_recent_alerts", {}) pa_OTA_items = BRC.Data.persist("pa_OTA_items", nil) pa_high_score = BRC.Data.persist("pa_high_score", { ac = 0, weapon = 0, plain_dmg = 0 }) pa_egos_alerted = BRC.Data.persist("pa_egos_alerted", {}) ---- Initialization ---- function f_pa_data.init() -- Set initial value of pa_OTA_items here, after config overrides are applied pa_OTA_items = pa_OTA_items or f_pickup_alert.Config.Alert.one_time end ---- Local functions ---- local function get_pa_keys(it, use_plain_name) if it.class(true) == "bauble" then return it.name("qual"):gsub('"', ""), 0 elseif BRC.it.is_talisman(it) or BRC.it.is_orb(it) then return it.name():gsub('"', ""), 0 elseif BRC.it.is_magic_staff(it) then return it.name("base"):gsub('"', ""), 0 else local name = it.name(use_plain_name and "plain" or "base"):gsub('"', "") local value = tonumber(name:sub(1, 3)) if not value then return name, 0 end return util.trim(name:sub(4)), value end end local function validate_high_scores() pa_high_score = pa_high_score or {} pa_high_score.ac = pa_high_score.ac or 0 pa_high_score.weapon = pa_high_score.weapon or 0 pa_high_score.plain_dmg = pa_high_score.plain_dmg or 0 end ---- Public API ---- function f_pa_data.already_alerted(it) local name, value = get_pa_keys(it) if pa_items_alerted[name] ~= nil and tonumber(pa_items_alerted[name]) >= value then return name end end function f_pa_data.remember_alert(it) if not (it.is_weapon or BRC.it.is_armour(it, true) or BRC.it.is_talisman(it)) then return end local name, value = get_pa_keys(it) local cur_val = tonumber(pa_items_alerted[name]) if not cur_val or value > cur_val then pa_items_alerted[name] = value end -- Add lesser versions of same item, to avoid alerting an inferior item. -- Use name comparison instead of get_ego(it) because ego() returns nil for floor items -- even when the item is visibly branded (DCSS API limitation for unequipped items). if name ~= it.name("db") and not BRC.eq.is_risky(it) and not BRC.it.is_talisman(it) then -- Add plain unbranded version name = it.name("db") cur_val = tonumber(pa_items_alerted[name]) if not cur_val or value > cur_val then pa_items_alerted[name] = value end -- For branded artefact, add the plain branded version local verbose_ego = it.ego(false) if it.artefact and verbose_ego then local branded_name if BRC.ADJECTIVE_EGOS[verbose_ego] then branded_name = BRC.ADJECTIVE_EGOS[verbose_ego] .. " " .. name else branded_name = name .. " of " .. verbose_ego end cur_val = tonumber(pa_items_alerted[name]) if not cur_val or value > cur_val then pa_items_alerted[branded_name] = value end end -- Armour may hit multiple egos based on artefact properties. Add each plain branded version. if it.artefact and BRC.it.is_armour(it) then for k, v in pairs(it.artprops) do if v > 0 and BRC.ARTPROPS_EGO[k] then local branded_name = name .. " of " .. BRC.ARTPROPS_EGO[k] cur_val = tonumber(pa_items_alerted[branded_name]) if not cur_val or value > cur_val then pa_items_alerted[branded_name] = value end end end end end end function f_pa_data.forget_alert(it) local name, _ = get_pa_keys(it) pa_items_alerted[name] = nil end function f_pa_data.add_recent_alert(it) if it.is_weapon or BRC.it.is_armour(it, true) or BRC.it.is_talisman(it) then pa_recent_alerts[#pa_recent_alerts + 1] = it.name() end end function f_pa_data.remove_recent_alert(it) util.remove(pa_recent_alerts, it.name()) end function f_pa_data.find_OTA(it) local qualname = it.name("qual") for _, v in ipairs(pa_OTA_items) do if v and qualname:find(v) then return v end end if it.class(true) == "book" and it.spells then local lower_spells = {} for _, s in ipairs(it.spells) do lower_spells[#lower_spells + 1] = s:lower() end for _, v in ipairs(pa_OTA_items) do local v_lower = v:lower() for _, s in ipairs(lower_spells) do if s:find(v_lower) then return v end end end end end function f_pa_data.remove_OTA(it) repeat local item_name = f_pa_data.find_OTA(it) if item_name == nil then return end util.remove(pa_OTA_items, item_name) until item_name == nil end --- Return name with plus included and quotes removed; used as key in tables function f_pa_data.get_keyname(it, use_plain_name) local name, value = get_pa_keys(it, use_plain_name) if not (BRC.it.is_armour(it) or it.is_weapon) then return name end if value >= 0 then value = string.format("+%s", value) end return string.format("%s %s", value, name) end --- Return string of the high score type if item sets a new high score, else nil function f_pa_data.update_high_scores(it) if not it then return end local ret_val = nil validate_high_scores() if BRC.it.is_armour(it) then local ac = BRC.eq.get_ac(it) if ac > pa_high_score.ac then pa_high_score.ac = ac if not ret_val then ret_val = "Highest AC" end end elseif it.is_weapon then -- Don't alert for unusable weapons if BRC.eq.get_hands(it) == 2 and not BRC.you.free_offhand() then return end local dmg = BRC.eq.get_avg_dmg(it, BRC.DMG_TYPE.branded) if dmg > pa_high_score.weapon then pa_high_score.weapon = dmg if not ret_val then ret_val = "Highest damage" end end dmg = BRC.eq.get_avg_dmg(it, BRC.DMG_TYPE.plain) if dmg > pa_high_score.plain_dmg then pa_high_score.plain_dmg = dmg if not ret_val then ret_val = "Highest plain damage" end end end return ret_val end } ############################ End lua/features/pickup-alert/pa-data.lua ############################ ################################################################################################### ########################## Begin lua/features/pickup-alert/pa-armour.lua ########################## #### https://github.com/brianfaires/crawl-rc/blob/main/lua/features/pickup-alert/pa-armour.lua #### { --------------------------------------------------------------------------------------------------- -- BRC feature module: pickup-alert-armour -- @submodule f_pa_armour -- @author Medar, gammafunk, buehler -- Armour pickup and alert functions for the pickup-alert feature. --------------------------------------------------------------------------------------------------- f_pa_armour = {} ---- Local constants ---- local ENCUMB_ARMOUR_DIVISOR = 2 -- Encumbrance penalty is offset by (Armour / ENCUMB_ARMOUR_DIVISOR) local SAME = "same_ego" local LOST = "lost_ego" local GAIN = "gain_ego" local NEW = "new_ego" local DIFF = "diff_ego" local HEAVIER = "Heavier" local LIGHTER = "Lighter" ---- Local variables ---- local H -- heuristic tuning alias local E -- emoji config alias local A -- alert config alias local M -- more config alias local ARMOUR_ALERT ---- Initialization ---- function f_pa_armour.init() H = f_pickup_alert.Config.Tuning.Armour E = f_pickup_alert.Config.Emoji A = f_pickup_alert.Config.Alert M = f_pickup_alert.Config.Alert.More ARMOUR_ALERT = { artefact = { msg = "Artefact armour", emoji = E.ARTEFACT }, [GAIN] = { msg = "Gain ego", emoji = E.EGO }, [NEW] = { msg = "New ego", emoji = E.EGO }, [DIFF] = { msg = "Diff ego", emoji = E.EGO }, [LIGHTER] = { [GAIN] = { msg = "Gain ego (Lighter armour)", emoji = E.EGO }, [NEW] = { msg = "New ego (Lighter armour)", emoji = E.EGO }, [DIFF] = { msg = "Diff ego (Lighter armour)", emoji = E.EGO }, [SAME] = { msg = "Lighter armour", emoji = E.LIGHTER }, [LOST] = { msg = "Lighter armour (Lost ego)", emoji = E.LIGHTER }, }, [HEAVIER] = { [GAIN] = { msg = "Gain ego (Heavier armour)", emoji = E.EGO }, [NEW] = { msg = "New ego (Heavier armour)", emoji = E.EGO }, [DIFF] = { msg = "Diff ego (Heavier armour)", emoji = E.EGO }, [SAME] = { msg = "Heavier Armour", emoji = E.HEAVIER }, [LOST] = { msg = "Heavier Armour (Lost ego)", emoji = E.HEAVIER }, }, } -- ARMOUR_ALERT (do not remove this comment) end ---- Local functions ---- local function aux_slot_is_impaired(it) local st = it.subtype() -- Skip boots/gloves/helmet if wearing Lear's hauberk local worn = items.equipped_at("armour") if worn and worn.name("qual") == "Lear's hauberk" and st ~= "cloak" then return true end -- Mutation interference if st == "gloves" then return BRC.you.mut_lvl("demonic touch") >= 3 and not BRC.you.free_offhand() or BRC.you.mut_lvl("claws") > 0 and not items.equipped_at("weapon") elseif st == "boots" then return BRC.you.mut_lvl("hooves") > 0 or BRC.you.mut_lvl("talons") > 0 elseif it.name("base"):contains("helmet") then return BRC.you.mut_lvl("horns") > 0 or BRC.you.mut_lvl("beak") > 0 or BRC.you.mut_lvl("antennae") > 0 end return false end local function get_adjusted_ev_delta(encumb_delta, ev_delta) local encumb_skills = you.skill("Spellcasting") + you.skill("Ranged Weapons") - you.skill("Armour") / ENCUMB_ARMOUR_DIVISOR local encumb_impact = encumb_skills / you.xl() encumb_impact = math.max(0, math.min(1, encumb_impact)) -- Clamp to 0-1 -- Subtract weighted encumbrance penalty, to align with ev_delta (heavier is negative) return ev_delta - encumb_delta * encumb_impact * H.encumb_penalty_weight end local function get_ego_change_type(cur_ego, it_ego) if it_ego == cur_ego then return SAME elseif not it_ego then return LOST elseif not cur_ego then return GAIN elseif not util.contains(pa_egos_alerted, it_ego) then return NEW else return DIFF end end --- Decides if an ego change is good enough to skip the min_gain check. --- For DIFF egos (neutral change), true for Shields/Aux, configurable for body armour local function is_good_ego_change(ego_change, is_body_armour) if ego_change == DIFF then return not is_body_armour or H.diff_body_ego_is_good end return ego_change == GAIN or ego_change == NEW end local function send_armour_alert(it, t_alert) return f_pickup_alert.do_alert(it, t_alert.msg, t_alert.emoji, M.body_armour) end -- Local functions: Pickup local function pickup_body_armour(it) local cur = items.equipped_at("armour") if not cur then return false end -- surely am naked for a reason -- No pickup if wearing an artefact if cur.artefact then return false end -- No pickup if adding encumbrance or losing AC local encumb_delta = it.encumbrance - cur.encumbrance if encumb_delta > 0 then return false end local ac_delta = BRC.eq.get_ac(it) - BRC.eq.get_ac(cur) if ac_delta < 0 then return false end -- Pickup: Pure upgrades local it_ego = BRC.eq.get_ego(it) local cur_ego = BRC.eq.get_ego(cur) if it_ego == cur_ego then return (ac_delta > 0 or encumb_delta < 0) end return not cur_ego and (ac_delta >= 0 or encumb_delta <= 0) end local function pickup_shield(it) -- Don't replace these local cur = items.equipped_at("offhand") if not BRC.it.is_shield(cur) then return false end if cur.encumbrance ~= it.encumbrance then return false end if cur.artefact then return false end -- Pickup: artefact if it.artefact then return true end -- Pickup: Pure upgrades local it_plus = it.plus or 0 local it_ego = BRC.eq.get_ego(it) local cur_ego = BRC.eq.get_ego(cur) if it_ego == cur_ego then return it_plus > cur.plus end return not cur_ego and it_plus >= cur.plus end local function pickup_aux_armour(it) -- Pickup: Anything if the slot is empty, unless downside from mutation if aux_slot_is_impaired(it) then return false end local all_equipped, num_slots = BRC.you.equipped_at(it) if #all_equipped < num_slots then -- If we're carrying one (implying a blocking mutation), don't pickup another if num_slots == 1 then local ST = it.subtype() return not util.exists(items.inventory(), function(inv) return inv.subtype() == ST end) end return true end -- Pickup: artefact, unless slot(s) already full of artefact(s) for i, cur in ipairs(all_equipped) do if not cur.artefact then break end if i == num_slots then return false end end if it.artefact then return true end -- Pickup: Pure upgrades local it_ac = BRC.eq.get_ac(it) local it_ego = BRC.eq.get_ego(it) for _, cur in ipairs(all_equipped) do local cur_ac = BRC.eq.get_ac(cur) local cur_ego = BRC.eq.get_ego(cur) if it_ego == cur_ego then if it_ac > cur_ac then return true end elseif not cur_ego then if it_ac >= cur_ac then return true end end end return false end -- Local functions: Alerting local function should_alert_body_armour(weight, gain, loss, ego_change) -- Check if armour stat trade-off meets configured ratio thresholds local meets_ratio = loss <= 0 or (gain / loss > H[weight][ego_change] / A.armour_sensitivity) if not meets_ratio then return false end -- Additional ego-specific restrictions if ego_change == SAME or is_good_ego_change(ego_change, true) then return loss <= H[weight].max_loss * A.armour_sensitivity else return gain >= H[weight].min_gain / A.armour_sensitivity end end -- Alert when finding higher AC than previously seen, unless training spells/ranged and NOT armour local function alert_highest_ac(it) if you.xl() > 12 then return false end local total_skill = you.skill("Spellcasting") + you.skill("Ranged Weapons") if total_skill > 0 and you.skill("Armour") == 0 then return false end if pa_high_score.ac == 0 then local worn = items.equipped_at("armour") if not worn then return false end pa_high_score.ac = BRC.eq.get_ac(worn) else local itAC = BRC.eq.get_ac(it) if itAC > pa_high_score.ac then pa_high_score.ac = itAC return f_pickup_alert.do_alert(it, "Highest AC", E.STRONGEST, M.high_score_armour) end end return false end local function alert_body_armour(it) local cur = items.equipped_at("armour") if not cur then return false end -- Always alert artefacts once identified if it.artefact then return send_armour_alert(it, ARMOUR_ALERT.artefact) end -- Get changes to ego, AC, EV, encumbrance local it_ego = BRC.eq.get_ego(it) local cur_ego = BRC.eq.get_ego(cur) local ego_change = get_ego_change_type(cur_ego, it_ego) local ac_delta = BRC.eq.get_ac(it) - BRC.eq.get_ac(cur) local ev_delta = BRC.eq.get_armour_ev(it) - BRC.eq.get_armour_ev(cur) local encumb_delta = it.encumbrance - cur.encumbrance -- Alert new egos if same encumbrance, or small change to total (AC+EV) if is_good_ego_change(ego_change, true) then if encumb_delta == 0 then return send_armour_alert(it, ARMOUR_ALERT[ego_change]) end local weight = encumb_delta < 0 and LIGHTER or HEAVIER if math.abs(ac_delta + ev_delta) <= H[weight].ignore_small * A.armour_sensitivity then BRC.mpr.debug("small change: AC:" .. ac_delta .. ", EV:" .. ev_delta) return send_armour_alert(it, ARMOUR_ALERT[weight][ego_change]) end end -- Check if lighter/heavier armour meets stat trade-off thresholds if encumb_delta < 0 then if should_alert_body_armour(LIGHTER, ev_delta, -ac_delta, ego_change) then BRC.mpr.debug("Lighter: AC:" .. ac_delta .. ", EV:" .. ev_delta .. ", " .. ego_change) return send_armour_alert(it, ARMOUR_ALERT[LIGHTER][ego_change]) end elseif encumb_delta > 0 then local adj_ev_delta = get_adjusted_ev_delta(encumb_delta, ev_delta) if should_alert_body_armour(HEAVIER, ac_delta, -adj_ev_delta, ego_change) then BRC.mpr.debug("Heavier: AC:" .. ac_delta .. ", EV:" .. ev_delta .. ", " .. ego_change) return send_armour_alert(it, ARMOUR_ALERT[HEAVIER][ego_change]) end end -- Check for record AC values or early-game ego armour if alert_highest_ac(it) then return true end if it_ego and you.xl() <= H.early_xl then return f_pickup_alert.do_alert(it, "Early armour", E.EGO) end end local function alert_shield(it) if it.artefact then return f_pickup_alert.do_alert(it, "Artefact shield", E.ARTEFACT, M.shields) end -- Don't alert shields if not wearing one (one_time_alerts fire for the first of each type) local cur = items.equipped_at("offhand") if not BRC.it.is_shield(cur) then return false end -- Alert: New ego, Gain SH local ego_change = get_ego_change_type(BRC.eq.get_ego(cur), BRC.eq.get_ego(it)) if is_good_ego_change(ego_change, false) then local alert_msg = BRC.txt.capitalize(ego_change):gsub("_", " ") return f_pickup_alert.do_alert(it, alert_msg, E.EGO, M.shields) elseif BRC.eq.get_sh(it) > BRC.eq.get_sh(cur) then return f_pickup_alert.do_alert(it, "Higher SH", E.STRONGER, M.shields) end end local function alert_aux_armour(it, unworn_inv_item) if it.artefact then return f_pickup_alert.do_alert(it, "Artefact aux armour", E.ARTEFACT, M.aux_armour) end local all_equipped, num_slots = BRC.you.equipped_at(it) if #all_equipped < num_slots then if unworn_inv_item then all_equipped[#all_equipped + 1] = unworn_inv_item else -- Catch dangerous brands or items blocked by non-innate mutations return f_pickup_alert.do_alert(it, "Aux armour", BRC.EMOJI.EXCLAMATION, M.aux_armour) end end local it_ego = BRC.eq.get_ego(it) for _, cur in ipairs(all_equipped) do local ego_change = get_ego_change_type(BRC.eq.get_ego(cur), it_ego) if is_good_ego_change(ego_change, false) then local alert_msg = BRC.txt.capitalize(ego_change):gsub("_", " ") return f_pickup_alert.do_alert(it, alert_msg, E.EGO, M.aux_armour) elseif BRC.eq.get_ac(it) > BRC.eq.get_ac(cur) then return f_pickup_alert.do_alert(it, "Higher AC", E.STRONGER, M.aux_armour) end end end ---- Public API ---- function f_pa_armour.pickup_armour(it) if BRC.eq.is_risky(it) then return false end if BRC.it.is_body_armour(it) then return pickup_body_armour(it) elseif BRC.it.is_shield(it) then return pickup_shield(it) else return pickup_aux_armour(it) end end --- Alerts armour items that didn't auto-pickup but are worth considering. --- This comes after pickup, so there will be no pure upgrades. -- @param unworn_inv_item (optional) to compare against an unworn aux armour item in inventory. function f_pa_armour.alert_armour(it, unworn_inv_item) if BRC.it.is_body_armour(it) then return alert_body_armour(it) elseif BRC.it.is_shield(it) then return alert_shield(it) else return alert_aux_armour(it, unworn_inv_item) end end } ########################### End lua/features/pickup-alert/pa-armour.lua ########################### ################################################################################################### ########################### Begin lua/features/pickup-alert/pa-misc.lua ########################### ##### https://github.com/brianfaires/crawl-rc/blob/main/lua/features/pickup-alert/pa-misc.lua ##### { --------------------------------------------------------------------------------------------------- -- BRC feature module: pickup-alert-misc -- @submodule f_pa_misc -- Miscellaneous item pickup and alert functions for the pickup-alert feature. --------------------------------------------------------------------------------------------------- f_pa_misc = {} ---- Local variables ---- local E -- emoji config alias local A -- alert config alias local M -- more config alias ---- Initialization ---- function f_pa_misc.init() E = f_pickup_alert.Config.Emoji A = f_pickup_alert.Config.Alert M = f_pickup_alert.Config.Alert.More end ---- Local functions ---- ---- Public API ---- function f_pa_misc.alert_orb(it) return f_pickup_alert.do_alert(it, "New orb", E.ORB, M.orbs) end function f_pa_misc.alert_OTA(it) local ota_item = f_pa_data.find_OTA(it) if not ota_item then return end local do_alert = true if BRC.it.is_shield(it) then if you.skill("Shields") < A.OTA_require_skill.shield then return end -- Don't alert if already wearing a larger shield if ota_item == "buckler" then if BRC.you.have_shield() then do_alert = false end elseif ota_item == "kite shield" then local sh = items.equipped_at("offhand") if sh and sh.name("qual") == "tower shield" then do_alert = false end end elseif BRC.it.is_armour(it) then if you.skill("Armour") < A.OTA_require_skill.armour then return end elseif it.is_weapon then if you.skill(it.weap_skill) < A.OTA_require_skill.weapon then return end end f_pa_data.remove_OTA(it) if not do_alert then return false end if it.class(true) == "book" and not it.name():find(ota_item) then return f_pickup_alert.do_alert(it, "Found " .. ota_item, E.RARE_ITEM, M.one_time_alerts) end return f_pickup_alert.do_alert(it, "Found first", E.RARE_ITEM, M.one_time_alerts) end function f_pa_misc.alert_staff(it) local basename = it.name("base") local tag local tag_color if basename == "staff of air" then if you.res_shock() > 0 then return false end tag = "rElec" tag_color = BRC.COL.lightcyan elseif basename == "staff of chemistry" then if you.res_poison() > 0 then return false end tag = "rPois" tag_color = BRC.COL.lightgreen elseif basename == "staff of cold" then if you.res_cold() > 0 then return false end tag = "rC+" tag_color = BRC.COL.lightblue elseif basename == "staff of fire" then if you.res_fire() > 0 then return false end tag = "rF+" tag_color = BRC.COL.lightred elseif basename == "staff of necromancy" then if you.res_draining() > 0 then return false end tag = "rN+" tag_color = BRC.COL.lightmagenta else return false end for _, inv in ipairs(items.inventory()) do if inv.is_weapon and inv.name("plain"):contains(tag) then return false end end tag = BRC.txt[tag_color]("(" .. tag .. ")") return f_pickup_alert.do_alert(it, "Staff resistance " .. tag, E.STAFF_RES, M.staff_resists) end function f_pa_misc.alert_talisman(it) if not it.is_identified then return false end -- Necessary to avoid firing on '\' menu if it.artefact then return f_pickup_alert.do_alert(it, "Artefact talisman", E.TALISMAN, M.talismans) end local required_skill = BRC.it.get_talisman_min_level(it) - A.talisman_lvl_diff if required_skill > BRC.you.shapeshifting_skill() then return false end return f_pickup_alert.do_alert(it, "New talisman", E.TALISMAN, M.talismans) end function f_pa_misc.is_unneeded_ring(it) if not BRC.it.is_ring(it) or it.artefact or you.race() == "Octopode" then return false end local missing_hand = BRC.you.mut_lvl("missing a hand") > 0 local st = it.subtype() local found_first = false for _, inv in ipairs(items.inventory()) do if BRC.it.is_ring(inv) and inv.subtype() == st then if found_first or missing_hand then return true end found_first = true end end return false end function f_pa_misc.pickup_staff(it) if f_pa_data.already_alerted(it) then return false end if BRC.you.skill(BRC.it.get_staff_school(it)) == 0 then return false end local qualname = it.name("qual") local max_slots = BRC.you.num_eq_slots(it) local count = 0 for _, inv in ipairs(items.inventory()) do if inv.name("qual") == qualname then count = count + 1 if count >= max_slots then return false end end end return true end } ############################ End lua/features/pickup-alert/pa-misc.lua ############################ ################################################################################################### ########################## Begin lua/features/pickup-alert/pa-weapons.lua ######################### #### https://github.com/brianfaires/crawl-rc/blob/main/lua/features/pickup-alert/pa-weapons.lua ### { --------------------------------------------------------------------------------------------------- -- BRC feature module: pickup-alert-weapons -- @submodule f_pa_weapons -- Weapon pickup and alert functions for the pickup-alert feature. -- _weapon_cache table stores info about inventory weapons, to avoid repeat calculations. --------------------------------------------------------------------------------------------------- f_pa_weapons = {} ---- Persistent variables ---- pa_lowest_hands_alerted = BRC.Data.persist("pa_lowest_hands_alerted", { ["Ranged Weapons"] = 3, -- Track lowest hand count alerted for this weapon school ["Polearms"] = 3, -- Track lowest hand count alerted for this weapon school }) ---- Local constants ---- local FIRST_WEAPON_XL_CUTOFF = 6 -- Stop first-weapon alerts after this experience level local POLEARM_RANGED_CUTOFF = 3 -- Stop polearm alerts when ranged skill reaches this level local UPGRADE_SKILL_FACTOR = 0.5 -- No upgrade alerts if weapon skill is this % of top skill -- Weapon cache constants local RANGED_PREFIX = "range_" local MELEE_PREFIX = "melee_" local WEAP_CACHE_KEYS = { "melee_1", "melee_1b", "melee_2", "melee_2b", "range_1", "range_1b", "range_2", "range_2b" } -- WEAP_CACHE_KEYS (do not remove this comment) ---- Local variables ---- local C -- config alias local W -- heuristic tuning alias local E -- emoji config alias local A -- alert config alias local M -- more config alias local top_attack_skill local _weapon_cache = {} -- Cache info for inventory weapons to avoid repeat calculations ---- Initialization ---- function f_pa_weapons.init() C = f_pickup_alert.Config W = f_pickup_alert.Config.Tuning.Weap E = f_pickup_alert.Config.Emoji A = f_pickup_alert.Config.Alert M = f_pickup_alert.Config.Alert.More top_attack_skill = BRC.you.top_wpn_skill() or "Unarmed Combat" _weapon_cache.init() if not A.first_ranged then pa_lowest_hands_alerted["Ranged Weapons"] = 0 end if not A.first_polearm then pa_lowest_hands_alerted["Polearms"] = 0 end end ---- Local functions ---- local function get_score(it, no_brand_bonus) if it.dps and it.acc then -- Handle cached / high-score tuples in _weapon_cache return it.dps + it.acc * W.Pickup.accuracy_weight end local dmg_type = no_brand_bonus and BRC.DMG_TYPE.unbranded or BRC.DMG_TYPE.scoring local acc_bonus = (it.accuracy + (it.plus or 0)) * W.Pickup.accuracy_weight return BRC.eq.get_dps(it, dmg_type) + acc_bonus end local function is_upgradable_weapon(it, cur) return cur.is_ranged == it.is_ranged and BRC.it.is_polearm(cur) == BRC.it.is_polearm(it) and ( you.race() == "Gnoll" or BRC.you.skill(it.weap_skill) >= UPGRADE_SKILL_FACTOR * BRC.you.skill(cur.weap_skill) ) end -- is_weapon_upgrade() -> boolean: compares floor weapon to one in inventory -- `cur` comes from _weapon_cache - it has some pre-computed values local function is_weapon_upgrade(it, cur, strict) if not cur.allow_upgrade then return false end if strict then -- Pure upgrades only if cur.artefact or it.subtype() ~= cur.subtype() then return false end if it.artefact then return true end local it_plus = it.plus or 0 local cur_ego = BRC.eq.get_ego(cur) if BRC.eq.get_ego(it) == cur_ego then return it_plus > cur.plus end return not cur_ego and it_plus >= cur.plus end -- Check if it's a very likely upgrade if it.subtype() == cur.subtype() then if it.artefact then return true end if cur.artefact and not A.allow_arte_weap_upgrades then return false end local it_ego = BRC.eq.get_ego(it) local cur_ego = BRC.eq.get_ego(cur) if cur_ego and not it_ego then return false end if it_ego and not cur_ego then return get_score(it) / cur.score > W.Pickup.add_ego end return it_ego == cur_ego and (it.plus or 0) > cur.plus elseif it.weap_skill == cur.weap_skill or you.race() == "Gnoll" then if BRC.eq.get_hands(it) > cur.hands then return false end if cur.is_ranged ~= it.is_ranged then return false end if BRC.it.is_polearm(cur) ~= BRC.it.is_polearm(it) then return false end if it.artefact then return true end if cur.artefact and not A.allow_arte_weap_upgrades then return false end local min_ratio = it.is_ranged and W.Pickup.same_type_ranged or W.Pickup.same_type_melee return get_score(it) / cur.score > min_ratio end return false end local function make_alert(it, msg, emoji, fm_option) return { it = it, msg = msg, emoji = emoji, fm_option = fm_option } end local function need_first_weapon() return you.xl() < FIRST_WEAPON_XL_CUTOFF and _weapon_cache.is_empty() -- Fail faster than iterating inventory and you.skill("Unarmed Combat") == 0 and BRC.you.mut_lvl("claws") == 0 and not util.exists(items.inventory(), function(i) return i.is_weapon end) end -- Weapon cache functions function _weapon_cache.init() _weapon_cache.turn = -1 _weapon_cache.refresh() end function _weapon_cache.ready() -- Ensure that a full refresh happens after each player action, even if turncount hasn't changed -- This is necessary because player actions + autopickup() often occur before turncount increases -- Resetting turn in ready() refreshes cache on first autopickup() after applying player action _weapon_cache.turn = -1 end function _weapon_cache.get_primary_key(it) local tokens = {} tokens[1] = it.is_ranged and RANGED_PREFIX or MELEE_PREFIX tokens[2] = tostring(it.hands) if BRC.eq.get_ego(it) then tokens[3] = "b" end return table.concat(tokens) end --- Get all categories this weapon fits into (including more-restrictive categories) function _weapon_cache.get_keys(is_ranged, hands, is_branded) local ranged_types = is_ranged and { RANGED_PREFIX, MELEE_PREFIX } or { MELEE_PREFIX } local handed_types = hands == 1 and { "1", "2" } or { "2" } local branded_types = is_branded and { "b", "" } or { "" } -- Generate all combinations local keys = {} for _, r in ipairs(ranged_types) do for _, h in ipairs(handed_types) do for _, b in ipairs(branded_types) do keys[#keys + 1] = table.concat({ r, h, b }) end end end return keys end function _weapon_cache.add_weapon(it) local weap_data = {} weap_data.is_weapon = it.is_weapon weap_data.basename = it.name("base") weap_data._subtype = it.subtype() weap_data.subtype = function() -- For consistency with crawl item.subtype() return weap_data._subtype end weap_data.weap_skill = it.weap_skill weap_data.skill_lvl = BRC.you.skill(it.weap_skill) weap_data.is_ranged = it.is_ranged weap_data.hands = BRC.eq.get_hands(it) weap_data.artefact = it.artefact weap_data._ego = BRC.eq.get_ego(it) weap_data.ego = function() -- For consistency with crawl item.ego() return weap_data._ego end weap_data.plus = it.plus or 0 weap_data.acc = it.accuracy + weap_data.plus weap_data.damage = it.damage weap_data.dps = BRC.eq.get_dps(it) weap_data.score = get_score(it) weap_data.unbranded_score = get_score(it, true) -- Check for exclusion tags local lower_insc = it.inscription:lower() weap_data.allow_upgrade = not (lower_insc:contains("!u") or lower_insc:contains("!brc")) -- Track unique egos if weap_data._ego and not util.contains(_weapon_cache.egos, weap_data._ego) then _weapon_cache.egos[#_weapon_cache.egos + 1] = weap_data._ego end -- Track max damage for applicable weapon categories local keys = _weapon_cache.get_keys(weap_data.is_ranged, weap_data.hands, weap_data._ego ~= nil) -- Update the max DPS for each category for _, key in ipairs(keys) do if weap_data.dps > _weapon_cache.max_dps[key].dps then _weapon_cache.max_dps[key].dps = weap_data.dps _weapon_cache.max_dps[key].acc = weap_data.acc end end _weapon_cache.weapons[#_weapon_cache.weapons + 1] = weap_data return weap_data end function _weapon_cache.is_empty() return _weapon_cache.max_dps["melee_2"].dps == 0 -- The most restrictive category end function _weapon_cache.refresh() local cur_turn = you.turns() if _weapon_cache.turn == cur_turn then return end _weapon_cache.turn = cur_turn _weapon_cache.weapons = {} _weapon_cache.egos = {} -- Can reuse max_dps table if _weapon_cache.max_dps then for _, key in ipairs(WEAP_CACHE_KEYS) do _weapon_cache.max_dps[key].dps = 0 _weapon_cache.max_dps[key].acc = 0 end else _weapon_cache.max_dps = {} for _, key in ipairs(WEAP_CACHE_KEYS) do _weapon_cache.max_dps[key] = { dps = 0, acc = 0 } end end for _, inv in ipairs(items.inventory()) do if inv.is_weapon and not BRC.it.is_magic_staff(inv) then _weapon_cache.add_weapon(inv) f_pa_data.update_high_scores(inv) end end end -- Local functions: Alerting local function get_first_of_skill_alert(it) local skill = it.weap_skill if not pa_lowest_hands_alerted[skill] then return end local hands = BRC.eq.get_hands(it) if pa_lowest_hands_alerted[skill] > hands then -- Some early checks to skip alerts if hands == 2 and BRC.you.have_shield() then return end if skill == "Polearms" and you.skill("Ranged Weapons") >= POLEARM_RANGED_CUTOFF then return end -- Update lowest # hands alerted, and alert pa_lowest_hands_alerted[skill] = hands local msg = "First " .. string.sub(skill, 1, -2) .. (hands == 1 and " (1-handed)" or "") return make_alert(it, msg, E.WEAPON, M.early_weap) end end local function get_early_weapon_alert(it) -- Alert really good usable ranged weapons if it.is_ranged and you.xl() <= W.Alert.EarlyRanged.xl then local min_plus = W.Alert.EarlyRanged[BRC.eq.get_ego(it) and "branded_min_plus" or "min_plus"] if (it.plus or 0) >= min_plus / A.weapon_sensitivity then local low_shield_training = you.skill("Shields") <= W.Alert.EarlyRanged.max_shields if BRC.eq.get_hands(it) == 1 or not BRC.you.have_shield() or low_shield_training then return make_alert(it, "Ranged weapon", E.RANGED, M.early_weap) end end end if you.xl() <= W.Alert.Early.xl then -- Ignore items if we're clearly going another route local skill_setting = W.Alert.Early.skill local skill_diff = BRC.you.skill(top_attack_skill) - BRC.you.skill(it.weap_skill) if skill_diff > you.xl() * skill_setting.factor + skill_setting.offset then return false end local it_plus = it.plus or 0 if BRC.eq.get_ego(it) or it_plus >= W.Alert.Early.branded_min_plus / A.weapon_sensitivity then return make_alert(it, "Early weapon", E.WEAPON, M.early_weap) end end return false end local function get_weap_high_score_alert(it) if _weapon_cache.is_empty() then return end -- Skip if not using weapons local category = f_pa_data.update_high_scores(it) if not category then return end return make_alert(it, category, E.WEAPON, M.high_score_weap) end -- get_upgrade_alert() subroutines local function can_use_2h_without_losing_shield() return BRC.you.free_offhand() or (you.skill("Shields") < W.Alert.AddHand.ignore_sh_lvl) end local function check_upgrade_free_offhand(it, ratio) local it_ego = BRC.eq.get_ego(it) if it_ego and not util.contains(_weapon_cache.egos, it_ego) and ratio > W.Alert.new_ego then return make_alert(it, "New ego (2-handed)", E.EGO, M.weap_ego) elseif ratio > W.Alert.AddHand.not_using then return make_alert(it, "2-handed weapon", E.TWO_HAND, M.upgrade_weap) end return false end local function check_upgrade_lose_shield(it, cur, ratio) if ( BRC.eq.get_ego(it) and not BRC.eq.get_ego(cur) and ratio > W.Alert.AddHand.add_ego_lose_sh ) then return make_alert(it, "2-handed weapon (Gain ego)", E.TWO_HAND, M.weap_ego) end return false end local function check_upgrade_no_hand_loss(it, cur, ratio) if BRC.eq.get_ego(it, true) then -- Don't overvalue Speed/Heavy egos (only consider their DPS) local it_ego = BRC.eq.get_ego(it) if not BRC.eq.get_ego(cur) then if ratio > W.Alert.gain_ego then return make_alert(it, "Gain ego", E.EGO, M.weap_ego) end elseif not util.contains(_weapon_cache.egos, it_ego) and ratio > W.Alert.new_ego then return make_alert(it, "New ego", E.EGO, M.weap_ego) end end if ratio > W.Alert.pure_dps then return make_alert(it, "DPS increase", E.WEAPON, M.upgrade_weap) end return false end local function check_upgrade_same_subtype(it, cur, best_dps, best_score) local it_ego = BRC.eq.get_ego(it, true) -- Don't overvalue speed/heavy (only consider their DPS) local cur_ego = BRC.eq.get_ego(cur) if it_ego and it_ego ~= cur_ego then local change = cur_ego and "Diff ego" or "Gain ego" return make_alert(it, change, E.EGO, M.weap_ego) end local s = A.weapon_sensitivity if get_score(it) > best_score / s or BRC.eq.get_dps(it) > best_dps / s then return make_alert(it, "Weapon upgrade", E.WEAPON, M.upgrade_weap) end end --- Check if weapon is worth alerting for, compared against one weapon currently in inventory -- @param cur (weapon) comes from _weapon_cache - it has some pre-computed values local function get_upgrade_alert(it, cur, best_dps, best_score) -- Ensure the non-strict upgrade is checked, if not already done in pickup_weapon() if C.Pickup.weapons_pure_upgrades_only and is_weapon_upgrade(it, cur, false) then return make_alert(it, "Weapon upgrade", E.WEAPON, M.upgrade_weap) end if it.artefact then return make_alert(it, "Artefact weapon", E.ARTEFACT) end if cur.artefact and not A.allow_arte_weap_upgrades then return false end if not is_upgradable_weapon(it, cur) then return end if cur.subtype() == it.subtype() then return check_upgrade_same_subtype(it, cur, best_dps, best_score) end -- Get ratio of weap_score / best_score. Penalize lower-trained skills local damp = W.Alert.low_skill_penalty_damping local penalty = (BRC.you.skill(it.weap_skill) + damp) / (BRC.you.skill(top_attack_skill) + damp) local ratio = penalty * get_score(it) / best_score * A.weapon_sensitivity if BRC.eq.get_hands(it) <= cur.hands then return check_upgrade_no_hand_loss(it, cur, ratio) elseif can_use_2h_without_losing_shield() then return check_upgrade_free_offhand(it, ratio) else return check_upgrade_lose_shield(it, cur, ratio) end end local function get_inventory_upgrade_alert(it) -- Once, find the top dps & score for inventory weapons of the same category local inv_best = _weapon_cache.max_dps[_weapon_cache.get_primary_key(it)] local top_dps = inv_best and inv_best.dps or 0 local top_score = inv_best and get_score(inv_best) or 0 -- Compare against all inventory weapons, even from other categories for _, inv in ipairs(_weapon_cache.weapons) do if inv.allow_upgrade then local best_dps = math.max(inv.dps, top_dps) local best_score = math.max(inv.score, top_score) local a = get_upgrade_alert(it, inv, best_dps, best_score) if a then return a end end end end local function get_weapon_alert(it) return get_inventory_upgrade_alert(it) or get_first_of_skill_alert(it) or get_early_weapon_alert(it) or get_weap_high_score_alert(it) end ---- Public API ---- function f_pa_weapons.pickup_weapon(it) _weapon_cache.refresh() if BRC.eq.is_risky(it) then return false end if need_first_weapon() then return true end for _, inv in ipairs(_weapon_cache.weapons) do if is_weapon_upgrade(it, inv, C.Pickup.weapons_pure_upgrades_only) then return true end end end function f_pa_weapons.alert_weapon(it) _weapon_cache.refresh() local a = get_weapon_alert(it) if not a then return false end return f_pickup_alert.do_alert(a.it, a.msg, a.emoji, a.fm_option) end ---- Crawl hook functions ---- function f_pa_weapons.ready() top_attack_skill = BRC.you.top_wpn_skill() or "Unarmed Combat" _weapon_cache.ready() end } ########################### End lua/features/pickup-alert/pa-weapons.lua ########################## ################################################################################################### ############## Lua Hook Functions ############## { function c_message(text, channel) BRC.c_message(text, channel) end function c_answer_prompt(prompt) return BRC.c_answer_prompt(prompt) end function c_assign_invletter(it) return BRC.c_assign_invletter(it) end function ch_start_running(kind) BRC.ch_start_running(kind) end function ready() BRC.ready() end BRC.init() } seed=345173118996450743