#################################### 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 = "Explicit" -- Testing, Explicit } ##################################### End lua/core/_header.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 = true, mpr = { show_debug_messages = true, logs_to_stderr = true, }, disable_other_features = false, -- only use features explicitly configured below dump = { max_lines_per_table = 80, -- 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.2, 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 = 2 }, flame = { factor = 1.25, offset = 0 }, freeze = { factor = 1.25, offset = 0 }, heavy = { factor = 1.8, offset = 0 }, -- Speed is accounted for elsewhere vorpal = { factor = 1.17, offset = 0 }, -- crawl.chaosforge.org/Vorpal pain = { factor = 1.0, offset = you.skill("Necromancy") / 2 }, spect = { factor = 1.35, 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.15, offset = 0 }, concuss = { factor = 1.15, offset = 0 }, devious = { factor = 1.1, offset = 0 }, holy = { factor = 1.15, offset = 0 }, penet = { factor = 1.3, offset = 0 }, protect = { factor = 1.1, offset = 0 }, reap = { factor = 1.3, offset = 0 }, rebuke = { factor = 1.1, offset = 0 }, vamp = { factor = 1.2, offset = 0 }, }, }, -- BRC.Configs.Default.BrandBonus (do not remove this comment) hotkey = { key = { keycode = -1010, name = "[NPenter]" }, 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 = false, -- 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 ---- ["my-feature"] = { disabled = false, }, ["announce-hp-mp"] = { disabled = false, dmg_flash_threshold = 0.10, -- Flash screen when losing this % of max HP dmg_fm_threshold = 0.20, -- 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 = 2, -- Announce when MP loss >= this mp_gain_limit = 3, -- Announce when MP gain >= this hp_first = true, -- Show HP first in the message same_line = false, -- Show HP/MP on the same line always_both = false, -- If showing one, show both very_low_hp = 0.25, -- 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, }, ["answer-prompts"] = { disabled = false, -- No config; See answer-prompts.lua for Questions/Answers }, ["display-realtime"] = { disabled = false, -- Disabled by default interval_s = 300, -- seconds between updates emoji = "🕒", init = function() if not BRC.Config.emojis then f_display_realtime.Config.emoji = BRC.txt.white("--") end end, }, ["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 }, ["exclude-dropped"] = { disabled = false, not_weapon_scrolls = true, -- Don't exclude enchant/brand scrolls if holding enchantable weapon }, ["fully-recover"] = { disabled = false, }, ["inscribe-stats"] = { disabled = false, inscribe_weapons = true, -- Inscribe weapon stats on pickup inscribe_armour = true, -- Inscribe armour stats on pickup dmg_type = 4, -- unbranded, plain, branded, scoring skip_dps = false, -- Skip DPS in weapon inscriptions prefix_staff_dmg = false, -- 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 = 55, -- % 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 }, ["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 = 2, -- 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"] = { 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 }, ---- Large config sections ---- ["pickup-alert"] = { disabled = false, Pickup = { armour = true, weapons = true, weapons_pure_upgrades_only = true, -- Only pick up better versions of same exact weapon staves = true, }, Alert = { armour_sensitivity = 1.0, -- Adjust all armour alerts; 0 to disable all (range 0.5-2.0) weapon_sensitivity = 1.0, -- Adjust all weapon alerts; 0 to disable all (range 0.5-2.0) orbs = true, staff_resists = true, talismans = false, 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 = { "demon blade", "demon trident", "partisan", "trishula", "glaive", "bardiche", "demon whip", "morningstar", "eveningstar", "sacred scourge", "broad axe", "hand cannon", "orcbow", "arbalest", "longbow", "triple crossbow", "buckler", "kite shield", "tower shield", "wand of digging", "quill talisman", "medusa talisman", "troll leather armour", "steam dragon scales", "crystal plate armour", "golden dragon scales", "storm dragon scales", "swamp dragon scales", "quicksilver dragon scales", "acid dragon scales", "pearl dragon scales", "shadow dragon scales", }, -- 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 = false, 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 = true, -- Better DPS / weapon_score weap_ego = false, -- New or diff egos body_armour = true, shields = true, aux_armour = true, armour_ego = true, -- New or diff egos high_score_weap = true, -- Highest damage found high_score_armour = true, -- Highest AC found one_time_alerts = true, artefact = true, -- 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 = true, -- 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, }, -- Tuning.Armour.Heavier 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, }, -- pickup-alert 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/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 = 80, -- 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.2, 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 = 2 }, flame = { factor = 1.25, offset = 0 }, freeze = { factor = 1.25, offset = 0 }, heavy = { factor = 1.8, offset = 0 }, -- Speed is accounted for elsewhere vorpal = { factor = 1.17, offset = 0 }, -- crawl.chaosforge.org/Vorpal pain = { factor = 1.0, offset = you.skill("Necromancy") / 2 }, spect = { factor = 1.35, 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.15, offset = 0 }, concuss = { factor = 1.15, offset = 0 }, devious = { factor = 1.1, offset = 0 }, holy = { factor = 1.15, offset = 0 }, penet = { factor = 1.3, offset = 0 }, protect = { factor = 1.1, offset = 0 }, reap = { factor = 1.3, offset = 0 }, rebuke = { factor = 1.1, offset = 0 }, vamp = { factor = 1.2, offset = 0 }, }, }, -- BRC.Configs.Default.BrandBonus (do not remove this comment) hotkey = { key = { keycode = -1010, name = "[NPenter]" }, 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 = false, -- 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 ---- ["my-feature"] = { disabled = false, }, ["announce-hp-mp"] = { disabled = false, dmg_flash_threshold = 0.10, -- Flash screen when losing this % of max HP dmg_fm_threshold = 0.20, -- 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 = 2, -- Announce when MP loss >= this mp_gain_limit = 3, -- Announce when MP gain >= this hp_first = true, -- Show HP first in the message same_line = false, -- Show HP/MP on the same line always_both = false, -- If showing one, show both very_low_hp = 0.25, -- 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, }, ["answer-prompts"] = { disabled = false, -- No config; See answer-prompts.lua for Questions/Answers }, ["display-realtime"] = { disabled = false, -- Disabled by default interval_s = 300, -- seconds between updates emoji = "🕒", init = function() if not BRC.Config.emojis then f_display_realtime.Config.emoji = BRC.txt.white("--") end end, }, ["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 }, ["exclude-dropped"] = { disabled = false, not_weapon_scrolls = true, -- Don't exclude enchant/brand scrolls if holding enchantable weapon }, ["fully-recover"] = { disabled = false, }, ["inscribe-stats"] = { disabled = false, inscribe_weapons = true, -- Inscribe weapon stats on pickup, string.format("DPS:%s (/%s) A%s", dps, delay_str, acc) inscribe_armour = true, -- Inscribe armour stats on pickup dmg_type = 4, -- unbranded, plain, branded, scoring skip_dps = false, -- Skip DPS in weapon inscriptions prefix_staff_dmg = false, -- 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 = 55, -- % 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 }, ["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 = 2, -- 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"] = { 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 }, ---- Large config sections ---- ["pickup-alert"] = { disabled = false, Pickup = { armour = true, weapons = true, weapons_pure_upgrades_only = true, -- Only pick up better versions of same exact weapon staves = true, }, Alert = { armour_sensitivity = 1.0, -- Adjust all armour alerts; 0 to disable all (range 0.5-2.0) weapon_sensitivity = 1.0, -- Adjust all weapon alerts; 0 to disable all (range 0.5-2.0) orbs = true, staff_resists = true, talismans = false, 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 = { --"dragonskin cloak", "ratskin cloak", "moon troll leather armour", "Cigotuvi's embrace", --"boots of flying", "cloak of willpower", "scarf of resistance", "hat of willpower", "helmet of see invisible", --"pair of gloves of strength", "pair of gloves of dexterity", "amulet of regeneration", "6 ring of strength", "6 ring of dexterity", "demon blade", "demon trident", "partisan", "trishula", "glaive", "bardiche", "demon whip", "morningstar", "eveningstar", "sacred scourge", "broad axe", "hand cannon", "orcbow", "arbalest", "longbow", "triple crossbow", --"of sniping", "of archery", "buckler", "kite shield", "tower shield", "wand of digging", "quill talisman", "medusa talisman", "troll leather armour", "steam dragon scales", "crystal plate armour", "golden dragon scales", "storm dragon scales", "swamp dragon scales", "quicksilver dragon scales", "acid dragon scales", "pearl dragon scales", "shadow dragon scales", }, -- 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 = true, -- Better DPS / weapon_score weap_ego = false, -- New or diff egos body_armour = true, shields = true, aux_armour = true, armour_ego = true, -- New or diff egos high_score_weap = true, -- Highest damage found high_score_armour = true, -- Highest AC found one_time_alerts = true, artefact = true, -- 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 = true, -- 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, }, -- Tuning.Armour.Heavier 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 ################################### sound_on = true one_SDL_sound_channel = true sound_volume = 0.07 bgm_volume = 0.04 sound_fade_time = 2.6 # sound_pack += https://sound-packs.nemelex.cards/sdlaonline/sdlaonline.zip sound_pack += https://osp.nemelex.cards/build/latest.zip:["init.txt"] # sound_pack += https://sound-packs.nemelex.cards/Autofire/BindTheEarth/BindTheEarth.zip sound_pack += https://sound-packs.nemelex.cards/DCSS-UST/v1.0.1.zip record_wtrec = true ####################### ### Mini Map Colors ### ####################### # https://pbs.twimg.com/media/G5DZ7Xka0AAer59?format=jpg&name=large # tile_portal_col = #ff4f1a # tile_downstairs_col = red tile_downstairs_col = #ff1a1a tile_branchstairs_col = yellow # tile_branchstairs_col = #ff006a tile_upstairs_col = green # tile_upstairs_col = #99ff33 tile_explore_horizon_col = #bfbfbf tile_floor_col = #262626 tile_water_col = #0086b3 tile_deep_water_col = #1f1fed tile_lava_col = #6f0b00 tile_wall_col = #595959 tile_door_col = #cb7d4d # tile_feature_col = #d4be21 tile_plant_col = #5c8745 tile_transporter_col = #ff80bf tile_transporter_landing_col = #59ff89 tile_trap_col = #d24dff ############### ### Display ### ############### # github.com/crawl/crawl/blob/master/crawl-ref/source/dat/defaults/menu_colours.txt tile_font_crt_size = 21 tile_font_stat_size = 21 tile_font_msg_size = 21 tile_font_tip_size = 21 tile_font_lbl_size = 21 tile_font_ft_light = false tile_cell_pixels = 38 tile_viewport_scale = 1.5 tile_font_crt_family = UD デジタル 教科書体 N-B tile_font_stat_family = UD デジタル 教科書体 N-B tile_font_msg_family = UD デジタル 教科書体 N-B tile_font_lbl_family = UD デジタル 教科書体 N-B tile_realtime_anim = true # dungeoncrawlchili.org/changesInDCChili.php opt-out_coaching-help = yes : if you.race() ~= "Felid" and you.race() ~= "Gargoyle" and you.race() ~= "Gale Centaur" and you.race() ~= "Human" and you.race() ~= "Fairy" and you.race() ~= "Silent Spectre" then tile_show_player_species = true : end # github.com/crawl/crawl/tree/master/crawl-ref/source/rltiles/player/felids # cat1, cat6, cat9 : if you.race() == "Felid" then tile_player_tile = tile:felid_6 : end tile_player_status_icons = slow, constr, fragile, petr, mark, will/2, haste, weak, corr, might, brill, -move cloud_status = true action_panel_font_size = 19 action_panel_font_family = UD デジタル 教科書体 N-B action_panel_orientation = vertical action_panel_show = false menu := menu_colour # menu ^= lightgrey:potions? of (attraction|lignification|mutation) # menu ^= lightgrey:scrolls? of (poison|torment|immolation|vulnerability|noise) menu ^= lightcyan:(scarf of invisibility|\+(Inv|Blink)) # : if you.race() == "Minotaur" then # menu ^= lightgrey:helmet # menu ^= lightgrey: hat of # : end msc := message_colour msc ^= white:(Done waiting|Done exploring|(start|finish) putting on) msc ^= lightgrey:(You start (resting|waiting)|continue putting on|misses you|you but does no damage|returns to the grave|disappears in a puff of smoke|removing your|You begin to imbue|You continue imbuing) msc ^= yellow:(miss |The.*misses (the|(?-i:[A-Z]))|You hit.*but do no damage|The.*hits (the|(?-i:[A-Z])).*but does no damage|You fail to reach past|You feel a bit more experienced|Something appears at your feet|Autopickup disabled for|You hear an? |The (?!butterfly).*leaves your sight|Partly explored|You continue winding your clockwork bee) msc ^= lightred:(You encounter) msc += mute:Search for what.*(~D|in_shop|ransp) msc += mute:There is an open door here msc += mute:You swap places with (your|(?-i:[A-Z])) msc += mute:(Your.*|The butterfly) leaves your sight msc += mute:The butterfly is (distracted by your dazzling golden aura|no longer dazed) msc += mute:Your.*is recalled msc += mute:What are your orders msc += mute:t - Shout! msc += mute:Shift-Dir - straight line msc += mute: - help\, Dir - move target msc += mute:Anything else - Cancel # BRC: Waypoint \\d (re-)?assigned # Waypoint 9 assigned to D:1, (8) Lair:5 (9) Orc:2 # msc += mute:Waypoint.*assigned # msc += mute:^\(8-9) [A-Z] # Lua Error: Safe Shout # msc += mute:Cannot currently process new keys # : if you.branch() == "Gauntlet" or you.branch() == "Necropolis" or you.branch() == "Trove" or you.branch() == "Temple" then # msc += mute:Found a transporter # : end # Religion msc += mute:accepts your kill msc += mute:is honoured by your kill msc += mute:appreciates the change of pace : if you.god() == "Yredelemnul" then msc += mute:Your.*(something|the (plant|bush|fungus|withered plant)) msc += mute:Something.*the (plant|bush|fungus|withered plant) msc += mute:(The|A nearby) (plant|bush|fungus|withered plant).*die msc += mute:Your.*web msc += mute:The confined air twists around weakly and strikes your : end # Spells msc += mute:Your foxfire dissipates msc += mute:Your fire vortex dissipates hp_colour = 100:green, 99:lightgray, 75:yellow, 50:lightred, 25:red mp_colour = 100:green, 99:lightgray, 75:yellow, 50:lightred, 25:red hp_warning = 50 mp_warning = 30 tile_show_threat_levels = trivial, easy, tough, nasty, unusual monster_list_colour += trivial:lightgrey, easy:white tile_show_demon_tier = true always_show_zot = true # always_show_gems = true more_gem_info = true # monster_item_view_coordinates = true ############# # Interface # ############# # tile_tooltip_ms = 0 # tile_web_mouse_control = false autofight_stampede = true autofight_nomove_fires = false : if you.class() ~= "Hunter" and you.class() ~= "Hexslinger" then autofight_fire_stop = true : end autofight_caught = true autofight_wait = true autofight_stop = 55 rest_wait_both = true rest_wait_ancestor = true fire_order = spell, throwing, evokable fire_order_ability -= all fail_severity_to_quiver = 3 fail_severity_to_confirm = -1 spell_menu = true warn_contam_cost = true show_paged_inventory = false item_stack_summary_minimum = 8 show_more = false easy_confirm = safe equip_unequip = true sort_menus = true:equipped,charged,art,ego,glowing,identified,basename stat_colour = 3:red, 7:lightred default_manual_training = true : if you.xl() <= 3 then skill_focus = true : end : if you.xl() >= 4 then skill_focus = false : end explore_delay = 1 rest_delay = -1 travel_delay = -1 view_delay = 600 level_map_cursor_step = 8 warn_hatches = true explore_stop = greedy_pickup_smart explore_stop += stairs,shops,altars,portals,branches,runed_doors,glowing_items,artefacts,runes # combo += MiFi . trident # combo += DgHu # combo += DECj force_spell_targeter += Silence : if you.res_shock() <= 0 then confirm_action += Conjure Ball Lightning, Chain Lightning : end # Blink[^bolt], ^Blink$ confirm_action += Potion Petition, Call Merchant, Silence, Maxwell's Capacitive Coupling, Sublimation of Blood, Borgnjor's Revivification, Death's Door ############ # Messages # ############ # github.com/crawl/crawl/blob/master/crawl-ref/source/dat/defaults/messages.txt # github.com/crawl/crawl/blob/master/crawl-ref/source/dat/defaults/runrest_messages.txt # github.com/crawl/crawl/blob/master/crawl-ref/source/dat/defaults/misc.txt # github.com/crawl/crawl/blob/master/crawl-ref/source/god-abil.cc ignore := runrest_ignore_message stop := runrest_stop_message more := force_more_message flash := flash_screen_message # stop += duration: ignore += recovery: ignore += contamination has completely ignore += your breath back ignore += engulfed in a cloud of smoke ignore += engulfed in white fluffiness ignore += safely over a trap interrupt_travel -= sense_monster stop -= it creaks loudly stop -= The hatch slams shut behind you more -= need to enable at least one skill for training more -= Okawaru grants you throwing weapons more -= Okawaru offers you a choice more -= You can now deal lightning-fast blows more -= The lock glows eerily # Zot's entrance more -= Heavy smoke blows from the lock more -= The gate opens wide more -= With a soft hiss the gate opens wide more -= You finish merging with the rock # Xom more -= You feel a strange sense of stasis # basilisk more -= You are unaffected stop ^= Your.*disappears in a puff of smoke,Your spellspark servitor fades away,Your battlesphere wavers and loses cohesion stop += You see here the more += You see here.*(of experience|of acquirement|of gain|of beneficial) flash += You see here.*(of experience|of acquirement|of gain|of beneficial) stop += - (a|[2-6]) scrolls? (of brand weapon|of bless item) stop += - (a|[2-7]) scrolls? of enchant weapon stop += - (a|[2-8]) scrolls? of revelation stop += - [2-9] scrolls labelled stop += - [7-9] potions of mutation stop += - [2-9] .* potions : if you.skill("Spellcasting") >= 5 then stop += - (a|[2-6]) scrolls? of amnesia : end : if you.xl() <= 14 then stop += You see here.*(ring of|the ring|amulet) : end : if you.xl() <= 16 then stop += You see here.*(scrolls? of identify|\+6 ring of) : end : if you.xl() >= 12 then stop += scroll labelled, - (a|[2-6]) .* potion : end : if you.xl() >= 18 then stop += You see here.*scrolls? of enchant armour : end more += You have reached level more += Extra life! more += press f on the ability menu to create your gizmo flash += press f on the ability menu to create your gizmo more += You (can|may) now (?!stomp|pass|merge) more += protects you from harm stop += You feel like a meek peon again more += You hear a faint sloshing more += seems mollified more += You rejoin the land of the living # Trog more += god:You feel the effects of Trog's Hand fading more += god:You feel less resistant to hostile enchantments more += Trog grants you a weapon # Makhleb more += You feel your soul grow more += Makhleb will allow you to brand your body flash += Makhleb will allow you to brand your body more += dares provoke your wrath more += Your infernal gateway subsides more += You are dragged down into the Crucible of Flesh # Ru more += Ru believes you are ready to make a new sacrifice flash += Ru believes you are ready to make a new sacrifice # Yredelemnul more += god:soul is no.* ripe for the taking more += god:dark mirror aura disappears flash += You offer up the Black Torch's flame more += Your bound.*is destroyed! # Wu Jian more += The heavenly storm settles # Hepliaklqana flash += You may now remember your ancestor's life more += You now drain nearby creatures when transferring your ancestor # : if you.god() == "Hepliaklqana" then # more += (?-i:[A-Z])(?!xecutioner|rb guardian).*is destroyed! # : end # Beogh more += Beogh will now send orc apostles to challenge you flash += Beogh will now send orc apostles to challenge you more += encounter.*the orc apostle more += the orc apostle comes? into view more += falls to his knees and submits more += has fought their way back out of the Abyss! more += Your prayer is nearing its end stop += Your prayer is nearing its end more += You reach the end of your prayer and your brethren are recalled stop += You reach the end of your prayer and your brethren are recalled : if you.god() == "Beogh" then more += (?-i:[A-Z])(?!xecutioner|rb guardian).*(dies!|is blown up!) : end # Gozag stop += You now have enough gold to more += Your bribe of.*has been exhausted # Fedhas more += god:Fedhas invokes the elements against you more += Your oklob plant disappears in a puff of smoke! # Qazlal flash += god:resistances upon receiving elemental damage flash += god:You are surrounded by a storm which can block enemy attacks # The Shining One more += god:Your divine shield starts to fade more += god:Your divine shield fades away # Zin more += god:will now cure all your mutations # Ashenzari more += Ashenzari invites you to partake flash += Ashenzari invites you to partake # Vehumet more += offers you knowledge of flash += offers you knowledge of more += Vehumet is now # Dithmenos more += god:You are shrouded in an aura of darkness more += god:You.*bleed smoke more += god:Your shadow.*tangibly mimics your actions # Lugonu more += god:Lugonu will now corrupt your weapon more += god:Lugonu sends minions to punish you # Kikubaaqudgha more += god:Kikubaaqudgha will grant you # Jivya more += god:will now unseal the treasures of the Slime Pits more += god:Jiyva alters your body # Xom # crawl.chaosforge.org/Chaos_Knight_of_Xom_Guide#Xom_rc_file # github.com/crawl/crawl/commit/41e759f0eee4a68cb4becd82386d540ef17e162c # github.com/crawl/crawl/commit/db04a1d58d73a1da99ae1d7e338ac4ec6a89585e # github.com/crawl/crawl/commit/0c9ac3fb94f1a3405762e005e397097afb7e35be more += .* erupts in a glittering mayhem of colour more += "Time to have some fun!" more += "Fight to survive, mortal." more += "Let's see if it's strong enough to survive yet." more += You hear Xom's maniacal laughter. more += Xom feels like there should be more of you to share with the Dungeon. more += Xom gently smashes a mirror over your more += Xom displaces most of your magic for a moment. more += Xom reveals to you one of the great and terrible secrets of magic. more += Xom curiously channels energy out of your body. more += Xom picks up your mind and accidentally drops it. more += Xom gives you a terrible headache. more += "You don't need this, do you?" more += Your magic slithers out of your reeling mind! more += Your power and thoughts are overwhelmed by magic-eaters manifest! more += Your magic wrenches itself out of your more += Your magic flows out through your more += Your magic rips itself out through your more += "Heh heh heh..." more += Xom's power touches on your mind for a moment. more += Xom titters. more += "Stumble well, my pet!" more += Xom's power touches upon your body and mind for a moment. more += Xom decides to rearrange the pieces. more += "First here, now there." more += "This might be better!" more += "This is how I like it!" more += "The weather's been a little too boring around here." more += Xom asks Qazlal to spice things up a little. more += mostly cloudy. more += (The|Your).*falls away! more += staircase.*moves more += Some monsters swap places flash += danger:sense of stasis #################### # Dungeon Features # #################### # github.com/crawl/crawl/blob/master/crawl-ref/source/dat/descript/features.txt # github.com/crawl/crawl/tree/master/crawl-ref/source/dat/des/portals # crawl.chaosforge.org/Portal # very nearby: 7–13, nearby: 14–20, distant: 21–27, very distant: 28+ flash += timed_portal: stop += timed_portal: flash += You hear.*very (nearby|distant) more += You hear.*(quick|urgent|loud (creaking of a portcullis|crackling of a melting archway)|rising multitudes|rapid|thunderous|frantic|ear-piercing|full choir) flash += You hear.*(quick|urgent|loud (creaking of a portcullis|crackling of a melting archway)|rising multitudes|rapid|thunderous|frantic|ear-piercing|full choir) more += Found.*(bazaar|trove|phantasmal|sand-covered|glowing drain|flagged portal|gauntlet|frozen archway|dark tunnel|crumbling gateway|ruined gateway|magical portal|ziggurat) flash += volcano erupts with a roar more += You enter a wizard's laboratory more += The walls (briefly|violently) shake more += The walls fall away. The entombed are set free! flash += Hurry and find it more += The walls and floor vibrate strangely for a moment flash += The walls and floor vibrate strangely for a moment # Found something important flash += You pick up the .* (gem|rune) and feel its more += You have a vision of.*gates? more += Press the corresponding letter to learn more about a god more += Found a faded altar of an unknown god more += Found a staircase to the Ecumenical Temple : if you.xl() <= 10 then flash += Found.*(Ashenzari|Beogh|Cheibriados|Fedhas|Gozag|Hepliaklqana|Ignis|Jiyva|Makhleb|Nemelex|Okawaru|Qazlal|Ru|Trog|Uskayaw|Wu Jian|Yredelemnul) : end : if you.xl() <= 12 and you.race() ~= "Mummy" and you.race() ~= "Demonspawn" and you.race() ~= "Revenant" and you.race() ~= "Poltergeist" and you.transform() ~= "death" and you.transform() ~= "vampire" then flash += Found.*(Elyvilon|The Shining One|Zin) : end : if you.xl() <= 12 and not (you.race() == "Minotaur" or you.race() == "Troll") then flash += Found.*(Vehumet|Sif Muna) : end : if you.xl() > 18 and you.race() ~= "Mummy" and you.race() ~= "Demonspawn" and you.race() ~= "Revenant" and you.race() ~= "Poltergeist" and you.transform() ~= "death" and you.transform() ~= "vampire" then flash += Found.*(The Shining One|Zin) : end stop += There is a portal to a secret trove of treasure here stop += Found a runed translucent door flash += There is a (staircase to the (?!Ecumenical)|gate to|hole to|gate leading to|gateway (?!back|out)) # Abyss more += Found a gateway leading.*Abyss more += and a gateway leading out appears more += You detect the abyssal rune! flash += You detect the abyssal rune! flash += Found.*runelights # Unique Realms more += The mighty Pandemonium lord.*resides here # The Demonic Rune more += The tension of great conflict fills the air #################### # Expiring Effects # #################### stop += You feel a little less mighty now more += Your transformation is almost over more += back to life more += You feel yourself slow down flash += You feel yourself slow down more += less insulated more += You are starting to lose your buoyancy more += You lose control over your flight more += Your hearing returns more += You have a feeling this form more += You feel yourself come back to life more += uncertain more += time is quickly running out more += life is in your own hands more += shroud falls apart more += You start to feel a little slower more += You flicker more += You feel less protected from missiles more += Your reaping aura expires more += You finish channelling your searing ray more += Your blazeheart golem falls apart more += Your rain of reagents ends # Death channel more += unholy channel is weakening ############################# # Bad and Unexpected Things # ############################# # github.com/crawl/crawl/blob/master/crawl-ref/source/mutation-data.h # github.com/crawl/crawl/blob/master/crawl-ref/source/dat/database/monspell.txt # github.com/crawl/crawl/blob/master/crawl-ref/source/beam.cc flash += mutation: more += You are dangerously contaminated with residual magic more += The horns on your head grow some more more += The horns on your head shrink a bit more += You feel vulnerable to flash += You feel vulnerable to more += You feel yourself grow more vulnerable to poison flash += You feel yourself grow more vulnerable to poison flash += Cherry-coloured flames burn away your fire resistance more += The klown pie hits you! (?!but) flash += The klown pie hits you! (?!but) more += hell_effect: more += Doom befalls you[^r] more += You feel an ill-omen more += You feel a malign power afflict you more += shimmers and splits apart flash += You are too injured to fight recklessly more += MASSIVE DAMAGE flash += MASSIVE DAMAGE more += ❗ BIG DAMAGE ❗ flash += ❗ BIG DAMAGE ❗ flash += LOW HITPOINT WARNING : if not (you.race() == "Minotaur" or you.race() == "Troll") then flash += LOW MAGIC WARNING : end # You Screwed Up more += is no longer ready more += You really shouldn't be using more += You don't have enough magic to cast this spell flash += Your body shudders with the violent release flash += power of Zot more += You fail to use your ability more += You stop (a|de)scending the stairs flash += You stop (a|de)scending the stairs more += surroundings become eerily quiet flash += surroundings become eerily quiet flash += You hear a loud "Zot" more += A malevolent force fills more += An alarm trap emits a blaring wail more += A sentinel's mark forms upon you more += invokes.*trap flash += invokes.*trap # Translocations more += Your surroundings.*(different|flicker) more += You.*re suddenly pulled into a different region : if you.transform() ~= "storm" then more += You blink (?!free) : end flash += danger:You feel strangely .*stable flash += delaying your translocation more += You cannot teleport right now more += A huge blade swings out and slices into you more += (blundered into a|invokes the power of) Zot more += That really hurt # Big damage more += The poison in your body grows stronger flash += You feel extremely sick more += You.*re lethally poisoned more += danger:You convulse more += You feel a (horrible|terrible) chill more += You are.*terribly more += Your body is wracked with pain more += The crystal spear hits you!!! flash += smites you[^r] more += You are (blasted|electrocuted) more += You are.*confused more += You are blinded flash += Something hits you[^r] more += infuriates you flash += Your magical (effects|defenses) are (unraveling|stripped away) flash += The pull of.*song draws you forward flash += engulfs you in water flash += You .*(blown|knocked back|mesmerised|trampled|encased) flash += You stumble backwards flash += You are shoved backwards flash += drags you backwards flash += grabs you[^r] flash += roots grab you[^r] flash += constricts you[^r] more += You are skewered in place flash += You are skewered in place more += The walls disappear more += wrath finds you more += god:(sends|finds|silent|anger) more += You feel a surge of divine spite more += disloyal to dabble more += lose consciousness more += You are too injured to fight blindly more += You feel your attacks grow feeble more += The blast of calcifying dust hits you[^r] flash += The blast of calcifying dust hits you[^r] more += danger:mutagenic more += Space warps horribly.*around you[^r] more += Space bends around you[^r] more += Your limbs are stiffening flash += Your limbs are stiffening more += Your body becomes as fragile as glass more += Water floods into your lungs flash += Water floods into your lungs more += You are covered in intense liquid fire # Unexpected monsters more += There is a strange disturbance nearby! more += appears in a (shower|flash) more += appears out of thin air more += You sense the presence of something unfriendly more += Wisps of shadow swirl around # Monsters doing things more += monster_warning:wielding.*of distortion more += There is.*feeling in your soul more += wretched star pulses more += Strange energies course through your body more += (?= 15 then more += Your magic feels tainted flash += Your magic feels tainted : end # Malign Gateway: oklob plant annihilator, Fedhas' Mad Dash : if you.skill("Summonings") < 15 then more += otherworldly place is opened : end more += An eldritch tentacle comes? into view # Hit by something more += Terrible wounds open more += The air around.*erupts in flames more += The air twists around and violently strikes you in flight more += You shudder from the earth-shattering force more += You feel.*(?= 26 and you.class() ~= "Conjurer" and you.class() ~= "Fire Elementalist" and you.class() ~= "Air Elementalist" and you.class() ~= "Earth Elementalist" and you.class() ~= "Hunter" and you.class() ~= "Hexslinger" then more += You add the spells?.*(Vhi's Electric|Manifold Assault|Fugue of the|Animate Dead|Death Channel|Awaken Armour) : end # Sprint VI: "Thunderdome" more += Get ready! stop += Get ready! flash += ROUND [1-9]?[0-9]! FIGHT! more += You sense a tension in the stop += You sense a tension in the more += Victory! ########## # Skills # ########## : if you.race() ~= "Gnoll" then stop += skill increases to level (9|18|26) more += Your Fighting skill increases to level (18|26) stop += Your Shields skill increases to level (4|6|9|15|21|25) stop += Your Short Blades skill increases to level (10|12|14) stop += Your Long Blades skill increases to level (14|16|18|24) stop += Your Maces & Flails skill increases to level (12|16|20|22) stop += Your Axes skill increases to level (16|18|20|26) more += Your Polearms skill increases to level (14|16|20|26) stop += Your Staves skill increases to level (12|14) stop += Your Throwing skill increases to level (6|10|16) more += Your Stealth skill increases to level (2|3|4|6|8) more += Your Evocations skill increases to level (2.5|6|10|11|13|15|18|21) more += Your Invocations skill increases to level (6|8|10|11|12|13|14|15|16|17) more += Your Shapeshifting skill increases to level (7|14|19|23|26) flash += Training target.*reached : end more += You have finished your manual flash += You have finished your manual more += You pick up a manual of flash += You pick up a manual of ############ # Monsters # ############ # Uniques and baddies # github.com/crawl/crawl/blob/master/crawl-ref/source/dat/des/builder/uniques.des # github.com/crawl/crawl/blob/master/crawl-ref/source/mon-gear.cc unusual_monster_items += ( the [^acrobat]|distortion|chaos|silver) more += encounter.*(undying armour(y|ies)|wendigo|antique champion|moths? of wrath|torpor snail|nekomata|oblivion hound|protean progenitor|acid blob|entropy weaver|ghost moth|death knight|apocalypse crab|eyes? of devastation)(?! (zombie|draugr|simulacr)) more += The undying armouty arms its allies with more += The nekomata weaves a phantasmal image and fires it at you more += The Royal Jelly spits out another jelly more += The dying Royal Jelly more += You kill Nessos! more += Xak'krixis conjures a prism more += Nobody ignites a memory of more += (Rupert|Snorg) goes berserk flash += (Rupert|Snorg) goes berserk more += BOSS flash += BOSS flash += changes into,Something shouts stop += encounter Crazy Yiuf monster_alert += pandemonium lord # Cloud of Thunder: 60 Damage more += Bai Suzhen roars in fury and transforms into a fierce dragon flash += Bai Suzhen roars in fury and transforms into a fierce dragon more += You kill.*(Bai Suzhen) flash += You kill.*(Bai Suzhen) # Dissolution (M2-5) # github.com/crawl/crawl/blob/master/crawl-ref/source/mon-act.cc : if you.branch() == "Slime" then flash += You hear a (sizzling sound|grinding noise) : end # Mara (S3-4 V2-5 E3 U) more += Suddenly you stand beside yourself flash += Suddenly you stand beside yourself more += sudden wrenching feeling in your soul flash += sudden wrenching feeling in your soul # Kirke (L3-5 D12-15 S1 E1), Butcher's Vault # : if you.xl() <= 16 then # flash += encounter.* hog # : end # Amaemon (L O1 D8-12) : if you.xl() <= 16 then more += encounter.*orange demon flash += encounter.*orange demon : end # Pikel (D4-9) : if you.xl() <= 13 then flash += encounter.*lemure : end # more += encounter (?!orb guardian|executioner)(?-i:[A-Z]) flash += encounter (?!orb guardian|executioner)(?-i:[A-Z]) flash += encounter.* and (?!orb guardian|executioner)(?-i:[A-Z]) more += (?!orb guardian|executioner)(?-i:[A-Z]).*comes? into view flash += (?!orb guardian|executioner)(?-i:[A-Z]).*comes? into view # Wizlab Bosses more += encounter.*(head instructor|cloud mage|lernaean hydra|seraph|boundless tesseract|wretched star|neqoxec|shining eye|cacodemon|zykzyl|orbs? of (fire|winter|entropy)) flash += encounter.*(cloud mage|lernaean hydra|seraph|boundless tesseract|wretched star|neqoxec|shining eye|cacodemon|zykzyl|orbs? of (fire|winter|entropy)) # Paralysis: 5+ 33% 1T, 7+ 50% 2T flash += encounter.*(starcursed mass) more += the reach of Zot diminish more += The shining eye gazes at you flash += encounter.*(death scarab) # Damnation/Flay more += encounter.*(deep elf (sorcerer|high priest)|(brimstone|ice) fiend)(?! (zombie|draugr|simulacr)) flash += encounter.*(deep elf (sorcerer|high priest)|(brimstone|ice) fiend)(?! (zombie|draugr|simulacr)) more += encounter.*(hell sentinel|hellion|draconian scorcher|flayed ghost) flash += encounter.*(hell sentinel|hellion|draconian scorcher|flayed ghost) # Miasma Breath/Death Rattle : if you.race() ~= "Gargoyle" and you.race() ~= "Revenant" and you.race() ~= "Poltergeist" and you.race() ~= "Mummy" and you.race() ~= "Djinni" then more += encounter.*(death drake|putrid mouth|ushabti)(?! (zombie|draugr|simulacr)) flash += encounter.*(death drake|putrid mouth|ushabti)(?! (zombie|draugr|simulacr)) : end # Torment/Drain Life/Siphon Essence : if not (you.race() == "Mummy" or you.race() == "Revenant" or you.race() == "Poltergeist" or you.transform() == "death") then more += encounter.*(royal mumm|mummy priest|tzitzimitl) flash += encounter.*(royal mumm|mummy priest|tzitzimitl) more += encounter.*(tormentor|curse toe|curse skull) flash += encounter.*(tormentor|curse toe|curse skull) more += encounter.*(deathcap|soul eater|vampire bloodprince|alderking) flash += encounter.*(deathcap|soul eater|vampire bloodprince|alderking) : end # Holy/Dispel Undead : if you.race() == "Mummy" or you.race() == "Demonspawn" or you.race() == "Revenant" or you.race() == "Poltergeist" or you.transform() == "death" or you.transform() == "vampire" then flash += encounter.*(? 4 or (you.class() == "Conjurer" or you.class() == "Fire Elementalist" or you.class() == "Air Elementalist" or you.class() == "Earth Elementalist" or you.class() == "Hunter" or you.class() == "Hexslinger") then ae += (? 12 and (you.class() == "Conjurer" or you.class() == "Fire Elementalist" or you.class() == "Air Elementalist" or you.class() == "Earth Elementalist") then ae = 18 and (you.race() == "Troll" or you.class() == "Fighter" or you.class() == "Berserker") then ae += (datura|atropa)-tipped dart : end : if you.xl() > 22 then ae += of identify, poisoned dart, amulet of (the acrobat|faith|guardian spirit|reflection|chemistry|dissipation|wildshape), ring of (evasion|strength|intelligence|dexterity|slaying|wizardry|magical power) : end ################ # Inscriptions # ################ # crawl.chaosforge.org/Inscriptions ai := autoinscribe ai += (throwing net|(curare|datura|atropa)-tipped dart|of disjunction):=f ai += potions? of heal wounds:@q1, 10-37HP ai += potions? of curing:@q2, 5-11HP ai += potions? of cancellation:@q3, UnaffectDrainBarbPoisBerserkDDoorExhGodEffects ai += potions? of might:34-74T 1d10 ai += potions? of haste:@q4, 40-79T ai += potions? of magic:@q5, 10-37MP ai += potions? of mutation:@q6, Remove2-3 Add1-3, 60%Good, 50%AddOneGood ai += potions? of ambrosia:3-5HPMP/T ai += potions? of lignification:1.5xHP (20+XL/2)AC rPo Torm0 Ev0 -MoBlTe GearWeapShie ai += potions? of enlightenment:25-64T ai += potions? of invisibility:15-54T, MonsSH/3, HuInt+8%Pos, -6EV -6ToHit Block/3 ai += potions? of resistance:10-29T, rFirColPoiCorEle ai += potions? of berserk rage:10-19T, ImmuneSleep ai += scrolls? of identify:@r1, 26%Iden 13%Common 4.7%Uncom, 18.4%Curi 10%Common 6.4%Uncom ai += scrolls? of teleportation:@r2, 3-5T, Zot8-14T ai += scrolls? of blinking:@r3, UnableTreeMesm-Tele ai += scrolls? of acquirement:200-1400Gold ai += scrolls? of enchant armour:@r4, GDR AC6Scale25% 10Plate28.5% 32+10PlateAux38% ai += scrolls? of enchant weapon:@r5, ToHit Dex/2+1d(FighS+1)+1d(WeaS+1) # Allow brand weapon scrolls (and randarts) to produce the new brands # github.com/crawl/crawl/commit/092f012dacd1664b29e7f4c764571602c4b3ef34 ai += scrolls? of (brand weapon|bless item):13.3% FlmFrzHvVnPro, 6.6% DrnElcSpcVmpChaos ai += scrolls? of fear:Q*f, 40Will90% 60Will73% ++41-80 ai += scrolls? of silence:30Turns ai += scrolls? of noise:25 Alarm40 FireStor25 FulmPris20 Qaz16 Shout12 IMB10 ai += scrolls? of immolation:Tiny3d15 Little-Large3d20 Giant3d25, 25-35T ai += scrolls? of torment:-10%N+ ai += scrolls? of summoning:Mons2d2, NamelessH HD(2*MiscastIntensity/3) Spd10 AC8 Ak30 ai += scrolls? of butterflies:12-28, Immo3d15, NamelessH HD(2*MiscastIntensity/3) Spd10 AC8 Atk30 ai += scrolls? of revelation:Necropolis900, Gauntlet400, Others600 ai += scrolls? of fog:AtkOut ExploIceblaTremor AlliNeuCharmBribFrenMons OoDTukimPoisSticF ai += scrolls? of amnesia:Intensity (SpellLv)d(SpellLv+Failure)/9, Contam 2x(SpellLv)^2xFailure+250 ai += wand of roots:@v1, \<, 2d(5.7+Evo/2)DmgT 3((15+3.5Evo)/22 To 4+((15+3.5Evo)/16)T ai += wand of warping:@v1, \<, 50+3.5Evo% MaxEvo12 ai += wand of iceblast:@v1, \<, 3d(7.66+3.5*Evo/5) ai += wand of acid:@v2, \>, 4d(5.5+10.5xEvo/20) ai += wand of light:@v2, \>, -35%ToHit ai += wand of quicksilver:@v2, \>, 6d(4.16+3.5*Evo/9) ai += wand of digging:@v3 # Enemy AC 2 (gnoll), Dizzy's DCSS Doodad, Mulch% # https://pastebin.com/raw/2DKqrVha ai += (? 4 then ai += (? 8 then ai += (? 12 then ai += (? 14 then ai += (? 16 then ai += (? 20 then ai += (? 22 then ai += (? 24 then ai += (?") end } macros += M \{NP+} ===godown_open { function goup_open() crawl.setopt("travel_open_doors = open") crawl.sendkeys("X","<") end } macros += M \{NP-} ===goup_open { function travel_open() crawl.setopt("travel_open_doors = open") crawl.sendkeys("G",{9}) end } macros += M T ===travel_open # reddit.com/r/dcss/comments/1ngwfhl/the_most_useful_dcss_command/ne77ryd/ # feat:find("transporter") # [NP.]: one key macro for going both up and down stairs { function smart_stairs() local feat = view.feature_at(0, 0) if feat:find("stairs_down") or feat:find("hatch_down") or feat:find("enter") then crawl.sendkeys(">") elseif feat:find("stairs_up") or feat:find("hatch_up") or feat:find("exit") or feat:find("return") then crawl.sendkeys("<") elseif feat:find("portal") or feat:find("transporter") or feat:find("shop") or feat:find("transit") or feat:find("altar") then crawl.sendkeys(">") else crawl.mpr("No stairs here.") crawl.mpr("Standing on: " .. feat) end end } macros += M \{NP.} ===smart_stairs # Lua error: Cannot currently process new keys (there is pending input) # reddit.com/r/dcss/comments/1qzzy2w/xdminsys_noise_guide/ # local insisted = crawl.yesno("You don't feel safe! Still shout? (y/n)", true, "n") { local shout_keys = "tt" function shout() -- crawl.do_commands({'CMD_WAIT'}) -- crawl.sendkeys(shout_keys) crawl.process_keys(shout_keys) end function safe_shout() if not you.feel_safe() then --crawl.setopt("tile_key_repeat_delay = 0") crawl.mpr("A monster is nearby!") --crawl.setopt("tile_key_repeat_delay = 200") else shout() end end } macros += M Y ===safe_shout more += A monster is nearby! # Tab:\{9}, Enter:\{13}, Esc:\{27}, Space:\{32}, Ctrl:*, Shift:/, Backspace:\{8} # List of Banes # macros += M \ ?/n\{32}\{13} # Ctrl-L: Help, Monster macros += M \{12} ?/m # [.]: Branches macros += M . *f<>\{32}&&\{32}!!one-\{13} # Ctrl-D: Go down closest stairs w/ {Cntl-G, '>'} # macros += M \{4} \{7}> # Ctrl-D: macros += M \{4} ?: # Ctrl-E: : if you.god() ~= "Hepliaklqana" and you.transform() ~= "jade" then macros += M \{5} x+k\{27} : end : if you.god() == "Hepliaklqana" or you.transform() == "jade" then macros += M \{5} x-k\{27} : end macros += M n x+ macros += M N x- macros += M l x+v macros += M L x-v macros += M C X*e macros += M I II macros += M R \\- macros += M y ff macros += M \{NP0} vf macros += M O aa macros += M K ab # [,]: @, Stash-Tracker # !!:Negate, &&:AND, ||:OR, <<>>:Grouping, /, = : if you.xl() <= 9 and (you.race() == "Troll" or you.class() == "Fighter" or you.class() == "Berserker") then macros += M , *f@\{32}&&\{32}!!ransp\{32}&&\{32}!!stair\{32}&&\{32}!!hatc\{32}&&\{32}!!trap\{32}&&\{32}!!gate\{32}&&\{32}!!door\{32}&&\{32}!!anim\{32}&&\{32}!!0\{32}rob\{32}&&\{32}!!a\{32}lea\{32}&&\{32}!!a\{32}tro\{32}&&\{32}!!a\{32}ring\{32}m\{32}&&\{32}!!a\{32}scal\{32}&&\{32}!!a\{32}cha\{32}&&\{32}!!a\{32}pla\{32}&&\{32}!!a\{32}buc\{32}&&\{32}!!a\{32}clo\{32}&&\{32}!!0\{32}clu\{32}&&\{32}!!0\{32}whi\{32}&&\{32}!!0\{32}mac\{32}&&\{32}!!0\{32}ham\{32}&&\{32}!!grea\{32}&&\{32}!!gian\{32}&&\{32}!!0\{32}spe\{32}&&\{32}!!0\{32}trid\{32}&&\{32}!!0\{32}hal\{32}&&\{32}!!0\{32}gla\{32}&&\{32}!!0\{32}bat\{32}&&\{32}!!0\{32}hand\{32}a\{32}&&\{32}!!0\{32}war\{32}&&\{32}!!0\{32}bro\{32}&&\{32}!!rang\{32}&&\{32}!!blad\{32}&&\{32}!!~D\{32}&&\{32}!!carri\{13} : end : if you.xl() >= 10 and you.xl() <= 16 and (you.race() == "Troll" or you.class() == "Fighter" or you.class() == "Berserker") then macros += M , *f@\{32}&&\{32}!!ransp\{32}&&\{32}!!stair\{32}&&\{32}!!hatc\{32}&&\{32}!!trap\{32}&&\{32}!!gate\{32}&&\{32}!!door\{32}&&\{32}!!quart\{32}&&\{32}!!laja\{32}&&\{32}!!rang\{32}&&\{32}!!blad\{32}&&\{32}!!club\{32}&&\{32}!!0\{32}whi\{32}&&\{32}!!0\{32}mac\{32}&&\{32}!!0\{32}ham\{32}&&\{32}!!grea\{32}&&\{32}!!gian\{32}&&\{32}!!nd\{32}ax\{32}&&\{32}!!0\{32}war\{32}&&\{32}!!0\{32}bro\{32}&&\{32}!!spea\{32}&&\{32}!!0\{32}trid\{32}&&\{32}!!0\{32}hal\{32}&&\{32}!!0\{32}gla\{32}&&\{32}!!0\{32}bat\{32}&&\{32}!!anim\{32}&&\{32}!!robe\{32}&&\{32}!!leat\{32}&&\{32}!!a\{32}ring\{32}m\{32}&&\{32}!!a\{32}scal\{32}&&\{32}!!a\{32}cha\{32}&&\{32}!!a\{32}pla\{32}&&\{32}!!a\{32}buc\{32}&&\{32}!!a\{32}clo\{32}&&\{32}!!orb\{32}&&\{32}!!~D\{32}&&\{32}!!carri\{13} : end : if you.xl() >= 17 and (you.race() == "Troll" or you.class() == "Fighter" or you.class() == "Berserker") then macros += M , *f@\{32}&&\{32}!!ransp\{32}&&\{32}!!stair\{32}&&\{32}!!hatc\{32}&&\{32}!!trap\{32}&&\{32}!!gate\{32}&&\{32}!!door\{32}&&\{32}!!quart\{32}&&\{32}!!laja\{32}&&\{32}!!rang\{32}&&\{32}!!blad\{32}&&\{32}!!club\{32}&&\{32}!!0\{32}whi\{32}&&\{32}!!r\{32}ax\{32}&&\{32}!!0\{32}mac\{32}&&\{32}!!ham\{32}&&\{32}!!0\{32}flai\{32}&&\{32}!!grea\{32}&&\{32}!!gian\{32}&&\{32}!!dire\{32}&&\{32}!!nd\{32}ax\{32}&&\{32}!!0\{32}war\{32}&&\{32}!!0\{32}bro\{32}&&\{32}!!spea\{32}&&\{32}!!0\{32}trid\{32}&&\{32}!!0\{32}hal\{32}&&\{32}!!0\{32}gla\{32}&&\{32}!!0\{32}bat\{32}&&\{32}!!mail\{32}&&\{32}!!0\{32}cha\{32}&&\{32}!!0\{32}pla\{32}&&\{32}!!0\{32}buc\{32}&&\{32}!!a\{32}clo\{32}&&\{32}!!anim\{32}&&\{32}!!robe\{32}&&\{32}!!leat\{32}&&\{32}!!~D\{32}&&\{32}!!carri\{13} : end : if you.race() ~= "Troll" and you.class() ~= "Fighter" and you.class() ~= "Berserker" and you.class() ~= "Hunter" and you.class() ~= "Hexslinger" then macros += M , *f@\{32}&&\{32}!!ransp\{32}&&\{32}!!stair\{32}&&\{32}!!hatc\{32}&&\{32}!!trap\{32}&&\{32}!!gate\{32}&&\{32}!!door\{32}&&\{32}!!anim\{32}&&\{32}!!a\{32}rob\{32}&&\{32}!!a\{32}lea\{32}&&\{32}!!a\{32}tro\{32}&&\{32}!!a\{32}ring\{32}m\{32}&&\{32}!!a\{32}scal\{32}&&\{32}!!a\{32}cha\{32}&&\{32}!!a\{32}pla\{32}&&\{32}!!a\{32}buc\{32}&&\{32}!!a\{32}clo\{32}&&\{32}!!0\{32}dag\{32}&&\{32}!!0\{32}sho\{32}&&\{32}!!0\{32}rap\{32}&&\{32}!!0\{32}fal\{32}&&\{32}!!0\{32}lon\{32}\{32}&&\{32}!!0\{32}gre\{32}&&\{32}!!0\{32}clu\{32}&&\{32}!!0\{32}whi\{32}&&\{32}!!0\{32}mac\{32}&&\{32}!!0\{32}ham\{32}&&\{32}!!gian\{32}&&\{32}!!0\{32}spe\{32}&&\{32}!!0\{32}trid\{32}&&\{32}!!0\{32}hal\{32}&&\{32}!!0\{32}gla\{32}&&\{32}!!0\{32}bat\{32}&&\{32}!!0\{32}hand\{32}a\{32}&&\{32}!!0\{32}war\{32}&&\{32}!!0\{32}bro\{32}&&\{32}!!~D\{32}&&\{32}!!carri\{13} : end : if you.class() == "Hunter" or you.class() == "Hexslinger" then macros += M , *f@\{32}&&\{32}!!ransp\{32}&&\{32}!!stair\{32}&&\{32}!!hatc\{32}&&\{32}!!trap\{32}&&\{32}!!gate\{32}&&\{32}!!door\{32}&&\{32}!!anim\{32}&&\{32}!!a\{32}rob\{32}&&\{32}!!a\{32}lea\{32}&&\{32}!!a\{32}tro\{32}&&\{32}!!a\{32}ring\{32}m\{32}&&\{32}!!scale\{32}m\{32}&&\{32}!!chai\{32}&&\{32}!!plat\{32}&&\{32}!!ice\{32}d\{32}&&\{32}!!dow\{32}d\{32}&&\{32}!!den\{32}d\{32}&&\{32}!!a\{32}buc\{32}&&\{32}!!kite\{32}&&\{32}!!towe\{32}&&\{32}!!a\{32}clo\{32}&&\{32}!!a\{32}hel\{32}&&\{32}!!0\{32}dag\{32}&&\{32}!!a\{32}sho\{32}&&\{32}!!0\{32}rap\{32}&&\{32}!!0\{32}fal\{32}&&\{32}!!0\{32}gre\{32}&&\{32}!!axe\{32}&&\{32}!!mace\{32}&&\{32}!!pole\{32}&&\{32}!!quart\{32}&&\{32}!!laja\{32}&&\{32}!!a\{32}sli\{32}&&\{32}!!a\{32}sho\{32}&&\{32}!!a\{32}orc\{32}&&\{32}!!a\{32}arb\{32}&&\{32}!!a\{32}lon\{32}&&\{32}!!~D\{32}&&\{32}!!carri\{13} : end # [P]: Shop Items : if you.race() == "Troll" or you.class() == "Fighter" or you.class() == "Berserker" then macros += M P *fin_shop\{32}&&\{32}!!rang\{32}&&\{32}!!blad\{32}&&\{32}!!0\{32}clu\{32}&&\{32}!!0\{32}whi\{32}&&\{32}!!0\{32}mac\{32}&&\{32}!!0\{32}ham\{32}&&\{32}!!gian\{32}&&\{32}!!dire\{32}&&\{32}!!0\{32}spe\{32}&&\{32}!!0\{32}trid\{32}&&\{32}!!0\{32}hal\{32}&&\{32}!!0\{32}gla\{32}&&\{32}!!0\{32}bat\{32}&&\{32}!!0\{32}hand\{32}a\{32}&&\{32}!!0\{32}war\{32}&&\{32}!!0\{32}bro\{32}&&\{32}!!anim\{32}&&\{32}!!0\{32}rob\{32}&&\{32}!!0\{32}lea\{32}&&\{32}!!0\{32}ring\{32}m\{32}&&\{32}!!0\{32}scal\{32}&&\{32}!!a\{32}cha\{32}&&\{32}!!a\{32}pla\{32}&&\{32}!!0\{32}buc\{32}&&\{32}!!a\{32}clo\{32}&&\{32}!!spell\{32}&&\{32}!!parchm\{32}&&\{32}!!carri\{13} : end : if you.race() ~= "Troll" and you.class() ~= "Fighter" and you.class() ~= "Berserker" then macros += M P *fin_shop\{32}&&\{32}!!0\{32}dag\{32}&&\{32}!!0\{32}sho\{32}&&\{32}!!0\{32}rap\{32}&&\{32}!!0\{32}fal\{32}\{32}&&\{32}!!0\{32}gre\{32}&&\{32}!!a\{32}sli\{32}&&\{32}!!0\{32}clu\{32}&&\{32}!!0\{32}whi\{32}&&\{32}!!0\{32}mac\{32}&&\{32}!!0\{32}ham\{32}&&\{32}!!gian\{32}&&\{32}!!0\{32}spe\{32}&&\{32}!!0\{32}trid\{32}&&\{32}!!0\{32}hal\{32}&&\{32}!!0\{32}gla\{32}&&\{32}!!0\{32}bat\{32}&&\{32}!!0\{32}hand\{32}a\{32}&&\{32}!!0\{32}war\{32}&&\{32}!!0\{32}bro\{32}&&\{32}!!anim\{32}&&\{32}!!0\{32}rob\{32}&&\{32}!!a\{32}lea\{32}&&\{32}!!a\{32}ring\{32}m\{32}&&\{32}!!a\{32}scal\{32}&&\{32}!!a\{32}cha\{32}&&\{32}!!a\{32}pla\{32}&&\{32}!!a\{32}buc\{32}&&\{32}!!a\{32}clo\{32}&&\{32}!!carri\{13} : end # [S]: Scrolls, Potions macros += M S *f<>\{32}&&\{32}!!iden\{32}&&\{32}!!~D\{32}&&\{32}!!carri\{13} # [%]: Evocable, Throwable, Talismans, Baubles # macros += M % *f<>\{32}&&\{32}!!~D\{32}&&\{32}!!carri\{13} # ["]: Jewellery, Orbs macros += M " *f<>\{32}&&\{32}!!statu\{32}&&\{32}!!carri\{13} # [_]: Artefacts macros += M _ *f<>\{32}&&\{32}!!talis\{32}&&\{32}!!carri\{13} # [-]: Dragon Scales, Runed Items macros += M - *f@/dragon|artef|ego|an?\{32}\\+[1-9]\{13} # "[": Body, Aux macros += M [ *f<>\{32}&&\{32}!!orb\{32}&&\{32}!!anim\{32}&&\{32}!!a\{32}rob\{32}&&\{32}!!a\{32}lea\{32}&&\{32}!!a\{32}ring\{32}m\{32}&&\{32}!!a\{32}scal\{32}&&\{32}!!a\{32}cha\{32}&&\{32}!!a\{32}pla\{32}&&\{32}!!a\{32}buc\{32}&&\{32}!!a\{32}clo\{32}&&\{32}!!talis\{32}&&\{32}!!~D\{32}&&\{32}!!carri\{13} # [`]: Dexterity Weapons, Magical Staves macros += M ` *f<>\{32}&&\{32}!!jewell\{32}&&\{32}!!0\{32}dag\{32}&&\{32}!!0\{32}sho\{32}&&\{32}!!0\{32}rap\{32}&&\{32}!!0\{32}fal\{32}\{32}&&\{32}!!0\{32}gre\{32}&&\{32}!!a\{32}sli\{32}&&\{32}!!body\{32}&&\{32}!!talis\{32}&&\{32}!!~D\{32}&&\{32}!!carri\{13} # [&]: Axes, Cross-Trained Weapons # macros += M & *f<>\{32}&&\{32}!!magica\{32}&&\{32}!!0\{32}clu\{32}&&\{32}!!0\{32}whi\{32}&&\{32}!!0\{32}mac\{32}&&\{32}!!0\{32}ham\{32}&&\{32}!!gian\{32}&&\{32}!!0\{32}spe\{32}&&\{32}!!0\{32}trid\{32}&&\{32}!!0\{32}hal\{32}&&\{32}!!0\{32}gla\{32}&&\{32}!!0\{32}bat\{32}&&\{32}!!0\{32}hand\{32}a\{32}&&\{32}!!0\{32}war\{32}&&\{32}!!0\{32}bro\{32}&&\{32}!!~D\{32}&&\{32}!!carri\{13} # [_]: Maces & Flails, Cross-Trained Weapons # macros += M _ *f<>\{32}&&\{32}!!magica\{32}&&\{32}!!0\{32}clu\{32}&&\{32}!!0\{32}whi\{32}&&\{32}!!0\{32}mac\{32}&&\{32}!!0\{32}ham\{32}&&\{32}!!gian\{32}&&\{32}!!0\{32}hand\{32}a\{32}&&\{32}!!0\{32}war\{32}&&\{32}!!0\{32}bro\{32}&&\{32}!!~D\{32}&&\{32}!!carri\{13} # [:]: Polearms, Cross-Trained Weapons : if you.xl() <= 16 then macros += M : *f<>\{32}&&\{32}!!magica\{32}&&\{32}!!0\{32}spe\{32}&&\{32}!!0\{32}trid\{32}&&\{32}!!0\{32}hal\{32}&&\{32}!!0\{32}gla\{32}&&\{32}!!0\{32}bat\{32}&&\{32}!!0\{32}hand\{32}a\{32}&&\{32}!!0\{32}war\{32}&&\{32}!!0\{32}bro\{32}&&\{32}!!~D\{32}&&\{32}!!carri\{13} : end : if you.xl() >= 17 then macros += M : *f<>\{32}&&\{32}!!magica\{32}&&\{32}!!~D\{32}&&\{32}!!carri\{13} : end # F1:\{-265}:poisoned dart, F2:\{-266}:boomerang, H:javelin, ")":wand of flame, "(":mindburst, "<":iceblast|roots|warping, ">":acid|light|quicksilver macros += M \{-265} F15 macros += M \{-266} F25 macros += M H F3 macros += M ) V45 macros += M ( V55 macros += M < V15 macros += M > V25 bindkey = [Tab] CMD_AUTOFIGHT_NOMOVE # macros += M \{9} zaf # Shift-Tab: macros += M \{-233} zaf macros += M E zbf macros += M U zcf macros += M 1 za macros += M 2 zb macros += M 3 zc macros += M 4 zd macros += M 5 ze macros += M 6 zf macros += M 7 zg macros += M 8 zh macros += M 9 zi macros += M 0 zj # "K1" level-map context keymap (during the `X` view only) # "K2" targeting context keymap (during targeting and `x` view) # "K3" confirmation context keymap (when responding to a y/n prompt) # "K4" menu context keymap (all menus, except the skill menu) # "K" default context keymap (any other context) # Space to close menus macros += K1 \{32} \{27} macros += K2 \{32} \{27} # macros += K4 \{32} \{27} -- Inventory # macros += K \{32} \{27} -- Annotation # Confirm targeting with same keys as spellcasting macros += K2 1 \{13} macros += K2 2 \{13} macros += K2 3 \{13} macros += K2 4 \{13} macros += K2 5 \{13} macros += K2 6 \{13} macros += K2 7 \{13} macros += K2 8 \{13} macros += K2 9 \{13} macros += K2 0 \{13} macros += K2 a \{13} macros += K2 b \{13} macros += K2 c \{13} macros += K2 d \{13} macros += K2 e \{13} macros += K2 g \{13} macros += K2 h \{13} macros += K2 i \{13} macros += K2 j \{13} macros += K2 z \{13} macros += K2 s \{13} macros += K2 x \{13} macros += K2 w \{13} macros += K2 q \{13} macros += K2 r \{13} macros += K2 t \{13} spell_slot ^= Apportation :z spell_slot ^= Swiftness :s spell_slot ^= Forge Phalanx Beetle :x spell_slot ^= Blink :w spell_slot ^= Passage of Golubria :q spell_slot ^= Passwall :r spell_slot ^= Summon Forest :t ############### # Keybindings # ############### # github.com/crawl/crawl/blob/master/crawl-ref/docs/keybind.txt # bindkey = [-] CMD_GAME_MENU bindkey = [NP5] CMD_WAIT bindkey = [NP5] CMD_TARGET_SELECT_ENDPOINT bindkey = [B] CMD_EXPLORE_NO_REST bindkey = [s] CMD_REST bindkey = [c] CMD_CLOSE_DOOR bindkey = [C] CMD_MAP_CLEAR_EXCLUDES bindkey = [W] CMD_WIELD_WEAPON bindkey = [w] CMD_WEAR_ARMOUR bindkey = [p] CMD_WEAR_JEWELLERY bindkey = [k] CMD_AUTOFIRE bindkey = [u] CMD_PREV_CMD_AGAIN bindkey = [i] CMD_RESISTS_SCREEN bindkey = [e] CMD_FULL_VIEW bindkey = [e] CMD_TARGET_FULL_DESCRIBE bindkey = [e] CMD_MAP_EXIT_MAP bindkey = [k] CMD_TARGET_EXCLUDE bindkey = [k] CMD_MAP_EXCLUDE_AREA bindkey = [\{13}] CMD_DISPLAY_INVENTORY bindkey = [NPenter] CMD_DISPLAY_INVENTORY bindkey = [\{NP-}] CMD_MAP_FIND_UPSTAIR bindkey = [\{NP+}] CMD_MAP_FIND_DOWNSTAIR bindkey = [\{NP/}] CMD_MAP_PREV_LEVEL bindkey = [\{NP*}] CMD_MAP_NEXT_LEVEL bindkey = [\{NP/}] CMD_CYCLE_QUIVER_BACKWARD bindkey = [\{NP*}] CMD_CYCLE_QUIVER_FORWARD bindkey = [\{8}] CMD_SWAP_QUIVER_RECENT # bindkey = [;] CMD_REPLAY_MESSAGES bindkey = [ ] CMD_TARGET_CANCEL bindkey = [ ] CMD_MAP_EXIT_MAP bindkey = [b] CMD_TARGET_CANCEL bindkey = [h] CMD_TARGET_CANCEL bindkey = [j] CMD_TARGET_CANCEL bindkey = [l] CMD_TARGET_CANCEL bindkey = [n] CMD_TARGET_CANCEL bindkey = [u] CMD_TARGET_CANCEL bindkey = [y] CMD_TARGET_CANCEL bindkey = [B] CMD_TARGET_CANCEL bindkey = [H] CMD_TARGET_CANCEL bindkey = [J] CMD_TARGET_CANCEL bindkey = [K] CMD_TARGET_CANCEL bindkey = [L] CMD_TARGET_CANCEL bindkey = [N] CMD_TARGET_CANCEL bindkey = [U] CMD_TARGET_CANCEL bindkey = [Y] CMD_TARGET_CANCEL bindkey = [b] CMD_MAP_EXIT_MAP bindkey = [h] CMD_MAP_EXIT_MAP bindkey = [j] CMD_MAP_EXIT_MAP bindkey = [l] CMD_MAP_EXIT_MAP bindkey = [n] CMD_MAP_EXIT_MAP bindkey = [u] CMD_MAP_EXIT_MAP bindkey = [y] CMD_MAP_EXIT_MAP bindkey = [B] CMD_MAP_EXIT_MAP bindkey = [H] CMD_MAP_EXIT_MAP bindkey = [J] CMD_MAP_EXIT_MAP bindkey = [K] CMD_MAP_EXIT_MAP bindkey = [L] CMD_MAP_EXIT_MAP bindkey = [N] CMD_MAP_EXIT_MAP bindkey = [U] CMD_MAP_EXIT_MAP bindkey = [Y] CMD_MAP_EXIT_MAP # BRC management # github.com/brianfaires/crawl-rc?tab=readme-ov-file#in-game-commands # BRC.unregister("pickup-alert") # BRC.unregister("inscribe-stats") # BRC.unregister("hotkey") # BRC.Data.reset() # BRC.active = false # BRC.active = true bindkey = [~] CMD_LUA_CONSOLE macros += M & ~BRC.unregister("pickup-alert")\{13}\{27} macros += M % ~BRC.active\{32}=\{32}false\{13}\{27} ############################### End rc/main.rc ############################### ############################ Begin lua/my-feature.lua ############################ { my_feature = {} my_feature.BRC_FEATURE_NAME = "my-feature" -- This registers my_feature with BRC my_feature.Config = { } -- always put a comment after a lone '}' (or else crawl's RC parser breaks) local need_skills_opened = true function my_feature.ready() if you.turns() == 0 and you.race() ~= "Gnoll" and you.race() ~= "Djinni" and need_skills_opened then need_skills_opened = false crawl.sendkeys("!d4" .. string.char(13) .. "T:D4-7" .. string.char(13)) crawl.sendkeys("!d10" .. string.char(13) .. "L D12 O D15 S V4 E U/V5/C/A/H M Z4 Zig7" .. string.char(13)) crawl.sendkeys("!d11" .. string.char(13) .. "Ice:L D11-13 O, Vol:L O, Gau:L D11- O" .. string.char(13)) crawl.sendkeys("!d12" .. string.char(13) .. "Rup:D12- L3- O P1 N1, Tro:D12- S" .. string.char(13)) you.set_training_target("Maces & Flails",12) you.set_training_target("Axes",16) you.set_training_target("Polearms",14) you.set_training_target("Staves",12) you.set_training_target("Throwing",2) you.set_training_target("Short Blades",14) you.set_training_target("Long Blades",12) you.set_training_target("Ranged Weapons",14) you.set_training_target("Armour",4) you.set_training_target("Dodging",4) you.set_training_target("Shields",4) you.set_training_target("Stealth",2) you.set_training_target("Hexes",6) you.set_training_target("Summonings",6) you.set_training_target("Necromancy",6) you.set_training_target("Forgecraft",6) you.set_training_target("Translocations",6) you.set_training_target("Alchemy",6) you.set_training_target("Fire Magic",18) you.set_training_target("Air Magic",6) you.set_training_target("Ice Magic",18) you.set_training_target("Earth Magic",18) you.set_training_target("Invocations",6) you.set_training_target("Evocations",3) you.set_training_target("Shapeshifting",8) crawl.sendkeys("m","C","c","a") end end } ############################ End lua/my-feature.lua ############################ ### buehler.rc core files ### ################################### Begin lua/core/constants.lua ################################## ############# https://github.com/brianfaires/crawl-rc/blob/main/lua/core/constants.lua ############ { --------------------------------------------------------------------------------------------------- -- BRC core module -- @module BRC -- Constants and definitions used throughout BRC. --------------------------------------------------------------------------------------------------- ---- Cosmetic settings BRC.EMOJI = {} BRC.init_emojis = function() if BRC.Config.emojis then BRC.EMOJI.CAUTION = "⚠️" BRC.EMOJI.EXCLAMATION = "❗" BRC.EMOJI.EXCLAMATION_2 = "‼️" BRC.EMOJI.SUCCESS = "✅" else BRC.EMOJI.CAUTION = "!" 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) A%s", dps, 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 vorpal = { factor = 1.17, offset = 0 }, -- crawl.chaosforge.org/Vorpal 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.NPenter, name = "[NPenter]" }, 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.magenta) 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.1" 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/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.lightred(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.red elseif cur <= (max * 0.50) then ratio_color = BRC.COL.lightred elseif cur <= (max * 0.75) then ratio_color = BRC.COL.yellow elseif cur < max then ratio_color = BRC.COL.lightgrey 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/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("Really fire") and prompt:contains("your spellforged") then return true end if prompt:contains("Really refrigerate") and prompt:contains("your spellforged") then return true end if prompt:contains("Permafrost Eruption might hit") and prompt:contains("servitor") then return true end if prompt:contains("Plasma Beam") and prompt:contains("might") then return true end if prompt:contains("Plasma Beam") and prompt:contains("your spellforged") then return true end if prompt:contains("Really") and prompt:contains("fire vort") then return true end if prompt:contains("Really") and prompt:contains("battle") then return true end if prompt:contains("Really hurl") and prompt:contains("your spellforged") then return true end if prompt:contains("Really attack your") and prompt:contains("rat") then return true end if prompt:contains("Really conjure a freezing cloud near your ") then return true end if prompt:contains("into that cloud of flame") and you.res_fire() == 3 then return true end if prompt:contains("into that cloud of freezing vapour") and you.res_cold() == 3 then return true end if prompt:contains("into a travel-excluded") then return true end if prompt:contains("icy armour will break") then return true 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/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/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 an 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/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/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 "" 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/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 = false, -- 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 ############################## ################################################################################################### ### 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() }