# Key bindings: # F5 — open known-spells menu (pick which one to cast) # F6 — toggle autofight_throw (ranged autofight on/off) # F7 — toggle explore_auto_rest (autoexplore rests at low HP/MP on/off) # F8 — toggle more-mores (extra force_more lines) # F9 — toggle portal-timer display # F10 — toggle Cheibriados walk (force_more on every enemy) # # Notable behaviours: # * Enter at the cast prompt repeats your last spell (enable_recast_spell). # * Newly-learned library spells start hidden (auto_hide_spells). # * autoexplore waits out negatives/cooldowns/contam before resuming. # * Tab is pure melee — quivered spells/wands are NOT auto-fired. # * Failure-≥3 spells prompt; bad items prompt; one-way hatches prompt. # * Auto-exclude rings around oklobs, statues, curse skulls, etc. ############ GENERAL SETTINGS ############ default_manual_training = true show_more = false view_delay = 300 auto_hide_spells = false enable_recast_spell = true show_resist_percent = true show_paged_inventory = false sort_menus = inv: true : equipped, charged, identified, usefulness, slot # Animations — match myRC: full default set (beam + monster flicker + player + # pickup + hp_on_scroll). toggle_autothrow (F6) flips beam,monster on/off. use_animations = beam, hp, monster_in_sight, monster, player, pickup, branch_entry, range reduce_animations = false # Combat / exploration pacing autofight_stop = 70 autofight_warning = 20 autofight_caught = true autofight_wait = true autofight_fires = false autofight_prompt_range = true fail_severity_to_quiver = 4 rest_wait_percent = 100 rest_wait_both = true rest_wait_ancestor = true explore_auto_rest = true explore_auto_rest_status = all_negative, all_cooldown, contam explore_delay = 1 travel_delay = -1 travel_key_stop = false travel_one_unsafe_move = false explore_wall_bias = 0 explore_stop_pickup_ignore = arrow, dart travel_open_doors = open # Targeting darken_beyond_range = true # Auto-explore stops explore_stop = stairs,shops,altars,portals,branches,runed_doors,runes explore_stop += artefacts,glowing_items,greedy_pickup_smart,greedy_visited_item_stack # Auto-mark exclusions around dangerous stationary/sleeping threats so # autoexplore won't walk into LOS. auto_exclude = oklob,statue,curse skull,roxanne,hyperactive ballistomycete auto_exclude += lurking horror,silent spectre,floating eye,glowing orange brain auto_exclude += guardian mummy,torpor snail,moth of wrath,sleepcap auto_exclude += ironbound convoker # HP/MP bar colors hp_colour = 100:lightgreen, 99:lightgray, 75:yellow, 50:lightred, 25:red mp_colour = 100:cyan, 99:lightgray, 75:yellow, 50:lightred, 25:red stat_colour = 1:lightred, 3:red, 7:yellow hp_warning = 50 enemy_hp_colour = green green brown brown magenta red # Safe-cast / confirmation prompts allow_self_target = prompt fail_severity_to_confirm = 3 warn_contam_cost = true warn_hatches = true bad_item_prompt = true show_invis_targeter = true equip_unequip = true always_show_zot = false always_show_gems = false more_gem_info = false confirm_action += Death's Door confirm_action += Borgnjor's Revivification note_skill_levels = 1,3,6,9,12,15,18,21,24,27 note_messages += magical essence is drained by the effort dump_message_count = 50 dump_book_spells = false dump_on_save = true dump_kill_breakdowns = true dump_item_origins = >0 regex_search = true autopickup_search = true stash_tracking = all annotate_item_class = always easy_floor_use = true prompt_menu = true equip_bar = true mlist_allow_alternative_layout = true status_caption_colour = brown # Webtiles-only HUD (silently ignored in console builds) tile_show_threat_levels = true tile_show_demon_tier = true tile_player_status_icons = constr, haste, might, brill, weak, corr, petr, -move action_panel = !?/} action_panel_show_unidentified = true action_panel_orientation = horizontal tile_layout_priority = minimap, inventory, gold_turn, command, spell, ability, monster tile_level_map_hide_messages = true tile_show_player_species = false ############ EN0N's Mini-Map Color Scheme ############ tile_upstairs_col = green tile_downstairs_col = red tile_branchstairs_col = #ffa500 tile_door_col = #c27149 tile_wall_col = #5a524c tile_explore_horizon_col = #aaaaaa tile_floor_col = #1e1b1a tile_item_col = #1e1b1a tile_feature_col = #d4be21 tile_plant_col = #4b6d39 tile_water_col = #0b5d79 tile_deep_water_col = #1212b3 tile_trap_col = #f447ff tile_transporter_col = #ff5656 tile_transporter_landing_col= #59ff89 tile_lava_col = #5f0a00 dos_use_background_intensity = true # Monster list colors (text UI) monster_list_colour = monster_list_colour += friendly:green,neutral:brown monster_list_colour += good_neutral:brown monster_list_colour += trivial:darkgrey,easy:lightgrey monster_list_colour += tough:yellow,nasty:lightred ############ AUTOPICKUP ############ ae := autopickup_exceptions # Avoid stale rings/amulets pickups ae += >ring of (poi|resist cor|fli|see) ae += >amulet of (the acr|fai|the gou|gua|har|mag|rag|ref|reg) # Junk scrolls/wands/rings ae += >scrolls? of (amn|noise) ae += >wand of rand ae += >ring of (protection from (mag|fire|cold)|mag|ste|ice|fire|pos|wil|wiz) ae += >ring of (dex|int|str) # Worth picking up ae ^= staff of # Conditional pickup tweaks : if you.race() == "Mummy" or you.race() == "Demonspawn" or you.race() == "Revenant" then ae ^= scrolls? of torment : end : if you.race() == "Tengu" or you.race() == "Merfolk" or you.race() == "Barachi" or you.race() == "Octopode" then ae += >potions? of flight : end # Avoid routing through deep water for permanent-flyers / amphibious races # (they swim/fly safely but it's still slow / risky for travel pathing). : if you.race() == "Tengu" or you.race() == "Felid" or you.race() == "Spriggan" then travel_avoid_terrain = deep water : end : if you.race() == "Troll" or you.race() == "Oni" then ae += potions? of brilliance ae += >potions? of berserk ae += >magical staff : end : if you.god() == "Zin" then ae ^= potions? of ambro : end : if you.god() == "Cheibriados" then default_autopickup = false : end : if you.race() == "Spriggan" or you.race() == "Gnoll" then default_autopickup = false : else default_autopickup = true : end ############ AUTO INSCRIBE ############ ai := autoinscribe ai += scrolls? of identify:@r1 ai += scrolls? of fog:@r2 ai += scrolls? of teleportation:@r3 ai += scrolls? of blinking:@r4 ai += scrolls? of revelation:@r5 ai += potions? of heal wounds:@q1 ai += potions? of curing:@q2 ai += potions? of might:@q3 ai += potions? of haste:@q5 ai += potions? of berserker rage:@q6 ai += potion of blood:@q9 ai += potions? of moonshine:!q ai += scrolls? of poison:!r ai += scrolls? of vulnerability:!r ai += scrolls? of silence:!r ai += scrolls? of summoning:!r ai += staff of conjuration:!a ai += staff of wizardry:Wiz, !a ai += staff of air:rElec !a ai += staff of fire:rF+ !a ai += staff of cold:rC+ !a ai += staff of death:rN+ !a ai += staff of poison:rPois !a autoinscribe ^= scroll of silence:!r ############ FORCE MORES ############ more := force_more_message # Generic dangers (status / effects) more += Your body becomes as fragile as glass! more += malevolent more += Your transformation has ended more += You feel weirdly uncertain more += You occasionally lose the ability to read scrolls when taking damage more += You feel firmly anchored in space more += You feel a little pissed off more += You are yanked towards a nearby monster! more += You are yanked towards some nearby monsters! more += You feel more firmly anchored in space more += Found a gateway leading deeper into the Abyss more += A sentinel's mark forms upon you more += You lose the ability to read scrolls when threatened more += You stop ascending the stairs more += wielding .*distort more += watched by something more += mighty Pandemonium lord more += calcifying dust hits you more += you have finished your manual of more += crystal spear hits you more += you have mastered more += feel a terrible chill more += strangely unstable more += power of zot more += expect to remain undetected more += your icy armour evaporates more += come back to life more += revert to your normal more += enter a teleport trap more += Vehumet offers you more += You fall through a shaft more += Your time is quickly running out! more += It guards the demonic rune of Zot more += You turn into a (filthy swine|insubstantial wisp|sentient fungus|bat) # Always announce a new enemy in LOS more += comes? into view # Nasty threats entering LOS more += guardian serpent.*come more += orbs? of (fire|winter|entropy) .*come more += curse toe.*come more += greater mumm.*come more += hell sentinel.*come more += curse skull.*come more += ghost moth.*come more += swamp worm bursts forth more += floating eye.*come more += vault sentinel.*come more += vault warden.*come more += ironbound (preserver|convoker|frostheart|thunderhulk).*come more += royal jelly.*come more += tzitzimitl.*come more += seraph.*come more += pearl dragon.*come more += shadow dragon.*come more += quicksilver dragon.*come more += The royal jelly comes? into view more += Cerebov.*come more += Asmodeus.*come more += Antaeus.*come more += Dispater.*come more += Ereshkigal.*come more += Mnoleg.*come more += Lom Lobon.*come more += Gloorx Vloq.*come more += Mlioglotl.*come more += The Serpent of Hell.*come # Pan lords / orb-run threats (catch-all by phrasing) more += zenata.*(comes? into view|opens the). more += parghit.*(comes? into view|opens the). more += josephina.*(comes? into view|opens the). more += vv.*(comes? into view|opens the). more += lodul.*(comes? into view|opens the). more += amaemon.*(comes? into view|opens the). # Named uniques (extant in 0.34; excludes Jozef/Purgy which are gone). more += Agnes.*(comes? into view|opens the). more += Aizul.*(comes? into view|opens the). more += Antaeus.*(comes? into view|opens the). more += Asmodeus.*(comes? into view|opens the). more += Asterion.*(comes? into view|opens the). more += Azrael.*(comes? into view|opens the). more += Blork the orc.*(comes? into view|opens the). more += Boris.*(comes? into view|opens the). more += Cerebov.*(comes? into view|opens the). more += Dispater.*(comes? into view|opens the). more += Dissolution.*(comes? into view|opens the). more += Donald.*(comes? into view|opens the). more += Dowan.*(comes? into view|opens the). more += Duvessa.*(comes? into view|opens the). more += Edmund.*(comes? into view|opens the). more += Ereshkigal.*(comes? into view|opens the). more += Erica.*(comes? into view|opens the). more += Erolcha.*(comes? into view|opens the). more += Eustachio.*(comes? into view|opens the). more += Fannar.*(comes? into view|opens the). more += Frances.*(comes? into view|opens the). more += Frederick.*(comes? into view|opens the). more += Gastronok.*(comes? into view|opens the). more += Gloorx Vloq.*(comes? into view|opens the). more += Grinder.*(comes? into view|opens the). more += Grum.*(comes? into view|opens the). more += Harold.*(comes? into view|opens the). more += Ignacio.*(comes? into view|opens the). more += Ijyb.*(comes? into view|opens the). more += Ilsuiw.*(comes? into view|opens the). more += Jessica.*(comes? into view|opens the). more += Jorgrun.*(comes? into view|opens the). more += Jory.*(comes? into view|opens the). more += Joseph.*(comes? into view|opens the). more += Josephine.*(comes? into view|opens the). more += Khufu.*(comes? into view|opens the). more += Kirke.*(comes? into view|opens the). more += Lom Lobon.*(comes? into view|opens the). more += Louise.*(comes? into view|opens the). more += Maggie.*(comes? into view|opens the). more += Mara.*(comes? into view|opens the). more += Margery.*(comes? into view|opens the). more += Maurice.*(comes? into view|opens the). more += Menkaure.*(comes? into view|opens the). more += Mennas.*(comes? into view|opens the). more += Mioglotl.*(comes? into view|opens the). more += Mnoleg.*(comes? into view|opens the). more += Murray.*(comes? into view|opens the). more += Nergalle.*(comes? into view|opens the). more += Nessos.*(comes? into view|opens the). more += Nikola.*(comes? into view|opens the). more += Norris.*(comes? into view|opens the). more += Pikel.*(comes? into view|opens the). more += Polyphemus.*(comes? into view|opens the). more += Prince Ribbit.*(comes? into view|opens the). more += Psyche.*(comes? into view|opens the). more += Robin.*(comes? into view|opens the). more += Rupert.*(comes? into view|opens the). more += Saint Roka.*(comes? into view|opens the). more += Sigmund.*(comes? into view|opens the). more += Snorg.*(comes? into view|opens the). more += Sonja.*(comes? into view|opens the). more += Terence.*(comes? into view|opens the). more += The Lernaean hydra.*(comes? into view|opens the). more += The royal jelly.*(comes? into view|opens the). more += The Serpent of Hell.*(comes? into view|opens the). more += Tiamat.*(comes? into view|opens the). more += Urug.*(comes? into view|opens the). more += Xtahua.*(comes? into view|opens the). # XL-gated reminders : if you.xl() <= 7 then more += is wielding .*elec : end : if you.xl() <= 11 then more += two-headed ogre.*come : end : if you.xl() <= 16 then more += sorcerer.*come more += ogre mage.*come : end : if you.xl() <= 18 then more += (a|2|3|4|5|6|7|8|9) wizard.*come more += occultist.*come : end : if you.xl() <= 22 then more += wielding .*distortion more += demonologist.*come ai += weapon .*distort:!w : end : if you.race() == "Demonspawn" or you.race() == "Mummy" or you.race() == "Revenant" then more += wielding .*holy : end ############ MONSTER ALERT (first-sight --More--) ############ # Use monster_alert for "first time I see X this level" prompts that don't # spam every turn the monster is visible. monster_alert += uniques, unusual monster_alert += orb of (fire|winter|entropy) monster_alert += curse skull monster_alert += ghost moth monster_alert += tentacled monstrosity monster_alert += pan lord unusual_monster_items = disto, chaos, curare, throwing net, silver, pain, freezing, flaming, electrocution ############ RUNREST / TRAVEL INTERRUPTS ############ # Ignore harmless interrupts during rest/travel. runrest_ignore_message += You feel .* sick runrest_ignore_message += disappears? in a puff of smoke runrest_ignore_message += engulfed in a cloud of smoke runrest_ignore_message += standing in the rain runrest_ignore_message += safely over a trap runrest_ignore_message += A.+toadstool withers and dies runrest_ignore_message += Your scales? feels? .* against your skin runrest_ignore_message += Your magical effects are unaffected runrest_ignore_message += Your transformation is almost over # Stop rest/travel on critical status changes. runrest_stop_message += You feel yourself slow down runrest_stop_message += sense of stasis runrest_stop_message += You are starting to lose your buoyancy runrest_stop_message += Your icy armour evaporates runrest_stop_message += Your transformation is almost over runrest_stop_message += Your shroud falls apart runrest_stop_message += Your hand stops glowing runrest_stop_message += Your antimagic field dissipates # Ignore distant non-threats (they don't actually approach during rest). runrest_ignore_monster += butterfly:1 runrest_ignore_monster += plant:1 runrest_ignore_monster += fungus:1 runrest_ignore_monster += bush:1 # Suppress noisy regen messages during rest. message_colour += mute:You start (resting|searching) message_colour += mute:Done waiting message_colour += mute:^Unknown command\.$ message_colour += lightred:warning:You are in a bad position message_colour += lightcyan:god:Vehumet message_colour += lightmagenta:god:Xom ############ MENU COLOURS / ITEM HIGHLIGHTS ############ # Highlight items by tag inside menus. menu_colour ^= inventory:lightcyan:.*god gift menu_colour ^= inventory:lightmagenta:.*unrand menu_colour ^= inventory:yellow:.*artefact menu_colour ^= inventory:lightred:.*forbidden menu_colour ^= inventory:lightblue:.*emergency_item menu_colour ^= inventory:darkgrey:.*useless_item menu_colour ^= inventory:lightgreen:.*ego menu_colour ^= pickup:white:gold piece menu_colour ^= notes:white:Reached XP level menu_colour ^= notes:cyan:Identified menu_colour ^= notes:lightred:HP: 1/ # Highlight unidentified consumables (yellow) and uncast books (green). menu_colour ^= inventory:yellow:potion of (unknown|.*\?) menu_colour ^= inventory:yellow:scroll labelled menu_colour ^= inventory:yellow:wand of (unknown|.*\?) menu_colour ^= inventory:lightgreen:book of (?!.*memorised) menu_colour ^= inventory:lightgreen:.*manual of ############ DROP FILTER ############ df := drop_filter df += brilliance df += torment df += amnesia df += ring of positive energy df += ring of magical power df += amulet of magic regeneration df += ring of flight df += wand of charming df += wand of iceblast df += wand of paralysis df += wand of disintegration ############ ITEM SLOTS ############ item_slot ^= wand of digging:v item_slot ^= wand of acid:c item_slot ^= wand of quicksilver:c item_slot ^= wand of light:c item_slot ^= wand of iceblast:i item_slot ^= wand of roots:i item_slot ^= (condenser vane):V item_slot ^= (tremorstone):B item_slot ^= (lightning rod):R item_slot ^= (box of beasts):C item_slot ^= (phantom mirror):M ############ SPELL SLOTS (0.34-current only) ############ # Removed compared to sg.txt: Conjure flame, Dazzling flash, Gell's gravitas, # Spider form, Spellforged servitor (renamed), Animate armour (renamed), # Summon lightning spire (renamed), Lightning bolt (replaced). # Renames applied: Awaken Armour, Spellspark Servitor, Forge Lightning Spire. spell_slot += Airstrike:aAeEiIrRsStTkK spell_slot += Alistair's intoxication:aiAIsnSNlLtToOxXcCrR spell_slot += Animate dead:adADeEnNiImMtT spell_slot += Apportation:aAnNpPoOrRtTiI spell_slot += Awaken Armour:aAerERnNimIMoOuUtT spell_slot += Blink:bBkKlLiInN spell_slot += Borgnjor's revivification:brBRsnSNoeOEvVgiGIjJfFcCaAtT spell_slot += Borgnjor's vile clutch:bvcBVCsehSEHoilOILruRUgtGTnNjJ spell_slot += Call canine familiar:cfCFlerLERaAnmNMiI spell_slot += Call imp:ciCIlpLPamAM spell_slot += Cause fear:cfCFerERaAuUsS spell_slot += Chain lightning:clCLngNGhiHIaAtT spell_slot += Confusing touch:ctCTghGHoOnuNUfFsSiI spell_slot += Conjure ball lightning:cblCBLegEGoaiOAInNjhJHutUTrR spell_slot += Death channel:dcDChlHLeEaAtnTN spell_slot += Death's door:dDsrSReoEOaAtThH spell_slot += Discord:dDiIsScCoOrR spell_slot += Disjunction:JdDnNiIsSjJuUcCtToO spell_slot += Dispel undead:duDUlLinINsSpePEaA spell_slot += Dispersal:dDlLiIsSpPeErRaA spell_slot += Dragon's call:dcDCslSLraRAgGoOnN spell_slot += Ensorcelled hibernation:ehEHdnDNiIsbSBoOrRcCaAltLT spell_slot += Eringya's noxious bog:enbENBsgSGroROixIXyuYUaA spell_slot += Fire storm:fsFSemEMitITroRO spell_slot += Fireball:fFlLiIrReEbBaA spell_slot += Forge Lightning Spire:slSLngeNGEuipUIPmMhrHRotOT spell_slot += Foxfire:fFeEoOxXiIrR spell_slot += Freeze:fFeErRzZ spell_slot += Freezing cloud:fcFCgdGDrlRLeoEOuUzZiInN spell_slot += Frozen ramparts:frFRnsNSaAomOMzpZPeEtT spell_slot += Fulminant prism:fpFPtmTMurURliLIsSnNaA spell_slot += Hailstorm:hHmMaAiIlLsStToOrR spell_slot += Haunt:hHtTaAuUnN spell_slot += Ignite poison:ipIPenENgoGOsStT spell_slot += Ignition:iInNgGtToO spell_slot += Infestation:iInNfFeEsStTaAoO spell_slot += Inner flame:ifIFreREnlNLaAmM spell_slot += Irradiate:iIeErRaAdDtT spell_slot += Iskenderun's battlesphere:ibIBseSEaAktKTnlNLdDrpRPuhUH spell_slot += Iskenderun's mystic blast:imbIMBsctSCTylYLkaKAeEnNdDrRuU spell_slot += Leda's liquefaction:lLsnSNeiEIdqDQauAUfFcCtToO spell_slot += Lee's rapid deconstruction:lrdLRDsnSNeaEApcPCioIOtTuU spell_slot += Lehudib's crystal spear:lcsLCSrRepEPhyHYuaUAdtDTiIbB spell_slot += Lesser beckoning:lbLBrgRGeEscSCkKoOnNiI spell_slot += Magic dart:mdMDctCTaAgrGRiI spell_slot += Malign gateway:mgMGnyNYaAltLTieIEwW spell_slot += Manifold assault:maMAdtDTsSnNiIfuFUolOL spell_slot += Maxwell's capacitive coupling:mcMCsegSEGaoAOxpuXPUwWlLiItnTNvV spell_slot += Mephitic cloud:mcMCdDelELpoPOhuHUiItT spell_slot += Metabolic englaciation:meMEcnCNtgTGalALbBoOiI spell_slot += Monstrous menagerie:mMseSEoOnNaAtgTGrRuiUI spell_slot += Olgreb's toxic radiance:otrOTRsceSCElaLAgxdGXDiIbnBN spell_slot += Orb of destruction:odODbnBNreREsStTuUcCiI spell_slot += Ozocubu's armour:oaOAsrSRzZmMcCuUbB spell_slot += Ozocubu's refrigeration:orORsnSNzeZEfFcCuiUIbgBGaAtT spell_slot += Passage of golubria:pgPGeaEAoOslSLuUbBrRiI spell_slot += Passwall:pPlLaAsSwW spell_slot += Petrify:pPyYeEtTrRiIfF spell_slot += Poisonous vapours:pvPVsSoaOAiIuUnrNR spell_slot += Polar vortex:pvPVrxRXoOlLatATeE spell_slot += Sandblast:sStTaAnNdDbBlL spell_slot += Searing ray:srSRgyGYeaEAiInN spell_slot += Shatter:sSrRhHaAtTeE spell_slot += Shock:sSkKhHoOcC spell_slot += Silence:sSeEiIlLnNcC spell_slot += Slow:sSwWlLoO spell_slot += Spellspark Servitor:sSdrDRpePElvLViIftFToOgG spell_slot += Starburst:sStTaArRbBuU spell_slot += Static discharge:sdSDceCEtiTIaAhHrRgG spell_slot += Sticky flame:sfSFyeYEtlTLiaIAcmCMkK spell_slot += Sting:sSgGtTiInN spell_slot += Stone arrow:saSAewEWtrTRoOnN spell_slot += Sublimation of blood:sbSBndNDulULoOiImMaAtT spell_slot += Summon forest:sfSFntNTuoUOmrMReE spell_slot += Summon horrible things:shtSHTneNEuoUOmriMRIgGbBlL spell_slot += Summon hydra:shSHnaNAuyUYmdMDrRoO spell_slot += Summon ice beast:sibSIBnetNETucUCmaMAoO spell_slot += Summon mana viper:smvSMVnarNARuiUIpPeEoO spell_slot += Summon small mammal:smSMnlNLuaUAoO spell_slot += Swiftness:sSwWiIfFtTnNeE spell_slot += Teleport other:toTOrReElhLHpP spell_slot += Tukima's dance:tdTDseSEuaUAknKNicICmM spell_slot += Vampiric draining:vdVDcgCGarARmMpiPInN spell_slot += Yara's violent unravelling:yvuYVUstgSTGainAINroROlLeE ############ ANTI-PROMPT SQUELCHERS ############ { function c_answer_prompt(prompt) if prompt == "Annotate level on other end of current stairs?" then return false end if prompt == "Are you sure you want to leave the Dungeon? This will make you lose the game!" then return false end end } ############ LUA: STARTUP / READY HOOK ############ { local opened_skills = false local greeted = false function ready() if you.turns() == 0 and not opened_skills then opened_skills = true crawl.sendkeys("m") end if not greeted then greeted = true crawl.mpr("F5=spells " .. "F6=autothrow " .. "F7=autorest " .. "F8=more-mores " .. "F9=portal-time " .. "F10=cheiwalk") end end } ############ LUA: TOGGLES ############ { local aft = false function toggle_autothrow() if aft then crawl.setopt("use_animations += beam, monster") crawl.setopt("autofight_throw = false") crawl.mpr("Autofight_throw is OFF.") else crawl.setopt("use_animations -= beam, monster") crawl.setopt("autofight_throw = true") crawl.mpr("Autofight_throw is ON.") end aft = not aft end local autorestmode = true function toggle_autorest() if autorestmode then crawl.setopt("explore_auto_rest = false") crawl.mpr("Autorest during explore is OFF.") else crawl.setopt("explore_auto_rest = true") crawl.mpr("Autorest during explore is ON.") end autorestmode = not autorestmode end local mmores = false function toggle_more_mores() if mmores then crawl.setopt("force_more_message -= Found") crawl.mpr("Found-X mores OFF.") else crawl.setopt("force_more_message += Found") crawl.mpr("Found-X mores ON.") end mmores = not mmores end local portalmode = false function toggle_portal_mode() if portalmode then crawl.setopt("show_game_time = false") crawl.mpr("Portal timer display OFF.") else crawl.setopt("show_game_time = true") crawl.mpr("Portal timer display ON.") end portalmode = not portalmode end local cheiwalk = false function toggle_cheiwalk() if cheiwalk then crawl.setopt("force_more_message -= comes? into view") crawl.mpr("Cheiwalk OFF (no more on enemy sight).") else crawl.setopt("force_more_message += comes? into view") crawl.mpr("Cheiwalk ON (more on every enemy).") end cheiwalk = not cheiwalk end -- Pretty-print current resistance pips on demand. local function pip(n) if n == nil or n == 0 then return "·" end if n < 0 then return "x" .. (-n) .. "" end if n >= 3 then return "+++" end return "" .. string.rep("+", n) .. "" end function print_resists() local hp, hpm = you.hp() local mp, mpm = you.mp() crawl.mpr(string.format( "HP %d/%d MP %d/%d " .. "rF%s rC%s rElec%s rPois%s rN%s rCorr%s Will%s SInv%s", hp, hpm, mp, mpm, pip(you.res_fire()), pip(you.res_cold()), pip(you.res_shock()), pip(you.res_poison()), pip(you.res_draining()), pip(you.res_corr()), pip(you.willpower()), pip(you.see_invisible() and 1 or 0))) end } ############ AUTOPICKUP LUA HOOK ############ { local function autopickup(it, name) local class = it.class(true) local weap = items.equipped_at("Weapon") local shie = items.equipped_at("Shield") if it.is_useless then return false end if class == "armour" then local aux_slots = {cloak="Cloak", helmet="Helmet", gloves="Gloves", boots="Boots", shield="Shield", barding="Barding"} local st, _ = it.subtype() if aux_slots[st] == "Shield" then if (weap == nil or weap.hands == 1) and (shie == nil or it.branded) then return true end elseif aux_slots[st] ~= nil and items.equipped_at(aux_slots[st]) == nil then return true elseif st ~= "body" and it.branded then return true end end return nil end add_autopickup_func(autopickup) } ############ ABILITY SLOTS (stable letters for god abilities) ############ ability_slot += Renounce Religion:!@ ability_slot += Channel Magic:cC ability_slot += Stop Channelling:sS ability_slot += Bend Time:bB ability_slot += Enkindle:eE ############ GEAR SLOTS (stable letters for common artefacts) ############ gear_slot += amulet of reflection:R gear_slot += amulet of regeneration:G gear_slot += amulet of guardian spirit:S gear_slot += amulet of the acrobat:A gear_slot += ring of willpower:W gear_slot += ring of evasion:V gear_slot += ring of protection:P gear_slot += ring of resist corrosion:C ############ MACROS / KEYBINDINGS ############ # F5: open the known-spells menu (manual pick from learned spells). bindkey = [f5] CMD_DISPLAY_SPELLS show_more = false macros += M 4 ===bonsai_smart_forge { local function cdist(x,y) return math.max(math.abs(x),math.abs(y)) end local function is_solid(x,y) local f=view.feature_at(x,y) if not f then return false end return f=="rock_wall" or f=="stone_wall" or f=="permarock_wall" or f=="unnaturally_hard_rock_wall" or f=="metal_wall" or f=="crystal_wall" or f=="closed_door" or f=="runed_door" or f=="sealed_door" or f=="tree" or f=="mangrove" or f=="slimy_wall" end local function has_adjacent_wall() for dx=-1,1 do for dy=-1,1 do if (dx~=0 or dy~=0) and is_solid(dx,dy) then return true end end end return false end local function detect_corridor() local walls=0 for x=-1,1 do for y=-1,1 do if (x~=0 or y~=0) and is_solid(x,y) then walls=walls+1 end end end return walls>=5 end -- Summon spell table (sc.rc-style scoring) local summon_spells={ {n="Awaken Armour",short="Armour",cap=1,fn={"animated armour","armour echo"}, needs_armour=true,role="tank", power=function(p) return 20+p/3 end,escape=3,tank=9,multi=2}, {n="Forge Lightning Spire",short="Spire",cap=1,fn={"lightning spire"},role="ranged", power=function(p) return 25+p/3 end,escape=2,tank=3,multi=5}, {n="Forge Blazeheart Golem",short="Golem",cap=1,fn={"blazeheart golem"},role="ranged", power=function(p) return 25+p/3 end,escape=3,tank=7,multi=6}, {n="Forge Phalanx Beetle",short="Beetle",cap=1,fn={"phalanx beetle"},role="tank", power=function(p) return 18+p/4 end,escape=7,tank=9,multi=2}, {n="Alistair's Walking Alembic",short="Alembic",cap=1,fn={"walking alembic"},role="ranged", power=function(p) return 35+p/3 end,escape=2,tank=4,multi=6}, {n="Hoarfrost Cannonade",short="Cannon",cap=0,fn={"hoarfrost cannon"},role="ranged", power=function(p) return 28+p/3 end,escape=2,tank=3,multi=5}, {n="Forge Monarch Bomb",short="Bomb",cap=1,fn={"monarch bomb"},role="ranged", power=function(p) return 30+p/3 end,escape=2,tank=4,multi=5}, {n="Diamond Sawblades",short="Saw",cap=0,fn={"diamond sawblade"},role="tank", power=function(p) return 22+p/4 end,escape=2,tank=5,multi=3}, {n="Splinterfrost Shell",short="Shell",cap=0,fn={"splinterfrost"},role="tank", power=function(p) return 24+p/4 end,escape=4,tank=7,multi=2, target_dir=true}, {n="Spellspark Servitor",short="Servitor",cap=1,fn={"spellforged servitor"},role="ranged", power=function(p) return 30+p/3 end,escape=2,tank=4,multi=5}, {n="Platinum Paragon",short="Paragon",cap=1,fn={"platinum paragon"},recast=true,role="tank", power=function(p) return 50+p/3 end,escape=3,tank=8,multi=5}, {n="Launch Clockwork Bee",short="Bee",cap=1,fn={"clockwork bee"},role="ranged", target_enemy=true, power=function(p) return 15+p/4 end,escape=2,tank=2,multi=2}} forge_last_cast = forge_last_cast or {} function bonsai_smart_forge() local turn=you.turns() local mp,mp_max=_G.you.mp() local ok_race,race=pcall(function() return you.race() end) if ok_race and race=="Djinni" then mp=you.hp()-10 end if you.silenced() then crawl.mpr("Silenced!") return end local antimagic_debuff=you.status("antimagic") local fail_cap=antimagic_debuff and 10 or 20 local xl=you.xl() local depth_tier=2 if xl<=10 then depth_tier=1 elseif xl>=18 then depth_tier=3 end -- Reserve MP for escape spells local escape_spells={"Blink","Passage of Golubria","Dispersal","Disjunction"} local reserve_mp=0 local reserve_spell=nil for _,esc in ipairs(escape_spells) do if spells.memorised(esc) then local c=spells.mana_cost(esc) if c and c>reserve_mp then reserve_mp=c;reserve_spell=esc end end end -- Scan LOS local los=you.los() local t={} local friends={} local adj_hostiles=0 local enemy_at={} local total_threat=0 local max_single_threat=0 local has_caster=false local has_ranged_enemy=false local has_wounded=false local min_enemy_dist=99 -- Construct tracking local found_bomb=false local bomblet_positions={} local found_bee=false local cannon_positions={} local found_sawblades=false local found_barricades=false local found_armour=false local found_bsph=false local found_servitor=false local found_mortar=false -- Friendly constructs for Tempering local constructs={} for x=-los,los do for y=-los,los do local m=monster.get_monster_at(x,y) if m and not m:is_firewood() then if m:attitude()==0 then local d=cdist(x,y) local ok_hp,mhp=pcall(function() return m:max_hp() end) local ehp=20 if ok_hp and mhp then local s=tostring(mhp):gsub("about ",""):gsub("~","") ehp=tonumber(s) or 20 end local ok_sp,sp=pcall(function() return m:spells() end) local is_caster=(ok_sp and sp and #sp>0) local ok_dl,dl=pcall(function() return m:damage_level() end) local dmg_level=(ok_dl and dl) or 0 local desc=m:target_desc() or "" local is_summoned=string.find(desc,"summoned") and true or false local ok_sf,sf=pcall(function() return m:is_safe() end) t[#t+1]={m=m,x=x,y=y,dist=d,hp=ehp,is_caster=is_caster,damage_level=dmg_level, desc=desc,is_summoned=is_summoned,is_safe=(ok_sf and sf) or false} enemy_at[x..","..y]=true total_threat=total_threat+ehp if ehp>max_single_threat then max_single_threat=ehp end if d<=1 then adj_hostiles=adj_hostiles+1 end if d=4 then has_ranged_enemy=true end end if dmg_level>=2 then has_wounded=true end elseif m:attitude()>0 then local nm=m:name() -- Construct detection if string.find(nm,"echoed") or string.find(nm,"animated armour") then found_armour=true elseif nm=="monarch bomb" then found_bomb=true elseif string.find(nm,"bomblet") then bomblet_positions[#bomblet_positions+1]={x=x,y=y} elseif string.find(nm,"clockwork bee") then found_bee=true elseif nm=="hoarfrost cannon" then cannon_positions[#cannon_positions+1]={x=x,y=y} elseif nm=="diamond sawblade" then found_sawblades=true elseif string.find(nm,"splinterfrost") then found_barricades=true elseif nm=="battlesphere" then found_bsph=true elseif string.find(nm,"spellforged servitor") or nm=="spellspark servitor" then found_servitor=true elseif string.find(nm,"mortar") then found_mortar=true end -- Track all friendlies for summon cap checking + combo awareness local ok_dl2,dl2=pcall(function() return m:damage_level() end) local hr=1.0 if ok_dl2 and dl2 then hr=(6-dl2)/6 end friends[#friends+1]={name=nm,x=x,y=y,hp_ratio=hr} -- Tempering: un-tempered constructs local ok_t,tempered=pcall(function() return m:status("tempered") end) if not (ok_t and tempered) then local adj_en=0 for dx=-1,1 do for dy=-1,1 do if (dx~=0 or dy~=0) and enemy_at[(x+dx)..",".. (y+dy)] then adj_en=adj_en+1 end end end constructs[#constructs+1]={x=x,y=y,name=nm,adj_enemies=adj_en} end end end end end local has_enemies=(#t>0) local hp_r,hp_max=you.hp() local hp_ratio=hp_r/hp_max local in_corridor=detect_corridor() -- Neighbor counts for enemies for _,e in ipairs(t) do local nb=0 for dx=-1,1 do for dy=-1,1 do if (dx~=0 or dy~=0) and enemy_at[(e.x+dx)..",".. (e.y+dy)] then nb=nb+1 end end end e.neighbors=nb end -- Enemy centroid for Shell aiming local cx,cy=0,0 if #t>0 then for _,e in ipairs(t) do cx=cx+e.x;cy=cy+e.y end cx=cx/#t;cy=cy/#t local len=math.sqrt(cx*cx+cy*cy) if len>0 then cx=math.floor(cx/len+0.5);cy=math.floor(cy/len+0.5) else cx=1;cy=0 end end -- Sort constructs by adjacent enemy count (best tempering target first) table.sort(constructs,function(a,b) return a.adj_enemies>b.adj_enemies end) local temper_target=nil if #constructs>0 and constructs[1].adj_enemies>0 then temper_target=constructs[1] end -- Bomblet detonation scoring local bomblets_adj_enemies=0 for _,bp in ipairs(bomblet_positions) do local near=false for dx=-1,1 do for dy=-1,1 do if not near and enemy_at[(bp.x+dx)..",".. (bp.y+dy)] then near=true end end end if near then bomblets_adj_enemies=bomblets_adj_enemies+1 end end -- Cannon effectiveness local found_cannons=(#cannon_positions>0) local cannons_effective=false for _,cp in ipairs(cannon_positions) do for _,e in ipairs(t) do local d=math.max(math.abs(e.x-cp.x),math.abs(e.y-cp.y)) if d<=los then cannons_effective=true break end end if cannons_effective then break end end -- Spike launcher detection + effectiveness local found_spike=false local spike_x,spike_y=0,0 for dx=-1,1 do for dy=-1,1 do if dx~=0 or dy~=0 then local f=view.feature_at(dx,dy) if f and string.find(f,"spike") then found_spike=true;spike_x=dx;spike_y=dy end end end end local spike_effective=false if found_spike then for _,e in ipairs(t) do local d=math.max(math.abs(e.x-spike_x),math.abs(e.y-spike_y)) -- Spike launcher only hits enemies adjacent to itself (d<=1) -- But melee enemies walking toward player will pass through if d<=1 or (not e.is_caster and d<=3) then spike_effective=true break end end end -- Best enemy targets local best_target=nil local best_thr=0 for _,e in ipairs(t) do local thr=e.hp/(e.dist+1) if thr>best_thr then best_thr=thr;best_target=e end end local grapnel_target=nil local grapnel_best=0 for _,e in ipairs(t) do if e.dist>=1 then local thr=e.hp/(e.dist+1) if thr>grapnel_best then grapnel_best=thr;grapnel_target=e end end end -- === Situation assessment (sc.rc style for summons) === local escape_urgency=0 if hp_ratio<0.3 then escape_urgency=2.0 elseif hp_ratio<0.5 then escape_urgency=1.0+(0.5-hp_ratio)*4 elseif adj_hostiles>0 then escape_urgency=0.5+adj_hostiles*0.3 end local swarm_factor=math.min(2.0,total_threat/120) local tank_need=0 if adj_hostiles>0 and hp_ratio>=0.5 then tank_need=1.0+math.min(max_single_threat/40,2.0) end if in_corridor then tank_need=tank_need*1.5;swarm_factor=swarm_factor*0.5 end local retreating=(hp_ratio<0.3 and adj_hostiles==0) local have_time=(min_enemy_dist>=4) -- Combo awareness: categorize existing friends local has_tank_friend=false local has_ranged_friend=false local has_blocker=false local tank_names={"phalanx beetle","animated armour","armour echo","cactus giant", "platinum paragon","hydra","ice beast","inugami","crocodile","martyred shade", "blazeheart golem","splinterfrost"} local ranged_names={"lightning spire","spellforged servitor","blazeheart golem", "walking alembic","mana viper","cerulean imp","hoarfrost cannon","monarch bomb"} for _,f in ipairs(friends) do if not has_tank_friend then for _,nm in ipairs(tank_names) do if string.find(f.name,nm) then has_tank_friend=true break end end end if not has_ranged_friend then for _,nm in ipairs(ranged_names) do if string.find(f.name,nm) then has_ranged_friend=true break end end end if not has_blocker then for dx=-1,1 do for dy=-1,1 do if enemy_at[(f.x+dx)..",".. (f.y+dy)] then has_blocker=true end end end end end -- Helper: count active summons matching name list local function count_friends(names) local c=0 for _,f in ipairs(friends) do for _,nm in ipairs(names) do if string.find(f.name,nm) then c=c+1 break end end end return c end -- Helper: check spell availability local function check_spell(name) if not spells.memorised(name) then return false end local cost=spells.mana_cost(name) if cost>mp then return false end if reserve_mp>0 and (mp-cost)=fail_cap then return false end local ok_sev,sev=pcall(function() return spells.fail_severity(name) end) if ok_sev and sev>2 then return false end return true,cost end local candidates={} -- ===================================================== -- SECTION 1: CONSTRUCT MANAGEMENT (priority-based) -- ===================================================== -- Monarch Bomb DETONATE (recast when bomblets near enemies — massive AoE) if has_enemies and found_bomb and bomblets_adj_enemies>=1 then local n="Forge Monarch Bomb" local ok,cost=check_spell(n) if ok then local pri=88+bomblets_adj_enemies*5 candidates[#candidates+1]={n=n,pri=pri,x=0,y=0, tag="DETONATE("..bomblets_adj_enemies.."hit)",c=cost} end end -- Fortress Blast (panic: 3+ adjacent hostiles) if adj_hostiles>=3 then local n="Fortress Blast" local ok,cost=check_spell(n) if ok then candidates[#candidates+1]={n=n,pri=95,x=0,y=0,tag="BLAST",c=cost} end end -- Percussive Tempering (buff un-tempered construct with most adjacent enemies) if has_enemies and temper_target then local n="Nazja's Percussive Tempering" local ok,cost=check_spell(n) if ok then local pri=80+temper_target.adj_enemies*5 candidates[#candidates+1]={n=n,pri=pri,x=temper_target.x,y=temper_target.y, tag="TEMPER("..temper_target.name..")",c=cost} end end -- Kinetic Grapnel (pull + damage) if has_enemies and grapnel_target then local n="Kinetic Grapnel" local ok,cost=check_spell(n) if ok then local range=spells.range(n) or 4 if grapnel_target.dist<=range then local pri=52 -- Better at range (pull effect matters more) if grapnel_target.dist>=3 and adj_hostiles==0 then pri=pri+8 end -- Fewer enemies = pull more impactful if #t<=2 and grapnel_target.hp>=30 then pri=pri+6 end -- Less useful if ranged summons already handling it if has_ranged_friend then pri=pri-8 end candidates[#candidates+1]={n=n,pri=pri,x=grapnel_target.x,y=grapnel_target.y, tag="PULL",c=cost} end end end -- Battlesphere (pre-combat setup: only when all enemies >3 tiles) if has_enemies and not found_bsph and min_enemy_dist>3 then local n="Iskenderun's Battlesphere" local ok,cost=check_spell(n) if ok then candidates[#candidates+1]={n=n,pri=68,x=0,y=0,tag="BSPH",c=cost} end end -- Spike Launcher (deploy only if none exists) if not found_spike then local n="Construct Spike Launcher" local ok,cost=check_spell(n) if ok then local pri=has_enemies and 48 or 55 -- Launcher placed adj to player, hits enemies adj to itself (1-2 tiles from player) -- Best when enemies at 2-3 tiles: will be in range next turn if has_enemies and min_enemy_dist>=2 and min_enemy_dist<=3 then pri=pri+14 end -- OK when enemies already adjacent (launcher fires immediately on some) if has_enemies and min_enemy_dist<=1 then pri=pri+5 end -- Bad when enemies far — may not reach before duration expires if has_enemies and min_enemy_dist>=5 then pri=pri-15 end if depth_tier==3 then pri=pri-15 end -- less useful late game candidates[#candidates+1]={n=n,pri=pri,x=0,y=0,tag="SPIKE",c=cost} end end -- ===================================================== -- SECTION 2: SUMMON SCORING (sc.rc style) -- ===================================================== if has_enemies then for _,sp in ipairs(summon_spells) do if spells.memorised(sp.n) then local cost=spells.mana_cost(sp.n) local fail=spells.fail(sp.n) local ok_sev,sev=pcall(function() return spells.fail_severity(sp.n) end) if fail0 and current>=sp.cap and not sp.recast if not at_cap then -- Precondition checks local skip=false if sp.needs_armour and not items.equipped_at("body armour") then skip=true end -- Skip already-active unique constructs if sp.n=="Awaken Armour" and found_armour then skip=true end if sp.n=="Spellspark Servitor" and found_servitor then skip=true end if sp.n=="Forge Monarch Bomb" and found_bomb then skip=true end if sp.n=="Launch Clockwork Bee" and (found_bee or min_enemy_dist<3) then skip=true end if sp.n=="Diamond Sawblades" and found_sawblades then skip=true end if sp.n=="Splinterfrost Shell" and found_barricades then skip=true end -- Hoarfrost Cannonade: deploy or recast if ineffective if sp.n=="Hoarfrost Cannonade" then if found_cannons and cannons_effective then skip=true elseif found_cannons and not cannons_effective then skip=false -- recast end end if not skip then local pow=spells.power_perc(sp.n) or 0 local base=sp.power(pow) local escape_bonus=sp.escape*escape_urgency*3 local tank_bonus=sp.tank*tank_need*2 local multi_bonus=sp.multi*swarm_factor*2 local danger_mult=1.0 -- Platinum Paragon recast: boost in swarms if sp.recast and sp.n=="Platinum Paragon" and current>=sp.cap then if #t>=3 then danger_mult=danger_mult*1.5 else danger_mult=danger_mult*0.8 end end -- Combo synergy local combo_mult=1.0 if has_tank_friend and sp.role=="ranged" then combo_mult=combo_mult*1.4 end if has_ranged_friend and sp.role=="tank" then combo_mult=combo_mult*1.4 end if has_blocker and sp.role=="multi" then combo_mult=combo_mult*1.3 end -- Retreat mode if retreating then if sp.escape>=5 or sp.tank>=7 then combo_mult=combo_mult*1.6 end if sp.role=="ranged" and sp.tank<=3 then combo_mult=combo_mult*0.5 end end -- Lightning Spire urgency: shoots immediately at long range if sp.n=="Forge Lightning Spire" then if min_enemy_dist<=4 then combo_mult=combo_mult*1.8 end -- enemies close, spire fires NOW if adj_hostiles>=2 then combo_mult=combo_mult*1.3 end -- surrounded, need ranged help end -- Monarch Bomb: deploy ASAP, needs time to spawn bomblets for detonation if sp.n=="Forge Monarch Bomb" then combo_mult=combo_mult*2.0 -- always high priority, needs early deploy if min_enemy_dist>=3 then combo_mult=combo_mult*1.5 end -- more time = more bomblets if #t>=3 then combo_mult=combo_mult*1.3 end -- detonation AoE shines vs groups end -- Spellspark Servitor: long-range AoE caster, massive damage over time if sp.n=="Spellspark Servitor" then combo_mult=combo_mult*1.8 -- always strong, extremely mana-efficient if min_enemy_dist>=3 then combo_mult=combo_mult*1.4 end -- deploy early = more casts if #t>=3 then combo_mult=combo_mult*1.3 end -- AoE spells shine vs groups end -- Diamond Sawblades: SHRED hits enemies adjacent to each blade at (±2,±2). -- Kill zone = cells with min(|x|,|y|)>=1 and max(|x|,|y|)<=3. -- Score by enemies currently in zone + enemies whose Chebyshev-approach path -- toward player crosses the zone within 2 turns. if sp.n=="Diamond Sawblades" then local function in_kill_zone(x,y) local ax,ay=math.abs(x),math.abs(y) return ax>=1 and ay>=1 and ax<=3 and ay<=3 end local saw_score=0 for _,e in ipairs(t) do if in_kill_zone(e.x,e.y) then saw_score=saw_score+1.0 -- already in kill zone, hit immediately elseif not e.is_caster then -- Project up to 3 steps of Chebyshev approach toward player local px,py=e.x,e.y for step=1,3 do if px==0 and py==0 then break end if px>0 then px=px-1 elseif px<0 then px=px+1 end if py>0 then py=py-1 elseif py<0 then py=py+1 end if in_kill_zone(px,py) then saw_score=saw_score+(step==1 and 0.7 or step==2 and 0.4 or 0.2) break end end end end if saw_score>=2.0 then combo_mult=combo_mult*1.7 elseif saw_score>=1.0 then combo_mult=combo_mult*1.4 elseif saw_score>=0.4 then combo_mult=combo_mult*1.15 else combo_mult=combo_mult*0.6 end -- nobody enters kill zone (e.g. all ranged casters at dist>=4) if in_corridor then combo_mult=combo_mult*1.2 end -- funneled enemies must walk through corners if min_enemy_dist>=4 and saw_score>=1.0 then combo_mult=combo_mult*1.2 end -- pre-deploy end -- Walking Alembic: AoE poison DoT + Might buff — great vs groups if sp.n=="Alistair's Walking Alembic" then if #t>=3 then combo_mult=combo_mult*1.6 end -- AoE DoT shines in swarms if #t>=2 then combo_mult=combo_mult*1.3 end -- still good vs 2 if has_tank_friend then combo_mult=combo_mult*1.3 end -- Might buff on tank = huge end -- Blazeheart Golem: boost when enemies in medium range (golem walks+explodes) if sp.n=="Forge Blazeheart Golem" then if min_enemy_dist>=3 and min_enemy_dist<=5 then combo_mult=combo_mult*1.3 end if adj_hostiles>=2 then combo_mult=combo_mult*1.2 end end -- Focus fire if has_wounded and sp.role=="ranged" then combo_mult=combo_mult*1.3 end -- Ranged enemy priority if has_ranged_enemy and sp.role=="ranged" then combo_mult=combo_mult*1.2 end -- Turn efficiency if have_time then if sp.role=="multi" then combo_mult=combo_mult*1.2 end end -- Depth scaling if depth_tier==1 and cost>=7 then combo_mult=combo_mult*0.6 elseif depth_tier==3 and cost<=3 then combo_mult=combo_mult*0.5 end combo_mult=math.max(0.2,math.min(combo_mult,3.0)) local raw_score=base+escape_bonus+tank_bonus+multi_bonus local val=raw_score*danger_mult*combo_mult/math.sqrt(cost) -- Find target for targeted spells local tx,ty=0,0 if sp.target_enemy then local best_e_thr=0 local range=spells.range(sp.n) or los for _,e in ipairs(t) do if e.dist<=range then local thr=e.hp*(1+0.2*(e.neighbors or 0)) if thr>best_e_thr then best_e_thr=thr;tx=e.x;ty=e.y end end end if best_e_thr==0 then skip=true end elseif sp.target_dir then tx=cx;ty=cy if tx==0 and ty==0 then tx=1;ty=0 end end if not skip then -- Convert summon score to priority scale (50-75 range) local pri=50+math.min(25,val/4) -- Recast tag local tag=sp.short if found_cannons and not cannons_effective and sp.n=="Hoarfrost Cannonade" then tag=sp.short.."(recast)" end candidates[#candidates+1]={n=sp.n,pri=pri,x=tx,y=ty, tag=tag,c=cost,short=sp.short} end end -- not skip end -- not at_cap end -- fail/cost check end -- memorised end -- summon_spells loop end -- has_enemies -- Hellfire Mortar (deploy if not active, enemies present) if has_enemies and not found_mortar then local n="Hellfire Mortar" local ok,cost=check_spell(n) if ok then candidates[#candidates+1]={n=n,pri=62,x=0,y=0,tag="MORTAR",c=cost} end end -- ===================================================== -- SECTION 3: NO-ENEMY SETUP CANDIDATES -- ===================================================== if not has_enemies then -- Battlesphere pre-setup if not found_bsph then local n="Iskenderun's Battlesphere" local ok,cost=check_spell(n) if ok then candidates[#candidates+1]={n=n,pri=55,x=0,y=0,tag="BSPH(setup)",c=cost} end end -- Summon deployments still useful without enemies (lower priority) for _,sp in ipairs(summon_spells) do if spells.memorised(sp.n) and not sp.target_enemy then local ok,cost=check_spell(sp.n) if ok then local current=count_friends(sp.fn) local at_cap=sp.cap>0 and current>=sp.cap and not sp.recast local skip=false if sp.needs_armour and not items.equipped_at("body armour") then skip=true end if sp.n=="Awaken Armour" and found_armour then skip=true end if sp.n=="Spellspark Servitor" and found_servitor then skip=true end if sp.n=="Forge Monarch Bomb" and found_bomb then skip=true end if sp.n=="Diamond Sawblades" and found_sawblades then skip=true end if sp.n=="Splinterfrost Shell" and found_barricades then skip=true end if sp.n=="Hoarfrost Cannonade" and found_cannons then skip=true end if not at_cap and not skip then local tx,ty=0,0 if sp.target_dir then tx=1;ty=0 end candidates[#candidates+1]={n=sp.n,pri=40,x=tx,y=ty, tag=sp.short.."(setup)",c=cost} end end end end end -- Clockwork Bee channeling: if actively channeling, continue immediately local bee_lt=forge_last_cast["Launch Clockwork Bee"] if bee_lt and (turn-bee_lt)<=3 and spells.memorised("Launch Clockwork Bee") and mp>=1 then local tx,ty=0,0 if best_target then tx=best_target.x;ty=best_target.y end crawl.mpr("Launch Clockwork Bee [Bee(channel)]") spells.cast("Launch Clockwork Bee",tx,ty,true) forge_last_cast["Launch Clockwork Bee"]=turn return end -- No candidates if #candidates==0 then if has_enemies then crawl.mpr("No forge spell (enemies:"..#t.." adj:"..adj_hostiles.." min_d:"..min_enemy_dist..")") else crawl.mpr("No forge action") end return end -- Noise awareness: penalize loud constructs when few distant enemies if #t<=2 and adj_hostiles==0 then for _,c in ipairs(candidates) do local lvl=spells.level(c.n) or 5 if lvl>=7 then c.pri=c.pri-15 end if lvl>=5 and lvl<7 then c.pri=c.pri-8 end end end -- Sort by priority table.sort(candidates,function(a,b) return a.pri>b.pri end) -- Debug: show top candidates local dbg="" for i=1,math.min(3,#candidates) do local b=candidates[i] if i>1 then dbg=dbg..", " end dbg=dbg..b.tag.."("..math.floor(b.pri)..")" end if dbg~="" then crawl.mpr("Forge: "..dbg) end -- Try candidates in order local cast_ok=false for _,b in ipairs(candidates) do if reserve_mp>0 and b.c>1 and (mp-b.c)reserve_mp then reserve_mp=c;reserve_spell=esc end end end local los=you.los() local t={} local friends={} local found_bsph=false local found_servitor=false local servitor=nil local found_star=false local found_mortar=false for x=-los,los do for y=-los,los do local m=monster.get_monster_at(x,y) if m and not m:is_firewood() then if m:attitude()==0 then t[#t+1]={m=m,x=x,y=y} elseif m:attitude()>0 then if m:name()=="battlesphere" then found_bsph=true elseif m:name()=="spellspark servitor" then found_servitor=true;servitor={m=m,x=x,y=y} elseif string.find(m:name(),"shooting star") then found_star=true;friends[#friends+1]={x=x,y=y} elseif m:name()~="orb of destruction" then friends[#friends+1]={x=x,y=y} end end end if m and string.find(m:name() or "","mortar") then found_mortar=true end end end if #t==0 then crawl.mpr("No")return end -- view.cell_see_cell memoization (#19): hits in 3 hot loops (any_target, AoE offsets, can_reach) local los_vis_cache={} local function los_vis(x,y) local k=x..","..y local v=los_vis_cache[k] if v==nil then v=view.cell_see_cell(0,0,x,y) and true or false;los_vis_cache[k]=v end return v end local function get_ac(m) local pips=m:ac() if pips==0 then return 0 end -- Use exact AC from lookup table if pip count matches local known=monster_ac[m:name()] if known and known>=0 and math.ceil(known/5.0)==pips then return known end return 2.5+(pips-1)*5 end -- Status snapshot helper (#19): one m:status() call per status, reused for ev + scoring local function get_status(m) return { paralysed=m:status("paralysed"), petrified=m:status("petrified"), petrifying=m:status("petrifying"), asleep=m:status("asleep"), magdust=m:status("covered in magnetic dust"), confused=m:status("confused") and true or false, caught=m:is_caught(), constricted=m:is_constricted(), regen=m:status("regenerating"), poisoned=m:status("poisoned")} end local function get_ev_cached(m,st) local pips=m:ev() if pips==0 then return 0 end local ev=2.5+(pips-1)*5 if st.paralysed or st.petrified or st.petrifying or st.asleep or st.magdust then return 0 end if st.caught then return math.max(0,ev/5) end if st.confused then return math.max(0,ev/2) end if st.constricted then ev=ev-10 end return math.max(0,ev) end local function get_ev(m) return get_ev_cached(m,get_status(m)) end local function get_max_hp(m) -- m:max_hp() returns "45", "~45" (estimate), or "*45*" (Beogh apostle). -- Extract the first run of digits so any prefix/suffix is tolerated. local n=tostring(m:max_hp()):match("%d+") return tonumber(n) or 20 end local hp_frac={[0]=1.0,[1]=0.9,[2]=0.7,[3]=0.5,[4]=0.3,[5]=0.1,[6]=0} local function get_hp(m) local mhp=get_max_hp(m) local dl=m:damage_level() return math.max(1, mhp*(hp_frac[dl] or 0.5)) end local closest_dist=99 local enemy_at={} for _,e in ipairs(t) do local d=math.max(math.abs(e.x),math.abs(e.y)) if d0) e.is_regen=st.regen e.is_magnetised=st.magdust e.is_caught=st.caught e.is_paralysed=st.paralysed or st.petrified or st.petrifying or st.asleep e.is_confused=st.confused e.is_poisoned=st.poisoned e.mon_range=e.m:range() or 0 local ok_th,th=pcall(function() return e.m:threat() end) e.threat=(ok_th and th or 0)+1 -- Speed cache (#13): "very slow"/"slow"/"normal"/"fast"/"very fast"/"extremely fast" local sd=e.m:speed_description() or "normal" e.speed_desc=sd e.fast_factor=(sd=="extremely fast") and 2.0 or (sd=="very fast") and 1.5 or (sd=="fast") and 1.25 or (sd=="slow") and 0.75 or (sd=="very slow") and 0.5 or 1.0 end -- Pre-compute adjacency counts for each enemy (O(n) via enemy_at hash) -- Track max enemy fast_factor for combo lookahead (#13) local max_fast=1.0 for _,e in ipairs(t) do e.adj_enemies=0 for dx=-1,1 do for dy=-1,1 do if not (dx==0 and dy==0) then local nb=enemy_at[(e.x+dx)..","..((e.y+dy))] if nb then e.adj_enemies=e.adj_enemies+1 end end end end if e.fast_factor>max_fast then max_fast=e.fast_factor end end local n_poisoned=0 if spells.memorised("Ignite Poison") then for _,e in ipairs(t) do -- Ignite deals fire damage: only count poisoned enemies it can actually hurt -- (not rF+++). Mirrors the combo-lookahead eligibility below. if e.is_poisoned and (e.m:res_fire() or 0)<3 then n_poisoned=n_poisoned+1 end end end -- Fast AC/EV lookup: use cached enemy entry if available, else compute local function fast_ac(m,x,y) local e=enemy_at[x..","..y] if e then return e.ac end return get_ac(m) end local function fast_ev(m,x,y) local e=enemy_at[x..","..y] if e then return e.ev end return get_ev(m) end local function has_friendly(cx,cy,radius) for _,f in ipairs(friends) do if math.max(math.abs(f.x-cx),math.abs(f.y-cy))<=radius then return true end end return false end -- LRD: determine dice, radius, and ice flag for a monster target (by name) -- Returns dice,radius,is_ice or 0,0,false if not fraggable local function lrd_dice_mon(m) local mn=string.lower(m:name()) -- Metal monsters (4 dice) — check before rock due to war gargoyle/gargoyle overlap local metal={"iron golem","iron elemental","peacekeeper","war gargoyle","clockwork bee", "lightning spire","blazeheart golem","walking alembic","monarch bomb","bomblet", "phalanx beetle","spellspark servitor","platinum paragon","crawling flesh cage"} for _,p in ipairs(metal) do if string.find(mn,p) then return 4,1,false end end -- Crystal monsters (4 dice, radius 2) local crystal={"crystal guardian","crystal echidna","orange statue","obsidian bat", "obsidian statue","diamond sawblade","roxanne","glass eye","screaming refraction"} for _,p in ipairs(crystal) do if string.find(mn,p) then return 4,2,false end end -- Rock/bone monsters (3 dice) local rock={"toenail golem","saltling","pillar of salt","pile of debris","petrified flower", "earth elemental","mountainshell","rockslime","boulder","ushabti","statue","gargoyle", "rock fish","hellfire mortar"} for _,p in ipairs(rock) do if string.find(mn,p) and mn~="molten gargoyle" and mn~="war gargoyle" then return 3,1,false end end -- Ice monsters (3 dice, cold damage) local ice={"ice beast","simulacr","ice statue","block of ice","nargun", "hoarfrost cannon","pillar of rime","splinterfrost barricade"} for _,p in ipairs(ice) do if string.find(mn,p) then return 3,1,true end end -- Skeletal monsters (3 dice) local skel={"draugr","bone dragon","skeletal warrior","ancient champion","revenant", "weeping skull","laughing skull","curse skull","marrowcuda","murray","nameless revenant"} for _,p in ipairs(skel) do if string.find(mn,p) then return 3,1,false end end -- Petrified/petrifying monsters if m:status("petrified") or m:status("petrifying") then return 3,1,false end return 0,0,false end -- LRD: determine dice and radius for terrain -- Returns dice,radius or 0,0 if not fraggable local function lrd_dice_terrain(f) local fl=string.lower(f or "") -- Exclude stairs early (stone_stairs contains "stone") if string.find(fl,"stair") then return 0,0 end -- Rock/stone terrain (3 dice) if string.find(fl,"rock_wall") or string.find(fl,"stone_wall") or string.find(fl,"slimy_wall") or string.find(fl,"door") or string.find(fl,"stone_arch") or string.find(fl,"granite_statue") or string.find(fl,"orcish_idol") or string.find(fl,"petrified_tree") or string.find(fl,"spike_launcher") then return 3,1 end -- Metal terrain (4 dice) if string.find(fl,"metal") or string.find(fl,"iron") or string.find(fl,"grate") then return 4,1 end -- Crystal terrain (4 dice, radius 2) if string.find(fl,"crystal") then return 4,2 end return 0,0 end -- Track last cast turn for lingering spells last_cast_turn = last_cast_turn or {} local turn=you.turns() -- Check if spell was cast recently local function recently_cast(name) local lt=last_cast_turn[name] if not lt then return false end -- Channeled spells: active for up to 3 turns after initial cast if name=="Flame Wave" or name=="Searing Ray" then return (turn-lt)<=3 end -- Persistent summons: always active once cast if name=="Iskenderun's Battlesphere" or name=="Conjure Ball Lightning" then return true end return false end -- Mark spell as cast local function mark_cast(name) last_cast_turn[name]=turn end local function cdist(x,y) return math.max(math.abs(x),math.abs(y)) end local function score_dmg(dmg,m,dist,e) -- e: optional pre-cached enemy entry from t[] local hp=e and e.hp or get_hp(m) local mhp=e and e.mhp or get_max_hp(m) local threat if e then threat=e.threat else local ok_th,th=pcall(function() return m:threat() end) threat=(ok_th and th or 0)+1 end -- Overkill waste: only 20% credit for damage beyond kill threshold local effective=dmg if dmg>hp then effective=hp+(dmg-hp)*0.2 end -- Finish-off bonus: killing removes threat entirely local finish=1.0 if dmg>=hp then finish=1.5 elseif e and e.m:damage_level()>=4 and dmg>=hp*0.7 then finish=1.3 end -- Distance urgency: adjacent monsters are most dangerous dist=dist or 4 local urgency=1.0 if dist<=1 then urgency=2.0 elseif dist<=2 then urgency=1.5 elseif dist<=3 then urgency=1.2 end -- Ranged threat: monsters already in attack range are more urgent if e then if e.mon_range>1 and dist<=e.mon_range then urgency=urgency*1.3 end else local mon_rng=m:range() or 0 if mon_rng>1 and dist<=mon_rng then urgency=urgency*1.3 end end -- Base score: effective damage weighted by threat, finish, urgency, HP ratio local score=effective*finish*urgency*threat/mhp if e then if e.is_summoned then score=score*0.2 end if e.is_safe then score=score*0.1 end if e.is_stationary then score=score*0.4 end if e.is_unique then score=score*1.3 end if e.is_caster then score=score*1.15 end if e.is_regen then score=score*1.3 end -- Caught/paralysed/petrified: locked in place, easy kill (#3) if e.is_caught or e.is_paralysed then score=score*1.5 end else -- Fallback: slow path for non-cached monsters local desc=m:target_desc() or "" if string.find(desc,"summoned") then score=score*0.2 end if m:is_safe() then score=score*0.1 end if m:is_stationary() then score=score*0.4 end if m:is_unique() then score=score*1.3 end local mon_sp=m:spells() if mon_sp and #mon_sp>0 then score=score*1.15 end if m:status("regenerating") then score=score*1.3 end end return score end -- Power caps: all non-200 caps listed; unlisted spells default to 200 (verified against spl-data.h 0.34.0) local pow_cap={["Foxfire"]=25,["Freeze"]=25,["Magic Dart"]=25,["Poisonous Vapours"]=25,["Shock"]=25, ["Sandblast"]=50,["Mercury Arrow"]=50,["Scorch"]=50,["Searing Ray"]=50,["Static Discharge"]=50,["Frozen Ramparts"]=50,["Stone Arrow"]=50, ["Hailstorm"]=100,["Dispel Undead"]=100,["Flame Wave"]=100,["Iskenderun's Battlesphere"]=100,["Iskenderun's Mystic Blast"]=100, ["Ignite Poison"]=100,["Olgreb's Toxic Radiance"]=100,["Sticky Flame"]=100,["Brom's Barrelling Boulder"]=100} local function get_pow(name) local p=spells.power_perc(name) if not p then return 0 end local cap=pow_cap[name] or 200 return p*cap/100 end local function player_res(e_type) if e_type==1 then return you.res_cold()end if e_type==2 then return you.res_shock()end if e_type==3 then return you.res_fire()end if e_type==4 then return you.res_poison()end return 0 end local function calc_dmg(base_dmg,ac,ev,pow,noac,nohit,ac3,halfac) local dmg=base_dmg -- pow is power_perc (0-100), already factored into get_spell_dmg formulas if noac then -- Skip AC reduction elseif ac>0 then -- halfac: electricity gets ac_type::half (saved = random2(1+ac)/2, avg=ac/4) -- ac3: BEAM_FRAG gets ac_type::triple (saved = 3*random2(1+ac), avg=3*ac/2) -- source: actor.cc apply_ac — single subtraction from damage local saved=ac/2 -- normal: avg of random2(1+ac) if ac3 then saved=3*ac/2 -- triple: 3 independent random2(1+ac) summed elseif halfac then saved=ac/4 end -- half: random2(1+ac)/2 dmg=math.max(0,dmg-saved) end if nohit then -- Skip EV reduction (always hits) elseif ev>0 then dmg=dmg*math.max(0,1.0-ev/50.0) end return dmg end local function spell_dmg(base,mult,ac,ev,pow,sp,elec,nohit_ov,m) local nh=nohit_ov~=nil and nohit_ov or sp.nohit -- Plasma Beam: 50% fire-zap + 50% elec-zap, two independent res checks if sp.n=="Plasma Beam" and m then local rf=m:res_fire() or 0 local re=m:res_shock() or 0 local mf=1.0; if rf<=-1 then mf=1.5 elseif rf==1 then mf=0.5 elseif rf==2 then mf=0.25 elseif rf>=3 then mf=0 end local me=1.0; if re<=-1 then me=1.5 elseif re==1 then me=0.5 elseif re==2 then me=0.25 elseif re>=3 then me=0 end return calc_dmg(base*0.5*mf,ac,ev,pow,sp.noac,nh,sp.ac3,false) +calc_dmg(base*0.5*me,ac,ev,pow,sp.noac,nh,sp.ac3,true) end if sp.halfres then return calc_dmg(base*0.5*mult,ac,ev,pow,sp.noac,nh,sp.ac3,elec) +calc_dmg(base*0.5,ac,ev,pow,sp.noac,nh,sp.ac3,elec) end return calc_dmg(base*mult,ac,ev,pow,sp.noac,nh,sp.ac3,elec) end -- Shield block: ~50% damage reduction for shielded monsters -- Reads cached e.shielded via enemy_at when available, falls back to target_desc otherwise local function sh_damage(dmg,m) local ok,px,py=pcall(function() return m:x_pos(),m:y_pos() end) if ok then local e=enemy_at[px..","..py] if e then return e.shielded and dmg*0.5 or dmg end end local desc=m:target_desc() or "" if string.find(desc,"shield") then return dmg*0.5 end return dmg end -- Evasion check using game API (more accurate than pip estimation) local function evasion_check(m,spellname) if not spellname then return 1.0 end local ok,desc=pcall(function() return m:target_spell(spellname) end) if ok and desc then local hit=string.match(desc,"(%d+)%% to hit") if hit then return tonumber(hit)/100.0 end end return nil -- fallback to pip-based end local function get_hit_chance(m,spellname,ev) return evasion_check(m,spellname) or math.max(0,1.0-(ev or get_ev(m))/50.0) end -- Calculate average damage using wiki formulas (Nd(X) avg = N*(X+1)/2) local function get_spell_dmg(name,pow) -- Level 1 if name=="Foxfire" then return 2*(4+pow/5+1)/2 end -- 2x 1d(4+pow/5) if name=="Freeze" then return (3+3*pow/10+1)/2 end -- 1d(3+3pow/10), ignores AC if name=="Magic Dart" then return (3+pow/5+1)/2 end -- 1d(3+pow/5) if name=="Sandblast" then return 2*(4+pow/3+1)/2 end -- 2d(4+pow/3), triple AC if name=="Poisonous Vapours" then return (1+pow/8+1)/2 end -- 1d(1+pow/8) if name=="Shock" then return (3+pow/4+1)/2 end -- 1d(3+pow/4) -- Level 2 if name=="Mercury Arrow" then return 2*(5.5+pow/8+1)/2 end -- calcdice<2,11,1,4> = 2d(5.5+pow/8) poison+30% irresist if name=="Scorch" then return 2*(5+pow/12+1)/2 end -- 2d(5+pow/12) if name=="Searing Ray" then return 2*(4.5+pow/14+1)/2 end -- calcdice<2,9,1,7> = 2d(4.5+pow/14) if name=="Static Discharge" then return 3+(2+pow/12)/2 end -- 3+random2(3+pow/12) -- Level 3 if name=="Frozen Ramparts" then return (1+0.3*pow+1)/2 end -- 1d(1+0.3pow) per turn if name=="Hailstorm" then return 3*(5+pow/9+1)/2 end -- 3d(5+pow/9) calcdice<3,15,1,3> 50cold+50phys if name=="Stone Arrow" then return 3*(7+pow/8+1)/2 end -- 3d(7+pow/8) -- Level 4 if name=="Airstrike" then return 2*((pow+13)/14+1)/2 end -- 2d((pow+13)/14), +2/space added separately if name=="Brom's Barrelling Boulder" then return 2*(4+pow/10+1)/2 end -- 2d(4+pow/10) if name=="Dispel Undead" then return 3*(6.66+pow/4+1)/2 end -- 3d(6.66+pow/4), ignores AC, undead only if name=="Flame Wave" then return 3*2*(4.5+pow/6+1)/2 end -- 2d(4.5+pow/6) x 3 turns if name=="Fulminant Prism" then return 3*(5+7*pow/40+1)/2 end -- 3d(5+7pow/40) irresistible; hd=pow/10, 3d(5+hd*7/4) if name=="Iskenderun's Battlesphere" then return 2*(7+pow/9+1)/2 end -- 2d(6+hd) where hd=1+pow/9 → 2d(7+pow/9) if name=="Iskenderun's Mystic Blast" then return 2*(3+pow/6+1)/2 end -- calcdice<2,6,1,3> = 2d(3+pow/6) + collision 2d(1+pow/10) if name=="IMB_collision" then return 2*(1+pow/10+1)/2 end -- 2d(1+pow/10) wall/monster impact if name=="Ignite Poison" then return 4*(12+pow*6/100+1)/2 end -- 2*pois_str d(12+pow*6/100), assume pois=2 if name=="Olgreb's Toxic Radiance" then return 3*(1+pow/20+1)/2 end -- 1d(1+pow/20)/turn x ~3 turns if name=="Sticky Flame" then return 2*(4+pow/9+1)/2 + (3+3*pow/40)*8 end -- 2d(4+pow/9) impact + 2d7/turn DoT -- Level 5 if name=="Arcjolt" then return (10+pow/2+1)/2 end -- 1d(10+pow/2) if name=="Borgnjor's Vile Clutch" then return 2*(4+pow/20+1)/2 end -- 2d(4+pow/20) per turn if name=="Freezing Cloud" then return 14 end -- cloud: 6+r2a(16,2) avg ~14/turn, not pow-scaled if name=="Fireball" then return 3*(3.33+pow/6+1)/2 end -- 3d(3.33+pow/6) if name=="Irradiate" then return 3*(11.66+pow/6+1)/2 end -- 3d(11.66+pow/6) if name=="Lee's Rapid Deconstruction" then return 3*(4+pow/5+1)/2 end -- 3d(4+pow/5) triple AC; needs wall/fraggable target -- Level 6 if name=="Bombard" then return 9*((13+2*pow/3)/9+1)/2 end -- 9d((13+2pow/3)/9) if name=="Permafrost Eruption" then return 2*4*(1.5+3*pow/32+1)/2 end -- 2x 4d(1.5+3pow/32) calcdice<4,6,3,8> if name=="Plasma Beam" then return 2*(10+11*pow/20+1)/2 end -- 2x 1d(10+11pow/20) elec+fire if name=="Starburst" then return 6*(3+pow/9+1)/2 end -- 6d(3+pow/9) per bolt (8 bolts, single target hit once) -- Level 7 if name=="Hellfire Mortar" then local hd=1+pow/10;local zp=hd*12;return 4*(4+zp*2/21+1)/2 end -- 4d(4+hd*12*2/21) per shot if name=="Magnavolt" then return 4*(9+pow/10+1)/2 end -- 4d(9+pow/10) if name=="Orb of Destruction" then return 9*(5+pow/12+1)/2 end -- 9d(5+pow/12) if name=="Ozocubu's Refrigeration" then return 4*(7.5+pow/9+1)/2 end -- 4d(7.5+pow/9) -- Level 8 if name=="Ignition" then return 3*(3.33+pow/9+1)/2 end -- 3d(3.33+pow/9) per enemy if name=="Lehudib's Crystal Spear" then return 10*(2.3+pow/10+1)/2 end -- 10d(2.3+pow/10) if name=="Fulsome Fusillade" then return 3*(5+pow/8+1)/2 end -- 3d(5+pow/8) per explosion -- Maxwell's Capacitive Coupling: scored specially (auto_target, channeled kill) -- Level 9 if name=="Fire Storm" then return 8*((5+pow)/8+1)/2 end -- 8d((5+pow)/8) 50% bypasses rF if name=="Chain Lightning" then return 3*(2*pow/3+1)/2 end -- 3d(2pow/3) if name=="Polar Vortex" then return 6*12*(pow/45+1)/2 end -- 12d(rpow/15) per turn x6 turns; rpow≈pow/3 avg (open space) if name=="Shatter" then return 3*(5+pow/3+1)/2 end -- 3d(5+pow/3) base; x2 for nonliving return 10 end local function get_res(m,e_type) if e_type==1 then return m:res_cold()end if e_type==2 then return m:res_shock()end if e_type==3 then return m:res_fire()end if e_type==4 then return m:res_poison()end return 0 end local function get_res_mult(r,halfres) local mult=1.0 if r<=-1 then mult=1.5 elseif r==1 then mult=0.5 elseif r==2 then mult=0.25 elseif r>=3 then mult=0 end if halfres then mult=0.5+mult*0.5 end return mult end local function get_range(name) local r=spells.range(name) if not r then return 0 end return r end local function can_reach(tx,ty,range) local dist=math.max(math.abs(tx),math.abs(ty)) if dist>range then return false end return los_vis(tx,ty) end local function is_solid(x,y) local f=view.feature_at(x,y) if not f then return false end return f=="rock_wall" or f=="stone_wall" or f=="permarock_wall" or f=="unnaturally_hard_rock_wall" or f=="metal_wall" or f=="crystal_wall" or f=="closed_door" or f=="runed_door" or f=="sealed_door" or f=="tree" or f=="mangrove" or f=="slimy_wall" end local function get_cost(name) local c=spells.mana_cost(name) if not c then return 99 end -- Flame Wave costs 1 MP to continue, full cost to start if name=="Flame Wave" and recently_cast("Flame Wave") then return 1 end return c end local function count_empty(x,y) local count=0 for dx=-1,1 do for dy=-1,1 do if dx~=0 or dy~=0 then local m=monster.get_monster_at(x+dx,y+dy) if not m and not is_solid(x+dx,y+dy) then count=count+1 end end end end return count end local function beam_blocked_by_friendly(spell_name,tx,ty) local path=spells.path(spell_name,tx,ty) if not path then return false end for _,p in ipairs(path) do local m=monster.get_monster_at(p[1],p[2]) if m and m:attitude()>0 and m:name()~="battlesphere" and m:name()~="orb of destruction" then return true end end return false end local function monsters_on_path(spell_name,tx,ty) local result={} local path=spells.path(spell_name,tx,ty) if not path then return result end for _,p in ipairs(path) do if not (p[1]==tx and p[2]==ty) then local m=monster.get_monster_at(p[1],p[2]) if m and m:attitude()==0 and not m:is_firewood() then result[#result+1]={m=m,x=p[1],y=p[2]} end end end return result end -- Shock bounce: get full path with ricochets (aimed_at_spot=false) -- Returns {hit_counts, enemies} where hit_counts[key]=count (max 2), enemies[key]={m,x,y} local shock_cache={} local function shock_bounce_hits(tx,ty,pow) local ck=tx..","..ty if shock_cache[ck] then return shock_cache[ck][1],shock_cache[ck][2],shock_cache[ck][3] end local path=spells.path("Shock",tx,ty,0,0,false) if not path then shock_cache[ck]={{},{},false} return {},{},false end local hit_counts={} local enemies={} local self_hit=false for _,p in ipairs(path) do if p[1]==0 and p[2]==0 then self_hit=true end -- ricochet returns onto player tile local m=monster.get_monster_at(p[1],p[2]) if m and m:attitude()==0 and not m:is_firewood() then local key=p[1]..","..p[2] if not hit_counts[key] then hit_counts[key]=0;enemies[key]={m=m,x=p[1],y=p[2]} end if hit_counts[key]<2 then hit_counts[key]=hit_counts[key]+1 end end end shock_cache[ck]={hit_counts,enemies,self_hit} return hit_counts,enemies,self_hit end -- Orb of Destruction path trace. OOD is a slow homing projectile (mon-project.cc): -- it launches with a fuzzed direction + 0.4-tile offset, moves ~1 tile/turn along a -- float ray rounded to the nearest cell, and can only veer ~17 deg/step -- so it -- cannot turn corners and the launch fuzz slams it into walls in tight/diagonal -- corridors. spells.path() returns nil for OOD (SPELL_IOOD has no zap), so we trace -- the ray ourselves: reject solid cells, firewood/friendlies, and diagonal steps that -- squeeze past a wall corner (fragile -- fuzz pushes the orb into the corner). local function ood_path_clear(tx,ty) local steps=math.max(math.abs(tx),math.abs(ty)) if steps<1 then return false end local nx,ny=tx/steps,ty/steps local px,py=0,0 for i=1,steps do local cx=math.floor(nx*i+0.5) local cy=math.floor(ny*i+0.5) if not (cx==tx and cy==ty) then if is_solid(cx,cy) then return false end local pm=monster.get_monster_at(cx,cy) if pm and pm:is_firewood() then return false end if pm and pm:attitude()>0 and pm:name()~="battlesphere" and pm:name()~="orb of destruction" then return false end end -- diagonal corner clearance: don't thread the orb between two wall corners if cx~=px and cy~=py and (is_solid(px,cy) or is_solid(cx,py)) then return false end px,py=cx,cy end return true end -- Straight-line wall check between two relative cells (Bresenham-ish, nearest-cell -- rounding). Returns false if any intermediate cell is solid. Used for Foxfire, -- whose summon walks toward the target and is blocked by walls in tight spaces. local function line_wall_clear(x0,y0,x1,y1) local dx,dy=x1-x0,y1-y0 local steps=math.max(math.abs(dx),math.abs(dy)) if steps<1 then return true end local sx,sy=dx/steps,dy/steps for i=1,steps-1 do local cx=math.floor(x0+sx*i+0.5) local cy=math.floor(y0+sy*i+0.5) if is_solid(cx,cy) then return false end end return true end local function monsters_on_line(tx,ty) local result={} local dx=tx>0 and 1 or (tx<0 and -1 or 0) local dy=ty>0 and 1 or (ty<0 and -1 or 0) local steps=math.max(math.abs(tx),math.abs(ty)) if steps==0 then return result end local x,y=0,0 for i=1,steps-1 do x=x+dx y=y+dy local m=monster.get_monster_at(x,y) if m and m:attitude()==0 and not m:is_firewood() then result[#result+1]={m=m,x=x,y=y} end end return result end local s={{ -- Level 1 n="Foxfire",e=3,summon=true,nohit=true -- fire, 2 foxfires, never miss },{n="Freeze",e=1,noac=true,nohit=true -- cold, range 1, ignores AC },{n="Magic Dart",e=0,nohit=true -- irresistible, never misses },{n="Poisonous Vapours",e=4,rpois_immune=true -- poison, range 3, rPois>=1 blocks cast },{n="Sandblast",e=0,ac3=true,slow=true -- physical, triple AC, 1.5 turns },{n="Shock",e=2,beam=true,penetrate=true,any_target=true -- elec, bouncing bolt, 1/2 AC via halfac -- Level 2 },{n="Mercury Arrow",e=4 -- poison+30% irresist },{n="Scorch",e=3,nohit=true -- fire, applies rF- },{n="Searing Ray",e=0,penetrate=true,channeled=true -- irresistible, channeled, penetrates, can miss },{n="Static Discharge",e=2,aoe=1,nohit=true,discharge=true -- elec, self-centered chain-arc, half-AC (ac_type::half) -- Level 3 },{n="Frozen Ramparts",e=1,aoe=2,centered=true,nohit=true -- cold, centered, adj walls },{n="Hailstorm",e=1,aoe=3,halfres=true,noadjacent=true -- cold+phys 50/50, donut AoE },{n="Stone Arrow",e=0,beam=true -- physical, bolt -- Level 4 },{n="Airstrike",e=0,nohit=true -- irresistible, smite, AC applies },{n="Brom's Barrelling Boulder",e=0,beam=true -- physical, line, knockback },{n="Dispel Undead",e=0,noac=true,nohit=true,undead=true,no_bsph=true -- ignores AC, undead only },{n="Flame Wave",e=3,aoe=3,nohit=true,channeled=true,centered=true -- fire, expanding AoE centered, 3 turns },{n="Fulminant Prism",e=0,aoe=2,nohit=true,no_bsph=true,prism=true -- irresistible, smite-placed, delayed 2 turns },{n="Ignite Poison",e=3,aoe=9,noac=true,nohit=true -- fire, smite all poisoned in LOS, ignores AC/EV },{n="Iskenderun's Battlesphere",e=0,summon=true,no_bsph=true -- irresistible, construct },{n="Iskenderun's Mystic Blast",e=0,aoe=2,nohit=true,centered=true -- irresistible, 2-tile AoE },{n="Olgreb's Toxic Radiance",e=4,aoe=9,noac=true,nohit=true,rpois_immune=true -- poison, LOS AoE, ignores AC, rPois>=1 blocks },{n="Sticky Flame",e=3,nohit=true,noac=true -- fire, DoT ignores AC (bulk of dmg) -- Level 5 },{n="Arcjolt",e=2,aoe=2,nohit=true -- elec, arcs, 1/2 AC via halfac },{n="Borgnjor's Vile Clutch",e=0,clutch=true,nohit=true,no_bsph=true -- necro+earth, bolt constricts all on path },{n="Fireball",e=3,aoe=2,nohit=true,any_target=true -- fire, 3x3 explosion },{n="Freezing Cloud",e=1,fcloud=true,nohit=true,no_bsph=true -- cold, smite cloud r2, zone control },{n="Irradiate",e=0,aoe=1,nohit=true,centered=true -- irresistible, all adjacent, player-centered },{n="Lee's Rapid Deconstruction",e=0,aoe=1,ac3=true,nohit=true -- physical, triple AC, needs wall/fraggable -- Level 6 },{n="Bombard",e=0,beam=true -- physical, bolt },{n="Conjure Ball Lightning",e=2,summon=true -- elec, 3 autonomous balls },{n="Permafrost Eruption",e=1,aoe=1,halfres=true,auto_target=true -- phys+cold 50/50, auto-targets best cluster },{n="Plasma Beam",e=2,penetrate=true,halfres=true,auto_target=true -- elec+fire, 2 penetrating bolts, auto-targets farthest },{n="Starburst",e=3,starburst=true -- fire, 8 penetrating bolts from player -- Level 7 },{n="Hellfire Mortar",e=3,mortar=true,halfres=true,no_bsph=true -- fire+earth, bolt path creates lava, summons mortar },{n="Magnavolt",e=2,nohit=true,magnavolt=true -- elec, smite, magnetise chain },{n="Orb of Destruction",e=0,nohit=true,ood=true -- irresistible, homing, distance scaling },{n="Ozocubu's Refrigeration",e=1,aoe=9,nohit=true,auto_target=true -- cold, full LOS, damage reduced by adj enemies },{n="Spellspark Servitor",e=0,summon=true,no_bsph=true -- varies, construct -- Level 8 },{n="Ignition",e=3,aoe=9,nohit=true,ignition=true -- fire, 3x3 on every enemy },{n="Lehudib's Crystal Spear",e=0 -- irresistible, short range },{n="Fulsome Fusillade",e=0,aoe=9,nohit=true -- irresistible (BEAM_MMISSILE), auto-hit, 3 explosions/turn },{n="Maxwell's Capacitive Coupling",e=2,noac=true,nohit=true,maxwells=true,auto_target=true -- elec, channeled kill -- Level 9 },{n="Fire Storm",e=3,aoe=3,nohit=true,halfres=true -- fire 50% irresist, smite },{n="Chain Lightning",e=2,aoe=9,halfres=true,nohit=true,chain_lightning=true -- elec, arcs all in LOS },{n="Polar Vortex",e=1,aoe=5,centered=true,halfres=true,nohit=true -- cold 50% irresist, 6-turn sustained, r5 },{n="Shatter",e=0,aoe=9,nohit=true -- physical, full LOS AoE, ignores EV }} -- === Tactical situation assessment === local hp_now,hp_max=you.hp() local hp_ratio=hp_now/hp_max -- Corridor detection: count solid tiles around player local corridor_walls=0 for cx=-1,1 do for cy=-1,1 do if (cx~=0 or cy~=0) and is_solid(cx,cy) then corridor_walls=corridor_walls+1 end end end local in_corridor=(corridor_walls>=5) -- Threat weighting: parse enemy HP, find min distance, detect boss local total_threat=0 local max_single_threat=0 local min_enemy_dist=99 local adj_hostiles=0 for _,e in ipairs(t) do local d=cdist(e.x,e.y) if dmax_single_threat then max_single_threat=e.mhp end end -- Boss fight: one enemy has >60% of total threat local boss_fight=(#t<=2 and max_single_threat>total_threat*0.6) -- Swarm fight: many weak enemies local swarm_fight=(#t>=4 and max_single_threat0 then local rmul=1.0 if xl>=18 then rmul=rmul+0.5 end if boss_fight then rmul=rmul+0.4 end if max_single_threat>=120 then rmul=rmul+0.3 end if hp_ratio<0.4 then rmul=rmul+0.4 end if #t==1 and max_single_threat<30 and adj_hostiles==0 then rmul=rmul-0.5 end if rmul<0.3 then rmul=0.3 end if rmul>2.0 then rmul=2.0 end reserve_mp=math.floor(reserve_mp*rmul+0.5) if reserve_mp>mp_max-1 then reserve_mp=mp_max-1 end end -- Retreat mode: low HP, no adjacent enemies -- Surround detection: boost centered AoE when surrounded local surround_boost=1.0 if adj_hostiles>=3 then surround_boost=1.5 elseif adj_hostiles>=2 then surround_boost=1.2 end -- Low HP panic: prefer nohit burst on closest enemy local panic_mode=(hp_ratio<0.25 and adj_hostiles>0) -- Opener boost: Fusillade rains 3 shots/turn for ~5 turns while you cast other spells. -- One turn's AoE score badly understates it; multiply so it wins the opener with 2+ foes. local FUSILLADE_OPENER=3.0 -- Resistance pressure: fraction of enemies resisting the player's elemental spells local res_pressure=0 if #t>0 then local res_total,res_els=0,0 for el=1,4 do local has_el=false for _,sp in ipairs(s) do if sp.e==el and spells.memorised(sp.n) then has_el=true;break end end if has_el then res_els=res_els+1 local rc=0 for _,e in ipairs(t) do if get_res(e.m,el)>=1 then rc=rc+1 end end res_total=res_total+rc/#t end end if res_els>0 then res_pressure=res_total/res_els end end -- AC pressure: mean enemy AC (used to temper the "prefer physical/irresistible" -- boost — AC-blocked physical spells don't shine against armoured foes). local ac_pressure=0 if #t>0 then local ac_sum=0 for _,e in ipairs(t) do ac_sum=ac_sum+e.ac end ac_pressure=ac_sum/#t end local candidates={} local almost={} -- spells needing 1 more MP -- Battlesphere detected during initial monster scan (found_bsph) local bsph_bonus=0 local bsph_raw=0 local bsph_target=nil local bsph_target_hp=0 local has_bsph=found_bsph if has_bsph and #t>0 then -- Battlesphere targets most-injured enemy: 2d(7+pow/9), irresistible, auto-hit local bsph_pow=0 if spells.memorised("Iskenderun's Battlesphere") then bsph_pow=get_pow("Iskenderun's Battlesphere") end local bsph_base=2*(7+bsph_pow/9+1)/2 -- Find most-injured enemy (most hp missing) local most_missing=0 for _,e in ipairs(t) do local missing=e.mhp-e.hp if missing>most_missing then most_missing=missing;bsph_target=e end end if not bsph_target then bsph_target=t[1] end bsph_target_hp=bsph_target.hp -- Battlesphere damage: irresistible (e=0), auto-hit, vs AC local dmg=calc_dmg(bsph_base,bsph_target.ac,0,0,false,true,false) dmg=sh_damage(dmg,bsph_target.m) bsph_bonus=score_dmg(dmg,bsph_target.m,cdist(bsph_target.x,bsph_target.y)) bsph_raw=dmg end -- Pre-compute magnetised targets for Magnavolt (reuse t[] instead of full LOS scan) local mag_cache={} if spells.memorised("Magnavolt") then for _,e in ipairs(t) do if e.is_magnetised then mag_cache[#mag_cache+1]={x=e.x,y=e.y,m=e.m} end end end for _,sp in ipairs(s) do local mem=spells.memorised(sp.n) local cost=get_cost(sp.n) local fail=spells.fail(sp.n) local ok_sev,sev=pcall(function() return spells.fail_severity(sp.n) end) if mem and fail<20 and (not ok_sev or sev<=2) and cost<=mp+1 then -- Chain Lightning requires rElec to safely cast -- Irradiate: avoid if post-cast contamination could reach dangerous glow (>=100) if (sp.n=="Chain Lightning" and you.res_shock()<1) or (sp.n=="Irradiate" and (you.contamination() or 0)+40>100) or (sp.n=="Polar Vortex" and you.status("in a vortex")) or (sp.n=="Frozen Ramparts" and you.status("freezing walls")) or (sp.n=="Fulsome Fusillade" and you.status("raining reagents")) or (sp.n=="Iskenderun's Battlesphere" and (has_bsph or closest_dist<=3)) or (sp.n=="Spellspark Servitor" and found_servitor) or (sp.n=="Hellfire Mortar" and (found_mortar or you.status("on hellfire mortar cooldown"))) or (sp.n=="Olgreb's Toxic Radiance" and you.status("radiating poison")) then else local range=get_range(sp.n) local pow=get_pow(sp.n) local elec=(sp.e==2) -- electricity: half-AC (beam.cc:4595) local best_val=0 local best_kn=99 local best_tx,best_ty=0,0 local best_ep=false -- per-candidate aimed_at_spot mode (Shock: true=straight-stop, false=ricochet) if sp.summon then local dmg=get_spell_dmg(sp.n,pow) -- Ball Lightning danger: check enemies nearby if sp.n=="Conjure Ball Lightning" then local adj_count=0 local near_count=0 for _,e in ipairs(t) do local dist=math.max(math.abs(e.x),math.abs(e.y)) if dist==1 then adj_count=adj_count+1 elseif dist<=2 then near_count=near_count+1 end end -- Very dangerous if adjacent enemies if adj_count>0 then dmg=dmg*0.1 -- Dangerous if enemies within 2 tiles elseif near_count>0 then dmg=dmg*0.3 -- Also needs rElec elseif you.res_shock()<1 then dmg=dmg*0.2 end end if sp.n=="Foxfire" then local clear=0 local reachable=0 -- Find closest enemy for pathing check local fx_e=t[1];local fx_d=99 for _,e in ipairs(t) do local d=cdist(e.x,e.y) if d0 and not sp.auto_target then local total_dmg=0 -- NOTE: do NOT redeclare best_tx/best_ty as local here — that shadows the -- outer (line ~772) vars, so target coords set in this branch (Fireball, -- Fire Storm, Hailstorm, Fulminant Prism, Static Discharge, LRD) are lost -- and the spell fires at (0,0)=self. Assign the outer vars instead. best_tx,best_ty=0,0 if sp.n=="Static Discharge" then -- Static Discharge is SELF-CENTERED (spl-cast.cc:2500 casts from you.pos(); -- the target arg is ignored). It seeds num_targs=1+random2(2+pow/25) arcs onto -- player-adjacent actors, and each arc random-walks arc_length=1+random2(2+pow/25) -- hops through the electricity-connected monster cluster, re-hitting allowed. -- rElec>=3 both takes no damage AND blocks the arc from propagating through it. -- Model: flood the conductive cluster from player-adjacent hostiles by hop-ring, -- distribute expected strikes (energy) across reachable enemies weighted toward -- nearer rings, and score each enemy's expected damage. Not a static 3x3 sum. local base=get_spell_dmg(sp.n,pow) local arc_ev=1.5+pow/50 -- E[1+random2(2+pow/25)] for both seed count and arc length local energy=arc_ev*arc_ev -- expected total strikes = num_targs * arc_length -- BFS ring assignment: ring 0 = player-adjacent conductive hostiles local ring={} -- key "x,y" -> hop distance local order={} -- reachable enemies in BFS order local frontier={} for _,e in ipairs(t) do if cdist(e.x,e.y)<=1 and get_res_mult(get_res(e.m,sp.e))>0 then local k=e.x..","..e.y if ring[k]==nil then ring[k]=0;frontier[#frontier+1]=e;order[#order+1]=e end end end local fi=1 while fi<=#frontier do local cur=frontier[fi];fi=fi+1 local ck=cur.x..","..cur.y for dx=-1,1 do for dy=-1,1 do if not (dx==0 and dy==0) then local nb=enemy_at[(cur.x+dx)..","..((cur.y+dy))] if nb then local nk=nb.x..","..nb.y if ring[nk]==nil and get_res_mult(get_res(nb.m,sp.e))>0 then ring[nk]=ring[ck]+1;frontier[#frontier+1]=nb;order[#order+1]=nb end end end end end end -- Weight enemies toward nearer rings (arcs decay outward); normalize to energy local wsum=0 local wt={} for _,e in ipairs(order) do local w=0.6^(ring[e.x..","..e.y]) wt[e]=w;wsum=wsum+w end local val=0 if wsum>0 then for _,e in ipairs(order) do -- Expected strikes on this enemy; cap at ~arc_ev (can't be re-hit unboundedly) local hits=math.min(energy*wt[e]/wsum,arc_ev) local r=get_res(e.m,sp.e) local mult=get_res_mult(r) local dmg=calc_dmg(base*mult,e.ac,0,pow,sp.noac,sp.nohit,sp.ac3,elec)*hits val=val+score_dmg(dmg,e.m,cdist(e.x,e.y),e) end end best_val=val*surround_boost best_tx=0;best_ty=0 elseif sp.n=="Arcjolt" then -- Chain-arc simulation: start from player, expand outward local hit={} hit["0,0"]=true local base=get_spell_dmg(sp.n,pow) local val=0 for ring=1,los do local found=false for _,e2 in ipairs(t) do local key=e2.x..","..e2.y if not hit[key] then -- Check if adjacent to any already-hit cell local adj=false for dx=-1,1 do for dy=-1,1 do if hit[(e2.x+dx)..","..((e2.y+dy))] then adj=true end end end if adj then hit[key]=true found=true local r=get_res(e2.m,sp.e) local mult=get_res_mult(r) if mult>0 then local dmg=spell_dmg(base,mult,e2.ac,0,pow,sp,elec) val=val+score_dmg(dmg,e2.m,cdist(e2.x,e2.y),e2) end end end end if not found then break end end best_val=val*surround_boost elseif sp.centered then if sp.n=="Iskenderun's Mystic Blast" then -- IMB: blast centered on player, hits all in radius 2, knockback bonus local val=0 for _,e in ipairs(t) do if cdist(e.x,e.y)<=range then local r=get_res(e.m,sp.e) local mult=get_res_mult(r) if mult>0 then local base=get_spell_dmg(sp.n,pow) local dmg=calc_dmg(base*mult,e.ac,e.ev,pow,sp.noac,sp.nohit,sp.ac3,elec) -- knockback collision bonus -- Source: actor::collide called only when pos()!=newpos (actor actually moved). -- If first tile in push direction is blocked, no movement, no collision damage. local dx=e.x>0 and 1 or (e.x<0 and -1 or 0) local dy=e.y>0 and 1 or (e.y<0 and -1 or 0) local kd=2+pow/50 local col_dmg=get_spell_dmg("IMB_collision",pow) local kx,ky=e.x,e.y local moved=0 for _=1,math.floor(kd) do local nx,ny=kx+dx,ky+dy if is_solid(nx,ny) then if moved>0 then dmg=dmg+calc_dmg(col_dmg,e.ac,0,pow,false,true) end break end local om=monster.get_monster_at(nx,ny) if om and om:attitude()==0 and not om:is_firewood() then if moved>0 then dmg=dmg+calc_dmg(col_dmg,e.ac,0,pow,false,true) dmg=dmg+calc_dmg(col_dmg,fast_ac(om,nx,ny),0,pow,false,true) end break end kx=nx;ky=ny;moved=moved+1 end if cdist(e.x,e.y)<=1 then dmg=dmg*1.3 end val=val+score_dmg(dmg,e.m,cdist(e.x,e.y),e) end end end best_val=val*surround_boost if best_val>0 then best_tx=0;best_ty=0 end elseif sp.n=="Irradiate" then -- Irradiate: hits all adjacent enemies (radius 1, player-centered) local val=0 for _,e in ipairs(t) do if cdist(e.x,e.y)<=1 then local base=get_spell_dmg(sp.n,pow) local dmg=calc_dmg(base,e.ac,e.ev,pow,sp.noac,sp.nohit,sp.ac3,elec) dmg=sh_damage(dmg,e.m) val=val+score_dmg(dmg,e.m,cdist(e.x,e.y),e) end end best_val=val*surround_boost if best_val>0 then best_tx=0;best_ty=0 end elseif sp.n=="Flame Wave" then -- Flame Wave: expanding AoE, 3 turns. Closer enemies hit more turns. local val=0 local base=get_spell_dmg(sp.n,pow)/3 -- per-turn damage for _,e in ipairs(t) do local d=cdist(e.x,e.y) if d<=3 then local turns_hit=4-d -- dist1=3turns, dist2=2turns, dist3=1turn local r=get_res(e.m,sp.e) local mult=get_res_mult(r) if mult>0 then local dmg=calc_dmg(base*turns_hit*mult,e.ac,e.ev,pow,sp.noac,sp.nohit,sp.ac3,elec) dmg=sh_damage(dmg,e.m) val=val+score_dmg(dmg,e.m,d,e) end end end best_val=val*surround_boost if best_val>0 then best_tx=0;best_ty=0 end elseif sp.n=="Frozen Ramparts" then -- Frozen Ramparts: icy walls within range 2 of player damage adjacent monsters per turn -- Score enemies adjacent to walls that are within range of player -- Chokepoint bonus: enemies in tight wall clusters can't avoid passing icy walls local base=get_spell_dmg(sp.n,pow)*3 -- ~3 turns conservative local val=0 for _,e in ipairs(t) do -- Count wall neighbors (gives both adjacency check and chokepoint signal) local wall_neighbors=0 for dx=-1,1 do for dy=-1,1 do if not (dx==0 and dy==0) then local wx,wy=e.x+dx,e.y+dy if is_solid(wx,wy) and cdist(wx,wy)<=range then wall_neighbors=wall_neighbors+1 end end end end if wall_neighbors>0 then local r=get_res(e.m,sp.e) local mult=get_res_mult(r) if mult>0 then local dmg=calc_dmg(base*mult,e.ac,0,pow,sp.noac,sp.nohit,sp.ac3,elec) -- Chokepoint: 2+ wall neighbors means enemy is squeezed → can't avoid icy contact local choke=1.0 if wall_neighbors>=4 then choke=1.5 elseif wall_neighbors>=2 then choke=1.25 end val=val+score_dmg(dmg,e.m,cdist(e.x,e.y),e)*choke end end end -- Global corridor bonus: ramparts shine when player is in narrow passage if corridor_walls>=5 then val=val*1.3 end best_val=val if best_val>0 then best_tx=0;best_ty=0 end else -- Other centered spells (Polar Vortex): hit all enemies within AoE range -- Wall penalty: tight spaces reduce vortex effectiveness local vortex_pen=1.0 if corridor_walls>=4 then vortex_pen=0.3 end local val=0 for _,e in ipairs(t) do if cdist(e.x,e.y)<=sp.aoe then local r=get_res(e.m,sp.e) local mult=get_res_mult(r) if mult>0 then local base=get_spell_dmg(sp.n,pow) local dmg=spell_dmg(base,mult,e.ac,0,pow,sp,elec) val=val+score_dmg(dmg,e.m,cdist(e.x,e.y),e) end end end best_val=val*vortex_pen if best_val>0 then best_tx=0;best_ty=0 end end elseif sp.n=="Lee's Rapid Deconstruction" then -- LRD: scan all cells in range for walls or fraggable monsters local best_dmg=0 for x=-range,range do for y=-range,range do if you.see_cell_solid_see(x,y) then local dice=0 local radius=1 local is_ice=false local from_mon=false -- Check monster first (by name-based lookup, matching source fraggable_monsters) local tm=monster.get_monster_at(x,y) if tm and tm:attitude()==0 and not tm:is_firewood() then local md,mr,mi=lrd_dice_mon(tm) if md>0 then dice=md;radius=mr;is_ice=mi;from_mon=true end end -- Then terrain if dice==0 then local f=view.feature_at(x,y) local td,tr=lrd_dice_terrain(f) if td>0 then dice=td;radius=tr end end if dice>0 and not has_friendly(x,y,radius) and math.max(math.abs(x),math.abs(y))>radius then local val=0 local dmg_per=4+pow/5 for _,e2 in ipairs(t) do local dist=math.max(math.abs(e2.x-x),math.abs(e2.y-y)) if dist<=radius and (e2.x~=0 or e2.y~=0) then local base=dice*(dmg_per+1)/2 local dmg=calc_dmg(base,e2.ac,0,pow,false,true,true) -- Ice explosion: apply cold resistance if is_ice then local cr=e2.m:res_cold() if cr>0 then dmg=dmg/(1+cr) elseif cr<0 then dmg=dmg*1.5 end end local sv=score_dmg(dmg,e2.m,cdist(e2.x,e2.y),e2) if from_mon and e2.x==x and e2.y==y then sv=sv*1.5 end val=val+sv end end if val>best_dmg then best_dmg=val;best_tx=x;best_ty=y end end end end end best_val=best_dmg else local aoe_centers={} local aoe_seen={} local function add_center(cx,cy) local k=cx..","..cy if not aoe_seen[k] then aoe_seen[k]=true;aoe_centers[#aoe_centers+1]={x=cx,y=cy} end end for _,e in ipairs(t) do add_center(e.x,e.y) end if sp.aoe and sp.aoe>0 and sp.aoe<9 then local p_res=player_res(sp.e) if (sp.n=="Fireball" or sp.n=="Fire Storm") and p_res>=2 then add_center(0,0) end -- For any_target spells, also add empty cells near player as potential targets if sp.any_target then for dx=-sp.aoe,sp.aoe do for dy=-sp.aoe,sp.aoe do if not (dx==0 and dy==0) and not enemy_at[dx..","..dy] and los_vis(dx,dy) then add_center(dx,dy) end end end end -- Add offset centers around each enemy (only for targeted AoE, not full-LOS) for _,e in ipairs(t) do for dx=-sp.aoe,sp.aoe do for dy=-sp.aoe,sp.aoe do if not (dx==0 and dy==0) then local cx,cy=e.x+dx,e.y+dy if not enemy_at[cx..","..cy] and los_vis(cx,cy) then add_center(cx,cy) end end end end end end -- Chain Lightning: arc decay model (0.6x per arc, initial range penalty) if sp.chain_lightning then local sorted={} for _,e2 in ipairs(t) do sorted[#sorted+1]=e2 end table.sort(sorted,function(a,b) return cdist(a.x,a.y)0 then local base=get_spell_dmg(sp.n,pow) local dmg=spell_dmg(base,mult,e2.ac,e2.ev,pow,sp,elec)*arc_mult dmg=sh_damage(dmg,e2.m) val=val+score_dmg(dmg,e2.m,cdist(e2.x,e2.y),e2) end arc_mult=arc_mult*0.6 end best_val=val best_tx=sorted[1].x;best_ty=sorted[1].y -- Ignition: independent 3x3 explosion per enemy, overlapping hits elseif sp.ignition then local val=0 for _,e2 in ipairs(t) do local r=get_res(e2.m,sp.e) local mult=get_res_mult(r,sp.halfres) if mult>0 then local explosions=1 for _,e3 in ipairs(t) do if e3~=e2 and math.max(math.abs(e3.x-e2.x),math.abs(e3.y-e2.y))<=1 then explosions=explosions+1 end end local base=get_spell_dmg(sp.n,pow) local dmg=calc_dmg(base*mult,e2.ac,e2.ev,pow,sp.noac,sp.nohit,sp.ac3,elec)*explosions dmg=sh_damage(dmg,e2.m) val=val+score_dmg(dmg,e2.m,cdist(e2.x,e2.y),e2) end end best_val=val best_tx=t[1].x;best_ty=t[1].y else for _,center in ipairs(aoe_centers) do -- Prism: must target empty non-solid non-self tile local prism_ok=true if sp.prism then if (center.x==0 and center.y==0) or is_solid(center.x,center.y) or monster.get_monster_at(center.x,center.y) then prism_ok=false end end -- Fireball/Fire Storm self-damage filter: blast radius hits player if center within sp.aoe local self_ok=true if (sp.n=="Fireball" or sp.n=="Fire Storm") and player_res(sp.e)<2 then if cdist(center.x,center.y)<=sp.aoe then self_ok=false end end if prism_ok and self_ok and not has_friendly(center.x,center.y,sp.aoe) then local val=0 for _,e2 in ipairs(t) do local dist=math.max(math.abs(e2.x-center.x),math.abs(e2.y-center.y)) local adj_dist=math.max(math.abs(e2.x),math.abs(e2.y)) if dist<=sp.aoe and (not sp.noadjacent or adj_dist>1) then local r=get_res(e2.m,sp.e) local mult=get_res_mult(r) if sp.undead and e2.m:holiness()~="undead" then mult=0 end if sp.rpois_immune and r>=1 then mult=0 end if sp.n=="Ignite Poison" and not e2.is_poisoned then mult=0 end if mult>0 then local base=get_spell_dmg(sp.n,pow) if sp.n=="Shatter" then -- 2x: fraggable (same list as LRD) + petrified/petrifying local lrd_d=lrd_dice_mon(e2.m) if lrd_d>0 then base=base*2 else -- 1/3: airborne or amorphous (jellies, slimes) local hdesc=e2.tdesc or "" if string.find(hdesc,"lying") or string.find(hdesc,"evitat") then base=base/3 end end end local dmg=spell_dmg(base,mult,e2.ac,e2.ev,pow,sp,elec) dmg=sh_damage(dmg,e2.m) local cl=view.cloud_at(e2.x,e2.y) if cl then if (sp.e==3 and (string.find(cl,"fire") or string.find(cl,"flame"))) or (sp.e==1 and string.find(cl,"freezing")) or (sp.e==4 and (string.find(cl,"poison") or string.find(cl,"miasma"))) then dmg=dmg*0.3 end end val=val+score_dmg(dmg,e2.m,cdist(e2.x,e2.y),e2) end end end -- Prism: discount for 2-turn delay (enemies may move away) if sp.prism then val=val*0.5 end -- Boost AoE centered on/near player when surrounded if cdist(center.x,center.y)<=1 then val=val*surround_boost end if val>total_dmg then total_dmg=val;best_tx=center.x;best_ty=center.y end end end best_val=total_dmg end end elseif sp.auto_target then -- Auto-target spells: Plasma Beam (farthest), Permafrost (largest cluster), Ozocubu (all in LOS) if sp.n=="Plasma Beam" then -- Plasma Beam: targets farthest enemy, hits all on line -- 50% fire-zap + 50% elec-zap — dual res handled inside spell_dmg via m param local farthest_dist=0 local farthest_e=nil for _,e in ipairs(t) do local dist=math.max(math.abs(e.x),math.abs(e.y)) if dist>farthest_dist then farthest_dist=dist;farthest_e=e end end if farthest_e then local val=0 local others=monsters_on_line(farthest_e.x,farthest_e.y) for _,target in ipairs(others) do local base=get_spell_dmg(sp.n,pow) local ac=fast_ac(target.m,target.x,target.y) local ev=fast_ev(target.m,target.x,target.y) local hitC=get_hit_chance(target.m,sp.n,ev) local tdmg=spell_dmg(base,1.0,ac,0,pow,sp,false,true,target.m)*hitC tdmg=sh_damage(tdmg,target.m) if tdmg>0 then val=val+score_dmg(tdmg,target.m,cdist(target.x,target.y)) end end -- Add the farthest target itself local fbase=get_spell_dmg(sp.n,pow) local fac=farthest_e.ac local fev=farthest_e.ev local fhitC=get_hit_chance(farthest_e.m,sp.n,fev) local ftdmg=spell_dmg(fbase,1.0,fac,0,pow,sp,false,true,farthest_e.m)*fhitC ftdmg=sh_damage(ftdmg,farthest_e.m) if ftdmg>0 then val=val+score_dmg(ftdmg,farthest_e.m,cdist(farthest_e.x,farthest_e.y)) end -- Lineup bonus for penetrating beam local lineup=#others if lineup>=1 then val=val*(1.0+lineup*0.3) end if val>0 then best_val=val;best_tx=farthest_e.x;best_ty=farthest_e.y end end elseif sp.n=="Permafrost Eruption" then -- Permafrost: auto-targets best cluster (game chooses), we estimate damage -- Source: picks enemy with most adjacent foes, skips dist<2 from caster -- Direct earth hit on target + cold explosion (ex_size=1) on neighbors local best_cluster=0 local best_target=nil for _,e in ipairs(t) do if cdist(e.x,e.y)>=2 then -- source: grid_distance(t,src)<2 skips adjacent local cluster_size=0 for _,e2 in ipairs(t) do if math.abs(e2.x-e.x)<=1 and math.abs(e2.y-e.y)<=1 then cluster_size=cluster_size+1 end end if cluster_size>best_cluster then best_cluster=cluster_size best_target=e end end end -- No fallback: game's permafrost_targets skips all enemies with grid_distance<2 -- from the caster (spl-damage.cc:1137). If every enemy is adjacent, targets is -- empty and the cast fizzles ("Nothing happens"), so Permafrost must score 0 here. if best_target then local val=0 local base=get_spell_dmg(sp.n,pow) for _,e2 in ipairs(t) do if math.abs(e2.x-best_target.x)<=1 and math.abs(e2.y-best_target.y)<=1 then local r=get_res(e2.m,sp.e) local mult=get_res_mult(r) if mult>0 then local tdmg=spell_dmg(base,mult,e2.ac,e2.ev,pow,sp,elec) tdmg=sh_damage(tdmg,e2.m) val=val+score_dmg(tdmg,e2.m,cdist(e2.x,e2.y),e2) end end end if val>0 then best_val=val;best_tx=0;best_ty=0 end end elseif sp.n=="Ozocubu's Refrigeration" then -- Ozocubu's Refrigeration: hits all in LOS, damage reduced by adjacent enemies (max 2) local total_dmg=0 for _,e in ipairs(t) do local reduction=math.min(e.adj_enemies,2)*0.3 local r=get_res(e.m,sp.e) local mult=get_res_mult(r) if mult>0 then local base=get_spell_dmg(sp.n,pow) local tdmg=calc_dmg(base*mult*(1-reduction),e.ac,0,pow,sp.noac,sp.nohit,sp.ac3,elec) tdmg=sh_damage(tdmg,e.m) total_dmg=total_dmg+score_dmg(tdmg,e.m,cdist(e.x,e.y),e) end end if total_dmg>0 then best_val=total_dmg;best_tx=0;best_ty=0 end end elseif sp.starburst then -- Starburst: 8 rays from player in cardinal+diagonal directions local base=get_spell_dmg(sp.n,pow) local val=0 local dirs={{1,0},{-1,0},{0,1},{0,-1},{1,1},{1,-1},{-1,1},{-1,-1}} for _,d in ipairs(dirs) do for R=1,range do local rx,ry=d[1]*R,d[2]*R if is_solid(rx,ry) then break end local m2=monster.get_monster_at(rx,ry) if m2 and m2:attitude()==0 and not m2:is_firewood() then local r=get_res(m2,sp.e) local mult=get_res_mult(r) if mult>0 then local ac=fast_ac(m2,rx,ry) local ev=fast_ev(m2,rx,ry) local dmg=calc_dmg(base*mult,ac,ev,pow,sp.noac,sp.nohit,sp.ac3,elec) dmg=sh_damage(dmg,m2) val=val+score_dmg(dmg,m2,cdist(rx,ry)) end end end end if val>0 then best_val=val;best_tx=0;best_ty=0 end elseif sp.magnavolt then -- Magnavolt: use pre-computed magnetised targets (mag_cache), find best new target local mag_targets=mag_cache local path_cache={} -- (#20) cache spells.path per target tile -- Calculate damage for a bolt aimed at (tx,ty): hits target + all enemies on path local base=get_spell_dmg(sp.n,pow) local function bolt_dmg_to_mon(m2,mx,my) local r=get_res(m2,sp.e) local mult=get_res_mult(r) if mult==0 then return 0 end local ac=fast_ac(m2,mx,my) local dmg=calc_dmg(base*mult,ac,0,pow,sp.noac,true,sp.ac3,elec) dmg=sh_damage(dmg,m2) return score_dmg(dmg,m2,cdist(mx,my)) end local function mag_bolt_val(tx,ty) -- Bolt to (tx,ty) penetrates through all enemies on the path local val=0 local pkey=tx..","..ty local path=path_cache[pkey] if path==nil then path=spells.path(sp.n,tx,ty) or false path_cache[pkey]=path end if path==false then path=nil end if path then for _,p in ipairs(path) do local pm=monster.get_monster_at(p[1],p[2]) if pm and pm:attitude()==0 and not pm:is_firewood() then val=val+bolt_dmg_to_mon(pm,p[1],p[2]) end end end -- Add the endpoint target itself (may not be in path) local tm=monster.get_monster_at(tx,ty) if tm and tm:attitude()==0 and not tm:is_firewood() then -- Check not already counted via path local counted=false if path then for _,p in ipairs(path) do if p[1]==tx and p[2]==ty then counted=true break end end end if not counted then val=val+bolt_dmg_to_mon(tm,tx,ty) end end return val end -- Existing magnetised damage (bolts to all current targets, each penetrating) local existing_val=0 for _,mt in ipairs(mag_targets) do existing_val=existing_val+mag_bolt_val(mt.x,mt.y) end -- Try each non-magnetised enemy as new target for _,e in ipairs(t) do if can_reach(e.x,e.y,range) then local already_mag=false for _,mt in ipairs(mag_targets) do if mt.x==e.x and mt.y==e.y then already_mag=true break end end -- New target gets bolt (penetrating) + all existing magnetised get bolts too local new_bolt=mag_bolt_val(e.x,e.y) local val=existing_val+new_bolt -- Ramp-up bonus: magnetising targets builds future value, scaled by target threat -- High-HP targets benefit more from stacking; weak enemies get less ramp-up local threat_scale=math.min(2.0,math.max(0.5,e.mhp/(max_single_threat+1))) val=val*(1.0+0.5*threat_scale+#mag_targets*0.3) if val>best_val then best_val=val;best_tx=e.x;best_ty=e.y end end end -- If all enemies already magnetised, pick highest-damage one as target if best_val==0 and #mag_targets>0 then for _,mt in ipairs(mag_targets) do if not mt.cloud and can_reach(mt.x,mt.y,range) then local val=existing_val if val>best_val then best_val=val;best_tx=mt.x;best_ty=mt.y end end end end elseif sp.clutch then -- Borgnjor's Vile Clutch: bolt constricts all enemies on path -- Score = sum of constriction DoT value for each non-constricted enemy hit -- Prioritize paths that constrict the most dangerous, closest enemies -- Duration: 4 + random_range(pow/35, pow/25+1); avg ~ 4.5 + pow/29 local constr_turns=4.5+pow/29 local constr_dmg=get_spell_dmg(sp.n,pow)*constr_turns -- Try each enemy as a bolt direction; score all enemies on the path for _,e in ipairs(t) do if can_reach(e.x,e.y,range) and not beam_blocked_by_friendly(sp.n,e.x,e.y) then local val=0 local path=spells.path(sp.n,e.x,e.y,0,0,false) local hits={} if path then for _,p in ipairs(path) do local pm=monster.get_monster_at(p[1],p[2]) if pm and pm:attitude()==0 and not pm:is_firewood() then hits[#hits+1]={m=pm,x=p[1],y=p[2]} end end end -- Score each hit: skip already constricted -- EV synergy now handled globally in get_ev() (-10 EV for constricted monsters) for _,h in ipairs(hits) do local he=enemy_at[h.x..","..h.y] local hdesc=(he and he.tdesc) or h.m:target_desc() or "" if not string.find(hdesc,"onstrict") then local ac=fast_ac(h.m,h.x,h.y) local dmg=calc_dmg(constr_dmg,ac,0,pow,false,true,false) -- Regen vs DoT: constriction damage offset by regeneration local he=enemy_at[h.x..","..h.y] if he and he.is_regen then dmg=dmg*0.6 end val=val+score_dmg(dmg,h.m,cdist(h.x,h.y)) end end if val>best_val then best_val=val;best_tx=e.x;best_ty=e.y end end end elseif sp.fcloud then -- Freezing Cloud: smite-targeted cloud, radius 2 -- Score by enemies covered + corridor blocking bonus -- Safe to self-cast at rC+3 local cloud_turns=3 local cloud_dmg=get_spell_dmg(sp.n,pow)*cloud_turns local cloud_r=2 local player_immune=(you.res_cold()>=3) -- Gather candidate cloud centers: on enemies, between player and enemies, and on player local centers={} for _,e in ipairs(t) do centers[#centers+1]={x=e.x,y=e.y} -- Also try midpoint between player and enemy (corridor blocking) local mx=math.floor(e.x/2+0.5) local my=math.floor(e.y/2+0.5) if (mx~=0 or my~=0) and not is_solid(mx,my) and cdist(mx,my)<=range then centers[#centers+1]={x=mx,y=my} end end if player_immune then centers[#centers+1]={x=0,y=0} end -- Score each center for _,c in ipairs(centers) do if cdist(c.x,c.y)<=range and not is_solid(c.x,c.y) and (player_immune or cdist(c.x,c.y)>cloud_r) and not has_friendly(c.x,c.y,cloud_r) then local val=0 -- Count enemies in cloud radius, skip those already in freezing cloud for _,e2 in ipairs(t) do local d=math.max(math.abs(e2.x-c.x),math.abs(e2.y-c.y)) local cl=view.cloud_at(e2.x,e2.y) local in_fcloud=cl and string.find(cl,"freezing") if d<=cloud_r and not in_fcloud then local r=get_res(e2.m,sp.e) if r<3 then -- rC+3 immune local mult=1.0 if r==-1 then mult=1.5 elseif r==1 then mult=0.5 elseif r==2 then mult=0.25 end local dmg=cloud_dmg*mult val=val+score_dmg(dmg,e2.m,cdist(e2.x,e2.y)) end end end -- Corridor bonus: count solid tiles adjacent to cloud center -- More walls = narrower passage = better zone control local walls=0 for dx=-1,1 do for dy=-1,1 do if (dx~=0 or dy~=0) and is_solid(c.x+dx,c.y+dy) then walls=walls+1 end end end if walls>=2 then val=val*(1+walls*0.15) end if val>best_val then best_val=val;best_tx=c.x;best_ty=c.y end end end elseif sp.mortar then -- Hellfire Mortar: bolt path creates lava (blocks non-flying), summons mortar -- Score = enemies on path damage + lava blocking bonus for nearby non-flying enemies -- Mortar fires ~path_len*1.5 turns; use conservative 3 shots estimate local shots=3 local base_dmg=get_spell_dmg(sp.n,pow)*shots for _,e in ipairs(t) do if can_reach(e.x,e.y,range) and not beam_blocked_by_friendly(sp.n,e.x,e.y) then local val=0 local path=spells.path(sp.n,e.x,e.y) local path_tiles={} if path then for _,p in ipairs(path) do path_tiles[#path_tiles+1]={x=p[1],y=p[2]} -- Score enemies on the path (mortar will fire at them) local pm=monster.get_monster_at(p[1],p[2]) if pm and pm:attitude()==0 and not pm:is_firewood() then local r=get_res(pm,sp.e) local mult=get_res_mult(r,sp.halfres) if mult>0 then local ac=fast_ac(pm,p[1],p[2]) local ev=fast_ev(pm,p[1],p[2]) local dmg=calc_dmg(base_dmg*mult,ac,ev,pow,false,false,false) val=val+score_dmg(dmg,pm,cdist(p[1],p[2])) end end end end -- Also score endpoint enemy local has_end=false if path then for _,p in ipairs(path) do if p[1]==e.x and p[2]==e.y then has_end=true break end end end if not has_end then local r=get_res(e.m,sp.e) local mult=get_res_mult(r,sp.halfres) if mult>0 then local dmg=calc_dmg(base_dmg*mult,e.ac,e.ev,pow,false,false,false) val=val+score_dmg(dmg,e.m,cdist(e.x,e.y),e) end end -- Lava blocking bonus: count nearby non-path enemies that would be blocked -- Lava blocks ground movement, so non-flying enemies near path get bonus if #path_tiles>=2 then local lava_bonus=0 for _,e2 in ipairs(t) do -- Check if enemy is adjacent to lava path (would be blocked) for _,pt in ipairs(path_tiles) do local d=math.max(math.abs(e2.x-pt.x),math.abs(e2.y-pt.y)) if d<=1 then -- Non-flying enemies blocked by lava get bonus local hdesc=e2.tdesc or "" local flying=string.find(hdesc,"lying") or string.find(hdesc,"evitat") if not flying then lava_bonus=lava_bonus+0.3 end break end end end val=val*(1+lava_bonus) end -- Corridor bonus: lava in narrow passages is more valuable local walls=0 for _,pt in ipairs(path_tiles) do for dx=-1,1 do for dy=-1,1 do if (dx~=0 or dy~=0) and is_solid(pt.x+dx,pt.y+dy) then walls=walls+1 end end end end if #path_tiles>0 then walls=walls/#path_tiles end if walls>=3 then val=val*1.5 elseif walls>=2 then val=val*1.2 end if val>best_val then best_val=val;best_tx=e.x;best_ty=e.y end end end elseif sp.ood then -- Orb of Destruction: distance scaling, obstacle check, adjacent orb check -- Don't fire if there's already an OoD adjacent to player (collision risk) local adj_ood=false for dx=-1,1 do for dy=-1,1 do local m2=monster.get_monster_at(dx,dy) if m2 and m2:name()=="orb of destruction" and m2:attitude()>0 then adj_ood=true end end end if not adj_ood then local base_M=5+pow/12 for _,e in ipairs(t) do if can_reach(e.x,e.y,range) then local dist=math.max(math.abs(e.x),math.abs(e.y)) -- Enemy likely steps toward player before orb arrives if dist>3 then dist=dist-1 end local scale=1.0 if dist<4 then scale=dist*0.3 end local dmg=calc_dmg(9*(base_M*scale+1)/2,e.ac,0,pow,false,true,false) dmg=sh_damage(dmg,e.m) -- Check the ray for walls/corners the homing orb can't clear (spells.path is nil for OOD) local blocked=not ood_path_clear(e.x,e.y) if not blocked and dmg>0 then local val=score_dmg(dmg,e.m,cdist(e.x,e.y),e) if val>best_val then best_val=val;best_tx=e.x;best_ty=e.y end end end end end elseif sp.maxwells then -- Maxwell's Capacitive Coupling: targets closest visible enemy, kills after channeling local closest_e=nil local closest_d=99 for _,e in ipairs(t) do local d=cdist(e.x,e.y) if d0 and pm:name()~="battlesphere" and pm:name()~="orb of destruction" then path_has_friendly=true break end end end end if can_reach(e.x,e.y,range) and not ((sp.beam or sp.penetrate) and (path_has_friendly or not path_reaches)) then local r=get_res(e.m,sp.e) local mult=get_res_mult(r) if sp.undead and e.m:holiness()~="undead" then mult=0 end if sp.rpois_immune and r>=1 then mult=0 end local hp=get_hp(e.m) local base=get_spell_dmg(sp.n,pow) -- Sticky Flame: heavily penalize target already burning (doesn't stack) if sp.n=="Sticky Flame" then if e.m:status("covered in liquid flames") then base=base*0.1 end -- Regen vs DoT: regeneration offsets burn damage over time if e.is_regen then base=base*0.6 end end if sp.n=="Airstrike" then local empty=count_empty(e.x,e.y) base=base+empty*2 end local ac=e.ac local ev=e.ev -- Use game API evasion if available local hit_pct=evasion_check(e.m,sp.n) local use_ev=ev if hit_pct and not sp.nohit then use_ev=0 end -- will apply hit_pct separately local dmg=spell_dmg(base,mult,ac,use_ev,pow,sp,elec) if hit_pct and not sp.nohit then dmg=dmg*hit_pct end dmg=sh_damage(dmg,e.m) -- Shock straight-stop (aimed_at_spot=true): the bolt pierces the line to the -- target and STOPS there, so it can never ricochet back onto the player. Pierced -- enemies on the straight line are scored via the penetrate block below (which -- uses cached_path). Ricochet + double-hit geometries (aimed_at_spot=false) are -- enumerated separately in shock_aim_targets and compete for best_val. if sp.n=="Iskenderun's Mystic Blast" then -- knockback: push enemy up to 2+pow/50 tiles away from player -- collision with wall or another monster deals 2d(1+pow/10) extra -- Only triggers if actor actually moved (pos()!=newpos in actor::collide) local kd=2+pow/50 local dx=e.x>0 and 1 or (e.x<0 and -1 or 0) local dy=e.y>0 and 1 or (e.y<0 and -1 or 0) local kx,ky=e.x,e.y local col_dmg=get_spell_dmg("IMB_collision",pow) local moved=0 for _=1,math.floor(kd) do local nx,ny=kx+dx,ky+dy if is_solid(nx,ny) then if moved>0 then dmg=dmg+calc_dmg(col_dmg,e.ac,0,pow,false,true) end break end local om=monster.get_monster_at(nx,ny) if om and om:attitude()==0 and not om:is_firewood() then if moved>0 then dmg=dmg+calc_dmg(col_dmg,e.ac,0,pow,false,true) dmg=dmg+calc_dmg(col_dmg,fast_ac(om,nx,ny),0,pow,false,true) end break end kx=nx;ky=ny;moved=moved+1 end if cdist(e.x,e.y)<=1 then dmg=dmg*1.3 end end -- Note: penetrate intermediate damage is scored per-monster below, not added to primary dmg if dmg>0 then local val=score_dmg(dmg,e.m,cdist(e.x,e.y),e) -- Straight-line pierce scoring (includes Shock's aimed_at_spot=true shot) if sp.penetrate then local reachChance2=1.0 -- Use cached path when available (avoids second spells.path call) local others if found_star then others=monsters_on_line(e.x,e.y) elseif cached_path then others={} for _,p in ipairs(cached_path) do if not (p[1]==e.x and p[2]==e.y) then local pm=monster.get_monster_at(p[1],p[2]) if pm and pm:attitude()==0 and not pm:is_firewood() then others[#others+1]={m=pm,x=p[1],y=p[2]} end end end else others=monsters_on_path(sp.n,e.x,e.y) end local lineup_count=0 for _,other in ipairs(others) do local r2=get_res(other.m,sp.e) local m2=get_res_mult(r2) if m2>0 then local b2=get_spell_dmg(sp.n,pow) local hitC=get_hit_chance(other.m,sp.n) local d2=calc_dmg(b2*m2,fast_ac(other.m,other.x,other.y),0,pow,sp.noac,true,sp.ac3,elec)*hitC*reachChance2 d2=sh_damage(d2,other.m) val=val+score_dmg(d2,other.m,cdist(other.x,other.y)) reachChance2=reachChance2*(1-hitC) lineup_count=lineup_count+1 end end -- Bolt alignment bonus: prefer penetrating bolts when enemies line up if lineup_count>=1 then val=val*(1.0+lineup_count*0.3) end end -- Soft penalty for single-target spells needing many casts to kill local kn=1 if not sp.aoe and not sp.penetrate and not sp.beam then local total_dmg=dmg+(has_bsph and not sp.no_bsph and bsph_raw or 0) kn=math.ceil(hp/math.max(total_dmg,1)) if kn>4 then val=val*math.max(0.25,0.8^(kn-4)) end end if val>best_val then best_val=val;best_tx=e.x;best_ty=e.y;best_kn=kn if sp.n=="Shock" then best_ep=true end end -- straight-stop: aimed_at_spot=true end end end -- Shock ricochet/double-hit: score every aim DIRECTION (aimed_at_spot=false). -- shock_aim_targets already excluded self-hitting directions. hits==2 means the -- bolt crosses that enemy twice (out + return leg) — a double-hit. These compete -- with the straight-stop shots above; the winner sets best_ep accordingly. if sp.n=="Shock" and #shock_aim_targets>0 then for _,wt in ipairs(shock_aim_targets) do local val=0 local bounce_count=0 local double_count=0 for key,info in pairs(wt.enemies) do local hits=wt.hits[key] local r2=get_res(info.m,sp.e) local m2=get_res_mult(r2) if m2>0 then local b2=get_spell_dmg(sp.n,pow) local hitC=get_hit_chance(info.m,sp.n) local iac=fast_ac(info.m,info.x,info.y) local d2=spell_dmg(b2,m2,iac,0,pow,sp,elec,true)*hitC d2=sh_damage(d2,info.m)*hits val=val+score_dmg(d2,info.m,cdist(info.x,info.y)) bounce_count=bounce_count+1 if hits>=2 then double_count=double_count+1 end end end -- Alignment bonus: reward multi-enemy ricochets; extra reward for double-hits if bounce_count>=1 then val=val*(1.0+bounce_count*0.5+double_count*0.5) end if val>best_val then best_val=val;best_tx=wt.x;best_ty=wt.y;best_kn=1;best_ep=false end end end end if best_val>0 then local penalty=1.0 if sp.channeled and sp.n=="Flame Wave" then penalty=0.33 end if sp.channeled and sp.n=="Searing Ray" then penalty=0.5 end if sp.slow then penalty=penalty*0.67 if adj_hostiles>=2 then penalty=penalty*0.6 end end -- Add battlesphere bonus: flat for multi-target, kills-based for single-target -- Fizzle: if our single-target spell one-shots bsph_target, bsph fires at fresh/less-injured -- replacement and the precomputed bonus is overstated — discount it. if has_bsph and not sp.no_bsph then local bb=bsph_bonus local same_target=bsph_target and not (sp.aoe or sp.penetrate or sp.beam) and best_tx==bsph_target.x and best_ty==bsph_target.y if same_target and best_kn==1 then -- Fizzle: spell one-shots bsph_target; sphere fires at less-injured replacement bb=bb*0.4 elseif same_target and best_kn<=3 then -- Combo: single-target spell + sphere both pile on same enemy → faster kill bb=bb*1.3 end if sp.aoe or sp.penetrate or sp.beam then best_val=best_val+bb elseif best_kn<=4 then best_val=best_val+bb*(1+(4-best_kn)*0.25) else best_val=best_val+bb*0.5 end end -- Low HP panic: boost nohit spells, penalize distant targets if panic_mode then if sp.nohit then best_val=best_val*1.4 end if best_tx and cdist(best_tx,best_ty)>2 then best_val=best_val*0.5 end end -- Tactical modifiers: depth, corridor, fight type local tac=1.0 -- Depth scaling: early game penalizes expensive, late game penalizes cheap (smooth) if cost>=7 then tac=tac*(0.6+0.4*depth_fac) end if cost<=3 then tac=tac*(1.0-0.5*depth_fac) end -- Corridor: boost beam/penetrate, penalize wide AoE if in_corridor then if sp.beam or sp.penetrate then tac=tac*1.3 elseif sp.aoe and sp.aoe>=2 then tac=tac*0.7 end end -- Boss fight: boost single-target burst if boss_fight and not sp.aoe then tac=tac*1.3 end -- Swarm fight: boost AoE if swarm_fight and sp.aoe then tac=tac*1.3 end -- Mana conservation: prefer cheap spells when low MP and no critical threat if mp<=mp_max*0.3 and adj_hostiles==0 and cost>=5 then tac=tac*0.6 end -- Ignite Poison: boost when many enemies already poisoned (OTR synergy) if sp.n=="Ignite Poison" and n_poisoned>0 then local pois_frac=n_poisoned/#t if pois_frac>=0.5 then tac=tac*(1.0+pois_frac) end end -- Noise: prefer quiet spells when few enemies, none adjacent if #t<=2 and adj_hostiles==0 then local lvl=spells.level(sp.n) or 5 if lvl>=7 then tac=tac*0.7 end if lvl>=5 then tac=tac*0.85 end end -- Distant threat: penalize expensive spells when enemies far, non-threatening if adj_hostiles==0 and min_enemy_dist>=4 and not boss_fight then if cost>=6 then tac=tac*0.7 end end -- Resistance pressure: boost irresistible spells when enemies resist most elements. -- Full boost only for spells that also bypass AC (truly unmitigated: Magic Dart, -- Airstrike, Dispel). AC-blocked physical (Sandblast, Stone Arrow, Bombard, LRD, -- Crystal Spear) still beats a resisted element, but is tempered by high enemy AC. if res_pressure>=0.6 and sp.e==0 then if sp.noac then tac=tac*1.3 else tac=tac*(1.3-math.min(0.25,ac_pressure/80)) end end -- Fulsome Fusillade opener: rains 3 shots/turn for ~5 turns while you cast other -- spells. Only reached when inactive (skipped at line 804 while raining). One turn's -- AoE score understates it heavily; boost so it wins the opener with 2+ enemies. -- A true lethal single-turn burst can still outscore it (this only scales the AoE val). if sp.n=="Fulsome Fusillade" and #t>=2 then tac=tac*FUSILLADE_OPENER end local eff_val=best_val*penalty*tac/math.sqrt(cost) local entry={n=sp.n,v=eff_val,x=best_tx,y=best_ty,c=cost,e=sp.e,ch=sp.channeled,manual=sp.mortar,ep=(sp.n=="Shock") and best_ep or not (sp.clutch or sp.penetrate),ki=best_kn} if cost<=mp then table.insert(candidates,entry) elseif cost==mp+1 then table.insert(almost,entry) end end end end end -- 2-Turn Combo Lookahead: a setup spell's value becomes the best two-turn plan it -- enables (setup now + discounted payoff next turn). All combos below are computed -- from a single BASE snapshot (base_v) and COMBINED VIA MAX into c.combo_v — they do -- NOT stack. Otherwise a spell like Scorch would collect its specific fire-combo -- bonus AND the generic-lookahead bonus on top of each other, inflating its score. local base_v={} for i,c in ipairs(candidates) do base_v[i]=c.v; c.combo_v=c.v end local function propose(i,total) if total>candidates[i].combo_v then candidates[i].combo_v=total end end -- Poison → Ignite Poison combo local has_ignite=spells.memorised("Ignite Poison") if has_ignite then local ig_pow=get_pow("Ignite Poison") local ig_cost=get_cost("Ignite Poison") local ig_base=get_spell_dmg("Ignite Poison",ig_pow) -- Count how many enemies are NOT currently poisoned (Ignite can't hit them yet) local unpoisoned={} for _,e in ipairs(t) do local r=get_res(e.m,4) -- poison resistance if r<2 then -- not poison-immune if not e.is_poisoned then unpoisoned[#unpoisoned+1]=e end end end if #unpoisoned>0 then -- For each poison-applying candidate, calculate Ignite Poison payoff next turn for i,c in ipairs(candidates) do local is_poison_setup=(c.n=="Poisonous Vapours" or c.n=="Mercury Arrow") local is_aoe_poison=(c.n=="Olgreb's Toxic Radiance") if is_poison_setup or is_aoe_poison then local combo_val=0 local targets_hit=is_aoe_poison and unpoisoned or {} -- Single-target poison: only the targeted enemy gets poisoned if is_poison_setup then for _,e in ipairs(unpoisoned) do if e.x==c.x and e.y==c.y then targets_hit={e};break end end end -- Simulate Ignite Poison damage on newly-poisoned targets for _,e in ipairs(targets_hit) do local r=get_res(e.m,3) -- fire resistance for Ignite local mult=get_res_mult(r) if mult>0 then local dmg=calc_dmg(ig_base*mult,e.ac,e.ev,ig_pow,true,true,false) combo_val=combo_val+score_dmg(dmg,e.m,cdist(e.x,e.y),e) end end -- Add discounted future value (0.7 = next turn uncertainty) -- MP check: can we afford setup + payoff next turn? (mp_regen ≈ mp_max/10) -- Fast enemies further discount future value (#13) if combo_val>0 and (mp-c.c+mp_regen)>=ig_cost then propose(i,base_v[i]+combo_val*(0.7/max_fast)/math.sqrt(ig_cost)) end end end end end -- BVC → EV reduction combo: constriction helps all future non-nohit spells for i,c in ipairs(candidates) do if c.n=="Borgnjor's Vile Clutch" then -- Find best non-nohit spell for EV synergy calculation (from BASE values) local best_followup=0 for j,c2 in ipairs(candidates) do if c2.n~=c.n and base_v[j]>best_followup then best_followup=base_v[j] end end -- Constriction gives -10 EV; rough estimate: 10/50 = 20% more hits if best_followup>0 then propose(i,base_v[i]+best_followup*0.2*0.7) -- 20% hit improvement, 0.7 discount end end end -- Scorch → fire spell combo: rF- makes follow-up fire spells deal ~50% more for i,c in ipairs(candidates) do if c.n=="Scorch" then local best_fire=0 for j,c2 in ipairs(candidates) do if c2.e==3 and c2.n~="Scorch" and c2.c<=(mp-c.c+mp_regen) and base_v[j]>best_fire then best_fire=base_v[j] end end if best_fire>0 then propose(i,base_v[i]+best_fire*(0.5/max_fast)*0.7) end end end -- Generic 2-turn lookahead: boost cheap spells that leave MP for a strong follow-up -- Only adds value when casting A now + B next turn beats casting B now directly -- (#13) Fast enemies attack more times before our follow-up; discount the lookahead. -- max_fast: 1.0 normal, 1.25 fast, 1.5 very fast, 2.0 extremely fast if #candidates>1 then -- Find best spell castable RIGHT NOW (what we'd do if no combo) local best_now=0 for i=1,#candidates do if base_v[i]>best_now then best_now=base_v[i] end end local follow_mult=0.5/max_fast -- 0.5 normal, 0.4 fast, 0.33 very fast, 0.25 extremely fast for i,c in ipairs(candidates) do local next_mp=mp-c.c+mp_regen if next_mp>0 then local best_next=0 for j,c2 in ipairs(candidates) do if j~=i and c2.c<=next_mp and base_v[j]>best_next then best_next=base_v[j] end end -- Combo value = this spell + discounted follow-up -- Only boost if combo total exceeds what we'd get casting best spell now + filler next turn if best_next>0 then local combo_total=base_v[i]+best_next*follow_mult if combo_total>best_now then propose(i,combo_total) end end end end end -- Commit the best two-turn plan for each candidate (max over base + all combos) for _,c in ipairs(candidates) do c.v=c.combo_v; c.combo_v=nil end table.sort(candidates,function(a,b) return a.v>b.v end) -- Debug: dump the top-3 scored candidates before final tiebreakers/overrides. if BONSAI_DEBUG then for i=1,math.min(3,#candidates) do local c=candidates[i] crawl.mpr(string.format("#%d %s v=%.1f mp=%d @(%d,%d) kn=%s", i,c.n,c.v,c.c,c.x or 0,c.y or 0,tostring(c.ki))) end end local b=candidates[1] -- Kill tiebreaker: prefer spell that kills its target if score is close if b and b.ki and b.ki>1 and #candidates>=2 then local c2=candidates[2] if c2.ki and c2.ki==1 and c2.v>=b.v*0.8 then b=c2 end end -- If top candidate is channeled but not significantly better than non-channeled, prefer non-channeled if b and b.ch and #candidates>1 then local best_non_ch=nil for _,c in ipairs(candidates) do if not c.ch then best_non_ch=c;break end end if best_non_ch and b.v < best_non_ch.v * 1.2 then b=best_non_ch end end -- If already channeling, don't override unless significantly better (2x) local active_channel=nil if recently_cast("Flame Wave") then active_channel="Flame Wave" elseif recently_cast("Searing Ray") then active_channel="Searing Ray" end if active_channel then -- If best option is the active channeled spell, just continue if b and b.n==active_channel then crawl.mpr("Cont") return end -- Find current channel value from candidates local ch_val=0 for _,c in ipairs(candidates) do if c.n==active_channel then ch_val=c.v;break end end if ch_val<=0 then ch_val=20 end -- fallback -- Check if any option is significantly better (2x) local has_better = false for _,c in ipairs(candidates) do if c.n~=active_channel and c.v > ch_val * 2 then has_better = true break end end if not has_better then crawl.mpr("Cont") return -- don't interrupt channeling end end -- Check if a significantly better spell is 1 MP away — wait instead of casting weaker -- Don't wait if current spell is practical (kills in ≤4 casts) if b and #almost>0 then table.sort(almost,function(a,ab) return a.v>ab.v end) if (not b.ki or b.ki>4) and almost[1].v > b.v*1.5 then crawl.mpr("Wait 1MP for "..almost[1].n) return end end if b then -- Servitor collateral: if the chosen AoE splashes our spellspark servitor, auto-answer -- the "Really ... near your spellspark servitor?" prompt — but ONLY when the servitor -- survives (est. dmg < its hp). If it would die, leave the normal prompt so we don't blow -- up our own summon. Beam/penetrate already route around allies (path_has_friendly), so -- only AoE reaches here. b was chosen with best_val>0, so an enemy is always hit too. -- ev=0 is conservative: it overestimates servitor dmg, biasing toward NOT auto-proceeding. local servitor_ok=false if servitor then local bsp=nil for _,sp in ipairs(s) do if sp.n==b.n then bsp=sp break end end if bsp and bsp.aoe and bsp.aoe>0 then local sdist=math.max(math.abs(servitor.x-b.x),math.abs(servitor.y-b.y)) local sadj=math.max(math.abs(servitor.x),math.abs(servitor.y)) local in_blast=(bsp.aoe>=9 or sdist<=bsp.aoe) and (not bsp.noadjacent or sadj>1) if in_blast then local spow=get_pow(b.n) local base=get_spell_dmg(b.n,spow) local mult=get_res_mult(get_res(servitor.m,bsp.e),bsp.halfres) local sdmg=spell_dmg(base,mult,get_ac(servitor.m),0,spow,bsp,bsp.e==2,nil,servitor.m) if sdmg0 and (mp-b.c)3) then crawl.mpr("Reserve "..reserve_mp.."MP for "..reserve_spell) else mark_cast(b.n) crawl.mpr(b.n) -- Queue the "Yes" answer immediately before casting so it's consumed by the -- servitor collateral prompt; only reached when the servitor survives the blast. if servitor_ok then crawl.sendkeys("Y") end if b.manual then crawl.sendkeys("z"..spells.letter(b.n)) else spells.cast(b.n,b.x,b.y,b.ep) end end else crawl.mpr("No") end end } # F6..F10 toggles. Key codes match Linux ncurses KEY_F(n) = 264 + n, # stored as negative numbers in DCSS macro syntax. # F6 = -270, F7 = -271, F8 = -272, F9 = -273, F10 = -274 # F11 is reserved by the browser (fullscreen) — print_resists() is callable # from the ~ Lua menu instead. # If a code doesn't trigger on your terminal, run `\` then the F-key in-game # to see the actual code, and edit the numbers here. macros += M \{-270} ===toggle_autothrow macros += M \{-271} ===toggle_autorest macros += M \{-272} ===toggle_more_mores macros += M \{-273} ===toggle_portal_mode macros += M \{-274} ===toggle_cheiwalk