/**
 * @file
 * @brief Functions related to ranged attacks.
**/

#include "AppHdr.h"

#include "beam.h"

#include <algorithm>
#include <cmath>
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <set>

#include "act-iter.h"
#include "areas.h"
#include "attack.h"
#include "attitude-change.h"
#include "bloodspatter.h"
#include "chardump.h"
#include "cloud.h"
#include "colour.h"
#include "coordit.h"
#include "delay.h"
#include "directn.h"
#include "dungeon.h"
#include "english.h"
#include "exercise.h"
#include "fight.h"
#include "fineff.h"
#include "god-abil.h"
#include "god-conduct.h"
#include "god-item.h"
#include "god-passive.h" // passive_t::convert_orcs
#include "item-use.h"
#include "item-prop.h"
#include "items.h"
#include "killer-type.h"
#include "libutil.h"
#include "losglobal.h"
#include "los.h"
#include "message.h"
#include "misc.h"
#include "mon-behv.h"
#include "mon-clone.h"
#include "mon-death.h"
#include "mon-explode.h"
#include "mon-place.h"
#include "mon-poly.h"
#include "mon-util.h"
#include "mutation.h"
#include "nearby-danger.h"
#include "options.h"
#include "player-stats.h"
#include "potion.h"
#include "prompt.h"
#include "ranged-attack.h"
#include "religion.h"
#include "shout.h"
#include "spl-book.h"
#include "spl-clouds.h"
#include "spl-damage.h"
#include "spl-goditem.h"
#include "spl-monench.h"
#include "spl-summoning.h"
#include "spl-transloc.h"
#include "spl-util.h"
#include "spl-zap.h"
#include "state.h"
#include "stepdown.h"
#include "stringutil.h"
#include "target.h"
#include "teleport.h"
#include "terrain.h"
#include "throw.h"
#ifdef USE_TILE
 #include "tilepick.h"
#endif
#include "tiles-build-specific.h"
#include "transform.h"
#include "traps.h"
#include "viewchar.h"
#include "view.h"
#include "xom.h"

// Helper functions (some of these should probably be public).
static beam_type _chaos_beam_flavour();
static string _beam_type_name(beam_type type);
int _ench_pow_to_dur(int pow);

tracer_info::tracer_info()
{
    reset();
}

void tracer_info::reset()
{
    count = power = 0;
}

bolt::bolt() : animate(bool(Options.use_animations & UA_BEAM)) {}

bool bolt::is_blockable() const
{
    // BEAM_ELECTRICITY is added here because chain lightning is not
    // a true beam (stops at the first target it gets to and redirects
    // from there)... but we don't want it shield blockable.
    return !pierce && !is_explosion && flavour != BEAM_ELECTRICITY
           && hit != AUTOMATIC_HIT;
}

/// Can 'omnireflection' (from the Warlock's Mirror) potentially reflect this?
bool bolt::is_omnireflectable() const
{
    return !is_explosion
            && origin_spell != SPELL_GLACIATE
            && flavour != BEAM_VAMPIRIC_DRAINING; // buggy :(
}

void bolt::emit_message(const char* m)
{
    const string message = m;
    if (!message_cache.count(message))
        mpr(m);

    message_cache.insert(message);
}

kill_category bolt::whose_kill() const
{
    if (YOU_KILL(thrower) || source_id == MID_YOU_FAULTLESS)
        return KC_YOU;
    else if (MON_KILL(thrower))
    {
        if (source_id == MID_ANON_FRIEND)
            return KC_FRIENDLY;
        const monster* mon = monster_by_mid(source_id);
        if (mon && mon->friendly())
            return KC_FRIENDLY;
    }
    return KC_OTHER;
}

static element_type _ench_beam_to_element(beam_type flavour)
{
    switch (flavour)
    {
    case BEAM_HEALING:
        return ETC_HEAL;
    case BEAM_INFESTATION:
    case BEAM_PAIN:
    case BEAM_AGONY:
    case BEAM_CURSE_OF_AGONY:
    case BEAM_VILE_CLUTCH:
    case BEAM_VAMPIRIC_DRAINING:
    case BEAM_SOUL_SPLINTER:
        return ETC_UNHOLY;
    case BEAM_DISPEL_UNDEAD:
        return ETC_HOLY;
    case BEAM_POLYMORPH:
    case BEAM_MALMUTATE:
        return ETC_MUTAGENIC;
    case BEAM_CHAOS:
        return ETC_RANDOM;
    case BEAM_TELEPORT:
    case BEAM_BANISH:
    case BEAM_BLINK:
    case BEAM_BLINK_CLOSE:
    case BEAM_BECKONING:
        return ETC_WARP;
    case BEAM_MAGIC:
        return ETC_MAGIC;
    case BEAM_ROOTS:
        return ETC_EARTH;
    default:
        return ETC_ENCHANT;
    }
}

// A simple animated flash from Rupert Smith (expanded to be more
// generic).
static void _ench_animation(beam_type flavour, const actor& act, bool force)
{
    const coord_def p = act.pos();

    if (!force && act.is_monster() && !act.visible_to(&you))
        return;

    if (!you.see_cell(p))
        return;

    const int colour = element_colour(_ench_beam_to_element(flavour), p);
    const coord_def drawp = grid2view(p);

    if (in_los_bounds_v(drawp))
    {
#ifdef USE_TILE
        view_add_tile_overlay(p, tileidx_zap(colour, p));
#endif
        view_add_glyph_overlay(p, {dchar_glyph(DCHAR_FIRED_ZAP),
                                   static_cast<unsigned short>(colour)});
        animation_delay(50, true);
    }
}

// If needs_tracer is true, we need to check the beam path for friendly
// monsters.
spret zapping(zap_type ztype, int power, bolt &pbolt,
                   bool needs_tracer, const char* msg, bool fail)
{
    dprf(DIAG_BEAM, "zapping: power=%d", power);

    pbolt.thrower = KILL_YOU_MISSILE;

    // Check whether tracer goes through friendlies.
    // NOTE: Whenever zapping() is called with a randomised value for power
    // (or effect), player_tracer should be called directly with the highest
    // power possible respecting current skill, experience level, etc.
    if (needs_tracer && !player_tracer(ztype, power, pbolt))
        return spret::abort;

    fail_check();
    // Fill in the bolt structure.
    zappy(ztype, power, false, pbolt);

    if (msg)
        mpr(msg);

    if (ztype == ZAP_DIG)
        pbolt.aimed_at_spot = false;
    // XXX: Not an explosion internally, which would do this automatically,
    //      but is aimed like one and should behavior like one UI-wise.
    else if (ztype == ZAP_RUST_BREATH)
        pbolt.aimed_at_spot = true;

    pbolt.fire();

    return spret::success;
}

void fire_partial_player_tracer(zap_type ztype, int power,
                                player_beam_tracer& tracer,
                                bolt &pbolt, int range)
{
    // Non-controlleable during confusion.
    // (We'll shoot in a different direction anyway.)
    if (you.confused())
        return;

    zappy(ztype, power, false, pbolt);

    pbolt.source        = you.pos();
    pbolt.source_id     = MID_PLAYER;
    pbolt.attitude      = ATT_FRIENDLY;
    pbolt.thrower       = KILL_YOU_MISSILE;
    pbolt.overshoot_prompt = false;
    pbolt.passed_target = false;

    // Clear misc
    pbolt.seen          = false;
    pbolt.heard         = false;
    pbolt.reflections   = 0;
    pbolt.bounces       = 0;
    pbolt.loudness      = 0;

    // Save range before overriding it
    const int old_range = pbolt.range;
    if (range)
        pbolt.range = range;

    pbolt.fire(tracer);

    if (range)
        pbolt.range = old_range;
}

// Returns true if the path is considered "safe", and false if there are
// monsters in the way the player doesn't want to hit.
bool player_tracer(zap_type ztype, int power, bolt &pbolt, int range)
{
    // Non-controlleable during confusion.
    // (We'll shoot in a different direction anyway.)
    if (you.confused())
        return true;

    player_beam_tracer tracer;
    fire_partial_player_tracer(ztype, power, tracer, pbolt, range);
    if (cancel_beam_prompt(pbolt, tracer))
    {
        dprf(DIAG_BEAM, "Beam cancelled.");
        you.turn_is_over = false;
        return false;
    }

    if (pbolt.friendly_past_target)
        pbolt.aimed_at_spot = true;

    return true;
}

// Returns true if the player wants / needs to abort based on god displeasure
// with targeting this target with this spell. Returns false otherwise.
static bool _stop_because_god_hates_target_prompt(const monster* mon,
                                                  spell_type spell,
                                                  bool check_only = false)
{
    if (spell == SPELL_TUKIMAS_DANCE)
    {
        const item_def * const first = mon->weapon(0);
        const item_def * const second = mon->weapon(1);
        bool prompt = first && god_hates_item(*first)
                      || second && god_hates_item(*second);
        if (prompt)
        {
            if (check_only)
                return true;
            if (!yesno("Animating this weapon would place you under penance. "
                "Really cast this spell?", false, 'n'))
            {
                canned_msg(MSG_OK);
                return true;
            }
        }
    }

    return false;
}

struct zap_info
{
    zap_type ztype;
    const char* name;           // nullptr means handled specially
    dam_deducer* player_damage;
    tohit_deducer* player_tohit;    // Enchantments have power modifier here
    dam_deducer* monster_damage;
    tohit_deducer* monster_tohit;
    colour_t colour;
    bool is_enchantment;
    beam_type flavour;
    dungeon_char_type glyph;
    bool can_beam;
    bool is_explosion;
    tileidx_t tile_beam;
    tileidx_t tile_explode;
};

#include "zap-data.h"

static int zap_index[NUM_ZAPS];

void init_zap_index()
{
    for (int i = 0; i < NUM_ZAPS; ++i)
        zap_index[i] = -1;

    for (unsigned int i = 0; i < ARRAYSZ(zap_data); ++i)
        zap_index[zap_data[i].ztype] = i;
}

static const zap_info* _seek_zap(zap_type z_type)
{
    ASSERT_RANGE(z_type, 0, NUM_ZAPS);
    if (zap_index[z_type] == -1)
        return nullptr;
    else
        return &zap_data[zap_index[z_type]];
}

bool zap_explodes(zap_type z_type)
{
    const zap_info* zinfo = _seek_zap(z_type);
    return zinfo && zinfo->is_explosion;
}

bool zap_is_enchantment(zap_type z_type)
{
    const zap_info* zinfo = _seek_zap(z_type);
    return zinfo && zinfo->is_enchantment;
}

int zap_to_hit(zap_type z_type, int power, bool is_monster)
{
    const zap_info* zinfo = _seek_zap(z_type);
    if (!zinfo)
        return 0;
    const tohit_deducer* hit_calc = is_monster ? zinfo->monster_tohit
                                               : zinfo->player_tohit;
    if (zinfo->is_enchantment)
        return 0;
    ASSERT(hit_calc);
    const int hit = (*hit_calc)(power);
    if (hit != AUTOMATIC_HIT && !is_monster && crawl_state.need_save)
        return max(0, hit - you.inaccuracy_penalty());
    return hit;
}

dice_def zap_damage(zap_type z_type, int power, bool is_monster, bool random)
{
    const zap_info* zinfo = _seek_zap(z_type);
    if (!zinfo)
        return dice_def(0,0);
    const dam_deducer* dam_calc = is_monster ? zinfo->monster_damage
                                             : zinfo->player_damage;
    return dam_calc ? (*dam_calc)(power, random) : dice_def(0,0);
}

colour_t zap_colour(zap_type z_type)
{
    const zap_info* zinfo = _seek_zap(z_type);
    if (!zinfo)
        return BLACK;
    return zinfo->colour;
}

int zap_ench_power(zap_type z_type, int pow, bool is_monster)
{
    const zap_info* zinfo = _seek_zap(z_type);
    if (!zinfo)
        return pow;

    tohit_deducer* ench_calc = is_monster ? zinfo->monster_tohit
                                          : zinfo->player_tohit;
    if (zinfo->is_enchantment && ench_calc)
        return (*ench_calc)(pow);
    else
        return pow;
}

static int _zap_loudness(zap_type zap, spell_type spell)
{
    const zap_info* zinfo = _seek_zap(zap);
    const int noise = spell_effect_noise(spell);
    const spell_flags flags = get_spell_flags(spell);

    // Explosions have noise handled separately.
    if (zinfo->is_explosion || testbits(flags, spflag::silent))
        return 0;
    else if (noise > 0)
        return noise;
    // Enchantments are (usually) silent.
    else if (zinfo->is_enchantment)
        return 0;
    else if (spell != SPELL_NO_SPELL)
        return spell_difficulty(spell);

    return 0;
}

#ifdef WIZARD
static bool _needs_monster_zap(const zap_info &zinfo)
{
    // for enchantments we can't know from this data
    if (zinfo.is_enchantment)
        return false;
    // if player tohit is missing from a non-enchantment, then it has to use
    // a fake monster cast
    if (!zinfo.player_tohit)
        return true;

    // otherwise, it should be possible to use a player zap (damage may or may
    // not be defined)
    return false;
}
#endif

void zappy(zap_type z_type, int power, bool is_monster, bolt &pbolt)
{
    const zap_info* zinfo = _seek_zap(z_type);

    // None found?
    if (zinfo == nullptr)
    {
        dprf("Couldn't find zap type %d", z_type);
        return;
    }

#ifdef WIZARD
    // we are in a wizmode cast scenario: use monster zap data to avoid crashes.
    // N.b. this suppresses some player effects, such as inaccuracy.
    if (!is_monster && you.wizard && _needs_monster_zap(*zinfo))
        is_monster = true;
#endif

    // Fill
    pbolt.name           = zinfo->name;
    pbolt.flavour        = zinfo->flavour;
    pbolt.real_flavour   = zinfo->flavour;
    pbolt.colour         = zinfo->colour;
    pbolt.glyph          = dchar_glyph(zinfo->glyph);
    pbolt.pierce         = zinfo->can_beam;
    pbolt.is_explosion   = zinfo->is_explosion;

    ASSERT(zinfo->is_enchantment == pbolt.is_enchantment());

    pbolt.ench_power = zap_ench_power(z_type, power, is_monster);

    if (zinfo->is_enchantment)
        pbolt.hit = AUTOMATIC_HIT;
    else
        pbolt.hit = zap_to_hit(z_type, power, is_monster);

    pbolt.damage = zap_damage(z_type, power, is_monster);

    if (pbolt.origin_spell == SPELL_NO_SPELL)
        pbolt.origin_spell = zap_to_spell(z_type);

    if (pbolt.loudness == 0)
        pbolt.loudness = _zap_loudness(z_type, pbolt.origin_spell);

    pbolt.tile_beam = zinfo->tile_beam;
    pbolt.tile_explode = zinfo->tile_explode;
}

bool bolt::can_affect_actor(const actor *act) const
{
    // Blinkbolt doesn't hit its caster, since they are the bolt.
    if (origin_spell == SPELL_BLINKBOLT && act->mid == source_id)
        return false;
    // Damnation doesn't blast the one firing.
    else if (safe_to_user && act->mid == source_id)
        return false;
    // Xak'krixis' prisms are smart enough not to affect friendlies
    else if (origin_spell == SPELL_FULMINANT_PRISM && thrower == KILL_MON
        && act->temp_attitude() == attitude)
    {
        return false;
    }
    auto cnt = hit_count.find(act->mid);
    if (cnt != hit_count.end() && cnt->second >= 2)
    {
        // Note: this is done for balance, even if it hurts realism a bit.
        // It is arcane knowledge which wall patterns will cause lightning
        // to bounce thrice, double damage for ordinary bounces is enough.
#ifdef DEBUG_DIAGNOSTICS
        if (!quiet_debug)
            dprf(DIAG_BEAM, "skipping beam hit, affected them twice already");
#endif
        return false;
    }

    return true;
}

// Choose the beam effect for the damage portion of BEAM_CHAOS.
// (Secondary effects have a chance of being applied on top of this.)
static beam_type _chaos_beam_flavour()
{
    return random_choose_weighted(
            80, BEAM_FIRE,
            80, BEAM_COLD,
            80, BEAM_ELECTRICITY,
            80, BEAM_POISON,
            80, BEAM_NEG,
            40, BEAM_HOLY,
            20, BEAM_ACID,
            10, BEAM_FOUL_FLAME);
}

dice_def combustion_breath_damage(int pow, bool allow_random)
{
    if (allow_random)
        return dice_def(3, 4 + div_rand_round(pow * 10, 9));
    else
        return dice_def(3, 4 + pow * 10 / 9);
}

static void _copy_affected_counts(bolt& dest, const bolt& source) noexcept
{
    dest.foes_hurt = source.foes_hurt;
    dest.foes_helped = source.foes_helped;
    dest.friends_hurt = source.friends_hurt;
    dest.friends_helped = source.friends_helped;
}

static void _combustion_breath_explode(bolt *parent, coord_def pos)
{
    bolt beam;

    bolt_parent_init(*parent, beam);
    beam.name         = "fiery explosion";
    beam.aux_source   = "combustion breath";
    beam.damage       = combustion_breath_damage(parent->ench_power);
    beam.colour       = RED;
    beam.flavour      = BEAM_FIRE;
    beam.is_explosion = true;
    beam.source       = pos;
    beam.target       = pos;
    beam.origin_spell = SPELL_COMBUSTION_BREATH;
    beam.refine_for_explosion();
    beam.explode();
    _copy_affected_counts(*parent, beam);
}

bool bolt::visible() const
{
    return !is_tracer() && glyph != 0 && !is_enchantment();
}

void bolt::initialise_fire()
{
    // Fix some things which the tracer might have set.
    extra_range_used   = 0;
    in_explosion_phase = false;
    use_target_as_pos  = false;
    enchant_chaining_done = false;
    hit_count.clear();

    if (special_explosion != nullptr)
    {
        ASSERT(!is_explosion);
        ASSERT(special_explosion->is_explosion);
        ASSERT(special_explosion->special_explosion == nullptr);
        special_explosion->in_explosion_phase = false;
        special_explosion->use_target_as_pos  = false;
    }

    if (chose_ray)
    {
        ASSERT_IN_BOUNDS(ray.pos());

        if (source == coord_def())
            source = ray.pos();
    }

    if (target == source)
    {
        range             = 0;
        aimed_at_feet     = true;
        aimed_at_spot     = true;
        use_target_as_pos = true;
    }

    if (flavour == BEAM_VISUAL)
        affects_nothing = true;

    ASSERT_IN_BOUNDS(source);
    ASSERT_RANGE(flavour, BEAM_NONE + 1, BEAM_FIRST_PSEUDO);
    ASSERT(!drop_item || ranged_atk);
    ASSERTM(range >= 0, "beam '%s', source '%s', has range -1",
            name.c_str(),
            (source_id == MID_PLAYER ? "player" :
                          monster_by_mid(source_id) ?
                             monster_by_mid(source_id)->name(DESC_PLAIN, true) :
                          "unknown").c_str());
    ASSERT(!aimed_at_feet || source == target);

    real_flavour = flavour;

    message_cache.clear();

    // seen might be set by caller to suppress this.
    if (!seen && you.see_cell(source) && range > 0 && visible())
    {
        seen = true;
        const monster* mon = monster_at(source);

        if (flavour != BEAM_VISUAL
            && !YOU_KILL(thrower)
            && !crawl_state.is_god_acting()
            && (!mon || !mon->observable()))
        {
            mprf("%s appears from out of thin air!",
                 article_a(name, false).c_str());
        }
    }

    // Visible self-targeted beams are always seen, even though they don't
    // leave a path.
    if (you.see_cell(source) && target == source && visible())
        seen = true;

    // The agent may die during the beam's firing, need to save these now.
    // If the beam was reflected, assume it can "see" anything, since neither
    // the reflector nor the original source was particularly aiming for this
    // target. WARNING: if you change this logic, keep in mind that
    // env.mons[YOU_FAULTLESS] cannot be safely queried for properties like
    // can_see_invisible.
    if (reflections > 0)
        nightvision = can_see_invis = true;
    else
        precalc_agent_properties();

#ifdef DEBUG_DIAGNOSTICS
    // Not a "real" tracer, merely a range/reachability check.
    if (quiet_debug)
        return;

    dprf(DIAG_BEAM, "%s%s%s [%s] (%d,%d) to (%d,%d): "
          "gl=%d col=%d flav=%d hit=%d dam=%dd%d range=%d",
          (pierce) ? "beam" : "missile",
          (is_explosion) ? "*" :
          (is_big_cloud()) ? "+" : "",
          (is_tracer()) ? " tracer" : "",
          name.c_str(),
          source.x, source.y,
          target.x, target.y,
          glyph, colour, flavour,
          hit, damage.num, damage.size,
          range);
#endif
}

void bolt::precalc_agent_properties()
{
    const actor* a = agent();
    // XXX: Should non-agents count as seeing invisible?
    if (!a) return;
    nightvision = a->nightvision();
    can_see_invis = a->can_see_invisible();
}

void bolt::apply_beam_conducts()
{
    if (is_tracer() && BLAME_KILL(thrower))
    {
        switch (flavour)
        {
        case BEAM_DAMNATION:
        {
            const int level = 2 + random2(3);
            did_god_conduct(DID_EVIL, level, god_cares());
            break;
        }
        default:
            break;
        }
    }
}

void bolt::choose_ray()
{
    if ((!chose_ray || reflections > 0)
        && !find_ray(source, target, ray, opc_solid_see)
        // If fire is blocked, at least try a visible path so the
        // error message is better.
        && !find_ray(source, target, ray, opc_default))
    {
        fallback_ray(source, target, ray);
    }
}

// Draw the bolt at p if needed.
void bolt::draw(const coord_def& p, bool force_refresh)
{
    if (is_tracer() || is_enchantment() || !you.see_cell(p))
        return;

    // We don't clean up the old position.
    // First, most people like to see the full path,
    // and second, it is hard to do it right with
    // respect to killed monsters, cloud trails, etc.

    const coord_def drawpos = grid2view(p);

    if (!in_los_bounds_v(drawpos))
        return;

#ifdef USE_TILE
    // Set default value if none specified.
    if (tile_beam == 0)
        tile_beam = tileidx_zap(colour, p);
    view_add_tile_overlay(p, vary_bolt_tile(tile_beam, source, target, p));
#endif
    colour_t adjusted_colour = colour == BLACK ? colour_t{ETC_RANDOM} : colour;
    const unsigned short c = element_colour(adjusted_colour, p);
    view_add_glyph_overlay(p, {glyph, c});

    // If reduce_animations is set, the redraw is unnecessary and
    // should be done only once outside the loop calling the bolt::draw
    if (Options.reduce_animations)
        return;

    // to avoid redraws, set force_refresh = false, and draw_delay = 0. This
    // will still force a refresh if there is a draw_delay regardless of the
    // param, because a delay on drawing is pretty meaningless without a
    // redraw.
    if (force_refresh || draw_delay > 0)
        animation_delay(draw_delay, true);
}

// Bounce a bolt off a solid feature.
// The ray is assumed to have just been advanced into
// the feature.
void bolt::bounce()
{
    ASSERT(cell_is_solid(ray.pos()));
    // Don't bounce player tracers off unknown cells, or cells that we
    // incorrectly thought were non-bouncy.
    if (is_tracer() && agent() == &you)
    {
        const dungeon_feature_type feat = env.map_knowledge(ray.pos()).feat();

        if (feat == DNGN_UNSEEN || !feat_is_solid(feat) || !is_bouncy(feat))
        {
            ray.regress();
            finish_beam();
            return;
        }
    }

    do
    {
        ray.regress();
    }
    while (cell_is_solid(ray.pos()));

    extra_range_used += range_used(true);
    bounce_pos = ray.pos();
    bounces++;
    reflect_grid rg;
    for (adjacent_iterator ai(ray.pos(), false); ai; ++ai)
        rg(*ai - ray.pos()) = cell_is_solid(*ai);
    ray.bounce(rg);
    extra_range_used += 2;

    // Keep length of bounced spells constant despite reduced los (scarf of
    // shadows, Robe of Night)
    if (bounces == 1)
    {
        const int orig_range = origin_spell == SPELL_NO_SPELL ? LOS_RADIUS
                                : spell_range(origin_spell, agent(), ench_power, true);

        extra_range_used -= orig_range - range;
    }

    ASSERT(!cell_is_solid(ray.pos()));
}

void bolt::fake_flavour()
{
    if (real_flavour == BEAM_RANDOM)
        flavour = static_cast<beam_type>(random_range(BEAM_FIRST_RANDOM, BEAM_LAST_RANDOM));
    else if (real_flavour == BEAM_CHAOS)
        flavour = is_tracer() ? BEAM_MMISSILE : _chaos_beam_flavour();
}

void bolt::digging_wall_effect()
{
    if (env.markers.property_at(pos(), MAT_ANY, "veto_destroy") == "veto")
    {
        finish_beam();
        return;
    }

    const dungeon_feature_type feat = env.grid(pos());
    if (feat_is_diggable(feat))
    {
        // If this is temporary terrain and the underlying terrain is *not*
        // diggable, just revert it to whatever it was before. But if the
        // original terrain was also diggable, simply destroy it.
        if (is_temp_terrain(pos()))
        {
            revert_terrain_change(pos());
            if (feat_is_diggable(env.grid(pos())))
                destroy_wall(pos());
        }
        else
            destroy_wall(pos());
        if (!msg_generated)
        {
            if (!you.see_cell(pos()))
            {
                if (!silenced(you.pos()))
                {
                    mprf(MSGCH_SOUND, "You hear a grinding noise.");
                    obvious_effect = true; // You may still see the caster.
                    msg_generated = true;
                }
                return;
            }

            obvious_effect = true;
            msg_generated = true;

            string wall;
            if (feat == DNGN_GRATE)
            {
                // XXX: should this change for monsters?
                mpr("The damaged grate falls apart.");
                return;
            }
            else if (feat == DNGN_SLIMY_WALL)
                wall = "slime";
            else if (player_in_branch(BRANCH_PANDEMONIUM))
                wall = "weird stuff";
            else
                wall = "rock";

            mprf("%s %s shatters into small pieces.",
                 agent() && agent()->is_player() ? "The" : "Some",
                 wall.c_str());
        }
    }
    else if (feat_is_wall(feat))
        finish_beam();
}

void bolt::burn_wall_effect()
{
    dungeon_feature_type feat = env.grid(pos());
    // Fire only affects things that can burn.
    if (!feat_is_flammable(feat)
        || env.markers.property_at(pos(), MAT_ANY, "veto_destroy") == "veto"
        || !can_burn_trees()) // sanity
    {
        finish_beam();
        return;
    }

    if (have_passive(passive_t::shoot_through_plants))
    {
        emit_message("Fedhas protects the tree from harm.");
        finish_beam();
        return;
    }

    if (you.see_cell(pos()))
    {
        if (feat == DNGN_TREE)
            emit_message("The tree burns like a torch!");
        else if (feat == DNGN_MANGROVE)
            emit_message("The mangrove smoulders and burns.");
        else if (feat == DNGN_DEMONIC_TREE)
            emit_message("The demonic tree burns, releasing chaotic energy.");
    }
    else if (you.can_smell())
        emit_message("You smell burning wood.");

    // If the tree we're destroying is temporary, immediately revert
    // terrain changes on this tile rather than permanently changing it.
    if (is_temp_terrain(pos()))
        revert_terrain_change(pos());
    else
        destroy_wall(pos());

    // But burning a temporary tree that was created on open ground can still
    // start a fire.
    if (!cell_is_solid(pos()))
    {
        if (feat == DNGN_TREE)
            place_cloud(CLOUD_FOREST_FIRE, pos(), random2(30)+25, agent());
        // Mangroves do not burn so readily.
        else if (feat == DNGN_MANGROVE)
            place_cloud(CLOUD_FIRE, pos(), random2(12)+5, agent());
        // Demonic trees produce a chaos cloud instead of fire.
        else if (feat == DNGN_DEMONIC_TREE)
            place_cloud(CLOUD_CHAOS, pos(), random2(30)+25, agent());
    }
    // If a tree turned back into a wall, place some fire around it to simulate
    // the normal burning effect without removing the wall.
    else if (feat == DNGN_TREE)
    {
        for (adjacent_iterator ai(pos()); ai; ++ai)
        {
            if (!cell_see_cell(*ai, source, LOS_NO_TRANS))
                continue;

            if (one_chance_in(3))
                place_cloud(CLOUD_FIRE, *ai, random_range(11, 25), agent());
        }
    }

    obvious_effect = true;

    finish_beam();
}

void bolt::sporangium_wall_effect()
{
    if (monster_at(pos()))
        return;

    // Trace the ownership of this sporangium explosion back to the original
    // creeping plasmodium, and exit if we can't find it.
    const monster* ag = cached_monster_copy_by_mid(source_id);
    if (!ag)
        return;
    monster* orig_plasmodium = monster_by_mid(ag->summoner);
    if (!orig_plasmodium)
        return;

    // Don't make plasmodia too close together.
    for (distance_iterator di(pos(), false, true, 2); di; ++di)
        if (monster* mon_at = monster_at(*di))
            if (mon_at->type == MONS_NASCENT_PLASMODIUM && mons_aligned(orig_plasmodium, mon_at))
                return;

    mgen_data mg(MONS_NASCENT_PLASMODIUM, BEH_COPY, pos(), orig_plasmodium->foe,
                 MG_FORCE_PLACE);
    mg.set_summoned(orig_plasmodium, SPELL_LAUNCH_SPORANGIUM, INFINITE_DURATION, false);

    if (monster* plasm = create_monster(mg))
        simple_monster_message(*plasm, " begins to grow on the wall!", false, MSGCH_PLAIN, 0, DESC_A);
}

int bolt::range_used(bool leg_only) const
{
    const int leg_length = pos().distance_from(leg_source());
    return leg_only ? leg_length : leg_length + extra_range_used;
}

void bolt::finish_beam()
{
    extra_range_used = BEAM_STOP;
}

void bolt::affect_wall()
{
    if (is_tracer())
    {
        if (!in_bounds(pos()) || !can_affect_wall(pos(), true))
            finish_beam();

        // Digging is the only thing that doesn't stop at walls,
        // (and even it needs to stop if an underlying feature isn't diggable).
        if (flavour != BEAM_DIGGING
            || is_temp_terrain(pos()) && !feat_is_diggable(orig_terrain(pos())))
        {
            finish_beam();
        }
        return;
    }
    if (in_bounds(pos()))
    {
        if (origin_spell == SPELL_LAUNCH_SPORANGIUM)
            sporangium_wall_effect();
        else if (flavour == BEAM_DIGGING)
            digging_wall_effect();
        else if (can_burn_trees())
            burn_wall_effect();
    }
    if (cell_is_solid(pos()))
        finish_beam();
}

coord_def bolt::pos() const
{
    if (in_explosion_phase || use_target_as_pos)
        return target;
    else
        return ray.pos();
}

// Returns the position of the last actor affected by this beam (or, optionally
// some specified number of steps before or after).
coord_def bolt::get_last_affected_pos(int step)
{
    if (step == 0)
        return last_affected_actor_pos;

    // If we want to step forward/backward from this spot, we must iterate the path taken.
    for (unsigned int i = 0; i < path_taken.size(); ++i)
    {
        if (path_taken[i] == last_affected_actor_pos)
        {
            const int index = i + step;

            // If we want a step further than the beam took, we must trace a ray again.
            if (index >= (int)path_taken.size())
            {
                bolt path_tracer = *this;
                path_tracer.set_is_tracer(true);
                path_tracer.range = i + step + 1;
                path_tracer.fire();
                return path_tracer.path_taken.back();
            }
            // Returning an earlier step is easy.
            else if (index >= 0)
                return path_taken[index];
            // Unless we're trying to rewind past the source, in which case return the source.
            else
                return source;
        }
    }

    return last_affected_actor_pos;
}

bool bolt::need_regress() const
{
    // XXX: The affects_wall check probably makes some of the
    //      others obsolete.
    return (is_explosion && !in_explosion_phase)
           || drop_item
           || cell_is_solid(pos()) && !can_affect_wall(pos())
           || origin_spell == SPELL_PRIMAL_WAVE;
}

void bolt::affect_cell()
{
    if (affects_nothing)
        return;

    fake_flavour();

    monster *m = monster_at(pos());

    // Note that this can change the solidity of the wall.
    if (cell_is_solid(pos())
        // Wall affecting beams still do even with a monster there.
        && (!m || can_affect_wall(pos())))
    {
        affect_wall();
    }

    if (origin_spell == SPELL_CHAIN_LIGHTNING && pos() != target)
        return;

    // If the player can ever walk through walls, this will need
    // special-casing too.
    bool hit_player = found_player() && !ignores_player();
    if (hit_player && can_affect_actor(&you))
    {
        const int prev_reflections = reflections;
        affect_player();
        last_affected_actor_pos = pos();
        if (reflections != prev_reflections)
            return;
        if (hit == AUTOMATIC_HIT && !pierce)
            finish_beam();
    }

    // Stop single target beams from affecting a monster if they already
    // affected the player on this square. -cao
    if (!hit_player || pierce || is_explosion)
    {
        if (m && can_affect_actor(m))
        {
            const bool ignored = ignores_monster(m);
            affect_monster(m);

            if (!ignored)
                last_affected_actor_pos = pos();

            const dungeon_feature_type feat = env.grid(pos());
            if (hit == AUTOMATIC_HIT && !ignored
                // Piercing beams are still stopped by wall monsters
                // unless they were going to bounce
                && (!pierce || cell_is_solid(pos())
                        && !is_bouncy(feat) && !is_explosion)
                && (!is_tracer() || (agent() && m->visible_to(agent()))))
            {
                finish_beam();
            }
        }
    }

    if (!cell_is_solid(pos()))
        affect_ground();
}

static void _undo_tracer(bolt &orig, bolt &copy)
{
    // FIXME: we should have a better idea of what gets changed!
    orig.target           = copy.target;
    orig.source           = copy.source;
    orig.aimed_at_spot    = copy.aimed_at_spot;
    orig.aimed_at_feet    = copy.aimed_at_feet;
    orig.extra_range_used = copy.extra_range_used;
    orig.ray              = copy.ray;
    orig.colour           = copy.colour;
    orig.flavour          = copy.flavour;
    orig.real_flavour     = copy.real_flavour;
    orig.bounces          = copy.bounces;
    orig.bounce_pos       = copy.bounce_pos;
}

// This saves some important things before calling do_fire().
void bolt::fire()
{
    path_taken.clear();

    if (is_tracer())
    {
        bolt boltcopy = *this;

        if (special_explosion != nullptr)
        {
            bolt special_explosion_copy = *special_explosion;
            do_fire();
            _undo_tracer(*special_explosion, special_explosion_copy);
        }
        else
            do_fire();

        _undo_tracer(*this, boltcopy);
    }
    else
        do_fire();

    //XXX: suspect, but code relies on path_taken being non-empty
    if (path_taken.empty())
        path_taken.push_back(source);

    if (special_explosion != nullptr)
    {
        seen           = seen  || special_explosion->seen;
        heard          = heard || special_explosion->heard;
    }
}

void bolt::fire(beam_tracer& new_tracer)
{
    unwind_var<beam_tracer*> old_tracer(tracer, &new_tracer);
    fire();
}

void bolt::fire_as_ranged_attack(ranged_attack& atk)
{
    unwind_var<ranged_attack*> old_atk(ranged_atk, &atk);
    fire();
}

void bolt::do_fire()
{
    initialise_fire();

    if (range < extra_range_used && range > 0)
    {
#ifdef DEBUG
        dprf(DIAG_BEAM, "fire_beam() called on already done beam "
             "'%s'", name.c_str());
#endif
        return;
    }

    apply_beam_conducts();
    cursor_control coff(false);

    msg_generated = false;
    if (!aimed_at_feet)
    {
        choose_ray();
        // Take *one* step, so as not to hurt the source.
        ray.advance();
    }

    // Tracks if the *last* cell seen was a wall monster, therefore pretend
    // next cell is solid for purposes of bouncing or stopping the beam.
    bool wall_monster_hit = false;

    // Note: nothing but this loop should be changing the ray.
    while (map_bounds(pos()))
    {
        if (range_used() > range)
        {
            ray.regress();
            extra_range_used++;
            ASSERT(range_used() >= range);
            break;
        }

        const dungeon_feature_type feat = env.grid(pos());

        if (in_bounds(target)
            // Starburst beams are essentially untargeted; some might even hit
            // a victim if others have LOF blocked.
            && origin_spell != SPELL_STARBURST
            // We ran into a solid wall with a real beam...
            && (feat_is_solid(feat)
                && flavour != BEAM_DIGGING && flavour <= BEAM_LAST_REAL
                && !cell_is_solid(target)
            // Or hit a monster that'll stop our beam...
                || at_blocking_monster())
            // and it's a player tracer that cares about blocked paths...
            && is_tracer() && tracer->is_collecting_warnings() && YOU_KILL(thrower)
            // and we're actually between you and the target...
            && !passed_target && pos() != target && pos() != source
            // ?
            && !tracer->has_hit_foe() && bounces == 0 && reflections == 0
            // and you aren't shooting out of LOS.
            && you.see_cell(target))
        {
            // Okay, with all those tests passed, this is probably an instance
            // of the player manually targeting something whose line of fire
            // is blocked, even though its line of sight isn't blocked. Give
            // a warning about this fact.
            const monster* mon = monster_at(target);

            string blockee;
            if (mon && mon->observable())
                blockee = mon->name(DESC_THE);
            else
            {
                blockee = "the targeted "
                        + feature_description_at(target, false, DESC_PLAIN);
            }

            const string blocker = feat_is_solid(feat) ?
                        feature_description_at(pos(), false, DESC_A) :
                        monster_at(pos())->name(DESC_A);

            tracer->blocked("Your line of fire to " + blockee
                            + " is blocked by " + blocker + ".");
            finish_beam();
            return;
        }

        // If requested to stop before hitting allies, do so now.
        const actor* act_at = actor_at(pos());
        if (act_at && stop_at_allies && mons_atts_aligned(attitude, act_at->temp_attitude())
            && can_affect_actor(act_at) && !aimed_at_feet
            && !(act_at->is_player() && ignores_player() || ignores_monster(act_at->as_monster())))
        {
            ray.regress();
            finish_beam();
            return;
        }

        // If this is a friendly monster, firing a penetrating beam in the player's
        // direction, always stop immediately before them if this attack wouldn't
        // be harmless to them.
        if (act_at && act_at->is_player()
            && agent() && agent()->is_monster() && mons_att_wont_attack(attitude)
            && !ignores_player() && !harmless_to_player() && pierce && !is_explosion)
        {
            ray.regress();
            finish_beam();
            return;
        }

        const monster* mon_at = monster_at(pos());
        // digging is taken care of in affect_cell
        if (feat_is_solid(feat) && !can_affect_wall(pos())
            && flavour != BEAM_DIGGING)
        {
            // If wall monster then don't bounce or explode, it's handled later
            if (mon_at && !wall_monster_hit)
                wall_monster_hit = true;
            else if (is_bouncy(feat))
            {
                // Reset so we can hit another
                wall_monster_hit = false;
                bounce();
                // see comment in bounce(); the beam will be cancelled if this
                // is a tracer and showing the bounce would be an info leak.
                // In that case, we have to break early to avoid adding this
                // square to path_taken twice, which would make it look like a
                // a bounce ANYWAY.
                if (range_used() > range)
                    break;
            }
            else
            {
                // Regress for explosions: blow up in an open grid (if regressing
                // makes any sense). Also regress when dropping items.
                if (pos() != source && need_regress())
                {
                    do
                    {
                        ray.regress();
                    }
                    while (ray.pos() != source && cell_is_solid(ray.pos()));

                    // target is where the explosion is centered, so update it.
                    if (is_explosion && !is_tracer())
                        target = ray.pos();
                }
                break;
            }
        }

        path_taken.push_back(pos());

        // Roots only have an effect during explosions.
        if (flavour == BEAM_ROOTS)
        {
            if (cell_is_solid(pos()))
                affect_wall();
            const actor *victim = actor_at(pos());
            if (victim
                && !ignores_monster(victim->as_monster())
                && (!is_tracer() || agent()->can_see(*victim)))
            {
                finish_beam();
            }
        }
        else
            affect_cell();

        if (range_used() > range)
            break;

        // Weapons of returning should find an inverse ray
        // through find_ray and setup_retrace, but they didn't
        // always in the past, and we don't want to crash
        // if they accidentally pass through a corner.
        // Dig tracers continue through unseen cells.
        ASSERT(!cell_is_solid(pos())
               || is_tracer() && can_affect_wall(pos(), true)
               || mon_at // If there *was* a monster (they might have died by now)
               || affects_nothing); // returning weapons and BEAM_VISUAL

        const bool was_seen = seen;
        if (!was_seen && range > 0 && visible() && you.see_cell(pos()))
            seen = true;

        if (flavour != BEAM_VISUAL && !was_seen && seen && !is_tracer())
        {
            mprf("%s appears from out of your range of vision.",
                 article_a(name, false).c_str());
        }

        // Reset chaos beams so that it won't be considered an invisible
        // enchantment beam for the purposes of animation.
        if (real_flavour == BEAM_CHAOS)
            flavour = real_flavour;

        // Actually draw the beam/missile/whatever, if the player can see
        // the cell.
        if (animate)
            draw(pos(), redraw_per_cell);

        if (pos() == target)
        {
            passed_target = true;
            if (stop_at_target())
                break;
        }

        noise_generated = false;

        // If a wall monster was hit and the beam is continuing, don't
        // actually advance the ray: next iteration will take care of the bounce
        if (!wall_monster_hit)
            ray.advance();
    }

    if (!map_bounds(pos()))
    {
        ASSERT(!aimed_at_spot);

        int tries = max(GXM, GYM);
        while (!map_bounds(ray.pos()) && tries-- > 0)
            ray.regress();

        // Something bizarre happening if we can't get back onto the map.
        ASSERT(map_bounds(pos()));
    }

    // The beam has terminated.
    affect_endpoint();

    // Tracers need nothing further.
    if (is_tracer() || affects_nothing)
        return;

    // final delay for any draws that happened in the above loop
    if (animate && Options.reduce_animations && draw_delay > 0)
        animation_delay(15 + draw_delay, true);

    // Canned msg for enchantments that affected no-one, but only if the
    // enchantment is yours (and it wasn't a chaos beam, since with chaos
    // enchantments are entirely random, and if it randomly attempts
    // something which ends up having no obvious effect then the player
    // isn't going to realise it).
    if (!msg_generated && !obvious_effect && is_enchantment()
        && real_flavour != BEAM_CHAOS
        && YOU_KILL(thrower))
    {
        canned_msg(MSG_NOTHING_HAPPENS);
    }

    // Reactions if a monster zapped the beam.
    if (monster_by_mid(source_id))
    {
        if (foes_hurt == 0 && friends_hurt > 0)
            xom_is_stimulated(100);
        else if (foes_helped > 0 && friends_helped == 0)
            xom_is_stimulated(100);

        // Allow friendlies to react to projectiles.
        const monster* mon = monster_by_mid(source_id);
        if (foes_hurt > 0 && !mon->wont_attack() && !crawl_state.game_is_arena()
            && you.pet_target == MHITNOT)
        {
            you.pet_target = mon->mindex();
        }
    }
}

// Returns damage taken by a monster from a "flavoured" (fire, ice, etc.)
// attack -- damage from clouds and branded weapons handled elsewhere.
int mons_adjust_flavoured(monster* mons, bolt &pbolt, int hurted,
                          bool doFlavouredEffects)
{
    // If we're not doing flavoured effects, must be preliminary
    // damage check only.
    // Do not print messages or apply any side effects!
    int original = hurted;

    switch (pbolt.flavour)
    {
    case BEAM_FIRE:
    case BEAM_STEAM:
        hurted = resist_adjust_damage(mons, pbolt.flavour, hurted);

        if (!hurted)
        {
            if (original > 0 && doFlavouredEffects)
                simple_monster_message(*mons, " completely resists.");
        }
        else if (original > hurted)
        {
            if (doFlavouredEffects)
                simple_monster_message(*mons, " resists.");
        }
        else if (original < hurted && doFlavouredEffects)
        {
            if (mons->is_icy())
                simple_monster_message(*mons, " melts!");
            else if (mons_species(mons->type) == MONS_BUSH
                     && mons->res_fire() < 0)
            {
                simple_monster_message(*mons, " is on fire!");
            }
            else if (pbolt.flavour == BEAM_FIRE)
                simple_monster_message(*mons, " is burned terribly!");
            else
                simple_monster_message(*mons, " is scalded terribly!");
        }
        break;

    case BEAM_WATER:
        hurted = resist_adjust_damage(mons, pbolt.flavour, hurted);
        if (hurted > original && doFlavouredEffects)
            simple_monster_message(*mons, " is doused terribly!");
        break;

    case BEAM_COLD:
        hurted = resist_adjust_damage(mons, pbolt.flavour, hurted);
        if (!hurted)
        {
            if (original > 0 && doFlavouredEffects)
                simple_monster_message(*mons, " completely resists.");
        }
        else if (original > hurted)
        {
            if (doFlavouredEffects)
                simple_monster_message(*mons, " resists.");
        }
        else if (original < hurted)
        {
            if (doFlavouredEffects)
                simple_monster_message(*mons, " is chilled terribly!");
        }
        break;

    case BEAM_STUN_BOLT:
    case BEAM_ELECTRICITY:
    case BEAM_THUNDER:
        hurted = resist_adjust_damage(mons, pbolt.flavour, hurted);
        if (!hurted)
        {
            if (original > 0 && doFlavouredEffects)
                simple_monster_message(*mons, " completely resists.");
        }
        else if (original > hurted)
        {
            if (doFlavouredEffects)
                simple_monster_message(*mons, " resists.");
        }
        else if (original < hurted)
        {
            if (doFlavouredEffects)
                simple_monster_message(*mons, " is electrocuted!");
        }
        break;

    case BEAM_ACID:
    {
        hurted = resist_adjust_damage(mons, pbolt.flavour, hurted);
        if (!hurted)
        {
            if (original > 0 && doFlavouredEffects)
                simple_monster_message(*mons, " completely resists.");
        }
        else if (doFlavouredEffects && !one_chance_in(3))
            mons->corrode(pbolt.agent());
        break;
    }

    case BEAM_POISON:
    {
        hurted = resist_adjust_damage(mons, pbolt.flavour, hurted);

        if (!hurted && doFlavouredEffects && original > 0)
            simple_monster_message(*mons, " completely resists.");
        else if (doFlavouredEffects && !one_chance_in(3))
        {
            if (pbolt.origin_spell == SPELL_SPIT_POISON &&
                pbolt.agent(true)->is_monster() &&
                pbolt.agent(true)->as_monster()->has_ench(ENCH_CONCENTRATE_VENOM)
                && mons->res_poison() <= 0)
            {
                // set the cause to the reflected agent for piety &c.
                //
                // (7 bonus poison will apply exactly 1 additional level of
                // poison to monsters, mirroring the normal effect of BEAM_POISON)
                curare_actor(pbolt.agent(), mons, "concentrated venom",
                             pbolt.agent(true)->name(DESC_PLAIN), 7);
            }
            else
                poison_monster(mons, pbolt.agent());
        }

        break;
    }

    case BEAM_POISON_ARROW:
    {
        hurted = resist_adjust_damage(mons, pbolt.flavour, hurted);
        const int stacks = pbolt.origin_spell == SPELL_STING ? 1 : 4;
        if (hurted < original)
        {
            if (doFlavouredEffects)
            {
                simple_monster_message(*mons, " partially resists.");
                poison_monster(mons, pbolt.agent(), div_rand_round(stacks, 2),
                               true);
            }
        }
        else if (doFlavouredEffects)
            poison_monster(mons, pbolt.agent(), stacks, true);

        break;
    }

    case BEAM_MERCURY:
    {
        hurted = resist_adjust_damage(mons, pbolt.flavour, hurted);
        if (doFlavouredEffects)
        {
            int chance = get_mercury_weaken_chance(mons->get_hit_dice());
            if (x_chance_in_y(chance, 100))
                mons->weaken(pbolt.agent(), 5);

            // Splash to adjacent actors.
            bool did_splash_msg = false;
            for (adjacent_iterator ai(mons->pos()); ai; ++ai)
            {
                if (actor* act = actor_at(*ai))
                {
                    if (act == pbolt.agent() || !could_harm(pbolt.agent(), act))
                        continue;

                    if (!did_splash_msg)
                    {
                        if (you.see_cell(mons->pos()))
                            mprf("The mercury splashes!");
                        did_splash_msg = true;
                    }

                    chance = get_mercury_weaken_chance(act->get_hit_dice());
                    if (x_chance_in_y(chance, 100))
                        act->weaken(pbolt.agent(), 5);
                }
            }
        }
        break;
    }

    case BEAM_NEG:
        if (mons->res_negative_energy() == 3)
        {
            if (doFlavouredEffects)
                simple_monster_message(*mons, " completely resists.");

            hurted = 0;
        }
        else
        {
            hurted = resist_adjust_damage(mons, pbolt.flavour, hurted);

            // Early out if no side effects.
            if (!doFlavouredEffects)
                return hurted;

            if (original > hurted)
                simple_monster_message(*mons, " resists.");
            else if (original < hurted)
                simple_monster_message(*mons, " is drained terribly!");

            if (mons->observable())
                pbolt.obvious_effect = true;

            mons->drain(pbolt.agent());

            if (BLAME_KILL(pbolt.thrower))
                did_god_conduct(DID_EVIL, 2, pbolt.god_cares());
        }
        break;

    case BEAM_MIASMA:
        if (mons->res_miasma())
        {
            if (doFlavouredEffects)
                simple_monster_message(*mons, " completely resists.");

            hurted = 0;
        }
        else
        {
            // Early out for tracer/no side effects.
            if (!doFlavouredEffects)
                return hurted;

            miasma_monster(mons, pbolt.agent());

            if (BLAME_KILL(pbolt.thrower))
                did_god_conduct(DID_UNCLEAN, 2, pbolt.god_cares());
        }
        break;

    case BEAM_HOLY:
    case BEAM_FOUL_FLAME:
    {
        hurted = resist_adjust_damage(mons, pbolt.flavour, hurted);
        if (doFlavouredEffects && original > 0
            && (!hurted || hurted != original))
        {
            simple_monster_message(*mons, hurted == 0 ? " completely resists." :
                                    hurted < original ? " resists." :
                                    " writhes in agony!");
        }
        break;
    }

    case BEAM_ICE:
        // ice - 40% of damage is cold, other 60% is impact and
        // can't be resisted (except by AC, of course)
        hurted = resist_adjust_damage(mons, pbolt.flavour, hurted);
        if (hurted < original)
        {
            if (doFlavouredEffects)
                simple_monster_message(*mons, " partially resists.");
        }
        else if (hurted > original)
        {
            if (doFlavouredEffects)
                simple_monster_message(*mons, " is chilled terribly!");
        }
        break;

    case BEAM_LAVA:
        hurted = resist_adjust_damage(mons, pbolt.flavour, hurted);

        if (hurted < original)
        {
            if (doFlavouredEffects)
                simple_monster_message(*mons, " partially resists.");
        }
        else if (hurted > original)
        {
            if (mons->is_icy())
            {
                if (doFlavouredEffects)
                    simple_monster_message(*mons, " melts!");
            }
            else
            {
                if (doFlavouredEffects)
                    simple_monster_message(*mons, " is burned terribly!");
            }
        }
        break;

    case BEAM_DAMNATION:
        if (mons->res_damnation())
        {
            if (doFlavouredEffects)
                simple_monster_message(*mons, " completely resists.");

            hurted = 0;
        }
        break;

    case BEAM_MEPHITIC:
        if (mons->res_poison() > 0)
        {
            if (original > 0 && doFlavouredEffects)
                simple_monster_message(*mons, " completely resists.");

            hurted = 0;
        }
        break;

    case BEAM_SPORE:
        if (mons->type == MONS_BALLISTOMYCETE)
            hurted = 0;
        break;

    case BEAM_ENSNARE:
        if (doFlavouredEffects)
            mons->trap_in_web();
        break;

    case BEAM_DEVASTATION:
        if (doFlavouredEffects)
            mons->strip_willpower(pbolt.agent(), random_range(20, 30));
        break;

    case BEAM_UMBRAL_TORCHLIGHT:
        if (mons->god == GOD_YREDELEMNUL
            || mons->holiness() & ~(MH_NATURAL | MH_DEMONIC | MH_HOLY))
        {
            if (doFlavouredEffects && !mons_aligned(mons, pbolt.agent(true)))
                simple_monster_message(*mons, " completely resists.");

            hurted = 0;
        }
        break;

    case BEAM_WARPING:
        if (doFlavouredEffects
            && x_chance_in_y(get_warp_space_chance(pbolt.ench_power), 100))
        {
            monster_blink(mons);
        }
        break;

    case BEAM_SEISMIC:
        if (mons->airborne())
            hurted = hurted / 3;
        break;

    case BEAM_BOLAS:
        if (doFlavouredEffects)
        {
            if (mons->is_insubstantial() || mons->is_amorphous())
            {
                mprf("The bolas passes through %s!",
                      mons->name(DESC_THE).c_str());
            }
            else
            {
                mons->add_ench(mon_enchant(ENCH_BOUND, pbolt.agent(),
                                           random_range(4, 8)));
                mprf("The bolas warps around %s and binds %s in place!",
                     mons->name(DESC_THE).c_str(),
                     mons->pronoun(PRONOUN_OBJECTIVE).c_str());
            }
        }
        break;

    default:
        break;
    }

    if (doFlavouredEffects)
    {
        const int burn_power = (pbolt.is_explosion) ? 5 :
                               (pbolt.pierce)       ? 3
                                                    : 2;
        mons->expose_to_element(pbolt.flavour, burn_power, pbolt.agent(), false);
    }

    return hurted;
}

// XXX: change mons to a reference
static bool _monster_resists_mass_enchantment(monster* mons,
                                              enchant_type wh_enchant,
                                              int pow,
                                              bool* did_msg)
{
    // Mass enchantments around lots of plants/fungi shouldn't cause a flood
    // of "is unaffected" messages. --Eino
    if (mons->is_firewood())
        return true;
    if (!could_harm(&you, mons))
        return true;

    switch (wh_enchant)
    {
    case ENCH_FEAR:
        if (mons->wont_attack())
            return true;

        if (!mons->can_feel_fear(true))
        {
            if (simple_monster_message(*mons, " is unaffected."))
                *did_msg = true;
            return true;
        }
        break;
    case ENCH_FRENZIED:
        if (!mons->can_go_frenzy())
        {
            if (simple_monster_message(*mons, " is unaffected."))
                *did_msg = true;
            return true;
        }
        break;
    case ENCH_ANGUISH:
        if (mons->wont_attack())
            return true;
        if (mons_intel(*mons) <= I_BRAINLESS)
        {
            if (simple_monster_message(*mons, " is unaffected."))
                *did_msg = true;
            return true;
        }
        break;
    default:
        break;
    }

    int res_margin = mons->check_willpower(&you, pow);
    if (res_margin > 0)
    {
        behaviour_event(mons, ME_ALERT, &you);
        if (simple_monster_message(*mons,
                mons->resist_margin_phrase(res_margin).c_str()))
        {
            *did_msg = true;
        }
        return true;
    }

    // If monster was affected, then there was a message.
    *did_msg = true;
    return false;
}

// Enchants all monsters in player's sight.
// If m_succumbed is non-nullptr, will be set to the number of monsters that
// were enchanted. If m_attempted is not nullptr, will be set to the number of
// monsters that we tried to enchant.
spret mass_enchantment(enchant_type wh_enchant, int pow, bool fail)
{
    fail_check();
    bool did_msg = false;

    // Give mass enchantments a power multiplier.
    pow *= 3;
    pow /= 2;

    pow = min(pow, 200);

    for (monster_iterator mi; mi; ++mi)
    {
        if (!you.see_cell_no_trans(mi->pos()))
            continue;

        if (mi->has_ench(wh_enchant))
            continue;

        bool resisted = _monster_resists_mass_enchantment(*mi, wh_enchant,
                                                          pow, &did_msg);

        if (resisted)
            continue;

        if ((wh_enchant == ENCH_FRENZIED && mi->go_frenzy(&you))
            || (wh_enchant != ENCH_FRENZIED
                && mi->add_ench(mon_enchant(wh_enchant, &you))))
        {
            // Do messaging.
            const char* msg = nullptr;
            if (wh_enchant == ENCH_FEAR)
                msg = " looks frightened!";
            else if (wh_enchant == ENCH_ANGUISH)
                msg = " is haunted by guilt!";

            if (msg && simple_monster_message(**mi, msg))
                did_msg = true;

            // Extra check for fear (monster needs to reevaluate behaviour).
            if (wh_enchant == ENCH_FEAR)
                behaviour_event(*mi, ME_SCARE, &you);
            else if (wh_enchant == ENCH_ANGUISH)
                behaviour_event(*mi, ME_ALERT, &you);
        }
    }

    if (!did_msg)
        canned_msg(MSG_NOTHING_HAPPENS);

    if (wh_enchant == ENCH_FRENZIED)
        did_god_conduct(DID_HASTY, 8, true);

    return spret::success;
}

void bolt::apply_bolt_paralysis(monster* mons)
{
    if (mons->paralysed() || mons->stasis())
        return;

    if (!mons->is_firewood()
        && simple_monster_message(*mons, " becomes paralysed!"))
    {
        obvious_effect = true;
    }

    mons->add_ench(mon_enchant(ENCH_PARALYSIS, agent(), _ench_pow_to_dur(ench_power)));
}

// Petrification works in two stages. First the monster is slowed down in
// all of its actions, and when that times out it remains properly petrified
// (no movement or actions). The second part is similar to paralysis,
// except that insubstantial monsters can't be affected and damage is
// drastically reduced.
void bolt::apply_bolt_petrify(monster* mons)
{
    if (mons->petrifying() || mons->petrified())
        return;

    if (mons->add_ench(mon_enchant(ENCH_PETRIFYING, agent())))
    {
        simple_monster_message(*mons, " begins to petrify.");
        obvious_effect = true;
    }
}

static bool _curare_hits_monster(actor *agent, monster* mons, int bonus_poison)
{
    if (!mons->alive())
        return false;

    if (mons->res_poison() > 0)
        return false;

    poison_monster(mons, agent, 2 + (bonus_poison / 7), true);

    simple_monster_message(*mons, " struggles to breathe.");
    mons->hurt(agent, roll_dice(2, 6), BEAM_POISON);

    if (mons->alive() && !mons->stasis())
    {
        simple_monster_message(*mons, mons->has_ench(ENCH_SLOW)
                                         ? " seems to be slow for longer."
                                         : " seems to slow down.");
        mons->add_ench(mon_enchant(ENCH_SLOW, agent));
    }

    return true;
}

// Actually poisons a monster (with message).
bool poison_monster(monster* mons, const actor *who, int levels,
                    bool force, bool verbose)
{
    if (!mons->alive() || levels <= 0)
        return false;

    if (monster_resists_this_poison(*mons, force))
        return false;

    const mon_enchant old_pois = mons->get_ench(ENCH_POISON);
    mons->add_ench(mon_enchant(ENCH_POISON, who, 0, levels));
    const mon_enchant new_pois = mons->get_ench(ENCH_POISON);

    // Actually do the poisoning. The order is important here.
    if (new_pois.degree > old_pois.degree
        || new_pois.degree >= MAX_ENCH_DEGREE_DEFAULT)
    {
        if (verbose)
        {
            const char* msg;
            if (new_pois.degree >= MAX_ENCH_DEGREE_DEFAULT)
                msg = " looks as sick as possible!";
            else if (old_pois.degree > 0)
                msg = " looks even sicker.";
            else
                msg = " is poisoned.";

            simple_monster_message(*mons, msg);
        }
    }

    return new_pois.duration > old_pois.duration;
}

// Actually poisons and/or slows a monster with miasma (with message).
bool miasma_monster(monster* mons, const actor* who)
{
    if (!mons->alive())
        return false;

    if (mons->res_miasma())
        return false;

    bool success = false;
    if (poison_monster(mons, who))
    {
        // Do additional impact damage to monsters who fail an rPois check
        mons->hurt(who, roll_dice(2, 4), BEAM_MMISSILE, KILLED_BY_CLOUD);
        success = true;
    }

    if (who && who->is_player() && is_good_god(you.religion))
        did_god_conduct(DID_EVIL, 5 + random2(3));

    if (mons->alive() && one_chance_in(3))
    {
        do_slow_monster(*mons, who, random_range(8, 12) * BASELINE_DELAY);
        success = true;
    }

    return success;
}

// Actually applies sticky flame to a monster (with message).
bool sticky_flame_monster(monster* mons, const actor *who, int dur, bool verbose)
{
    if (!mons->alive())
        return false;

    if (mons->res_sticky_flame() || dur <= 0)
        return false;

    const bool had_sticky_flame = mons->has_ench(ENCH_STICKY_FLAME);

    if (!had_sticky_flame && verbose)
        simple_monster_message(*mons, " is covered in liquid fire!");

    mons->add_ench(mon_enchant(ENCH_STICKY_FLAME, who, dur * BASELINE_DELAY));

    return !had_sticky_flame;
}

static bool _curare_hits_player(actor* agent, string name,
                                string source_name, int bonus_poison)
{
    ASSERT(!crawl_state.game_is_arena());

    if (player_res_poison() >= 3
        || player_res_poison() > 0 && !one_chance_in(3))
    {
        canned_msg(MSG_YOU_RESIST);
        return false;
    }

    // We force apply this poison, since the 1-in-3 chance was already passed
    poison_player(roll_dice(2, 12) + bonus_poison + 1, source_name, name, true);

    mpr("You have difficulty breathing.");
    ouch(roll_dice(2, 6), KILLED_BY_CURARE, agent->mid, "curare-induced apnoea");
    slow_player(10 + random2(2 + random2(6)));

    return true;
}


bool curare_actor(actor* source, actor* target, string name,
                  string source_name, int bonus_poison)
{
    if (target->is_player())
        return _curare_hits_player(source, name, source_name, bonus_poison);
    else
        return _curare_hits_monster(source, target->as_monster(), bonus_poison);
}

// XXX: This is a terrible place for this, but it at least does go with
// curare_actor().
int silver_damages_victim(actor* victim, int damage, string &dmg_msg)
{
    int ret = 0;
    if (victim->how_chaotic()
        || victim->is_player() && you.form != transformation::none)
    {
        ret = damage * 3 / 4;
    }
    else if (victim->is_player())
    {
        // For mutation damage, we want to count innate mutations for
        // demonspawn but not other species.
        int multiplier = 5 * you.how_mutated(true, true);
        if (multiplier == 0)
            return 0;

        if (multiplier > 75)
            multiplier = 75;

        ret = damage * multiplier / 100;
    }
    else
        return 0;

    dmg_msg = "The silver sears " + victim->name(DESC_THE) + "!";
    return ret;
}

//  Used by monsters in "planning" which spell to cast. Fires off a "tracer"
//  which tells the monster what it'll hit if it breathes/casts etc.
//
//  The output from this tracer function is written into the
//  tracer_info variables (friend_info and foe_info).
//
//  Note that beam properties must be set, as the tracer will take them
//  into account, as well as the monster's intelligence.
void fire_tracer(const monster* mons, targeting_tracer& tracer,
                 bolt &pbolt, bool explode_only, bool explosion_hole)
{
    // If this ASSERT triggers, your spell's setup code probably is doing
    // something bad when setup_mons_cast is called with check_validity=true.
    ASSERTM(crawl_state.game_started || crawl_state.test || crawl_state.script
        || crawl_state.game_is_arena(),
        "invalid game state for tracer '%s'!", pbolt.name.c_str());

    // Don't fiddle with any input parameters other than tracer stuff!
    pbolt.source        = mons->pos();
    pbolt.source_id     = mons->mid;
    pbolt.attitude      = mons_attitude(*mons);
    pbolt.precalc_agent_properties();

    // Init tracer variables.
    tracer.foe_info.reset();
    tracer.friend_info.reset();

    // Clear misc
    pbolt.reflections   = 0;
    pbolt.bounces       = 0;

    // If there's a specifically requested foe_ratio, honour it.
    if (!pbolt.foe_ratio)
        pbolt.foe_ratio     = 80;        // default - see mons_should_fire()

    pbolt.in_explosion_phase = false;

    // XXX: Make monsters calculate fire storm radius as if it rolled the
    // highest (or they will sometimes not realize when they might hit the
    // player).
    if (pbolt.origin_spell == SPELL_FIRE_STORM)
        pbolt.ex_size = 3;

    // Fire!
    if (explode_only)
        pbolt.explode(tracer, false, explosion_hole);
    else
        pbolt.fire(tracer);
}

set<coord_def> create_feat_splash(coord_def center,
                                int radius,
                                int number,
                                int duration,
                                dungeon_feature_type new_feat)
{
    set<coord_def> splash_coords;

    for (distance_iterator di(center, true, false, radius); di && number > 0; ++di)
    {
        const dungeon_feature_type feat = env.grid(*di);
        if ((feat == DNGN_FLOOR || feat == DNGN_SHALLOW_WATER)
            && cell_see_cell(center, *di, LOS_NO_TRANS))
        {
            number--;
            int time = random_range(duration, duration * 3 / 2) - (di.radius() * 20);
            temp_change_terrain(*di, new_feat, time,
                                TERRAIN_CHANGE_FLOOD);
            splash_coords.insert(*di);
        }
    }

    return splash_coords;
}

void bolt_parent_init(const bolt &parent, bolt &child)
{
    child.name           = parent.name;
    child.short_name     = parent.short_name;
    child.hit_verb       = parent.hit_verb;
    child.aux_source     = parent.aux_source;
    child.source_id      = parent.source_id;
    child.origin_spell   = parent.origin_spell;
    child.glyph          = parent.glyph;
    child.colour         = parent.colour;
    child.tile_beam      = parent.tile_beam;
    child.tile_explode   = parent.tile_explode;

    child.flavour        = parent.flavour;

    // We don't copy target since that is often overridden.
    child.thrower        = parent.thrower;
    child.source         = parent.source;
    child.source_name    = parent.source_name;
    child.attitude       = parent.attitude;

    child.loudness       = parent.loudness;
    child.pierce         = parent.pierce;
    child.aimed_at_spot  = parent.aimed_at_spot;
    child.stop_at_allies = parent.stop_at_allies;
    child.is_explosion   = parent.is_explosion;
    child.ex_size        = parent.ex_size;
    child.foe_ratio      = parent.foe_ratio;

    child.tracer         = parent.tracer;

    _copy_affected_counts(child, parent);

    child.range          = parent.range;
    child.hit            = parent.hit;
    child.damage         = parent.damage;
    if (parent.ench_power != -1)
        child.ench_power = parent.ench_power;

#ifdef DEBUG_DIAGNOSTICS
    child.quiet_debug    = parent.quiet_debug;
#endif
}

static void _malign_offering_effect(actor* victim, const actor* agent, int damage)
{
    if (!agent || damage < 1)
        return;

    // The victim may die.
    coord_def c = victim->pos();

    if (you.see_cell(c) || you.see_cell(agent->pos()))
        mprf("%s life force is offered up.", victim->name(DESC_ITS).c_str());
    damage = victim->hurt(agent, damage, BEAM_MALIGN_OFFERING, KILLED_BY_BEAM,
                          "", "by a malign offering");

    // Actors that had LOS to the victim (blocked by glass, clouds, etc),
    // even if they couldn't actually see each other because of blindness
    // or invisibility.
    for (actor_near_iterator ai(c, LOS_NO_TRANS); ai; ++ai)
    {
        if (mons_aligned(agent, *ai) && !(ai->holiness() & MH_NONLIVING)
            && *ai != victim)
        {
            if (ai->heal(max(1, damage * 2 / 3)) && you.can_see(**ai))
            {
                mprf("%s %s healed.", ai->name(DESC_THE).c_str(),
                                      ai->conj_verb("are").c_str());
            }
        }
    }
}

static void _vampiric_draining_effect(actor& victim, actor& agent, int damage)
{
    if (damage < 1 || !actor_is_susceptible_to_vampirism(victim))
        return;

    if (you.can_see(victim) || you.can_see(agent))
    {
        mprf("%s %s life force from %s%s",
             agent.name(DESC_THE).c_str(),
             agent.conj_verb("draw").c_str(),
             victim.name(DESC_THE).c_str(),
             attack_strength_punctuation(damage).c_str());
    }

    if (agent.is_player())
        majin_bo_vampirism(*victim.as_monster(), min(damage, victim.stat_hp()));

    const int drain_amount = victim.hurt(&agent, damage,
                                         BEAM_VAMPIRIC_DRAINING,
                                         KILLED_BY_BEAM, "",
                                         "by vampiric draining");

    if (agent.is_monster())
    {
        const int hp_gain = drain_amount * 2 / 3;
        if (agent.heal(hp_gain))
            simple_monster_message(*agent.as_monster(), " is healed by the life force!");
    }
    else
    {
        if (you.duration[DUR_DEATHS_DOOR] || you.hp == you.hp_max)
            return;

        const int hp_gain = div_rand_round(drain_amount, 2);
        if (hp_gain)
        {
            mprf("You feel life coursing into your body%s",
                 attack_strength_punctuation(hp_gain).c_str());
            inc_hp(hp_gain);
        }
    }
}

/**
 * Turn a BEAM_UNRAVELLING beam into a BEAM_UNRAVELLED_MAGIC beam, and make
 * it explode appropriately.
 *
 * @param[in,out] beam      The beam being turned into an explosion.
 */
static void _unravelling_explode(bolt &beam)
{
    beam.colour       = ETC_MUTAGENIC;
    beam.flavour      = BEAM_UNRAVELLED_MAGIC;
    beam.ex_size      = 1;
    beam.is_explosion = true;
    // and it'll explode 'naturally' a little later.
}

bool bolt::is_bouncy(dungeon_feature_type feat) const
{
    // Beams with no directionality will assert if we try to bounce them.
    if (aimed_at_feet)
        return false;

    // Don't bounce off open sea.
    if (feat_is_endless(feat))
        return false;

    if (real_flavour == BEAM_CHAOS
        && feat_is_solid(feat))
    {
        return true;
    }

    if ((origin_spell == SPELL_REBOUNDING_BLAZE ||
        origin_spell == SPELL_REBOUNDING_CHILL)
        && feat_is_solid(feat)
        && !feat_is_tree(feat))
    {
        return true;
    }

    if (is_enchantment())
        return false;

    if (flavour == BEAM_ELECTRICITY
        && origin_spell != SPELL_BLINKBOLT
        && origin_spell != SPELL_MAGNAVOLT
        && !feat_is_metal(feat)
        && !feat_is_tree(feat))
    {
        return true;
    }

    if ((flavour == BEAM_FIRE || flavour == BEAM_COLD)
        && feat == DNGN_CRYSTAL_WALL)
    {
        return true;
    }

    return false;
}

cloud_type bolt::get_cloud_type() const
{
    if (origin_spell == SPELL_NOXIOUS_CLOUD)
        return CLOUD_MEPHITIC;

    if (origin_spell == SPELL_POISONOUS_CLOUD)
        return CLOUD_POISON;

    if (origin_spell == SPELL_HOLY_BREATH)
        return CLOUD_HOLY;

    if (origin_spell == SPELL_FLAMING_CLOUD)
        return CLOUD_FIRE;

    if (origin_spell == SPELL_CHAOS_BREATH)
        return CLOUD_CHAOS;

    if (origin_spell == SPELL_MIASMA_BREATH)
        return CLOUD_MIASMA;

    if (origin_spell == SPELL_FREEZING_GUST)
        return CLOUD_COLD;

    if (origin_spell == SPELL_SPECTRAL_CLOUD)
        return CLOUD_SPECTRAL;

    if (origin_spell == SPELL_RAVENOUS_SWARM)
        return CLOUD_BATS;

    return CLOUD_NONE;
}

int bolt::get_cloud_pow() const
{
    if (origin_spell == SPELL_FREEZING_GUST
        || origin_spell == SPELL_POISONOUS_CLOUD)
    {
        return random_range(10, 15);
    }

    if (origin_spell == SPELL_SPECTRAL_CLOUD)
        return random_range(12, 20);

    if (origin_spell == SPELL_RAVENOUS_SWARM)
        return random_range(22, 40);

    return 0;
}

int bolt::get_cloud_size(bool min, bool max) const
{
    if (origin_spell == SPELL_MEPHITIC_CLOUD
        || origin_spell == SPELL_MIASMA_BREATH
        || origin_spell == SPELL_FREEZING_GUST)
    {
        return 10;
    }

    if (origin_spell == SPELL_RAVENOUS_SWARM)
    {
        if (min)
            return 10;
        else if (max)
            return 16;
        else
            return random_range(10, 16);
    }

    if (origin_spell == SPELL_RUST_BREATH)
        return ench_power;

    if (min)
        return 8;
    if (max)
        return 12;

    return 8 + random2(5);
}

static void _waterlog_mon(monster &mon, int ench_pow)
{
    if (!mon.alive() || mon.res_water_drowning() || mon.has_ench(ENCH_FLOODED))
        return;

    const int min_dur = ench_pow + 20;
    const int dur = random_range(min_dur, min_dur * 3 / 2);
    mon.floodify(&you, dur);
}

void bolt::special_explode()
{
    unwind_var<beam_tracer*> old_tracer(special_explosion->tracer, tracer);
    _copy_affected_counts(*special_explosion, *this);
    special_explosion->in_explosion_phase = false;
    special_explosion->target = pos();
    special_explosion->refine_for_explosion("the " + name + " explodes!");
    special_explosion->explode();
    _copy_affected_counts(*this, *special_explosion);
}

void bolt::affect_endpoint()
{
    if (affects_nothing)
        return;

    // Test if this shot should trigger Dimensional Bullseye.
    bool use_bullseye = false;
    if (can_trigger_bullseye)
    {
        // We've already verified that at least one valid triggering target was
        // aimed at, but we need to know that we didn't ALSO aim at the bullseye
        // target (and that it's still in range)
        monster* bullseye_targ = monster_by_mid(you.props[BULLSEYE_TARGET_KEY].get_int());
        if (bullseye_targ && hit_count.count(bullseye_targ->mid) == 0
            && you.can_see(*bullseye_targ)
            && !ranged_atk->weapon->is_type(OBJ_MISSILES, MI_THROWING_NET))
        {
            use_bullseye = true;
        }
    }

    // hack: we use hit_verb to communicate whether a ranged
    // attack hit. (And ranged attacks should only explode if
    // they hit the target, to avoid silliness with . targeting.)
    if (special_explosion && (is_tracer() || !hit_verb.empty()))
        special_explode();

    // Leave an object, if applicable.
    // (Dimensional bullseye should make objects drop at the bullseye target
    // instead of the end of the ray.)
    if (drop_item && !is_tracer() && !use_bullseye)
        drop_object();

    if (is_explosion)
    {
        target = pos();
        refine_for_explosion();
        explode();
        return;
    }

    const cloud_type cloud = get_cloud_type();

    if (is_tracer())
    {
        if (cloud == CLOUD_NONE)
            return;

        targeter_cloud tgt(agent(), cloud, range, get_cloud_size(true),
                           get_cloud_size(false, true));
        tgt.set_aim(pos());
        for (const auto &entry : tgt.seen)
        {
            if (entry.second != AFF_YES)
                continue;

            if (entry.first == you.pos())
                tracer_affect_player();
            else if (monster* mon = monster_at(entry.first))
                if (!ignores_monster(mon))
                    tracer_affect_monster(mon);
        }

        return;
    }

    if (!is_explosion && !noise_generated && loudness)
    {
        // Digging can target squares on the map boundary, though it
        // won't remove them of course.
        const coord_def noise_position = clamp_in_bounds(pos());
        noisy(loudness, noise_position, source_id);
        noise_generated = true;
    }

    if (cloud != CLOUD_NONE)
        big_cloud(cloud, agent(), pos(), get_cloud_pow(), get_cloud_size());

    // Not using big_cloud so that the area affected is more predictable to the player.
    if (origin_spell == SPELL_RUST_BREATH)
    {
        int to_place = ench_power;
        for (distance_iterator di(pos(), true, false, 2); di && to_place > 0; ++di)
        {
            if (cell_see_cell(*di, agent()->pos(), LOS_NO_TRANS))
            {
                place_cloud(CLOUD_RUST, *di, 5 + ench_power * 2 / 3 + random2(2), agent());
                to_place--;
            }
        }
    }

    // bail out of bullseye early if its target died somehow
    if (use_bullseye && you.duration[DUR_DIMENSIONAL_BULLSEYE] > 0)
    {
        monster* bullseye_targ = monster_by_mid(you.props[BULLSEYE_TARGET_KEY].get_int());
        mpr("Your projectile teleports!");
        use_target_as_pos = true;
        target = bullseye_targ->pos();
        affect_monster(bullseye_targ);
        if (drop_item)
        {
            // This is a little hacky, but the idea is to make the beam think we only
            // aimed at our bullseye target before dropping the item (so it will appear
            // beneath them). I think this should work fine?
            drop_object();
        }

        if (special_explosion && (is_tracer() || !hit_verb.empty()))
            special_explode();
    }

    // you like special cases, right?
    switch (origin_spell)
    {
    case SPELL_PRIMAL_WAVE:
    {
        if (you.see_cell(pos()))
            mpr("The wave splashes down.");
        else
            mpr("You hear a splash.");

        const bool is_player = agent(true) && agent(true)->is_player();
        const int num = is_player ? div_rand_round(ench_power * 3, 20) + 3 + random2(7)
                                  : random_range(3, 12, 2);
        const int dur = div_rand_round(ench_power * 4, 3) + 66;
        set<coord_def> splash_coords = create_feat_splash(pos(), is_player ? 2 : 1, num, dur);
        dprf(DIAG_BEAM, "Creating pool at %d,%d with %d tiles of water for %d auts.", pos().x, pos().y, num, dur);

        // Waterlog anything at the center, even if a pool wasn't generated there
        splash_coords.insert(pos());

        if (!is_player)
            break;
        for (const coord_def &coord : splash_coords)
        {
            monster* mons = monster_at(coord);
            if (mons)
                _waterlog_mon(*mons, ench_power);
        }
        break;
    }
    case SPELL_HURL_SLUDGE:
    {
        // short duration to limit the ability of the player to use it
        // against enemies
        const int dur = random_range(20, 30);
        // TODO: deduplicate with noxious_bog_cell
        if (env.grid(pos()) == DNGN_DEEP_WATER || env.grid(pos()) == DNGN_LAVA)
            break;

        // intentionally don't use TERRAIN_CHANGE_BOG to avoid it vanishing
        // when out of LOS
        temp_change_terrain(pos(), DNGN_TOXIC_BOG, dur,
                            TERRAIN_CHANGE_FLOOD,
                            agent() ? agent()->mid : MID_NOBODY);
        break;
    }
    case SPELL_BLINKBOLT:
    {
        actor *act = agent(true); // use orig actor even when reflected
        if (!act || !act->alive())
            return;

        for (vector<coord_def>::reverse_iterator citr = path_taken.rbegin();
             citr != path_taken.rend(); ++citr)
        {
            if (act->is_habitable(*citr) && (!act->is_player() || !monster_at(*citr))
                && act->blink_to(*citr, false))
            {
                return;
            }
        }
        return;
    }

    case SPELL_SEARING_BREATH:
        if (!path_taken.empty())
            place_cloud(CLOUD_FIRE, pos(), 5 + random2(5), agent());
        break;

    case SPELL_MUD_BREATH:
    {
        mpr("The mud splashes down.");
        const int num = 3 + div_rand_round(ench_power * 3, 7) + random2(4);
        const int dur = (div_rand_round(ench_power * 1, 3) + 15) * BASELINE_DELAY;
        const int rad = (num > 20) ? 3 : (num > 9) ? 2 : 1;
        create_feat_splash(pos(), rad, num, dur, DNGN_MUD);
    }
    break;

    case SPELL_FLASHING_BALESTRA:
    {
        // If the initial bolt is reflected or redirected back at the summoner,
        // we can crash on trying to create the summon.
        if (!agent(true) || !agent(true)->alive())
            break;

        monster* blade = create_monster(mgen_data(MONS_DANCING_WEAPON,
                                        SAME_ATTITUDE(agent(true)->as_monster()),
                                        pos(), agent(true)->as_monster()->foe)
                        .set_summoned(agent(true), SPELL_FLASHING_BALESTRA, summ_dur(1), false)
                        .set_range(1));

        if (blade)
            blade->add_ench(ENCH_MIGHT);
    }
    break;

    case SPELL_BOLT_OF_FLESH:
    {
        if (!agent(true) || !agent(true)->alive())
            break;

        // Try to place the pile of flesh at the last spot immediately after the
        // last thing it hits (provided that spot is somewhere flesh can go).
        coord_def p = get_last_affected_pos(1);
        if (actor_at(p) || !monster_habitable_grid(MONS_PILE_OF_FLESH, p))
            p = get_last_affected_pos(0);

        create_monster(mgen_data(MONS_PILE_OF_FLESH,
                       SAME_ATTITUDE(agent(true)->as_monster()),
                       p, agent(true)->as_monster()->foe)
                       .set_summoned(agent(true), SPELL_BOLT_OF_FLESH, summ_dur(3), false)
                       .set_range(0, 1));
    }
    break;

    case SPELL_PHANTOM_BLITZ:
    {
        if (!agent(true) || !agent(true)->alive())
            break;

        coord_def spot;
        int num_found = 0;
        for (fair_adjacent_iterator ai(pos()); ai; ++ai)
        {
            if (feat_is_solid(env.grid(*ai)) || actor_at(*ai))
                continue;

            if (one_chance_in(++num_found))
                spot = *ai;
        }

        if (!spot.origin())
        {
            // Clone the original monster and heal the clone to full.
            monster* blitzer = agent(true)->as_monster();
            bool obviousness; // dummy argument
            monster *mirror = clone_mons(blitzer, true, &obviousness,
                                         blitzer->attitude, spot);
            if (!mirror)
                break;
            mirror->mark_summoned(SPELL_PHANTOM_BLITZ, summ_dur(2), true, true);
            mirror->summoner = blitzer->mid;
            mirror->foe = blitzer->foe;
            mirror->hit_points = blitzer->max_hit_points;

#ifdef USE_TILE
            if (mirror->type == MONS_NEKOMATA)
                mirror->props[MONSTER_TILE_KEY] = TILEP_MONS_NEKOMATA_PHANTOM;
#endif
        }
    }
    break;

    case SPELL_GREATER_ENSNARE:
    {
        int count = random_range(3, 6);
        for (distance_iterator di(pos(), true, true, 1); di && count > 0; ++di)
        {
            trap_def *trap = trap_at(*di);
            if (trap && trap->type != TRAP_WEB
                || !trap && env.grid(*di) != DNGN_FLOOR)
            {
                continue;
            }

            if (actor_at(*di))
                actor_at(*di)->trap_in_web();
            else
            {
                temp_change_terrain(*di, DNGN_TRAP_WEB, random_range(60, 110),
                                    TERRAIN_CHANGE_WEBS);
            }
            count--;
        }
        break;
    }

    default:
        break;
    }
}

void bolt::affect_endpoint(beam_tracer& new_tracer)
{
    unwind_var<beam_tracer*> old_tracer(tracer, &new_tracer);
    affect_endpoint();
}

bool bolt::stop_at_target() const
{
    // the pos check is to avoid a ray.cc assert for a ray that goes nowhere
    return is_explosion || is_big_cloud() ||
            (source_id == MID_PLAYER && origin_spell == SPELL_BLINKBOLT) ||
            (aimed_at_spot && (pos() == source || flavour != BEAM_DIGGING));
}

void bolt::drop_object()
{
    ASSERT(ranged_atk);

    // If the player is throwing this item at a wall, attempt to place it at
    // the tile on the path immediately before hitting the wall.
    // XXX: If Dimensional Bullseye is active on a target in a wall, the
    //      projectile will end up rewinding relative to the initial target,
    //      not the bullseye one. This is technically incorrect, but probably
    //      too minor to worry about.
    const coord_def spot = cell_is_solid(pos())
                                ? path_taken.size() >= 2
                                    ? path_taken[path_taken.size() - 2]
                                    : source
                                : pos();
    int id = copy_item_to_grid(*ranged_atk->weapon, spot, 1);

    // If the player threw this, mark it as thrown so that they'll pick it up
    // again when they walk over it.
    if (id >= 0 && id != NON_ITEM && ranged_atk->attacker->is_player())
        env.item[id].flags |= ISFLAG_THROWN;
}

// Returns true if the beam hits the player, fuzzing the beam if necessary
// for monsters without see invis firing tracers at the player.
bool bolt::found_player() const
{
    const int dist = grid_distance(pos(), you.pos());

    // For monsters looking for an invisible player they cannot see, pretend
    // they might be anywhere within 2 tiles of their real location that isn't
    // a solid object or already occupied by something else.
    if (is_tracer()
        && !can_see_invis && you.invisible()
        && !YOU_KILL(thrower)
        && dist <= 2
        && (!agent()
            || (agent()->is_monster()
                && !agent()->as_monster()->friendly()
                && agent()->as_monster()->attitude != ATT_MARIONETTE))
        // No point in fuzzing to a position that could never be hit.
        && you.see_cell_no_trans(pos())
        && !cell_is_solid(pos())
        && !monster_at(pos()))
    {
        return true;
    }

    return dist == 0;
}

void bolt::affect_ground()
{
    // Explosions only have an effect during their explosion phase.
    // Special cases can be handled here.
    if (is_explosion && !in_explosion_phase)
        return;

    // XXX: This feels like an ugly place to put this, but it doesn't do any
    //      damage and the cloud placement code is otherwise skipped entirely
    //      for tracers.
    if (is_tracer() && origin_spell == SPELL_NOXIOUS_BREATH && ench_power > 10)
    {
        for (adjacent_iterator ai(pos()); ai; ++ai)
        {
            const monster* mon = monster_at(*ai);
            if (mon)
                tracer->monster_hit(*this, *mon);
        }
    }

    if (is_tracer())
        return;

    affect_place_clouds();
}

bool bolt::is_fiery() const
{
    return get_beam_resist_type(flavour) == BEAM_FIRE;
}

/// Can this bolt burn trees it hits?
bool bolt::can_burn_trees() const
{
    // XXX: rethink this
    switch (origin_spell)
    {
    case SPELL_LIGHTNING_BOLT:
    case SPELL_BOLT_OF_FIRE:
    case SPELL_REBOUNDING_BLAZE:
    case SPELL_BOLT_OF_MAGMA:
    case SPELL_MAGMA_BARRAGE:
    case SPELL_FIREBALL:
    case SPELL_FIRE_STORM:
    case SPELL_IGNITION:
    case SPELL_INNER_FLAME:
    case SPELL_STARBURST:
    case SPELL_FLAME_WAVE:
    case SPELL_FORGE_BLAZEHEART_GOLEM: // core breach!
    case SPELL_HELLFIRE_MORTAR:
    case SPELL_MONARCH_BOMB:
        return true;
    case SPELL_UNLEASH_DESTRUCTION:
        return flavour == BEAM_FIRE || flavour == BEAM_LAVA;
    default:
        return false;
    }
}

bool bolt::can_affect_wall(const coord_def& p, bool map_knowledge) const
{
    dungeon_feature_type wall = map_knowledge ? env.map_knowledge(p).feat()
                                              : env.grid(p);

    // digging
    if (flavour == BEAM_DIGGING)
    {
        if (feat_is_diggable(wall))
            return true;
        // digging might affect unseen squares, as far as the player knows
        if (wall == DNGN_UNSEEN)
            return true;
    }

    if (can_burn_trees())
        return feat_is_flammable(wall);

    // Lee's Rapid Deconstruction
    if (flavour == BEAM_FRAG)
        return true; // smite targeting, we don't care

    // Can create a new plasmodium on another wall
    if (origin_spell == SPELL_LAUNCH_SPORANGIUM)
        return true;

    return false;
}

void bolt::affect_place_clouds()
{
    if (in_explosion_phase)
        affect_place_explosion_clouds();

    const coord_def p = pos();

    // Is there already a cloud here?
    if (cloud_struct* cloud = cloud_at(p))
    {
        // fire cancelling cold & vice versa
        if ((cloud->type == CLOUD_COLD && is_fiery())
            || (cloud->type == CLOUD_FIRE && flavour == BEAM_COLD))
        {
            if (player_can_hear(p))
                mprf(MSGCH_SOUND, "You hear a sizzling sound!");

            delete_cloud(p);
            extra_range_used += 5;
            return;
        }
        // blastmote explosions
        if (cloud->type == CLOUD_BLASTMOTES && is_fiery())
            explode_blastmotes_at(p);
    }

    const dungeon_feature_type feat = env.grid(p);

    if (origin_spell == SPELL_POISONOUS_CLOUD)
        place_cloud(CLOUD_POISON, p, random2(5) + 3, agent());

    if (origin_spell == SPELL_HOLY_BREATH)
        place_cloud(CLOUD_HOLY, p, random2(4) + 2, agent());

    if (origin_spell == SPELL_FLAMING_CLOUD)
        place_cloud(CLOUD_FIRE, p, random2(4) + 2, agent());

    if (origin_spell == SPELL_RAVENOUS_SWARM)
        place_cloud(CLOUD_BATS, p, random2(4) + 4, agent());

    // Fire/cold over water/lava
    if (feat == DNGN_LAVA && flavour == BEAM_COLD
        || feat_is_water(feat) && is_fiery())
    {
        place_cloud(CLOUD_STEAM, p, 2 + random2(5), agent(), 11);
    }

    if (feat_is_water(feat) && flavour == BEAM_COLD
        && damage.num * damage.size > 35)
    {
        place_cloud(CLOUD_COLD, p, damage.num * damage.size / 30 + 1, agent());
    }

    if (flavour == BEAM_MIASMA)
        place_cloud(CLOUD_MIASMA, p, random2(5) + 2, agent());


    if (flavour == BEAM_STEAM)
    {
        if (origin_spell == SPELL_STEAM_BREATH)
            place_cloud(CLOUD_STEAM, p, div_rand_round(ench_power, 3) + 3, agent());
        else
            place_cloud(CLOUD_STEAM, p, random2(5) + 2, agent());
    }

    //XXX: these use the name for a gameplay effect.
    if (name == "poison gas")
        place_cloud(CLOUD_POISON, p, random2(4) + 3, agent());

    if (origin_spell == SPELL_PETRIFYING_CLOUD)
        place_cloud(CLOUD_PETRIFY, p, random2(4) + 4, agent());

    if (origin_spell == SPELL_SPECTRAL_CLOUD)
        place_cloud(CLOUD_SPECTRAL, p, random2(6) + 5, agent());

    if (origin_spell == SPELL_DEATH_RATTLE)
        place_cloud(CLOUD_MIASMA, p, random2(4) + 4, agent());

    if (origin_spell == SPELL_CAUSTIC_BREATH)
    {
        if (!actor_at(p))
        {
            place_cloud(CLOUD_ACID, p, 3 + random_range(ench_power / 2,
                                                        ench_power * 4 / 5),
                        agent());
        }
    }

    if (origin_spell == SPELL_NOXIOUS_BREATH)
    {
        place_cloud(CLOUD_MEPHITIC, p,
                    2 + random_range(ench_power / 2, ench_power * 4 / 5), agent());

        for (adjacent_iterator ai(pos()); ai; ++ai)
        {
            if (x_chance_in_y(max(0, ench_power - 10), ench_power - 5))
            {
                place_cloud(CLOUD_MEPHITIC, *ai,
                        2 + random_range(ench_power / 3, ench_power / 2), agent());
            }
        }
    }

    if (origin_spell == SPELL_RUST_BREATH)
        place_cloud(CLOUD_RUST, p, 5 + ench_power * 2 / 3 + random2(2), agent());

    if (origin_spell == SPELL_GOLDEN_BREATH)
        place_cloud(CLOUD_POISON, p, random_range(4, 8), agent());

    // Only place clouds on creatures who are immune to them
    if (origin_spell == SPELL_MOURNING_WAIL)
    {
        if (!actor_at(p) || actor_at(p)->res_negative_energy() == 3)
            place_cloud(CLOUD_MISERY, p, random2(4) + 2, agent());
    }

    if (origin_spell == SPELL_MARCH_OF_SORROWS)
    {
        place_cloud(CLOUD_MISERY, p, random2(5) + 8, agent());

        if (actor_at(p) && !mons_aligned(actor_at(p), agent()))
            for (adjacent_iterator ai(pos()); ai; ++ai)
                place_cloud(CLOUD_MISERY, *ai, random2(4) + 2, agent());
    }

}

void bolt::affect_place_explosion_clouds()
{
    const coord_def p = pos();

    // First check: fire/cold over water/lava.
    if (env.grid(p) == DNGN_LAVA && flavour == BEAM_COLD
        || feat_is_water(env.grid(p)) && is_fiery())
    {
        place_cloud(CLOUD_STEAM, p, 2 + random2(5), agent());
        return;
    }

    if (flavour == BEAM_MEPHITIC)
    {
        const coord_def center = (aimed_at_feet ? source : ray.pos());
        if (p == center || x_chance_in_y(125 + ench_power, 225))
        {
            place_cloud(CLOUD_MEPHITIC, p, roll_dice(2,
                        8 + div_rand_round(ench_power, 20)), agent());
        }
    }

    // Blazeheart core detonation
    if (origin_spell == SPELL_FORGE_BLAZEHEART_GOLEM)
        place_cloud(CLOUD_FIRE, p, 2 + random2avg(5,2), agent());

    if (origin_spell == SPELL_FIRE_STORM)
    {
        place_cloud(CLOUD_FIRE, p, 2 + random2avg(5,2), agent());

        if (!cell_is_solid(p) && !monster_at(p) && one_chance_in(4))
        {
            const god_type god =
                (crawl_state.is_god_acting()) ? crawl_state.which_god_acting()
                                              : GOD_NO_GOD;
            const beh_type att =
                (whose_kill() == KC_OTHER ? BEH_HOSTILE : BEH_FRIENDLY);

            actor* summ = agent();
            mgen_data mg(MONS_FIRE_VORTEX, att, p, MHITNOT, MG_NONE, god);
            mg.set_summoned(summ, SPELL_FIRE_STORM, summ_dur(1), false, false);

            // Spell-summoned monsters need to have a live summoner.
            if (summ == nullptr || !summ->alive_or_reviving())
            {
                if (!source_name.empty())
                    mg.non_actor_summoner = source_name;
                else if (god != GOD_NO_GOD)
                    mg.non_actor_summoner = god_name(god);
            }

            mons_place(mg);
        }
    }
}

// A little helper function to handle the calling of ouch()...
void bolt::internal_ouch(int dam)
{
    monster* monst = monster_by_mid(source_id);

    const char *what = aux_source.empty() ? name.c_str() : aux_source.c_str();

    if (YOU_KILL(thrower) && you.duration[DUR_QUAD_DAMAGE])
        dam *= 4;

    // The order of this is important.
    if (monst && mons_is_wrath_avatar(*monst))
    {
        ouch(dam, KILLED_BY_DIVINE_WRATH, MID_NOBODY,
             aux_source.empty() ? nullptr : aux_source.c_str(), true,
             source_name.empty() ? nullptr : source_name.c_str());
    }
    else if (is_death_effect)
    {
        ouch(dam, KILLED_BY_DEATH_EXPLOSION, source_id,
             aux_source.c_str(), true,
             source_name.empty() ? nullptr : source_name.c_str());
    }
    else if (flavour == BEAM_MINDBURST || flavour == BEAM_DESTRUCTION)
    {
        ouch(dam, KILLED_BY_DISINT, source_id, what, true,
             source_name.empty() ? nullptr : source_name.c_str());
    }
    else if (YOU_KILL(thrower) && aux_source.empty())
    {
        if (reflections > 0)
            ouch(dam, KILLED_BY_REFLECTION, reflector, name.c_str());
        else if (bounces > 0)
            ouch(dam, KILLED_BY_BOUNCE, MID_PLAYER, name.c_str());
        else
        {
            if (self_targeted() && effect_known)
                ouch(dam, KILLED_BY_SELF_AIMED, MID_PLAYER, name.c_str());
            else
                ouch(dam, KILLED_BY_TARGETING, MID_PLAYER, name.c_str());
        }
    }
    else if (MON_KILL(thrower))
    {
        ouch(dam, KILLED_BY_BEAM, source_id,
             what, true,
             source_name.empty() ? nullptr : source_name.c_str());
    }
    else // KILL_NON_ACTOR || (YOU_KILL && aux_source)
        ouch(dam, KILLED_BY_WILD_MAGIC, source_id, what);
}

// A first step towards to-hit sanity for beams. We're still being
// very kind to the player, but it should be fairer to monsters than
// 4.0.
static int _test_beam_hit(int attack, int defence, defer_rand &r)
{
    if (attack == AUTOMATIC_HIT)
        return true;

    attack = r[1].random2(attack);
    defence = r[2].random2avg(defence, 2);

    dprf(DIAG_BEAM, "Beam attack: %d, defence: %d", attack, defence);

    return attack - defence;
}

bool bolt::is_harmless(const monster* mon) const
{
    // For enchantments, this is already handled in nasty_to().
    if (is_enchantment())
        return !nasty_to(mon);

    // The others are handled here.
    switch (flavour)
    {
    case BEAM_VISUAL:
    case BEAM_DIGGING:
        return true;

    case BEAM_HOLY:
        return mon->res_holy_energy() >= 3;

    case BEAM_FOUL_FLAME:
        return mon->res_foul_flame() >= 3;

    case BEAM_STEAM:
        return mon->res_steam() >= 3;

    case BEAM_FIRE:
        return mon->res_fire() >= 3;

    case BEAM_COLD:
        return mon->res_cold() >= 3;

    case BEAM_MIASMA:
        return mon->res_miasma();

    case BEAM_NEG:
        return mon->res_negative_energy() == 3;

    case BEAM_ELECTRICITY:
        return mon->res_elec() >= 3;

    case BEAM_POISON:
        return mon->res_poison() >= 3;

    case BEAM_ACID:
        return mon->res_corr() >= 3;

    case BEAM_MEPHITIC:
        return mon->res_poison() > 0 || mon->clarity();

    case BEAM_UMBRAL_TORCHLIGHT:
        return mon->god == GOD_YREDELEMNUL
               || (bool)!(mon->holiness() & (MH_NATURAL | MH_DEMONIC | MH_HOLY));

    default:
        return false;
    }
}

// N.b. only called for player-originated beams and those used by the
// player's allies. This may not currently be comprehensive with ally spells
// that could affect some, but not all, players.
bool bolt::harmless_to_player() const
{
    dprf(DIAG_BEAM, "beam flavour: %d", flavour);

    // Marionettes can't hurt the player with anything, so don't worry about it.
    if (agent() && agent()->real_attitude() == ATT_MARIONETTE)
        return true;

    if (you.cloud_immune() && is_big_cloud())
        return true;

    if (origin_spell == SPELL_COMBUSTION_BREATH
        || origin_spell == SPELL_NULLIFYING_BREATH
        || origin_spell == SPELL_RIMEBLIGHT
        || origin_spell == SPELL_RUST_BREATH && you.form == transformation::fortress_crab)
    {
        return true;
    }

    switch (flavour)
    {
    case BEAM_VISUAL:
    case BEAM_DIGGING:
        return true;

    // Positive enchantments.
    case BEAM_HASTE:
    case BEAM_HEALING:
    case BEAM_MIGHT:
    case BEAM_AGILITY:
    case BEAM_INVISIBILITY:
    case BEAM_RESISTANCE:
    case BEAM_DOUBLE_VIGOUR:
        return true;

    case BEAM_HOLY:
        return you.res_holy_energy() >= 3;

    case BEAM_FOUL_FLAME:
        return you.res_foul_flame() >= 3;

    case BEAM_MIASMA:
        return you.res_miasma();

    case BEAM_NEG:
        return player_prot_life(false) >= 3;

    case BEAM_POISON:
        return player_res_poison(false) >= 3
               || is_big_cloud() && player_res_poison(false) > 0;

    case BEAM_MEPHITIC:
        return player_res_poison(false) > 0 || you.clarity();

    case BEAM_PETRIFY:
        return you.res_petrify() || you.petrified();

    case BEAM_COLD:
        return is_big_cloud() && actor_cloud_immune(you, CLOUD_COLD);

    case BEAM_VIRULENCE:
        return player_res_poison(false) >= 3;

    case BEAM_ROOTS:
        return mons_att_wont_attack(attitude) || !agent()->can_constrict(you, CONSTRICT_ROOTS);

    case BEAM_VILE_CLUTCH:
        return mons_att_wont_attack(attitude) || !agent()->can_constrict(you, CONSTRICT_BVC);

    case BEAM_UMBRAL_TORCHLIGHT:
        return you_worship(GOD_YREDELEMNUL)
               || (bool)!(you.holiness() & (MH_NATURAL | MH_DEMONIC | MH_HOLY));

    case BEAM_QAZLAL:
        return true;

    default:
        return false;
    }
}

bool bolt::is_reflectable(const actor &whom) const
{
    if (range_used() > range)
        return false;

    return whom.reflection();
}

bool bolt::is_big_cloud() const
{
    return testbits(get_spell_flags(origin_spell), spflag::cloud);
}

coord_def bolt::leg_source() const
{
    if (bounces > 0 && map_bounds(bounce_pos))
        return bounce_pos;
    else
        return source;
}

// Reflect a beam back the direction it came. This is used
// by shields of reflection.
void bolt::reflect()
{
    reflections++;

    target = leg_source();
    source = pos();

    // Reset bounce_pos, so that if we somehow reflect again before reaching
    // the wall that we won't keep heading towards the wall.
    bounce_pos.reset();

    if (pos() == you.pos())
    {
        reflector = MID_PLAYER;
        count_action(CACT_BLOCK, -1, BLOCK_REFLECT);
    }
    else if (monster* m = monster_at(pos()))
        reflector = m->mid;
    else
    {
        reflector = MID_NOBODY;
#ifdef DEBUG
        dprf(DIAG_BEAM, "Bolt reflected by neither player nor "
             "monster (bolt = %s)", name.c_str());
#endif
    }

    flavour = real_flavour;
    stop_at_allies = false;
    choose_ray();
}

void bolt::tracer_affect_player()
{
    if (flavour == BEAM_UNRAVELLING && player_is_debuffable())
        is_explosion = true;

    const bool friendly = agent() ? agent()->wont_attack()
                                  : mons_att_wont_attack(attitude);

    // XXX: For now, only friendly monsters can know player immunities this way.
    //      (In future, this should handle 'obvious' immunities from things like
    //      being undead, while hiding things like equipment modifiers.)
    const bool harmless = friendly && harmless_to_player();

    // Check whether thrower can see player, unless thrower == player.
    if (YOU_KILL(thrower))
    {
        if (!harmless)
            tracer->actor_affected(true, you.experience_level);
    }
    // Hostile monsters that can see the player or are targetting their rough
    // location.
    else if (!friendly && !harmless)
    {
        // If a monster is guessing at the player's position, don't act like
        // they might be in multiple tiles at once and assume that each
        // bounce can hit them at most once.
        //
        // This isn't *exactly* the right check - theoretically we could
        // have avoided the player on the initial shot and then hit them
        // repeatedly on the bounce - but it's going to be right almost
        // all the time.
        if (!can_see_invis && you.invisible()
            && tracer->player_hit_count() > bounces + reflections)
        {
            return;
        }

        int power = you.experience_level;
        tracer->actor_affected(false, power);
        tracer->player_hit(false);
    }

    // Friendly monsters will always abort harmful effects that hit the player.
    if (friendly && !harmless)
        tracer->player_hit();
    extra_range_used += range_used_on_hit();
}

int bolt::apply_lighting(int base_hit, const actor &targ) const
{
    if (targ.invisible() && !can_see_invis)
        base_hit /= 2;

    // We multiply these lighting effects by 2, since normally they're applied post-roll
    // where the effect (on average) counts doubled

    if (targ.backlit(false))
        base_hit += BACKLIGHT_TO_HIT_BONUS * 2;

    // Malus is already negative so must still be ADDED to the base_hit
    if (!nightvision && targ.umbra())
        base_hit += UMBRA_TO_HIT_MALUS * 2;

    return base_hit;
}

/* Determine whether the beam hit or missed the player, and tell them if it
 * missed.
 *
 * @return  true if the beam missed, false if the beam hit the player.
 */
bool bolt::misses_player()
{
    if ((is_explosion || aimed_at_feet)
        && origin_spell != SPELL_CALL_DOWN_LIGHTNING
        && origin_spell != SPELL_MOMENTUM_STRIKE)
    {
        return false;
    }

    int dodge = you.evasion();
    int real_tohit  = hit;

    if (real_tohit != AUTOMATIC_HIT)
        real_tohit = apply_lighting(real_tohit, you);

    const int SH = player_shield_class();
    if ((player_omnireflects() && is_omnireflectable()
         || is_blockable())
        && you.shielded()
        && !you.shield_exhausted()
        && !aimed_at_feet
        && (SH > 0 || you.duration[DUR_DIVINE_SHIELD]))
    {
        bool blocked = false;
        if (hit == AUTOMATIC_HIT)
        {
            // 50% chance of blocking ench-type effects at 10 displayed sh
            blocked = x_chance_in_y(SH, omnireflect_chance_denom(SH));

            dprf(DIAG_BEAM, "%smnireflected: %d/%d chance",
                 blocked ? "O" : "Not o", SH, omnireflect_chance_denom(SH));
        }
        else
        {
            // We use the original to-hit here.
            // (so that effects increasing dodge chance don't increase
            // block...?)
            const int testhit = random2(hit * 130 / 100);

            const int block = you.shield_bonus();

            dprf(DIAG_BEAM, "Beamshield: hit: %d, block %d", testhit, block);
            blocked = testhit < block;
        }

        // Divine shield only blocks conventionally blockable things, even if
        // the player is using the Warlock's Mirror.
        if (blocked || (you.duration[DUR_DIVINE_SHIELD] && is_blockable()))
        {
            const string refl_name = name.empty() &&
                                     origin_spell != SPELL_NO_SPELL ?
                                        spell_title(origin_spell) :
                                        name;

            const item_def *shield = you.shield();
            if (is_reflectable(you))
            {
                if (shield && shield_reflects(*shield))
                {
                    mprf("Your %s blocks the %s... and reflects it back!",
                            shield->name(DESC_PLAIN).c_str(),
                            refl_name.c_str());
                }
                else
                {
                    mprf("You block the %s... and reflect it back!",
                            refl_name.c_str());
                }
                reflect();
            }
            else
            {
                mprf("You block the %s.", name.c_str());
                finish_beam();
            }
            you.shield_block_succeeded(agent());

            // Use up a charge of Divine Shield, if active.
            tso_expend_divine_shield_charge();

            return true;
        }

        // Some training just for the "attempt".
        practise_shield_block(false);
    }

    if (is_enchantment())
        return false;

    if (!self_targeted())
        practise_being_shot_at();

    defer_rand r;

    const int repel = you.missile_repulsion();
    dodge += repel;

    const int hit_margin = you.duration[DUR_AUTODODGE] ? -1000
                            : _test_beam_hit(real_tohit, dodge, r);
    if (hit_margin < 0)
    {
        if (hit_margin > -repel)
        {
            mprf("The %s is repelled.", name.c_str());
            count_action(CACT_DODGE, DODGE_REPEL);
        }
        else
        {
            mprf("The %s misses you.", name.c_str());
            count_action(CACT_DODGE, DODGE_EVASION);
        }
    }
    else
        return false;

    return true;
}

void bolt::affect_player_enchantment(bool resistible)
{
    if (resistible
        && has_saving_throw()
        && you.check_willpower(agent(true), ench_power) > 0)
    {
        // You resisted it.

        // Give a message.
        bool need_msg = true;
        if (thrower != KILL_YOU_MISSILE)
        {
            const monster* mon = monster_by_mid(source_id);
            if (mon && !mon->observable())
            {
                mprf("Something tries to affect you, but you %s.",
                     you.willpower() == WILL_INVULN ? "are unaffected"
                                                   : "resist");
                need_msg = false;
            }
        }
        if (need_msg)
        {
            if (you.willpower() == WILL_INVULN)
                canned_msg(MSG_YOU_UNAFFECTED);
            else
            {
                // the message reflects the level of difficulty resisting.
                const int margin = you.willpower() - ench_power;
                mprf("You%s", you.resist_margin_phrase(margin).c_str());
            }
        }

        // punish the monster if we're a willful demon
        if (you.get_mutation_level(MUT_DEMONIC_WILL))
        {
            monster* mon = monster_by_mid(source_id);
            if (mon && mon->alive())
            {
                const int dam = 3 + random2(1 + div_rand_round(ench_power, 10));
                mprf("Your will lashes out at %s%s",
                     mon->name(DESC_THE).c_str(),
                     attack_strength_punctuation(dam).c_str());
                mon->hurt(&you, dam);
            }
        }

        // You *could* have gotten a free teleportation in the Abyss,
        // but no, you resisted.
        if (flavour == BEAM_TELEPORT && player_in_branch(BRANCH_ABYSS))
            xom_is_stimulated(200);

        extra_range_used += range_used_on_hit();
        return;
    }

    // Never affects the player.
    if (flavour == BEAM_INFESTATION)
        return;

    // You didn't resist it.
    if (animate)
        _ench_animation(effect_known ? real_flavour : BEAM_MAGIC, you, false);

    bool nasty = true, nice = false;

    const bool blame_player = god_cares() && YOU_KILL(thrower);

    switch (flavour)
    {
    case BEAM_HIBERNATION:
    case BEAM_SLEEP:
        you.put_to_sleep(nullptr, (4 + random2avg(8, 3)) * BASELINE_DELAY,
                            flavour == BEAM_HIBERNATION);
        break;

    case BEAM_CORONA:
        you.backlight();
        obvious_effect = true;
        break;

    case BEAM_POLYMORPH:
        obvious_effect = you.polymorph(20 + roll_dice(3, 10));
        break;

    case BEAM_MALMUTATE:
    case BEAM_UNRAVELLED_MAGIC:
        mpr("Strange energies course through your body.");
        you.malmutate(agent(), aux_source.empty() ? get_source_name() :
                      (get_source_name() + "/" + aux_source));
        obvious_effect = true;
        break;

    case BEAM_SLOW:
    case BEAM_SHADOW_TORPOR:
        slow_player(10 + random2(ench_power));
        obvious_effect = true;
        break;

    case BEAM_HASTE:
        haste_player(40 + random2(ench_power));
        did_god_conduct(DID_HASTY, 10, blame_player);
        obvious_effect = true;
        nasty = false;
        nice  = true;
        break;

    case BEAM_HEALING:
        potionlike_effect(POT_HEAL_WOUNDS, ench_power, true);
        obvious_effect = true;
        nasty = false;
        nice  = true;
        break;

    case BEAM_MIGHT:
        potionlike_effect(POT_MIGHT, ench_power);
        obvious_effect = true;
        nasty = false;
        nice  = true;
        break;

    case BEAM_INVISIBILITY:
        potionlike_effect(POT_INVISIBILITY, ench_power);
        obvious_effect = true;
        nasty = false;
        nice  = true;
        break;

    case BEAM_PARALYSIS:
        you.paralyse(agent(), random_range(2, 5));
        obvious_effect = true;
        break;

    case BEAM_PETRIFY:
        you.petrify(agent());
        obvious_effect = true;
        break;

    case BEAM_VEX:
        you.vex(agent(), random_range(3, 6));
        obvious_effect = true;
        break;

    case BEAM_CONFUSION:
        confuse_player(5 + random2(3));
        obvious_effect = true;
        break;

    case BEAM_WEAKNESS:
        // actor::weaken already is randomized- why are we randomizing input?
        you.weaken(agent(), 8 + random2(4));
        obvious_effect = true;
        break;

    case BEAM_TELEPORT:
        you_teleport();

        // An enemy helping you escape while in the Abyss, or an
        // enemy stabilizing a teleport that was about to happen.
        if (!mons_att_wont_attack(attitude) && player_in_branch(BRANCH_ABYSS))
            xom_is_stimulated(200);

        obvious_effect = true;
        break;

    case BEAM_BLINK:
        uncontrolled_blink();
        obvious_effect = true;
        break;

    case BEAM_BLINK_CLOSE:
        blink_other_close(&you, source);
        obvious_effect = true;
        break;

    case BEAM_BECKONING:
        obvious_effect = beckon(you, *this);
        break;

    case BEAM_CHARM:
        mprf(MSGCH_WARN, "Your will is overpowered!");
        confuse_player(5 + random2(3));
        obvious_effect = true;
        break;     // charming - confusion?

    case BEAM_BANISH:
        if (YOU_KILL(thrower))
        {
            mpr("This spell isn't strong enough to banish yourself.");
            break;
        }
        you.banish(agent(), get_source_name());
        obvious_effect = true;
        break;

    case BEAM_PAIN:
    {
        if (aux_source.empty())
            aux_source = "by nerve-wracking pain";

        const int dam = resist_adjust_damage(&you, flavour, damage.roll());
        if (dam)
        {
            mpr("Pain shoots through your body!");
            internal_ouch(dam);
            obvious_effect = true;
        }
        else
            canned_msg(MSG_YOU_UNAFFECTED);
        break;
    }

    case BEAM_AGONY:
        torment_player(agent(), TORMENT_AGONY);
        obvious_effect = true;
        break;

    case BEAM_DISPEL_UNDEAD:
        if (you.undead_state() == US_ALIVE)
        {
            canned_msg(MSG_YOU_UNAFFECTED);
            break;
        }

        mpr("You convulse!");

        if (aux_source.empty())
            aux_source = "by dispel undead";

        internal_ouch(damage.roll());
        obvious_effect = true;
        break;

    case BEAM_MINDBURST:
        mpr("Your mind is blasted!");

        if (aux_source.empty())
            aux_source = "mindburst bolt";

        {
            int amt = damage.roll();
            internal_ouch(amt);

            if (you.has_blood())
                blood_spray(you.pos(), MONS_PLAYER, amt / 5);
        }

        obvious_effect = true;
        break;

    case BEAM_PORKALATOR:
        if (!transform(20 + roll_dice(3, 10), transformation::pig, true))
        {
            mpr("You feel a momentary urge to oink.");
            break;
        }

        you.transform_uncancellable = true;
        obvious_effect = true;
        break;

    case BEAM_BERSERK:
        you.go_berserk(blame_player);
        obvious_effect = true;
        break;

    case BEAM_SENTINEL_MARK:
        you.sentinel_mark();
        obvious_effect = true;
        break;

    case BEAM_DIMENSION_ANCHOR:
        mprf("You feel %sfirmly anchored in space.",
             you.duration[DUR_DIMENSION_ANCHOR] ? "more " : "");
        you.increase_duration(DUR_DIMENSION_ANCHOR, 12 + random2(15), 50);
        if (you.duration[DUR_TELEPORT])
        {
            you.duration[DUR_TELEPORT] = 0;
            mpr("Your teleport is interrupted.");
        }
        you.redraw_evasion = true;
        obvious_effect = true;
        break;

    case BEAM_VULNERABILITY:
        you.strip_willpower(agent(), 12 + random2(18));
        obvious_effect = true;
        break;

    case BEAM_VITRIFY:
        you.vitrify(agent(), random_range(8, 18));
        obvious_effect = true;
        break;

    case BEAM_VITRIFYING_GAZE:
        you.vitrify(agent(), random_range(4, 8));
        obvious_effect = true;
        break;

    case BEAM_MALIGN_OFFERING:
    {
        const int dam = resist_adjust_damage(&you, flavour, damage.roll());
        if (dam)
        {
            _malign_offering_effect(&you, agent(), dam);
            obvious_effect = true;
        }
        else
            canned_msg(MSG_YOU_UNAFFECTED);
        break;
    }

    case BEAM_ROOTS:
    {
        const int turns = 1 + random_range(div_rand_round(ench_power, 20),
                                           div_rand_round(ench_power, 12) + 1);
        if (start_ranged_constriction(*agent(), you, turns, CONSTRICT_ROOTS))
            obvious_effect = true;
        break;
    }

    case BEAM_VILE_CLUTCH:
    {
        const int turns = 3 + random_range(div_rand_round(ench_power, 50),
                                           div_rand_round(ench_power, 35) + 1);
        if (start_ranged_constriction(*agent(), you, turns, CONSTRICT_BVC))
            obvious_effect = true;
        break;
    }

    case BEAM_VAMPIRIC_DRAINING:
    {
        const int dam = resist_adjust_damage(&you, flavour, damage.roll());
        if (dam && actor_is_susceptible_to_vampirism(you))
        {
            _vampiric_draining_effect(you, *agent(), dam);
            obvious_effect = true;
        }
        else
            canned_msg(MSG_YOU_UNAFFECTED);
        break;
    }

    case BEAM_VIRULENCE:
        // Those completely immune cannot be made more susceptible this way
        if (you.res_poison(false) >= 3)
        {
            canned_msg(MSG_YOU_UNAFFECTED);
            break;
        }

        mpr("You feel yourself grow more vulnerable to poison.");
        you.increase_duration(DUR_POISON_VULN, 12 + random2(18), 50);
        obvious_effect = true;
        break;

    case BEAM_AGILITY:
        you.be_agile(ench_power);
        obvious_effect = true;
        nasty = false;
        nice  = true;
        break;

    case BEAM_DIMINISH_SPELLS:
        you.diminish(agent(), 9);
        obvious_effect = true;
        break;

    case BEAM_SAP_MAGIC:
        mprf(MSGCH_WARN, "Your magic feels %stainted.",
             you.duration[DUR_SAP_MAGIC] ? "more " : "");
        flash_tile(you.pos(), GREEN, 120, TILE_BOLT_SAP_MAGIC);
        you.increase_duration(DUR_SAP_MAGIC, random_range(20, 30), 50);
        break;

    case BEAM_TUKIMAS_DANCE:
        cast_tukimas_dance(ench_power, &you);
        obvious_effect = true;
        break;

    case BEAM_RESISTANCE:
        potionlike_effect(POT_RESISTANCE, min(ench_power, 200));
        obvious_effect = true;
        nasty = false;
        nice  = true;
        break;

    case BEAM_UNRAVELLING:
        if (!player_is_debuffable())
            break;

        // If the player is unravelling themselves voluntarily, allow it to work.
        debuff_player(agent() && agent()->is_player());
        _unravelling_explode(*this);
        obvious_effect = true;
        break;

    case BEAM_SOUL_SPLINTER:
        obvious_effect = true;
        if (you.holiness() & (MH_NATURAL | MH_DEMONIC | MH_HOLY))
            make_soul_wisp(*agent(), you);
        else
            canned_msg(MSG_YOU_UNAFFECTED);
        break;

    case BEAM_ILL_OMEN:
        obvious_effect = true;
        if (!you.doom(random_range(16, 30) + ench_power / 20))
            mpr("You feel an ill-omen....");
        break;

    case BEAM_ENFEEBLE:
        obvious_effect = true;
        enfeeble_player(agent(), ench_power);
        break;

    case BEAM_WARP_BODY:
        obvious_effect = true;
        mpr("Your body is warped painfully!");
        if (temp_mutation_count() >= 3)
            you.vitrify(agent(), random_range(12, 22));
        else
            temp_mutate(RANDOM_CORRUPT_MUTATION, "warp body");
        you.hurt(agent(), damage.roll(), BEAM_WARP_BODY, KILLED_BY_BEAM);
        break;

    default:
        // _All_ enchantments should be enumerated here!
        mpr("Software bugs nibble your toes!");
        break;
    }

    if (nasty)
    {
        if (mons_att_wont_attack(attitude))
        {
            friends_hurt++;
            if (source_id == MID_PLAYER)
            {
                // Beam from player rebounded and hit player.
                if (!self_targeted())
                    xom_is_stimulated(200);
            }
            else
            {
                // Beam from an ally or neutral.
                xom_is_stimulated(100);
            }
        }
        else
            foes_hurt++;
    }
    else if (nice)
    {
        if (mons_att_wont_attack(attitude))
            friends_helped++;
        else
        {
            foes_helped++;
            xom_is_stimulated(100);
        }
    }

    handle_enchant_chaining(you.pos());

    // Regardless of effect, we need to know if this is a stopper
    // or not - it seems all of the above are.
    extra_range_used += range_used_on_hit();
}

void bolt::affect_actor(actor *act)
{
    if (act->is_monster())
        affect_monster(act->as_monster());
    else
        affect_player();
}

struct pie_effect
{
    const char* desc;
    function<bool(const actor& def)> valid;
    function<void (actor& def, const bolt &beam)> effect;
    int weight;
};

static const vector<pie_effect> pie_effects = {
    {
        "plum",
        [](const actor &defender) {
            return defender.is_player();
        },
        [](actor &/*defender*/, const bolt &/*beam*/) {
            if (you.duration[DUR_VERTIGO])
                mpr("You feel your light-headedness will last longer.");
            else
                mpr("You feel light-headed.");

            you.increase_duration(DUR_VERTIGO, 10 + random2(11), 50);
        },
        10
    },
    {
        "lemon",
        [](const actor &defender) {
            return defender.is_player() && (you.can_drink()
                                            || you.duration[DUR_NO_POTIONS]); // allow stacking
        },
        [](actor &/*defender*/, const bolt &/*beam*/) {
            if (you.duration[DUR_NO_POTIONS])
                mpr("You feel your inability to drink will last longer.");
            else
                mpr("You feel unable to drink.");

            you.increase_duration(DUR_NO_POTIONS, 10 + random2(11), 50);
        },
        10
    },
    {
        "blueberry",
        nullptr,
        [](actor &defender, const bolt &beam) {
            if (defender.is_monster())
            {
                monster *mons = defender.as_monster();
                silence_monster(*mons, beam.agent(), (4 + random2(7)) * BASELINE_DELAY);
            }
            else
            {
                if (you.duration[DUR_SILENCE])
                    mpr("You feel your silence will last longer.");
                else
                    mpr("An unnatural silence engulfs you.");

                silence_player(4 + random2(7));

            }
        },
        10
    },
    {
        "cherry",
        [](const actor &defender) {
            return defender.is_player() || defender.res_fire() < 3;
        },
        [](actor &defender, const bolt &beam) {
            if (defender.is_monster())
            {
                monster *mons = defender.as_monster();
                simple_monster_message(*mons,
                        " looks more vulnerable to fire.");
                mons->add_ench(mon_enchant(ENCH_FIRE_VULN, beam.agent(),
                                           15 + random2(11) * BASELINE_DELAY));
            }
            else
            {
                if (you.duration[DUR_FIRE_VULN])
                {
                    mpr("You feel your vulnerability to fire will last "
                        "longer.");
                }
                else
                    mpr("Cherry-coloured flames burn away your fire "
                        "resistance!");

                you.increase_duration(DUR_FIRE_VULN, 15 + random2(11), 50);
            }
        },
        6
    },
    {
        "peanut brittle",
        nullptr,
        [](actor &defender, const bolt &beam) {
            defender.vitrify(beam.agent(), random_range(16, 36));
        },
        4
    },
    {
        "glitter",
        [](const actor &defender) {
            return !defender.res_blind();
        },
        [](actor &defender, const bolt &beam) {
            if (defender.is_player())
                blind_player(random_range(16, 36), ETC_GLITTER);
            else
            {
                defender.as_monster()->add_ench(mon_enchant(ENCH_BLIND, beam.agent(),
                                                random_range(12, 18) * BASELINE_DELAY));
            }
        },
        5
    },
};

static pie_effect _random_pie_effect(const actor &defender)
{
    vector<pair<const pie_effect&, int>> weights;
    for (const pie_effect &effect : pie_effects)
        if (!effect.valid || effect.valid(defender))
            weights.push_back({effect, effect.weight});

    ASSERT(!weights.empty());

    return *random_choose_weighted(weights);
}

void bolt::affect_player()
{
    hit_count[MID_PLAYER]++;

    if (ignores_player() || !could_harm(agent(), &you, !is_tracer()))
        return;

    // Explosions only have an effect during their explosion phase.
    // Special cases can be handled here.
    if (is_explosion && !in_explosion_phase)
    {
        // Trigger the explosion.
        finish_beam();
        return;
    }

    if (is_tracer())
    {
        tracer_affect_player();
        return;
    }

    // Trigger an interrupt, so travel will stop on misses which
    // generate smoke.
    if (!YOU_KILL(thrower))
    {
        if (agent() && agent()->is_monster())
        {
            interrupt_activity(activity_interrupt::monster_attacks,
                               agent()->as_monster());
        }
        else
            interrupt_activity(activity_interrupt::monster_attacks);
    }

    if (ranged_atk)
    {
        do_ranged_attack(you);
        return;
    }

    // Visible beams reveal invisible monsters; otherwise animations confer
    // an information advantage for sighted players
    if (visible() && agent() && agent()->is_monster())
    {
        monster* mons = agent()->as_monster();
        mons->revealed_this_turn = true;
        mons->revealed_at_pos = mons->pos();
    }

    if (misses_player())
        return;

    if (!is_explosion && !noise_generated)
    {
        heard = noisy(loudness, pos(), source_id) || heard;
        noise_generated = true;
    }

    const bool engulfs = is_explosion || is_big_cloud();

    if (is_enchantment())
    {
        if (real_flavour == BEAM_RANDOM)
        {
            if (hit_verb.empty())
                hit_verb = engulfs ? "engulfs" : "hits";
            mprf("The %s %s %s!", name.c_str(), hit_verb.c_str(),
                you.hp > 0 ? "you" : "your lifeless body");
        }

        affect_player_enchantment();
        return;
    }

    msg_generated = true;

    // FIXME: Lots of duplicated code here (compare handling of
    // monsters)
    int pre_ac_dam = 0;

    // Roll the damage.
    pre_ac_dam += damage.roll();
    int pre_res_dam = apply_AC(&you, pre_ac_dam);

#ifdef DEBUG_DIAGNOSTICS
    dprf(DIAG_BEAM, "Player damage: before AC=%d; after AC=%d",
                    pre_ac_dam, pre_res_dam);
#endif

    practise_being_shot();

    bool was_affected = false;
    int  old_hp       = you.hp;

    pre_res_dam = max(0, pre_res_dam);

    // If the beam is of the MMISSILE type (Earth magic) we might bleed on the
    // floor.
    if (!engulfs && flavour == BEAM_MMISSILE)
    {
        // assumes DVORP_PIERCING, factor: 0.5
        int blood = min(you.hp, pre_res_dam / 2);
        bleed_onto_floor(you.pos(), MONS_PLAYER, blood, true);
    }

    // Apply resistances to damage, but don't print "You resist" messages yet
    int final_dam = check_your_resists(pre_res_dam, flavour, "", this, false);

    // Tell the player the beam hit
    if (hit_verb.empty())
        hit_verb = engulfs ? "engulfs" : "hits";

    if (!is_enchantment()
        && !(damage.num == 0 && is_big_cloud()))
    {
        mprf("The %s %s %s%s%s%s", name.c_str(), hit_verb.c_str(),
             you.hp > 0 ? "you" : "your lifeless body",
             real_flavour != BEAM_CHAOS ? ""
                    : make_stringf(" with %s", _beam_type_name(flavour).c_str()).c_str(),
             final_dam ? "" : " but does no damage",
             attack_strength_punctuation(final_dam).c_str());
    }

    // Now print the messages associated with checking resistances, so that
    // these come after the beam actually hitting.
    // Note that this must be called with the pre-resistance damage, so that
    // poison effects etc work properly.
    check_your_resists(pre_res_dam, flavour, "", this, true);

    if (real_flavour == BEAM_CHAOS && coinflip())
        chaos_affects_actor(&you, agent());

    if (flavour == BEAM_STUN_BOLT
        && !you.duration[DUR_PARALYSIS]
        && (you.res_elec() <= 0 || coinflip()))
    {
        const bool was_parad = you.duration[DUR_PARALYSIS];
        you.paralyse(agent(), 1);
        was_affected = (!was_parad && you.duration[DUR_PARALYSIS]) || was_affected;

    }

    if (flavour == BEAM_LIGHT && you.res_blind() <= 1)
        blind_player(random_range(7, 12), WHITE);

    if (flavour == BEAM_MIASMA && final_dam > 0)
        was_affected = miasma_player(agent(), name);

    if (flavour == BEAM_DESTRUCTION) // MINDBURST already handled
        blood_spray(you.pos(), MONS_PLAYER, final_dam / 5);

    // Confusion effect for spore explosions
    if (flavour == BEAM_SPORE && final_dam
        && !(you.holiness() & MH_UNDEAD)
        && !you.is_unbreathing())
    {
        confuse_player(2 + random2(3));
    }

    if (flavour == BEAM_UNRAVELLED_MAGIC)
        affect_player_enchantment();

    // Sticky flame.
    if (origin_spell == SPELL_STICKY_FLAME
        || flavour == BEAM_STICKY_FLAME)
    {
        // ench_power here is equal to 12 * caster HD for pyre arrow and
        // 8 * caster HD for sticky flame (mostly since player ghosts tend to
        // have higher HD)
        const int intensity = 2 + ench_power / 14;

        if (!player_res_sticky_flame())
        {
            sticky_flame_player(intensity, random_range(11, 21),
                                get_source_name(), aux_source);
            was_affected = true;
        }
    }

    // need to trigger qaz resists after reducing damage from ac/resists.
    //    for some reason, strength 2 is the standard. This leads to qaz's
    //    resists triggering 2 in 5 times at max piety.
    //    perhaps this should scale with damage?
    // what to do for hybrid damage?  E.g. bolt of magma, icicle, poison arrow?
    // Right now just ignore the physical component.
    // what about acid?
    you.expose_to_element(flavour, 2, agent(), false);

    // Manticore spikes
    if (origin_spell == SPELL_THROW_BARBS && final_dam > 0)
        barb_player(random_range(4, 8), 4);

    if (origin_spell == SPELL_GRAVE_CLAW && !you.unrand_equipped(UNRAND_SLICK_SLIPPERS))
    {
        mpr("You are skewered in place!");
        you.increase_duration(DUR_NO_MOMENTUM, random_range(2, 4));
    }

    if (flavour == BEAM_ENSNARE)
        was_affected = you.trap_in_web() || was_affected;

    if (origin_spell == SPELL_QUICKSILVER_BOLT)
        debuff_player();

    if (origin_spell == SPELL_NULLIFYING_BREATH)
    {
        debuff_player();
        you.drain_magic(agent(), ench_power * 8);
    }

    if (origin_spell == SPELL_DOOM_BOLT)
        you.doom(random_range(15, 25));

    dprf(DIAG_BEAM, "Damage: %d", final_dam);

    if (final_dam > 0 || old_hp < you.hp || was_affected)
    {
        if (mons_att_wont_attack(attitude))
        {
            friends_hurt++;

            // Beam from player rebounded and hit player.
            // Xom's amusement at the player's being damaged is handled
            // elsewhere.
            if (source_id == MID_PLAYER)
            {
                if (!self_targeted())
                    xom_is_stimulated(200);
            }
            else if (was_affected)
                xom_is_stimulated(100);
        }
        else
            foes_hurt++;
    }

    extra_range_used += range_used_on_hit();

    internal_ouch(final_dam);

    if (!you.alive())
        return;

    // Acid. (Apply this afterward, to avoid bad message ordering.)
    if (origin_spell == SPELL_CORROSIVE_BOLT && !one_chance_in(4))
        you.corrode(agent(), "the acid", 6);
    else if (flavour == BEAM_ACID && coinflip())
        you.corrode(agent());

    if (flavour == BEAM_CRYSTALLISING && !one_chance_in(4))
        you.vitrify(agent(), random_range(8, 18));

    if (flavour == BEAM_DRAIN_MAGIC)
        you.drain_magic(agent(), ench_power);

    if (origin_spell == SPELL_SOJOURNING_BOLT
        && final_dam > 0 && x_chance_in_y(2, 3))
    {
        you_teleport(true, agent(true) ? agent(true)->mid : MID_NOBODY);
    }

    if (origin_spell == SPELL_THROW_PIE && final_dam > 0)
    {
        const pie_effect effect = _random_pie_effect(you);
        mprf("%s!", effect.desc);
        effect.effect(you, *this);
    }

    knockback_actor(&you, final_dam);
    pull_actor(&you, final_dam);

    if (origin_spell == SPELL_FLASH_FREEZE
        || origin_spell == SPELL_CREEPING_FROST
        || name == "blast of ice"
        || origin_spell == SPELL_HOARFROST_BULLET
        || origin_spell == SPELL_GLACIATE && !is_explosion)
    {
        if (!you.duration[DUR_FROZEN])
        {
            mprf(MSGCH_WARN, "You are encased in ice.");
            you.duration[DUR_FROZEN] = (random_range(3, 5)) * BASELINE_DELAY;
        }
    }

    if (you.form == transformation::aqua
        && is_fiery() && x_chance_in_y(final_dam, you.hp_max * 2 / 3))
    {
        const int pow = div_rand_round(final_dam * 100 / you.hp_max, 10);
        mpr("A part of your body evaporates into steam!");
        big_cloud(CLOUD_STEAM, monster_by_mid(MID_YOU_FAULTLESS), you.pos(),
                  random_range(1, 4) + pow, random_range(2, 7));
    }
}

bool bolt::ignores_player() const
{
    // Digging -- don't care.
    if (flavour == BEAM_DIGGING)
        return true;

    if (origin_spell == SPELL_COMBUSTION_BREATH
        || origin_spell == SPELL_NULLIFYING_BREATH
        || origin_spell == SPELL_RIMEBLIGHT
        || origin_spell == SPELL_SHADOW_PRISM
        || origin_spell == SPELL_SHADOW_SHOT)
    {
        return true;
    }

    // XXX: Mostly to stop the targeter being weird with trying to keep you
    //      out of splash damage.
    if (origin_spell == SPELL_MERCURY_ARROW && agent() && agent()->is_player())
        return true;

    if (origin_spell == SPELL_HOARFROST_BULLET && is_explosion
        && agent() && agent()->wont_attack())
    {
        return true;
    }

    if (shoot_through_actor(agent(), &you))
        return true;

    if (source_id == MID_PLAYER_SHADOW_DUMMY)
        return true;

    if (flavour == BEAM_ROOTS || flavour == BEAM_VILE_CLUTCH)
    {
        return agent()->wont_attack()
               || !agent()->can_constrict(you, flavour == BEAM_ROOTS ? CONSTRICT_ROOTS
                                                                     : CONSTRICT_BVC);
    }

    if (flavour == BEAM_QAZLAL || flavour == BEAM_HAEMOCLASM)
        return true;

    // The player is immune to their own Destruction (both from Carnage / Annihilation
    // explosions, but also reflected lightning bolts or shots from hellmouths)
    if (origin_spell == SPELL_UNLEASH_DESTRUCTION
        && (!agent() || agent()->wont_attack()))
    {
        return true;
    }

    if ((origin_spell == SPELL_PERCUSSIVE_TEMPERING
         || origin_spell == SPELL_FORTRESS_BLAST)
        && attitude == ATT_FRIENDLY)
    {
        return true;
    }

    return false;
}

int bolt::apply_AC(const actor *victim, int hurted)
{
    // Apply automatic AC rules if ac_rule was not manually specified.
    if (ac_rule == ac_type::normal)
    {
        if (flavour == BEAM_DAMNATION)
            ac_rule = ac_type::none;
        else if (get_beam_resist_type(flavour) == BEAM_ELECTRICITY)
            ac_rule = ac_type::half;
        else if (flavour == BEAM_FRAG)
            ac_rule = ac_type::triple;
    }

    // beams don't obey GDR -> max_damage is 0
    return victim->apply_ac(hurted, 0, ac_rule, !is_tracer());
}

void bolt::update_hurt_or_helped(monster* mon)
{
    if (!mons_atts_aligned(attitude, mons_attitude(*mon)))
    {
        if (nasty_to(mon))
            foes_hurt++;
        else if (nice_to(monster_info(mon)))
        {
            foes_helped++;
            // Accidentally helped a foe.
            if (!is_tracer() && !effect_known && mons_is_threatening(*mon))
            {
                const int interest =
                    (flavour == BEAM_INVISIBILITY && can_see_invis) ? 25 : 100;
                xom_is_stimulated(interest);
            }
        }
    }
    else
    {
        if (nasty_to(mon))
        {
            friends_hurt++;

            // Harmful beam from this monster rebounded and hit the monster.
            if (!is_tracer() && mon->mid == source_id)
                xom_is_stimulated(100);
        }
        else if (nice_to(monster_info(mon)))
            friends_helped++;
    }
}

void bolt::tracer_enchantment_affect_monster(monster* mon)
{
    bool has_friendly_past_target = check_for_friendly_past_target(mon);
    if (has_friendly_past_target)
        return;

    // Only count tracers as hitting creatures they could potentially affect
    if (ench_flavour_affects_monster(agent(), flavour, mon, true)
        && !(has_saving_throw() && mons_invuln_will(*mon)))
    {
        // Update friend or foe encountered.
        bool friendly_fire = mons_atts_aligned(attitude, mons_attitude(*mon));
        int power = mon->get_experience_level();
        tracer->actor_affected(friendly_fire, power);
    }

    tracer->monster_hit(*this, *mon);
    extra_range_used += range_used_on_hit();
}

// Return false if we should skip handling this monster.
bool bolt::determine_damage(monster* mon, int& preac, int& postac, int& final)
{
    preac = postac = final = 0;

    // [ds] Changed how tracers determined damage: the old tracer
    // model took the average damage potential, subtracted the average
    // AC damage reduction and called that the average damage output.
    // This could easily predict an average damage output of 0 for
    // high AC monsters, with the result that monsters often didn't
    // bother using ranged attacks at high AC targets.
    //
    // The new model rounds up average damage at every stage, so it
    // will predict a damage output of 1 even if the average damage
    // expected is much closer to 0. This will allow monsters to use
    // ranged attacks vs high AC targets.
      // [1KB] What ds' code actually does is taking the max damage minus
      // average AC. This does work well, even using no AC would. An
      // attack that _usually_ does no damage but can possibly do some means
      // we'll ultimately get it through. And monsters with weak ranged
      // almost always would do no better in melee.
    //
    // This is not an entirely beneficial change; the old tracer
    // damage system would make monsters with weak ranged attacks
    // close in to their foes, while the new system will make it more
    // likely that such monsters will hang back and make ineffective
    // ranged attacks. Thus the new tracer damage calculation will
    // hurt monsters with low-damage ranged attacks and high-damage
    // melee attacks. I judge this an acceptable compromise (for now).
    //
    const int preac_max_damage = damage.num * damage.size;

    // preac: damage before AC modifier
    // postac: damage after AC modifier
    // final: damage after AC and resists
    // All these are invalid if we return false.

    if (is_tracer())
    {
        // Was mean between min and max;
        preac = preac_max_damage;
    }
    else
        preac = damage.roll();

    int tracer_postac_max = preac_max_damage;

    postac = apply_AC(mon, preac);

    if (is_tracer())
    {
        postac = div_round_up(tracer_postac_max, 2);

        const int adjusted_postac_max =
            mons_adjust_flavoured(mon, *this, tracer_postac_max, false);

        final = div_round_up(adjusted_postac_max, 2);
    }
    else
    {
        postac = max(0, postac);
        // Don't do side effects (beam might miss or be a tracer).
        final = mons_adjust_flavoured(mon, *this, postac, false);
    }

    // Sanity check. Importantly for
    // tracer_nonenchantment_affect_monster, final > 0
    // implies preac > 0.
    ASSERT(0 <= postac);
    ASSERT(postac <= preac);
    ASSERT(0 <= final);
    ASSERT(preac > 0 || final == 0);

    return true;
}

bool bolt::check_for_friendly_past_target(monster* mon)
{
    // If prompts for overshooting the target are disabled, instead
    // just let the caller know that there was something there. They
    // should be responsible and keep the player from shooting friends.
    // (We skip this for explosions, since stopping at our target is not
    // guaranteed to spare allies behind the target)

    if (!passed_target || overshoot_prompt || is_explosion
        || !you.can_see(*mon) || !tracer->should_stop() || is_harmless(mon))
    {
        return false;
    }

    string adj, suffix;
    bool penance;
    bool should_stop_at_target = bad_attack(mon, adj, suffix, penance, target);

    if (should_stop_at_target && special_explosion)
    {
        targeting_tracer special_tracer;
        bolt special_explosion_copy = *special_explosion;
        special_explosion_copy.tracer = &special_tracer;
        special_explosion_copy.in_explosion_phase = false;
        special_explosion_copy.target = target;
        special_explosion_copy.refine_for_explosion();
        special_explosion_copy.explode();

        // Only stop at our target if it would stop us hitting allies.
        should_stop_at_target = special_tracer.friend_info.count == 0;
    }

    if (should_stop_at_target)
    {
        finish_beam();
        friendly_past_target = true;
        use_target_as_pos = true;
    }

    return should_stop_at_target;
}

// Whether or not this non-enchantment effect would have a relevant non-damage
// side effect when used on a given monster.
//
// XXX: This function is a stop-gap! It is knowingly incomplete, but addresses
//      the immediate problem of monsters refusing to use certain spells on
//      other monsters, even though they would be useful.
// TODO: Expand and unify this with actually applying beam side-effects
bool bolt::has_relevant_side_effect(monster* mon)
{
    if (flavour == BEAM_STICKY_FLAME
        && !mon->res_sticky_flame() && mon->res_fire() < 3)
    {
        return true;
    }
    else if (flavour == BEAM_ENSNARE || flavour == BEAM_LIGHT)
        return true;

    const cloud_type cloud = flavour == BEAM_MEPHITIC ? CLOUD_MEPHITIC
                                                      : get_cloud_type();
    if (cloud != CLOUD_NONE && !actor_cloud_immune(*mon, cloud))
        return true;

    return false;
}

void bolt::tracer_nonenchantment_affect_monster(monster* mon)
{
    bool has_friendly_past_target = check_for_friendly_past_target(mon);
    if (has_friendly_past_target)
        return;

    // Special explosions (current exploding missiles) aren't
    // auto-hit, so we need to explode them at every possible
    // end-point?
    if (special_explosion)
    {
        bolt orig = *special_explosion;
        affect_endpoint();
        *special_explosion = orig;
    }

    int preac = 0, post = 0, final = 0;

    bool side_effect = has_relevant_side_effect(mon);

    // The projectile applying sticky flame often does no damage, but this
    // doesn't mean it's harmless.
    if (!determine_damage(mon, preac, post, final) && !side_effect)
        return;

    // Check only if actual damage and the monster is worth caring about.
    // Living spells do count as threats, but are fine being collateral damage.
    if ((final > 0 || side_effect)
        && (mons_is_threatening(*mon) || mons_class_is_test(mon->type))
        && mon->type != MONS_LIVING_SPELL)
    {
        ASSERT(side_effect || preac > 0);

        // Add some 'fake damage' for a relevant side-effect, so tracer power
        // will actually see it.
        // XXX: This is ugly and numerically arbitrary...
        if (side_effect)
        {
            final += 10;
            preac += 10;
        }

        // Monster could be hurt somewhat, but only apply the
        // monster's power based on how badly it is affected.
        // For example, if a fire giant (power 16) threw a
        // fireball at another fire giant, and it only took
        // 1/3 damage, then power of 5 would be applied.

        bool friendly_fire;
        int power;
        if (!mons_atts_aligned(attitude, mons_attitude(*mon)))
        {
            friendly_fire = false;
            power = 2 * final * mon->get_experience_level() / preac;
        }
        else
        {
            friendly_fire = true;
            power = 0;
            // Discourage summoned monsters firing on their summoner.
            const monster* mon_source = agent() ? agent()->as_monster() : nullptr;
            if (mon_source && mon_source->summoner == mon->mid)
                power = 100;
            // Marionettes will avoid harming other 'allies', but are
            // deliberately reckless about themselves.
            else if (!(mon_source == mon && mon_source->attitude == ATT_MARIONETTE))
                power = 2 * final * mon->get_experience_level() / preac;
        }
        tracer->actor_affected(friendly_fire, power);
    }

    // Check for bad targets in case the user wants to cancel
    tracer->monster_hit(*this, *mon);
    // Either way, we could hit this monster, so update range used.
    extra_range_used += range_used_on_hit();

    if (origin_spell == SPELL_COMBUSTION_BREATH && !in_explosion_phase)
        _combustion_breath_explode(this, mon->pos());
}

void bolt::tracer_affect_monster(monster* mon)
{
    // Ignore unseen monsters.
    if ((agent() && !agent()->can_see(*mon))
        || !cell_see_cell(source, mon->pos(), LOS_NO_TRANS))
    {
        return;
    }

    if (flavour == BEAM_UNRAVELLING && monster_can_be_unravelled(*mon))
        is_explosion = true;

    // Trigger explosion on exploding beams.
    if (is_explosion && !in_explosion_phase)
    {
        finish_beam();
        return;
    }

    if (is_enchantment())
        tracer_enchantment_affect_monster(mon);
    else
        tracer_nonenchantment_affect_monster(mon);
}

void bolt::enchantment_affect_monster(monster* mon)
{
    god_conduct_trigger conducts[3];

    bool hit_woke_orc = false;

    if (nasty_to(mon))
    {
        if (BLAME_KILL(thrower))
        {
            set_attack_conducts(conducts, *mon, you.can_see(*mon));

            if (have_passive(passive_t::convert_orcs)
                && mons_genus(mon->type) == MONS_ORC
                && mon->asleep() && you.see_cell(mon->pos()))
            {
                hit_woke_orc = true;
            }
            if (flavour != BEAM_HIBERNATION)
                you.pet_target = mon->mindex();
        }
    }

    // Doing this here so that the player gets to see monsters
    // "flicker and vanish" when turning invisible....
    if (animate)
    {
        _ench_animation(effect_known ? real_flavour
                                     : BEAM_MAGIC,
                        *mon, effect_known);
    }

    // Try to hit the monster with the enchantment. The behaviour_event above
    // may have caused a pacified monster to leave the level, so only try to
    // enchant it if it's still here. If the monster did leave the level, set
    // obvious_effect so we don't get "Nothing appears to happen".
    int res_margin = 0;
    const mon_resist_type ench_result = mon->alive()
                                      ? try_enchant_monster(mon, res_margin)
                                      : (obvious_effect = true, MON_OTHER);

    if (mon->alive())           // Aftereffects.
    {
        // Message or record the success/failure.
        switch (ench_result)
        {
        case MON_RESIST:
            if (simple_monster_message(*mon,
                                   mon->resist_margin_phrase(res_margin).c_str()))
            {
                msg_generated = true;
                // Hack: assume that BEAM_BANISH comes from Lugonu's Banishment
                // and hence causes malmutation on resist.
                if (real_flavour == BEAM_BANISH && agent() && agent()->is_player())
                    mon->malmutate(agent());
            }
            break;
        case MON_UNAFFECTED:
            if (simple_monster_message(*mon, " is unaffected."))
                msg_generated = true;
            break;
        case MON_AFFECTED:
        case MON_OTHER:         // Should this really be here?
            update_hurt_or_helped(mon);
            break;
        }

        if (hit_woke_orc)
            beogh_follower_convert(mon, true);
    }

    handle_enchant_chaining(mon->pos());

    extra_range_used += range_used_on_hit();

    // Nasty enchantments will annoy the monster, and are considered
    // naughty (even if a monster resisted).
    if (mon && mon->alive() && nasty_to(mon))
    {
        // Don't anger allies for the actions of confused enemies.
        const actor* to_blame = agent();
        if (thrower == KILL_YOU_CONF)
            to_blame = actor_by_mid(source_id);
        behaviour_event(mon, ME_ANNOY, to_blame);
    }
    else
        behaviour_event(mon, ME_ALERT, agent());
}

static void _add_chain_candidates(const bolt& beam, coord_def pos,
                                          set<coord_def> &candidates)
{
    for (adjacent_iterator ai(pos); ai; ++ai)
    {
        if (!cell_see_cell(beam.source, *ai, LOS_NO_TRANS))
            continue;

        actor* act = actor_at(*ai);

        // Ignore friendlies, firewood, and things we can fire through.
        if (!act
            || mons_aligned(beam.agent(), act)
            || act->is_peripheral()
            || shoot_through_actor(beam.agent(), act))
        {
            continue;
        }

        candidates.insert(*ai);
    }
}

void fill_chain_targets(const bolt& beam, coord_def centre,
                        vector<coord_def> &targs, bool random)
{
    set<coord_def> candidates;
    _add_chain_candidates(beam, centre, candidates);
    if (candidates.empty())
        return;

    const size_t MAX_CHAIN_BOUNCES = 2;
    if (candidates.size() >= MAX_CHAIN_BOUNCES)
    {
        for (coord_def candidate : candidates)
            targs.push_back(candidate);
        if (!random)
            return;

        shuffle_array(targs);
        targs.resize(MAX_CHAIN_BOUNCES);
        return;
    }

    // OK. Can we bounce further out?
    set<coord_def> scs;
    for (coord_def candidate : candidates) // only runs once
    {
        targs.push_back(candidate);
        // Ensure adjacent targets show up before later-ring ones.
        _add_chain_candidates(beam, candidate, scs);
    }
    if (random)
        shuffle_array(targs);

    scs.erase(centre);
    if (scs.empty())
        return;

    // OK, we found some later bounces.
    if (random)
    {
        vector<coord_def> subcand_array(scs.begin(), scs.end());
        // Assumes MAX_CHAIN_BOUNCES = 2. Sorry!
        targs.push_back(*random_iterator(subcand_array));
        return;
    }

    for (coord_def sc : scs)
        targs.push_back(sc);
}

void bolt::handle_enchant_chaining(coord_def centre)
{
    // Handle ray bounces
    if (!(origin_spell == SPELL_PETRIFY || origin_spell == SPELL_RIMEBLIGHT)
        || enchant_chaining_done)
    {
        return;
    }

    // Prevent the following calls to affect_actor from recursing into here
    enchant_chaining_done = true;

    vector<coord_def> chain_targs;
    fill_chain_targets(*this, centre, chain_targs, true);
    if (chain_targs.empty())
        return;

    // Bounces are at 2/3 power
    unwind_var<int> ep(ench_power, ench_power * 2 / 3);
    for (coord_def c : chain_targs)
        affect_actor(actor_at(c));
}

static void _print_pre_death_message(const monster &mon, bool goldify,
                                     spell_type origin_spell)
{
    if (mon.is_insubstantial()
        || (origin_spell != SPELL_GLACIATE && origin_spell != SPELL_GLACIAL_BREATH))
    {
        return;
    }
    if (goldify)
        simple_monster_message(mon, " shatters and turns to gold!");
    else
        simple_monster_message(mon, " is frozen into a solid block of ice!");
}

void bolt::kill_monster(monster &mon)
{
    // Preserve name of the source monster if it winds up killing
    // itself.
    if (mon.mid == source_id && source_name.empty())
        source_name = mon.name(DESC_A, true);

    const coord_def where = mon.pos();
    const monster_type species = mons_species(mons_base_type(mon));
    const bool goldify = mons_will_goldify(mon);

    _print_pre_death_message(mon, goldify, origin_spell);

    int kindex = actor_to_death_source(agent());

    killer_type ref_killer = thrower;
    if (!YOU_KILL(thrower) && reflector == MID_PLAYER)
    {
        ref_killer = KILL_YOU_MISSILE;
        kindex = YOU_FAULTLESS;
    }

    if (flavour == BEAM_BLOODRITE)
        mon.props[MAKHLEB_BLOODRITE_KILL_KEY] = true;

    item_def *corpse = monster_die(mon, ref_killer, kindex);

    if (origin_spell != SPELL_GLACIATE && origin_spell != SPELL_GLACIAL_BREATH
        || goldify)
    {
        return;
    }

    if (corpse)
        destroy_item(corpse->index());
    if (monster *pillar = create_monster(
                                         mgen_data(MONS_BLOCK_OF_ICE,
                                                   BEH_HOSTILE,
                                                   where,
                                                   MHITNOT,
                                                   MG_FORCE_PLACE).set_base(species),
                                         false))
    {
        // Make glacial breath ice blocks more sturdy, since they're supposed
        // to make useful obstacles
        if (origin_spell == SPELL_GLACIAL_BREATH)
        {
            pillar->max_hit_points = 15 + div_rand_round(ench_power * 5, 2);
            pillar->hit_points = pillar->max_hit_points;
        }

        const int time_left = random_range(7, 17) * BASELINE_DELAY;
        mon_enchant temp_en(ENCH_SLOWLY_DYING, nullptr, time_left);
        pillar->add_ench(temp_en);
    }
}

void bolt::monster_post_hit(monster* mon, int dmg)
{
    // Suppress the message for tremorstones.
    if (YOU_KILL(thrower) && you.see_cell(mon->pos())
        && name != "burst of rock shards")
    {
        print_wounds(*mon);
    }

    // Don't annoy friendlies or good neutrals if the player's beam
    // did no damage. Hostiles will still take umbrage.
    if (dmg > 0 || !mon->wont_attack() || !YOU_KILL(thrower))
    {
        const actor* to_blame = agent();
        if (thrower == KILL_YOU_CONF)
            to_blame = actor_by_mid(source_id);
        behaviour_event(mon, ME_ANNOY, to_blame);

        // behaviour_event can make a monster leave the level or vanish.
        if (!mon->alive())
            return;
    }

    if (YOU_KILL(thrower) && !mon->wont_attack() && !mon->is_firewood())
        you.pet_target = mon->mindex();

    if (real_flavour == BEAM_CHAOS && coinflip())
        chaos_affects_actor(mon, agent());

    // We check player Sticky Flame by name and other effects by flavour, since
    // the player spell has impact damage that the others do not, and thus is
    // BEAM_FIRE instead.
    //
    // Possibly this should be refactored.
    if (origin_spell == SPELL_STICKY_FLAME)
    {
        const int dur = 3 + random_range(div_rand_round(ench_power, 20),
                                         max(1, div_rand_round(ench_power, 10)));
        sticky_flame_monster(mon, agent(), dur);
    }
    else if (flavour == BEAM_STICKY_FLAME)
    {
        // Current numbers of monster sticky flame versus other monsters are
        // more arbitrary.
        sticky_flame_monster(mon, agent(), 3 + random2(ench_power / 20));
    }

    if (origin_spell == SPELL_QUICKSILVER_BOLT)
        debuff_monster(*mon);

    if (origin_spell == SPELL_NULLIFYING_BREATH)
    {
        debuff_monster(*mon);
        if (mon->antimagic_susceptible())
        {
            const int dur = (5 + random_range(div_rand_round(ench_power, 2),
                                             div_rand_round(ench_power * 3, 4) + 1))
                            * BASELINE_DELAY;
            mon->add_ench(mon_enchant(ENCH_ANTIMAGIC, agent(), dur));
        }
    }

    if (origin_spell == SPELL_SOJOURNING_BOLT
        && x_chance_in_y(2, 3) && !(mon->no_tele()))
    {
        monster_teleport(mon, false, false, false, agent());
    }

    if (flavour == BEAM_CRYSTALLISING && !one_chance_in(4))
        mon->vitrify(agent(), random_range(8, 18));

    if (flavour == BEAM_DRAIN_MAGIC)
        mon->drain_magic(agent(), ench_power);

    if (dmg)
        beogh_follower_convert(mon, true);

    knockback_actor(mon, dmg);
    pull_actor(mon, dmg);

    if (origin_spell == SPELL_COMBUSTION_BREATH && !in_explosion_phase)
        _combustion_breath_explode(this, mon->pos());

    if (origin_spell == SPELL_FLASH_FREEZE
             || origin_spell == SPELL_HOARFROST_BULLET
             || name == "blast of ice"
             || origin_spell == SPELL_GLACIATE && !is_explosion
             || origin_spell == SPELL_UNLEASH_DESTRUCTION
                && flavour == BEAM_COLD
                && you.has_mutation(MUT_MAKHLEB_DESTRUCTION_COC))
    {
        if (!mon->has_ench(ENCH_FROZEN))
        {
            simple_monster_message(*mon, " is flash-frozen.");
            mon->add_ench(mon_enchant(ENCH_FROZEN, agent()));
        }
    }

    if (origin_spell == SPELL_UNLEASH_DESTRUCTION
        && flavour == BEAM_FIRE
        && you.has_mutation(MUT_MAKHLEB_DESTRUCTION_GEH))
    {
        const int dur = 3 + div_rand_round(dmg, 3);
        if (!mon->has_ench(ENCH_FIRE_VULN))
        {
            mprf("%s fire resistance burns away.",
                mon->name(DESC_ITS).c_str());
            mon->add_ench(mon_enchant(ENCH_FIRE_VULN, agent(),
                                      dur * BASELINE_DELAY));
        }
    }

    if (origin_spell == SPELL_THROW_BARBS && dmg > 0
        && !(mon->is_insubstantial()))
    {
        mon->add_ench(mon_enchant(ENCH_BARBS, agent(),
                                  random_range(5, 7) * BASELINE_DELAY, 1));
    }

    if (origin_spell == SPELL_THROW_PIE && dmg > 0)
    {
        const pie_effect effect = _random_pie_effect(*mon);
        if (you.see_cell(mon->pos()))
            mprf("%s!", effect.desc);
        effect.effect(*mon, *this);
    }

    if (flavour == BEAM_STUN_BOLT
        && !mon->has_ench(ENCH_PARALYSIS)
        && mon->res_elec() <= 0
        && !mon->stasis())
    {
        simple_monster_message(*mon, " is paralysed.");
        mon->add_ench(mon_enchant(ENCH_PARALYSIS, agent(), BASELINE_DELAY));
    }

    if (flavour == BEAM_LIGHT
        && mon->res_blind() <= 1
        && !mon->has_ench(ENCH_BLIND))
    {
        const int dur = max(1, div_rand_round(54, mon->get_hit_dice())) * BASELINE_DELAY;
        auto ench = mon_enchant(ENCH_BLIND, agent(), random_range(dur, dur * 2));
        if (mon->add_ench(ench))
            simple_monster_message(*mon, " is blinded.");
    }

    if (origin_spell == SPELL_HURL_TORCHLIGHT && mon->holiness() & MH_UNDEAD
        && agent() && mons_aligned(mon, agent()))
    {
        int dur = random_range(2 + agent()->skill_rdiv(SK_INVOCATIONS, 1, 5),
                               4 + agent()->skill_rdiv(SK_INVOCATIONS, 1, 3))
                               * BASELINE_DELAY;
        mon->add_ench(mon_enchant(ENCH_MIGHT, agent(), dur));
        mon->speed_increment += 10;
        simple_monster_message(*mon, " is empowered.");
    }

    if (origin_spell == SPELL_GRAVE_CLAW && !mon->has_ench(ENCH_BOUND))
    {
        simple_monster_message(*mon, " is pinned in place!");
        mon->add_ench(mon_enchant(ENCH_BOUND, agent(), random_range(2, 4) * BASELINE_DELAY));
    }

    if (origin_spell == SPELL_KINETIC_GRAPNEL && dmg > 0)
        mon->add_ench(mon_enchant(ENCH_KINETIC_GRAPNEL, agent(), random_range(30, 50)));

    // Watery Grave
    if (name == "grasping water" && !mon->is_unbreathing())
        _waterlog_mon(*mon, ench_power);

    if (origin_spell == SPELL_DOOM_BOLT)
        mon->doom(random_range(15, 25));
}

static int _knockback_dist(spell_type origin, int pow)
{
    switch (origin)
    {
        case SPELL_ISKENDERUNS_MYSTIC_BLAST:
            return 2 + div_rand_round(pow, 50);
        case SPELL_FORCE_LANCE:
            return 4;
        default:
            return 1;
    }
}

static int _collision_damage(spell_type spell, int pow)
{
    switch (spell)
    {
        case SPELL_PRIMAL_WAVE:
        case SPELL_MUD_BREATH:
            return 0;

        default:
            return default_collision_damage(pow, true).roll();
    }
}

void bolt::knockback_actor(actor *act, int dam)
{
    if (!can_knockback(dam)) return;

    const int max_dist = _knockback_dist(origin_spell, ench_power);
    const monster_type montyp = act->is_monster() ? act->type
                                                  : you.mons_species();
    const int weight = max_corpse_chunks(montyp);
    const int roll = origin_spell == SPELL_FORCE_LANCE ? 42
                     : origin_spell == SPELL_ISKENDERUNS_MYSTIC_BLAST ? 17
                     : 25;
    const int dist = binomial(max_dist, roll - weight, roll); // This is silly! -- PF

    // not sure this belongs here, but it can't be done in agent::knockback
    // if the caster is dead, we can't get their former position for knockback
    auto caster = agent();
    if (!caster) return;
    act->knockback(*caster, dist, _collision_damage(origin_spell, ench_power), name);
}

void bolt::pull_actor(actor *act, int dam)
{
    if (!act || !can_pull(*act, dam) || act->resists_dislodge("being pulled"))
        return;

    // How far we'll try to pull the actor to make them adjacent to the source.
    const int distance = (act->pos() - source).rdist() - 1;
    ASSERT(distance > 0);

    const coord_def oldpos = act->pos();
    ASSERT(ray.pos() == oldpos);

    coord_def newpos = oldpos;
    for (int dist_travelled = 0; dist_travelled < distance; ++dist_travelled)
    {
        const ray_def oldray(ray);

        ray.regress();

        newpos = ray.pos();
        if (newpos == oldray.pos()
            || cell_is_solid(newpos)
            || actor_at(newpos)
            || !act->can_pass_through(newpos)
            || !act->is_habitable(newpos))
        {
            ray = oldray;
            break;
        }

        act->move_to(newpos, MV_DEFAULT, true);
    }

    if (newpos == oldpos)
        return;

    if (you.can_see(*act))
    {
        mprf("%s %s yanked forward by the %s.", act->name(DESC_THE).c_str(),
             act->conj_verb("are").c_str(), name.c_str());
    }

    if (act->pos() != newpos)
        act->collide(newpos, agent(), default_collision_damage(ench_power, true).roll());

    act->finalise_movement();
}

// Return true if the player's god will be unforgiving about the effects
// of this beam.
bool bolt::god_cares() const
{
    return effect_known || effect_wanton;
}

// Return true if the block succeeded (including reflections.)
bool bolt::attempt_block(monster* mon)
{
    const int shield_block = mon->shield_bonus();
    if (shield_block <= 0)
        return false;

    const int sh_hit = random2(hit * 130 / 100);
    if (sh_hit >= shield_block || mon->shield_exhausted())
        return false;

    item_def *shield = mon->mslot_item(MSLOT_SHIELD);
    if (is_reflectable(*mon))
    {
        if (mon->observable())
        {
            if (shield && shield_reflects(*shield))
            {
                mprf("%s blocks the %s with %s %s... and reflects it back!",
                     mon->name(DESC_THE).c_str(),
                     name.c_str(),
                     mon->pronoun(PRONOUN_POSSESSIVE).c_str(),
                     shield->name(DESC_PLAIN).c_str());
            }
            else
            {
                mprf("The %s reflects off an invisible shield around %s!",
                     name.c_str(),
                     mon->name(DESC_THE).c_str());
            }
        }
        else if (you.see_cell(pos()))
            mprf("The %s bounces off of thin air!", name.c_str());

        reflect();
    }
    else if (you.see_cell(pos()))
    {
        mprf("%s blocks the %s.",
             mon->name(DESC_THE).c_str(), name.c_str());
        finish_beam();
    }

    mon->shield_block_succeeded(agent());
    return true;
}

/// Is the given monster a bush or bush-like 'monster', and can the given beam
/// travel through it without harm?
bool bolt::bush_immune(const monster &mons) const
{
    return
        // Briars allow bolts fired from aligned actors to pass but block
        // those from unaligned actors. If this bolt has no agent, we're
        // assuming alignment.
        (mons.type == MONS_BRIAR_PATCH && mons_aligned(&mons, agent())
             // Bushes pass bolts regardless of alignment of the actor.
             || mons_species(mons.type) == MONS_BUSH)
        // We allow hitting the bush-like monster with a bolt when it's the
        // target.
        && target != mons.pos();
}

// Is there a visible monster at this position which will keep the beam from
// continuing onward? (And, if so, is it firewood or something else we'd never
// actually want to bother hitting?)
bool bolt::at_blocking_monster() const
{
    const monster *mon = monster_at(pos());
    if (!mon || !you.can_see(*mon))
        return false;

    if (!pierce && !ignores_monster(mon) && mon->is_firewood())
        return true;
    if (have_passive(passive_t::neutral_slimes)
        && mons_is_slime(*mon)
        && mon->wont_attack()
        && flavour != BEAM_VILE_CLUTCH)
    {
        return true;
    }
    return false;
}

void bolt::affect_monster(monster* mon)
{
    // Don't hit dead or fake monsters.
    if (!mon->alive() || mon->type == MONS_GOD_WRATH_AVATAR)
        return;

    hit_count[mon->mid]++;

    // Jiyva absorbs attacks on slimes.
    if (agent()
        && flavour != BEAM_VILE_CLUTCH
        && have_passive(passive_t::neutral_slimes)
        && mons_is_slime(*mon)
        && mon->wont_attack())  // allow attacking slime-shaped shifters
    {
        if (!is_tracer() && you.see_cell(mon->pos()))
        {
            mprf(MSGCH_GOD, GOD_JIYVA,
                 "%s absorbs the %s as it strikes your slime.",
                 god_speaker(GOD_JIYVA).c_str(), !name.empty() ? name.c_str()
                 : _beam_type_name(flavour).c_str());
        }

        finish_beam();
        return;
    }

    // Print messages for monsters avoiding attacks.
    bool should_skip = false;
    if (!is_tracer())
        should_skip = !could_harm(agent(), mon, true, true);

    if (flavour == BEAM_WATER && mon->type == MONS_WATER_ELEMENTAL && !is_tracer())
    {
        if (you.see_cell(mon->pos()))
            mprf("The %s passes through %s.", name.c_str(), mon->name(DESC_THE).c_str());
    }

    if (ignores_monster(mon) || should_skip)
        return;

    // Handle tracers separately.
    if (is_tracer())
    {
        tracer_affect_monster(mon);
        return;
    }

    if (ranged_atk)
    {
        // Test if this qualifies to trigger Dimensional Bullseye later on.
        if (agent()->is_player() && you.duration[DUR_DIMENSIONAL_BULLSEYE]
            && !can_trigger_bullseye
            && !mon->is_firewood() && mon->summoner != MID_PLAYER)
        {
            can_trigger_bullseye = true;
        }

        do_ranged_attack(*mon);
        return;
    }

    // Explosions always 'hit'.
    const bool engulfs = (is_explosion || is_big_cloud());

    if (is_enchantment())
    {
        if (real_flavour == BEAM_RANDOM)
        {
            if (hit_verb.empty())
                hit_verb = engulfs ? "engulfs" : "hits";
            if (you.see_cell(mon->pos()))
            {
                mprf("The %s %s %s.", name.c_str(), hit_verb.c_str(),
                     mon->name(DESC_THE).c_str());
            }
            else if (heard && !hit_noise_msg.empty())
                mprf(MSGCH_SOUND, "%s", hit_noise_msg.c_str());
        }
        // no to-hit check
        enchantment_affect_monster(mon);
        return;
    }

    if (is_explosion && !in_explosion_phase)
    {
        // It hit a monster, so the beam should terminate.
        // Don't actually affect the monster; the explosion
        // will take care of that.
        finish_beam();
        return;
    }

    // We need to know how much the monster _would_ be hurt by this,
    // before we decide if it actually hits.
    int preac, postac, final;
    if (!determine_damage(mon, preac, postac, final) && real_flavour != BEAM_CHAOS)
        return;

#ifdef DEBUG_DIAGNOSTICS
    dprf(DIAG_BEAM, "Monster: %s; Damage: pre-AC: %d; post-AC: %d; post-resist: %d",
         mon->name(DESC_PLAIN).c_str(), preac, postac, final);
#endif

    // Player beams which hit friendlies or good neutrals will annoy
    // them and be considered naughty if they do damage (this is so as
    // not to penalise players that fling fireballs into a melee with
    // fire elementals on their side - the elementals won't give a sh*t,
    // after all).

    god_conduct_trigger conducts[3];

    if (nasty_to(mon))
    {
        if (agent() && agent()->is_player()  && final > 0)
            set_attack_conducts(conducts, *mon, you.can_see(*mon));
    }

    if (engulfs && flavour == BEAM_SPORE // XXX: engulfs is redundant?
        && !mon->is_unbreathing())
    {
        apply_enchantment_to_monster(mon);
    }

    if (flavour == BEAM_UNRAVELLED_MAGIC)
    {
        int unused; // res_margin
        try_enchant_monster(mon, unused);
    }

    // Make a copy of the to-hit before we modify it.
    int beam_hit = hit;

    if (beam_hit != AUTOMATIC_HIT)
        beam_hit = apply_lighting(beam_hit, *mon);

    // The monster may block the beam.
    if (!engulfs && is_blockable() && attempt_block(mon))
        return;

    defer_rand r;
    const int repel = mon->missile_repulsion();
    int rand_ev = random2(mon->evasion() + repel);

    int hit_margin = _test_beam_hit(beam_hit, rand_ev, r);

    if (you.duration[DUR_BLIND] && beam_hit != AUTOMATIC_HIT && agent()
        && agent()->is_player())
    {
        const int distance = you.pos().distance_from(mon->pos());
        if (x_chance_in_y(player_blind_miss_chance(distance), 100))
            hit_margin = -1;
    }

    // FIXME: We're randomising mon->evasion, which is further
    // randomised inside test_beam_hit. This is so we stay close to the
    // 4.0 to-hit system (which had very little love for monsters).
    if (!engulfs && hit_margin < 0)
    {
        // If the PLAYER cannot see the monster, don't tell them anything!
        if (mon->observable())
        {
            // if it would have hit otherwise...
            if (hit_margin > -repel)
            {
                msg::stream << mon->name(DESC_THE) << " "
                            << "repels the " << name
                            << '!' << endl;
            }
            else
            {
                msg::stream << "The " << name << " misses "
                            << mon->name(DESC_THE) << '.' << endl;
            }
        }
        return;
    }

    update_hurt_or_helped(mon);

    // We'll say ballistomycete spore explosions don't trigger the ally attack
    // conduct for Fedhas worshipers. Mostly because you can accidentally blow
    // up a group of 8 plants and get placed under penance until the end of
    // time otherwise. I'd prefer to do this elsewhere but the beam information
    // goes out of scope.
    if (you_worship(GOD_FEDHAS) && flavour == BEAM_SPORE)
        conducts[0].set();

    if (!is_explosion && !noise_generated)
    {
        heard = noisy(loudness, pos(), source_id) || heard;
        noise_generated = true;
    }

    if (!mon->alive())
        return;

    // The beam hit.
    if (you.see_cell(mon->pos()))
    {
        // Monsters are never currently helpless in ranged combat.
        if (hit_verb.empty())
            hit_verb = engulfs ? "engulfs" : "hits";

        // If the beam did no damage because of resistances,
        // mons_adjust_flavoured below will print "%s completely resists", so
        // no need to also say "does no damage" here.
        if (damage.num > 0 || !is_big_cloud())
        {
            mprf("The %s %s %s%s%s%s",
                name.c_str(),
                hit_verb.c_str(),
                mon->name(DESC_THE).c_str(),
                real_flavour != BEAM_CHAOS ? ""
                    : make_stringf(" with %s", _beam_type_name(flavour).c_str()).c_str(),
                postac ? "" : " but does no damage",
                attack_strength_punctuation(final).c_str());
        }
    }
    else if (heard && !hit_noise_msg.empty())
        mprf(MSGCH_SOUND, "%s", hit_noise_msg.c_str());

    // Spell vampirism
    if (agent() && agent()->is_player() && is_player_book_spell(origin_spell))
        majin_bo_vampirism(*mon, min(final, mon->stat_hp()));

    // Apply flavoured specials.
    mons_adjust_flavoured(mon, *this, postac, true);

    // Arc out from the original target AFTER damage done to the main target
    if (origin_spell == SPELL_GALVANIC_BREATH)
        do_galvanic_jolt(*agent(), pos(), damage);

    // mons_adjust_flavoured may kill the monster directly.
    if (mon->alive())
    {
        // If the beam is of the MMISSILE type (Earth magic) we might bleed on
        // the floor.
        if (!engulfs && flavour == BEAM_MMISSILE && !mon->is_summoned())
        {
            // Using raw_damage instead of the flavoured one!
            // assumes DVORP_PIERCING, factor: 0.5
            const int blood = min(postac/2, mon->hit_points);
            bleed_onto_floor(mon->pos(), mon->type, blood, true);
        }
        // Now hurt monster.
        mon->hurt(agent(), final, flavour, KILLED_BY_BEAM, "", "", false);

        // Haemoclasm explosions will always chain-explode if they kill something
        if (!mon->alive() && flavour == BEAM_HAEMOCLASM && !mon->is_firewood()
            && you.has_mutation(MUT_MAKHLEB_MARK_HAEMOCLASM))
        {
            mon->props[MAKHLEB_HAEMOCLASM_KEY] = true;
        }
    }

    if (mon->alive())
        monster_post_hit(mon, final);
    // The monster (e.g. a spectral weapon) might have self-destructed in its
    // behaviour_event called from mon->hurt() above. If that happened, it
    // will have been cleaned up already (and is therefore invalid now).
    else if (!invalid_monster(mon))
        kill_monster(*mon);

    extra_range_used += range_used_on_hit();
}

bool bolt::ignores_monster(const monster* mon) const
{
    // This is the lava digging tracer. It will stop at anything which cannot
    // survive having lava put beneath it (ie: ignore everything else)
    if (origin_spell == SPELL_HELLFIRE_MORTAR)
        return mon && (mon->airborne() || monster_habitable_feat(mon, DNGN_LAVA));

    // Digging doesn't affect monsters (should it harm earth elementals?).
    if (flavour == BEAM_DIGGING)
        return true;

    // The targeters might call us with nullptr in the event of a remembered
    // monster that is no longer there. Treat it as opaque.
    if (!mon)
        return false;

    // Player shadows (or a dummy for a shadow spell tracer) can shoot through
    // other shadows (ie: where they are currently standing while contemplating
    // being somewhere else).
    if (mon->type == MONS_PLAYER_SHADOW
        && ((agent() && agent()->type == MONS_PLAYER_SHADOW)
            || source_id == MID_PLAYER_SHADOW_DUMMY))
    {
        return true;
    }

    // Shadow spells have no friendly fire, even against monsters.
    // (These should be the only ones that don't avoid that in other ways.)
    if ((origin_spell == SPELL_SHADOW_PRISM || origin_spell == SPELL_SHADOW_BEAM
         || origin_spell == SPELL_SHADOW_SHOT
         || origin_spell == SPELL_SHADOW_TORPOR
         || (origin_spell == SPELL_SHADOW_BALL && in_explosion_phase))
        && mons_atts_aligned(attitude, mons_attitude(*mon)))
    {
        return true;
    }

    // All kinds of beams go past orbs of destruction, the player can shoot
    // through friendly battlespheres, their own ancestor, plants if they're
    // worshipping Fedhas, etc.
    if (shoot_through_actor(agent(), mon))
        return true;

    // Missiles go past bushes and briar patches, unless aimed directly at them
    if (bush_immune(*mon))
        return true;

    // Fire storm creates these, so we'll avoid affecting them.
    if (origin_spell == SPELL_FIRE_STORM && mon->type == MONS_FIRE_VORTEX)
        return true;

    // Don't blow up blocks of ice with the spell that creates them.
    if (origin_spell == SPELL_GLACIATE && mon->type == MONS_BLOCK_OF_ICE)
        return true;

    if (flavour == BEAM_WATER && mon->type == MONS_WATER_ELEMENTAL)
        return true;

    // Rimeblight explosions won't hurt allies OR the monster that is exploding
    if (origin_spell == SPELL_RIMEBLIGHT && is_explosion)
        return mon->friendly() || mon->good_neutral() || mon->pos() == source;

    if (origin_spell == SPELL_HOARFROST_BULLET)
        return in_explosion_phase && mons_aligned(agent(), mon);

    // Explosions caused by Mark of Carnage don't cause friendly fire, and Mark
    // of the Legion allows firing through allies at all times.
    if ((origin_spell == SPELL_UNLEASH_DESTRUCTION && mon->friendly())
        && (is_explosion || you.has_mutation(MUT_MAKHLEB_MARK_LEGION)))
    {
        return true;
    }

    if ((flavour == BEAM_HAEMOCLASM || flavour == BEAM_BLOODRITE) && mon->friendly())
        return true;

    if (flavour == BEAM_QAZLAL && mon->was_created_by(you, MON_SUMM_AID))
        return true;

    if (origin_spell == SPELL_UPHEAVAL && agent() && agent() == mon)
        return true;

    if ((origin_spell == SPELL_PERCUSSIVE_TEMPERING
         || origin_spell == SPELL_FORTRESS_BLAST
         || origin_spell == SPELL_AWAKEN_FLESH)
        && mons_atts_aligned(attitude, mon->temp_attitude()))
    {
        return true;
    }

    return false;
}

bool bolt::self_targeted() const
{
    // aimed_at_feet isn't enough to determine this. Examples: wrath uses a
    // fake monster at the player's position, xom's chesspiece fakes smite
    // targeting by setting the monster as the source position.
    const actor* a = agent(true);
    // TODO: are there cases that should count as self-targeted without an
    // agent?
    return aimed_at_feet && a && a->pos() == source
        && (!a->is_monster() || !mons_is_wrath_avatar(*a->as_monster()));
}

bool bolt::has_saving_throw() const
{
    if (self_targeted() || no_saving_throw)
        return false;

    switch (flavour)
    {
    case BEAM_HASTE:
    case BEAM_MIGHT:
    case BEAM_BERSERK:
    case BEAM_HEALING:
    case BEAM_INVISIBILITY:
    case BEAM_DISPEL_UNDEAD:
    case BEAM_BLINK_CLOSE:
    case BEAM_BLINK:
    case BEAM_BECKONING:
    case BEAM_MALIGN_OFFERING:
    case BEAM_AGILITY:
    case BEAM_RESISTANCE:
    case BEAM_MALMUTATE:
    case BEAM_SAP_MAGIC:
    case BEAM_UNRAVELLING:
    case BEAM_UNRAVELLED_MAGIC:
    case BEAM_INFESTATION:
    case BEAM_IRRESISTIBLE_CONFUSION:
    case BEAM_VILE_CLUTCH:
    case BEAM_ROOTS:
    case BEAM_VAMPIRIC_DRAINING:
    case BEAM_CONCENTRATE_VENOM:
    case BEAM_ENFEEBLE:
    case BEAM_VITRIFYING_GAZE:
    case BEAM_RIMEBLIGHT:
    case BEAM_SHADOW_TORPOR:
    case BEAM_ILL_OMEN:
    case BEAM_WARP_BODY:
    case BEAM_DIMINISH_SPELLS:
        return false;
    case BEAM_VULNERABILITY:
        return !one_chance_in(3);  // Ignores will 1/3 of the time
    default:
        return true;
    }
}

bool ench_flavour_affects_monster(actor *agent, beam_type flavour,
                                  const monster* mon, bool intrinsic_only)
{
    bool rc = true;
    switch (flavour)
    {
    case BEAM_MALMUTATE:
    case BEAM_UNRAVELLED_MAGIC:
        rc = mon->can_mutate();
        break;

    case BEAM_SLOW:
    case BEAM_HASTE:
    case BEAM_PARALYSIS:
    case BEAM_SHADOW_TORPOR:
        rc = !mon->stasis();
        break;

    case BEAM_CONFUSION:
    case BEAM_IRRESISTIBLE_CONFUSION:
    case BEAM_VEX:
        rc = !mon->clarity();
        break;

    case BEAM_POLYMORPH:
        rc = mon->can_polymorph();
        break;

    case BEAM_DISPEL_UNDEAD:
        rc = bool(mon->holiness() & MH_UNDEAD);
        break;

    case BEAM_PAIN:
        rc = mon->res_negative_energy(intrinsic_only) < 3;
        break;

    case BEAM_AGONY:
    case BEAM_CURSE_OF_AGONY:
        rc = !mon->res_torment();
        break;

    case BEAM_HIBERNATION:
        rc = mon->can_hibernate(false, intrinsic_only);
        break;

    case BEAM_SLEEP:
        rc = mon->can_sleep();
        break;

    case BEAM_PORKALATOR:
        rc = (mon->holiness() & MH_DEMONIC && mon->type != MONS_HELL_HOG)
              || (mon->holiness() & MH_NATURAL && mon->type != MONS_HOG)
              || (mon->holiness() & MH_HOLY && mon->type != MONS_HOLY_SWINE);
        break;

    case BEAM_SENTINEL_MARK:
        rc = false;
        break;

    case BEAM_MALIGN_OFFERING:
        rc = (mon->res_negative_energy(intrinsic_only) < 3);
        break;

    case BEAM_VAMPIRIC_DRAINING:
        rc = actor_is_susceptible_to_vampirism(*mon)
                && (mon->res_negative_energy(intrinsic_only) < 3);
        break;

    case BEAM_VIRULENCE:
        rc = (mon->res_poison() < 3);
        break;

    case BEAM_DIMINISH_SPELLS:
    case BEAM_DRAIN_MAGIC:
        rc = mon->antimagic_susceptible();
        break;

    case BEAM_INNER_FLAME:
        rc = !mon->has_ench(ENCH_INNER_FLAME);
        break;

    case BEAM_PETRIFY:
        rc = !mon->res_petrify();
        break;

    case BEAM_INFESTATION:
        rc = (mons_gives_xp(*mon, you) || mon->props.exists(KIKU_WRETCH_KEY))
             && !mon->friendly()
             && !mon->has_ench(ENCH_INFESTATION);
        break;

    case BEAM_VILE_CLUTCH:
    case BEAM_ROOTS:
        ASSERT(agent);
    {
        const auto constr_typ = flavour == BEAM_ROOTS ? CONSTRICT_ROOTS : CONSTRICT_BVC;
        rc = !mons_aligned(agent, mon) && agent->can_constrict(*mon, constr_typ);
    }
        break;

    case BEAM_CHARM:
        rc = !mons_aligned(agent, mon);
        break;

    case BEAM_MINDBURST:
        rc = mons_intel(*mon) > I_BRAINLESS;
        break;

    case BEAM_ENFEEBLE:
        rc = mons_has_attacks(*mon)
             || mon->antimagic_susceptible()
             || !mons_invuln_will(*mon);
        break;

    case BEAM_RIMEBLIGHT:
        rc = !mon->has_ench(ENCH_RIMEBLIGHT);
        break;

    case BEAM_SOUL_SPLINTER:
        rc = mons_can_be_spectralised(*mon, true, true)
             && !mon->props.exists(SOUL_SPLINTERED_KEY);
        break;

    case BEAM_BECKONING:
        return can_beckon(*mon);

    default:
        break;
    }

    return rc;
}

bool enchant_actor_with_flavour(actor* victim, const actor *foe,
                                beam_type flavour, int powc)
{
    bolt dummy;
    dummy.flavour = flavour;
    dummy.ench_power = powc;
    dummy.set_agent(foe);
    dummy.animate = false;
    if (victim->is_player())
        dummy.affect_player_enchantment(false);
    else
        dummy.apply_enchantment_to_monster(victim->as_monster());
    return dummy.obvious_effect;
}

bool enchant_monster_invisible(monster* mon, const string &how)
{
    // Store the monster name before it becomes an "it". - bwr
    const string monster_name = mon->name(DESC_THE);
    const bool could_see = you.can_see(*mon);

    if (mon->has_ench(ENCH_INVIS) || !mon->add_ench(ENCH_INVIS))
        return false;

    if (could_see)
    {
        const bool is_visible = mon->visible_to(&you);

        // Can't use simple_monster_message(*) here, since it checks
        // for visibility of the monster (and it's now invisible).
        // - bwr
        mprf("%s %s%s",
             monster_name.c_str(),
             how.c_str(),
             is_visible ? " for a moment."
                        : "!");

        if (!is_visible && !mons_is_safe(mon))
            autotoggle_autopickup(true);
    }

    return true;
}

mon_resist_type bolt::try_enchant_monster(monster* mon, int &res_margin)
{
    // Early out if the enchantment is meaningless.
    if (!ench_flavour_affects_monster(agent(), flavour, mon))
        return MON_UNAFFECTED;

    // Check willpower.
    if (has_saving_throw())
    {
        if (mons_invuln_will(*mon))
            return MON_UNAFFECTED;

        // (Very) ugly things and shapeshifters will never resist
        // polymorph beams.
        if (flavour == BEAM_POLYMORPH
            && (mon->type == MONS_UGLY_THING
                || mon->type == MONS_VERY_UGLY_THING
                || mon->is_shapeshifter()))
        {
            ;
        }
        else if (mon->check_willpower(agent(true), ench_power) > 0)
        {
            // Note only actually used by messages in this case.
            res_margin = mon->willpower() - ench_power_stepdown(ench_power);
            return MON_RESIST;
        }
    }

    return apply_enchantment_to_monster(mon);
}

mon_resist_type bolt::apply_enchantment_to_monster(monster* mon)
{
    // Gigantic-switches-R-Us
    switch (flavour)
    {
    case BEAM_TELEPORT:
        if (mon->no_tele())
            return MON_UNAFFECTED;
        if (mon->observable())
            obvious_effect = true;
        monster_teleport(mon, false, false, false, agent());
        return MON_AFFECTED;

    case BEAM_BLINK:
        if (mon->no_tele())
            return MON_UNAFFECTED;
        if (mon->observable())
            obvious_effect = true;
        monster_blink(mon);
        return MON_AFFECTED;

    case BEAM_BLINK_CLOSE:
        if (mon->no_tele())
            return MON_UNAFFECTED;
        if (mon->observable())
            obvious_effect = true;
        blink_other_close(mon, source);
        return MON_AFFECTED;

    case BEAM_BECKONING:
        obvious_effect = beckon(*mon, *this);
        return obvious_effect ? MON_AFFECTED : MON_OTHER; // ?

    case BEAM_POLYMORPH:
        if (mon->polymorph(0))
            obvious_effect = true;
        if (BLAME_KILL(thrower))
        {
            const int level = 2 + random2(3);
            did_god_conduct(DID_DELIBERATE_MUTATING, level, god_cares());
        }
        return MON_AFFECTED;

    case BEAM_MALMUTATE:
    case BEAM_UNRAVELLED_MAGIC:
        if (mon->malmutate(agent())) // exact source doesn't matter
            obvious_effect = true;
        if (BLAME_KILL(thrower))
        {
            const int level = 2 + random2(3);
            did_god_conduct(DID_DELIBERATE_MUTATING, level, god_cares());
        }
        return MON_AFFECTED;

    case BEAM_BANISH:
        mon->banish(agent());
        obvious_effect = true;
        return MON_AFFECTED;

    case BEAM_DISPEL_UNDEAD:
    {
        const int dam = damage.roll();
        if (you.see_cell(mon->pos()))
        {
            mprf("%s is dispelled%s",
                 mon->name(DESC_THE).c_str(),
                 attack_strength_punctuation(dam).c_str());
            obvious_effect = true;
        }
        mon->hurt(agent(), dam);
        return MON_AFFECTED;
    }

    case BEAM_VAMPIRIC_DRAINING:
    {
        const int dam = resist_adjust_damage(mon, flavour, damage.roll());
        if (dam && actor_is_susceptible_to_vampirism(*mon))
        {
            _vampiric_draining_effect(*mon, *agent(), dam);
            obvious_effect = true;
            return MON_AFFECTED;
        }
        else
            return MON_UNAFFECTED;
    }

    case BEAM_PAIN:
    {
        const int dam = resist_adjust_damage(mon, flavour, damage.roll());
        if (!dam)
            return MON_UNAFFECTED;
        if (you.see_cell(mon->pos()))
        {
            mprf("%s writhes in agony%s",
                 mon->name(DESC_THE).c_str(),
                 attack_strength_punctuation(dam).c_str());
            obvious_effect = true;
        }
        mon->hurt(agent(), dam, flavour);
        return MON_AFFECTED;
    }

    case BEAM_SOUL_SPLINTER:
        if (you.see_cell(mon->pos()))
            obvious_effect = true;
        if (make_soul_wisp(*agent(), *mon))
            return MON_AFFECTED;
        else
            return MON_UNAFFECTED;

    case BEAM_AGONY:
        torment_actor(mon, agent(), TORMENT_AGONY);
        obvious_effect = true;
        return MON_AFFECTED;

    case BEAM_CURSE_OF_AGONY:
        // Don't allow stacking number of charges in a opaque fashion
        // by recasting the spell on already-cursed enemies.
        // (But allow refreshing duration, if someone wants)
        if (mon->has_ench(ENCH_CURSE_OF_AGONY))
            mon->del_ench(ENCH_CURSE_OF_AGONY, true, false);

        if (mon->add_ench(mon_enchant(ENCH_CURSE_OF_AGONY, agent(),
                                      random_range(6, 8) * BASELINE_DELAY, 2)))
        {
            obvious_effect = true;
            simple_monster_message(*mon, " is cursed with the promise of agony.");

        }
        return MON_AFFECTED;

    case BEAM_MINDBURST:
    {
        const int dam = damage.roll();
        if (you.see_cell(mon->pos()))
        {
            if (mon->type == MONS_GLOWING_ORANGE_BRAIN)
            {
                mprf("%s is blasted smooth%s",
                     mon->name(DESC_THE).c_str(),
                     attack_strength_punctuation(dam).c_str());
            } else {
                const bool plural = mon->heads() > 1;
                mprf("%s mind%s blasted%s",
                     mon->name(DESC_ITS).c_str(),
                     plural ? "s are" : " is",
                     attack_strength_punctuation(dam).c_str());
            }
            obvious_effect = true;
        }
        mon->hurt(agent(), dam, flavour);
        return MON_AFFECTED;
    }

    case BEAM_HIBERNATION:
        if (mon->can_hibernate())
        {
            if (simple_monster_message(*mon, " looks drowsy..."))
                obvious_effect = true;
            // Since 1 turn will pass for the player's own action here, EH will
            // put a monster to sleep for between ~20-55 aut, slanted towards
            // the low end.
            const int dur = max(30, 10 + random2avg(55, 3));
            mon->put_to_sleep(agent(), dur, true);
            return MON_AFFECTED;
        }
        return MON_UNAFFECTED;

    case BEAM_CORONA:
        if (corona_monster(mon, agent()))
        {
            obvious_effect = true;
            return MON_AFFECTED;
        }
        return MON_UNAFFECTED;

    case BEAM_SLOW:
    {
        const int dur = 1 + div_rand_round(ench_power, 2)
                          + random2(1 + ench_power);
        obvious_effect = do_slow_monster(*mon, agent(), dur * BASELINE_DELAY);
        return MON_AFFECTED;
    }

    case BEAM_HASTE:
        if (BLAME_KILL(thrower))
            did_god_conduct(DID_HASTY, 6, god_cares());

        if (mon->stasis())
            return MON_AFFECTED;

        if (!mon->has_ench(ENCH_HASTE)
            && mon->add_ench(ENCH_HASTE))
        {
            if (!mons_is_immotile(*mon)
                && simple_monster_message(*mon, " seems to speed up."))
            {
                obvious_effect = true;
            }
        }
        return MON_AFFECTED;

    case BEAM_MIGHT:
        if (!mon->has_ench(ENCH_MIGHT)
            && mons_has_attacks(*mon)
            && mon->add_ench(ENCH_MIGHT))
        {
            if (simple_monster_message(*mon, " seems to grow stronger."))
                obvious_effect = true;
        }
        return MON_AFFECTED;

    case BEAM_DOUBLE_VIGOUR:
        if (!mon->has_ench(ENCH_DOUBLED_VIGOUR)
            && mon->add_ench(ENCH_DOUBLED_VIGOUR))
        {
            if (simple_monster_message(*mon, " surges with doubled vitality!"))
                obvious_effect = true;
        }
        return MON_AFFECTED;

    case BEAM_WEAKNESS:
        mon->weaken(agent(), 8 + random2(4));
        obvious_effect = true;
        return MON_AFFECTED;

    case BEAM_BERSERK:
        if (!mon->berserk_or_frenzied())
        {
            // currently from potion, hence voluntary
            mon->go_berserk(true);
            // can't return this from go_berserk, unfortunately
            obvious_effect = you.can_see(*mon);
        }
        return MON_AFFECTED;

    case BEAM_HEALING:
        // No KILL_YOU_CONF, or we get "You heal ..."
        if (thrower == KILL_YOU || thrower == KILL_YOU_MISSILE)
        {
            const int pow = min(50, 3 + damage.roll());
            const int amount = pow + roll_dice(2, pow) - 2;
            if (heal_monster(*mon, amount))
                obvious_effect = true;
            msg_generated = true; // to avoid duplicate "nothing happens"
        }
        else if (mon->heal(3 + damage.roll()))
        {
            if (mon->hit_points == mon->max_hit_points)
            {
                if (simple_monster_message(*mon, " wounds heal themselves!", true))
                    obvious_effect = true;
            }
            else if (simple_monster_message(*mon, " is healed somewhat."))
                obvious_effect = true;
        }
        return MON_AFFECTED;

    case BEAM_PARALYSIS:
        apply_bolt_paralysis(mon);
        return MON_AFFECTED;

    case BEAM_PETRIFY:
        if (mon->res_petrify())
            return MON_UNAFFECTED;

        apply_bolt_petrify(mon);
        return MON_AFFECTED;

    case BEAM_VEX:
        if (mon->vex(agent(), random_range(3, 6)))
            return MON_AFFECTED;
        else
            return MON_UNAFFECTED;

    case BEAM_SPORE:
    case BEAM_CONFUSION:
    case BEAM_IRRESISTIBLE_CONFUSION:
    {
        if (mon->clarity())
        {
            if (you.can_see(*mon))
                obvious_effect = true;
            return MON_AFFECTED;
        }

        // irresistible confusion has a shorter duration and is weaker
        // against strong monsters
        int dur = ench_power;
        if (flavour == BEAM_IRRESISTIBLE_CONFUSION)
            dur = max(10, dur - mon->get_hit_dice());
        else
            dur = _ench_pow_to_dur(dur);

        if (mon->add_ench(mon_enchant(ENCH_CONFUSION, agent(), dur)))
        {
            // FIXME: Put in an exception for things you won't notice
            // becoming confused.
            if (simple_monster_message(*mon, " appears confused."))
                obvious_effect = true;
        }
        return MON_AFFECTED;
    }

    case BEAM_SLEEP:
        if (mon->asleep())
            return MON_UNAFFECTED;

        mon->put_to_sleep(agent(), random_range(4, 7) * BASELINE_DELAY);
        if (simple_monster_message(*mon, " falls asleep!"))
            obvious_effect = true;

        return MON_AFFECTED;

    case BEAM_INVISIBILITY:
    {
        if (enchant_monster_invisible(mon, "flickers and vanishes"))
            obvious_effect = true;

        return MON_AFFECTED;
    }

    case BEAM_CHARM:
        if (agent() && agent()->is_monster())
        {
            enchant_type good = (agent()->wont_attack()) ? ENCH_CHARM
                                                         : ENCH_HEXED;
            enchant_type bad  = (agent()->wont_attack()) ? ENCH_HEXED
                                                         : ENCH_CHARM;

            const bool could_see = you.can_see(*mon);
            if (mon->has_ench(bad))
            {
                obvious_effect = mon->del_ench(bad);
                return MON_AFFECTED;
            }
            if (simple_monster_message(*mon, " is charmed!"))
                obvious_effect = true;
            mon->add_ench(mon_enchant(good, agent()));
            if (!obvious_effect && could_see && !you.can_see(*mon))
                obvious_effect = true;
            check_lovelessness(*mon);
            return MON_AFFECTED;
        }

        // Being a puppet on magic strings is a nasty thing.
        // Mindless creatures shouldn't probably mind, but because of complex
        // behaviour of charmed neutrals, let's disallow that for now.
        mon->attitude = ATT_HOSTILE;

        // XXX: Another hackish thing for Pikel's band neutrality.
        if (mons_is_mons_class(mon, MONS_PIKEL))
            pikel_band_neutralise();

        if (simple_monster_message(*mon, " is charmed."))
            obvious_effect = true;
        mon->add_ench(mon_enchant(ENCH_CHARM, agent()));
        if (you.can_see(*mon))
            obvious_effect = true;
        return MON_AFFECTED;

    case BEAM_PORKALATOR:
    {
        // Monsters which use the ghost structure can't be properly
        // restored from hog form.
        if (mons_is_ghost_demon(mon->type))
            return MON_UNAFFECTED;

        monster orig_mon(*mon);
        if (monster_polymorph(mon, mon->holiness() & MH_DEMONIC ?
                      MONS_HELL_HOG : mon->holiness() & MH_HOLY ?
                      MONS_HOLY_SWINE : MONS_HOG))
        {
            obvious_effect = true;

            // Don't restore items to monster if it reverts.
            orig_mon.inv = mon->inv;

            // monsters can't cast spells in hog form either -doy
            mon->spells.clear();

            // For monster reverting to original form.
            mon->props[ORIG_MONSTER_KEY] = orig_mon;
        }

        return MON_AFFECTED;
    }

    case BEAM_INNER_FLAME:
        if (!mon->has_ench(ENCH_INNER_FLAME)
            && mon->add_ench(mon_enchant(ENCH_INNER_FLAME, agent())))
        {
            if (simple_monster_message(*mon,
                                       (mon->body_size(PSIZE_BODY) > SIZE_LARGE)
                                        ? " is filled with an intense inner flame!"
                                        : " is filled with an inner flame."))
            {
                obvious_effect = true;
            }
        }
        return MON_AFFECTED;

    case BEAM_DIMENSION_ANCHOR:
        if (!mon->has_ench(ENCH_DIMENSION_ANCHOR)
            && mon->add_ench(mon_enchant(ENCH_DIMENSION_ANCHOR, agent(),
                                         random_range(20, 30) * BASELINE_DELAY)))
        {
            if (simple_monster_message(*mon, " is firmly anchored in space."))
                obvious_effect = true;
        }
        return MON_AFFECTED;

    case BEAM_VULNERABILITY:
        if (!mon->has_ench(ENCH_LOWERED_WL))
        {
            if (mon->strip_willpower(agent(), random_range(20, 30)))
                obvious_effect = true;
        }
        return MON_AFFECTED;

    case BEAM_VITRIFY:
        if (!mon->has_ench(ENCH_VITRIFIED))
            mon->vitrify(agent(), random_range(8, 12));
        if (you.can_see(*mon))
            obvious_effect = true;
        return MON_AFFECTED;

    case BEAM_VITRIFYING_GAZE:
        mon->vitrify(agent(), random_range(4, 8));
        if (you.can_see(*mon))
            obvious_effect = true;
        return MON_AFFECTED;

    case BEAM_MALIGN_OFFERING:
    {
        const int dam = resist_adjust_damage(mon, flavour, damage.roll());
        if (dam)
        {
            _malign_offering_effect(mon, agent(), dam);
            obvious_effect = true;
            return MON_AFFECTED;
        }
        else
        {
            simple_monster_message(*mon, " is unaffected.");
            return MON_UNAFFECTED;
        }
    }

    case BEAM_VIRULENCE:
        if (!mon->has_ench(ENCH_POISON_VULN)
            && mon->add_ench(mon_enchant(ENCH_POISON_VULN, agent(),
                                         random_range(20, 30) * BASELINE_DELAY)))
        {
            if (simple_monster_message(*mon,
                                       " grows more vulnerable to poison."))
            {
                obvious_effect = true;
            }
        }
        return MON_AFFECTED;

    case BEAM_AGILITY:
        if (!mon->has_ench(ENCH_AGILE)
            && !mon->is_stationary()
            && mon->add_ench(ENCH_AGILE))
        {
            if (simple_monster_message(*mon, " suddenly seems more agile."))
                obvious_effect = true;
        }
        return MON_AFFECTED;

    case BEAM_SAP_MAGIC:
        if (!mon->has_ench(ENCH_SAP_MAGIC)
            && mon->add_ench(mon_enchant(ENCH_SAP_MAGIC, agent())))
        {
            if (you.can_see(*mon))
            {
                mprf("%s seems less certain of %s magic.",
                     mon->name(DESC_THE).c_str(), mon->pronoun(PRONOUN_POSSESSIVE).c_str());
                obvious_effect = true;
            }
        }
        return MON_AFFECTED;

    case BEAM_DIMINISH_SPELLS:
        mon->diminish(agent(), 9);
        obvious_effect = true;
        return MON_AFFECTED;

    case BEAM_TUKIMAS_DANCE:
        cast_tukimas_dance(ench_power, mon);
        obvious_effect = true;
        break;

    case BEAM_RESISTANCE:
        if (!mon->has_ench(ENCH_RESISTANCE)
            && mon->add_ench(ENCH_RESISTANCE))
        {
            if (simple_monster_message(*mon, " suddenly seems more resistant."))
                obvious_effect = true;
        }
        return MON_AFFECTED;

    case BEAM_UNRAVELLING:
        if (!monster_can_be_unravelled(*mon))
            return MON_UNAFFECTED;

        if (mon->is_abjurable())
        {
            mprf("The magic binding %s to this plane unravels!",
                 mon->name(DESC_THE).c_str());
            monster_die(*mon, KILL_RESET, actor_to_death_source(agent()));
        }
        else
            debuff_monster(*mon);
        _unravelling_explode(*this);
        return MON_AFFECTED;

    case BEAM_INFESTATION:
    {
        const int dur = (5 + random2avg(div_rand_round(ench_power,2), 2))
                            * BASELINE_DELAY;
        mon->add_ench(mon_enchant(ENCH_INFESTATION, &you, dur));
        if (simple_monster_message(*mon, " is infested!"))
            obvious_effect = true;
        return MON_AFFECTED;
    }

    case BEAM_VILE_CLUTCH:
    {
        const int dur = 4 + random_range(div_rand_round(ench_power, 35),
                                         div_rand_round(ench_power, 25) + 1);
        if (start_ranged_constriction(*agent(), *mon, dur, CONSTRICT_BVC))
            obvious_effect = true;
        return MON_AFFECTED;
    }

    case BEAM_ROOTS:
    {
        const int dur = 3 + random_range(div_rand_round(ench_power, 22),
                                         div_rand_round(ench_power, 16) + 1);
        if (start_ranged_constriction(*agent(), *mon, dur, CONSTRICT_ROOTS))
            obvious_effect = true;
        return MON_AFFECTED;
    }

    case BEAM_CONCENTRATE_VENOM:
        if (!mon->has_ench(ENCH_CONCENTRATE_VENOM)
            && mon->add_ench(ENCH_CONCENTRATE_VENOM))
        {
            if (simple_monster_message(*mon, " venom grows more potent.", true))
                obvious_effect = true;
        }
        return MON_AFFECTED;

    case BEAM_ENFEEBLE:
        if (enfeeble_monster(*mon, ench_power))
            obvious_effect = true;
        return MON_AFFECTED;

    case BEAM_RIMEBLIGHT:
        if (apply_rimeblight(*mon, ench_power, true))
        {
            mprf("A stygian plague fills %s body.",
                 apostrophise(mon->name(DESC_THE)).c_str());
            obvious_effect = true;
        }
        return MON_AFFECTED;

    case BEAM_SHADOW_TORPOR:
    {
        int dur = max(30, ench_power / 2 + 40 - random2(mon->get_hit_dice() * 4));
        obvious_effect = do_slow_monster(*mon, agent(), dur);
        return MON_AFFECTED;
    }

    case BEAM_ILL_OMEN:
        mon->doom(random_range(ench_power / 8, ench_power / 5));
        return MON_AFFECTED;

    case BEAM_WARP_BODY:
        obvious_effect = true;
        simple_monster_message(*mon, " body is warped painfully!", true);
        if (mon->has_ench(ENCH_WRETCHED))
            mon->vitrify(agent(), random_range(8, 12), true);
        else
            mon->malmutate(agent());
        mon->hurt(agent(), damage.roll(), BEAM_WARP_BODY, KILLED_BY_BEAM);
        return MON_AFFECTED;

    default:
        break;
    }

    return MON_AFFECTED;
}

// Extra range used on hit.
int bolt::range_used_on_hit() const
{
    if (is_tracer() && source_id == MID_PLAYER && hit < AUTOMATIC_HIT)
        return 0;

    // Non-beams can only affect one thing (player/monster).
    if (!pierce)
        return BEAM_STOP;
    else
        return 0;
}

// Information for how various explosions look & sound.
struct explosion_sfx
{
    // A message printed when the player sees the explosion.
    const char *seeMsg;
    // What the player hears when the explosion goes off unseen.
    const char *sound;
};

// A map from origin_spells to special explosion info for each.
const map<spell_type, explosion_sfx> spell_explosions = {
    { SPELL_HURL_DAMNATION, {
        "The sphere of damnation explodes!",
        "the wailing of the damned",
    } },
    { SPELL_CALL_DOWN_DAMNATION, {
        "The sphere of damnation explodes!",
        "the wailing of the damned",
    } },
    { SPELL_FIREBALL, {
        "The fireball explodes!",
        "an explosion",
    } },
    { SPELL_ORB_OF_ELECTRICITY, {
        "The orb of electricity explodes!",
        "a clap of thunder",
    } },
    { SPELL_FIRE_STORM, {
        "A raging storm of fire appears!",
        "a raging storm",
    } },
    { SPELL_MEPHITIC_CLOUD, {
        "The flask of dizzying concoctions shatters into a vile cloud!",
        "a loud shattering of glass",
    } },
    { SPELL_GHOSTLY_FIREBALL, {
        "The ghostly flame explodes!",
        "the shriek of haunting fire",
    } },
    { SPELL_VIOLENT_UNRAVELLING, {
        "The enchantments explode!",
        "a sharp crackling", // radiation = geiger counter
    } },
    { SPELL_ICEBLAST, {
        "The mass of ice explodes!",
        "an explosion",
    } },
    { SPELL_PERMAFROST_ERUPTION, {
        "Piercing cold boils outward!",
        "an explosion", // maybe?
    } },
    { SPELL_GHOSTLY_SACRIFICE, {
        "The ghostly flame explodes!",
        "the shriek of haunting fire",
    } },
    { SPELL_SERACFALL, {
        "The mass of ice explodes!",
        "an explosion",
    } },
    { SPELL_FLAME_WAVE, {
        "A wave of flame ripples out!",
        "the roar of flame",
    } },
    { SPELL_FASTROOT, {
        "The roots erupt in riotous growth!",
        "creaking and crackling",
    } },
    { SPELL_BLASTMOTE, {
        "The cloud of blastmotes explodes!",
        "a concussive explosion",
    } },
    { SPELL_HURL_TORCHLIGHT, {
        "The gout of umbral fire explodes!",
        "the shriek of umbral fire",
    } },
    { SPELL_WARP_SPACE, {
        "Space twists violently!",
        "shrill echo",
    } },
    { SPELL_COMBUSTION_BREATH, {
        "The embers explode!",
        "a fiery explosion",
    } },
    { SPELL_NULLIFYING_BREATH, {
        "The antimagic surges outward!",
        "a quiet echo",
    } },
    { SPELL_HOARFROST_BULLET, {
        "The shards fragment into shrapnel!",
        "an explosion",
    } },
    { SPELL_SHADOW_BALL, {
        "The flickering shadows explode!",
        "a quiet whistle",
    } },
    { SPELL_ACID_BALL, {
        "The ball of acid explodes!",
        "a violent squelch",
    } },
};

// Takes a bolt and refines it for use in the explosion function.
// Explosions which do not follow from beams bypass this function.
void bolt::refine_for_explosion(const string& explode_msg)
{
    ASSERT(!special_explosion);

    string seeMsg;
    string hearMsg;

    if (ex_size == 0)
        ex_size = 1;
    glyph   = dchar_glyph(DCHAR_FIRED_BURST);

    // Assume that the player can see/hear the explosion, or
    // gets burned by it anyway.  :)
    msg_generated = true;

    if (!explode_msg.empty())
    {
        seeMsg  = explode_msg;
        hearMsg = "You hear an explosion!";
    }
    else
    {
        const explosion_sfx *explosion = map_find(spell_explosions,
                                                  origin_spell);
        if (explosion)
        {
            seeMsg = explosion->seeMsg;
            hearMsg = make_stringf("You hear %s!", explosion->sound);
        }
        else
        {
            seeMsg  = "The beam explodes into a cloud of software bugs!";
            hearMsg = "You hear the sound of one hand!";
        }
    }

    if (origin_spell == SPELL_ORB_OF_ELECTRICITY)
    {
        colour     = LIGHTCYAN;
        ex_size    = 2;
    }
    else if (origin_spell == SPELL_NULLIFYING_BREATH)
    {
        colour  = MAGENTA;
        ex_size = 2;
    }

    if (!is_tracer() && !seeMsg.empty() && !hearMsg.empty())
    {
        heard = player_can_hear(target);
        // Check for see/hear/no msg.
        if (you.see_cell(target) || target == you.pos())
            mpr(seeMsg);
        else
        {
            if (!heard)
                msg_generated = false;
            else
                mprf(MSGCH_SOUND, "%s", hearMsg.c_str());
        }
    }
}

typedef vector< vector<coord_def> > sweep_type;

static sweep_type _radial_sweep(int r)
{
    sweep_type result;

    // Center first.
    result.emplace_back(1, coord_def(0,0));

    for (int rad = 1; rad <= r; ++rad)
    {
        sweep_type::value_type work;

        for (int d = -rad; d <= rad; ++d)
        {
            // Don't put the corners in twice!
            if (d != rad && d != -rad)
            {
                work.emplace_back(-rad, d);
                work.emplace_back(+rad, d);
            }

            work.emplace_back(d, -rad);
            work.emplace_back(d, +rad);
        }
        result.push_back(work);
    }
    return result;
}

/** How much noise does an explosion this big make?
 *
 *  @param the size of the explosion (radius, not diameter)
 *  @returns how much noise it would make.
 */
int explosion_noise(int rad)
{
    return 10 + rad * 5;
}

#define MAX_EXPLOSION_RADIUS 9
// Returns true if we saw something happening.
bool bolt::explode(bool show_more, bool hole_in_the_middle)
{
    ASSERT(!special_explosion);
    ASSERT(!in_explosion_phase);
    ASSERT(ex_size >= 0);

    // explode() can be called manually without setting real_flavour.
    // FIXME: The entire flavour/real_flavour thing needs some
    // rewriting!
    if (real_flavour == BEAM_CHAOS
        || real_flavour == BEAM_RANDOM)
    {
        flavour = real_flavour;
    }
    else
        real_flavour = flavour;

    if (flavour == BEAM_VISUAL)
        affects_nothing = true;

    const int r = min(ex_size, MAX_EXPLOSION_RADIUS);
    in_explosion_phase = true;
    // being hit by bounces doesn't exempt you from the explosion (not that it
    // currently ever matters)
    hit_count.clear();

    if (is_sanctuary(pos()) && flavour != BEAM_VISUAL)
    {
        if (!is_tracer() && you.see_cell(pos()) && !name.empty())
        {
            mprf(MSGCH_GOD, "By Zin's power, the %s is contained.",
                 name.c_str());
            return true;
        }
        return false;
    }

#ifdef DEBUG_DIAGNOSTICS
    if (!quiet_debug)
    {
        dprf(DIAG_BEAM, "explosion at (%d, %d) : g=%d c=%d f=%d hit=%d dam=%dd%d r=%d",
             pos().x, pos().y, glyph, colour, flavour, hit, damage.num, damage.size, r);
    }
#endif

    if (!is_tracer() && flavour != BEAM_VISUAL)
    {
        loudness = explosion_noise(r);

        // Not an "explosion", but still a bit noisy at the target location.
        if (origin_spell == SPELL_INFESTATION
            || origin_spell == SPELL_NULLIFYING_BREATH)
        {
            loudness = spell_effect_noise(origin_spell);
        }
        else if (origin_spell == SPELL_FLAME_WAVE)
        {
            loudness = spell_effect_noise(origin_spell)
                     + (r - 1) * 2; // at radius 1, base noise
        }
        // These explosions are too punishing for the player (and also a bit too
        // Qazlal-like in my opinion) if they make full uncontrollable noise
        // all the time.
        else if (flavour == BEAM_HAEMOCLASM)
            loudness = 5;

        // Make bloated husks quieter, both for balance (they're waking up
        // whole levels!) and for theme (it's not a huge fireball, it's a big
        // gas leak).
        if (name == "blast of putrescent gases")
            loudness = loudness * 2 / 3;
        // Shadow spells are silent
        else if (origin_spell == SPELL_SHADOW_BALL
                 || origin_spell == SPELL_SHADOW_PRISM)
        {
            loudness = 0;
        }

        // Lee's Rapid Deconstruction can target the tiles on the map
        // boundary.
        const coord_def noise_position = clamp_in_bounds(pos());
        bool heard_expl = noisy(loudness, noise_position, source_id);

        heard = heard || heard_expl;

        if (heard_expl && !explode_noise_msg.empty() && !you.see_cell(pos()))
            mprf(MSGCH_SOUND, "%s", explode_noise_msg.c_str());
    }

    // Run DFS to determine which cells are influenced
    explosion_map exp_map;
    exp_map.init(INT_MAX);
    const bool stop_at_walls = (origin_spell != SPELL_LAUNCH_SPORANGIUM);
    determine_affected_cells(exp_map, coord_def(), 0, r, stop_at_walls, stop_at_walls);

    // We get a bit fancy, drawing all radius 0 effects, then radius
    // 1, radius 2, etc. It looks a bit better that way.
    const vector< vector<coord_def> > sweep = _radial_sweep(r);
    const coord_def centre(9,9);

    // Draw pass.
    if (!is_tracer() && animate)
    {
        for (const auto &line : sweep)
        {
            bool pass_visible = false;
            for (const coord_def &delta : line)
            {
                if (delta.origin() && hole_in_the_middle)
                    continue;

                if (exp_map(delta + centre) < INT_MAX)
                    pass_visible |= explosion_draw_cell(delta + pos());
            }
            // redraw for an entire explosion block, so redraw_per_cell is not
            // relevant
            if (pass_visible && !Options.reduce_animations)
                animation_delay(explode_delay, true);
        }
    }

    // Affect pass.
    int cells_seen = 0;
    for (const auto &line : sweep)
    {
        for (const coord_def &delta : line)
        {
            if (delta.origin() && hole_in_the_middle)
                continue;

            if (exp_map(delta + centre) < INT_MAX)
            {
                if (you.see_cell(delta + pos()))
                    ++cells_seen;

                explosion_affect_cell(delta + pos());
            }
        }
    }

    // Delay after entire explosion has been drawn.
    if (!is_tracer() && cells_seen > 0 && show_more && animate)
        animation_delay(explode_delay * 3, Options.reduce_animations);

    return cells_seen > 0;
}

bool bolt::explode(beam_tracer& new_tracer, bool show_more,
                   bool hole_in_the_middle)
{
    unwind_var<beam_tracer*> old_tracer(tracer, &new_tracer);
    return explode(show_more, hole_in_the_middle);
}

/**
 * Draw one tile of an explosion, if that cell is visible.
 *
 * @param p The cell to draw, in grid coordinates.
 * @return True if the cell was actually drawn.
 */
bool bolt::explosion_draw_cell(const coord_def& p)
{
    if (you.see_cell(p))
    {
        const coord_def drawpos = grid2view(p);
        // bounds check
        if (in_los_bounds_v(drawpos))
        {
#ifdef USE_TILE
            // Use default value if none specified.
            if (tile_explode == 0)
            {
                if (tile_beam != 0)
                    tile_explode = tile_beam;
                else
                    tile_explode = tileidx_zap(colour, p);
            }
            view_add_tile_overlay(p, vary_bolt_tile(tile_explode, source, target, p));
#endif
            colour_t adjusted_colour = colour == BLACK ? colour_t{ETC_RANDOM} : colour;
            const unsigned short c = element_colour(adjusted_colour, p, false);
            view_add_glyph_overlay(p, {dchar_glyph(DCHAR_EXPLOSION), c});

            return true;
        }
    }
    return false;
}

void bolt::explosion_affect_cell(const coord_def& p)
{
    // pos() = target during an explosion, so restore it after affecting
    // the cell.
    const coord_def orig_pos = target;

    fake_flavour();
    target = p;
    affect_cell();
    flavour = real_flavour;

    target = orig_pos;
}

// Uses DFS
void bolt::determine_affected_cells(explosion_map& m, const coord_def& delta,
                                    int count, int r,
                                    bool stop_at_statues, bool stop_at_walls)
{
    const coord_def centre(9,9);
    const coord_def loc = pos() + delta;

    // A bunch of tests for edge cases.
    if (delta.rdist() > centre.rdist()
        || delta.rdist() > r
        || count > 10*r
        || !map_bounds(loc)
        || is_sanctuary(loc) && flavour != BEAM_VISUAL)
    {
        return;
    }

    const dungeon_feature_type dngn_feat = env.grid(loc);

    bool at_wall = false;

    // Check to see if we're blocked by a wall or a tree. Can't use
    // feat_is_solid here, since that includes statues which are a separate
    // check, nor feat_is_opaque, since that excludes transparent walls, which
    // we want. -ebering
    // XXX: We could just include trees as wall features, but this currently
    // would have some unintended side-effects. Would be ideal to deal with
    // those and simplify feat_is_wall() to return true for trees. -gammafunk
    if (feat_is_wall(dngn_feat)
        || feat_is_tree(dngn_feat)
           && (!feat_is_flammable(dngn_feat)
               || !can_burn_trees()
               || have_passive(passive_t::shoot_through_plants) // would be nice to message
               || env.markers.property_at(loc, MAT_ANY, "veto_destroy") == "veto")
        || feat_is_closed_door(dngn_feat))
    {
        // Special case: explosion originates from rock/statue
        // (e.g. Lee's Rapid Deconstruction) - in this case, ignore
        // solid cells at the center of the explosion.
        if (stop_at_walls && !(delta.origin() && can_affect_wall(loc))
            // Also affect *other* wall monsters around the area, as long
            // as caster still has LOS to them (i.e. they're not on the *other*
            // side of the wall) which the later recursion loop will check
            && !monster_at(loc))
        {
            return;
        }
        // But remember that we are at a wall.
        if (flavour != BEAM_DIGGING)
            at_wall = true;
    }

    if (feat_is_solid(dngn_feat) && !feat_is_wall(dngn_feat)
        && !can_affect_wall(loc) && stop_at_statues)
    {
        return;
    }

    m(delta + centre) = min(count, m(delta + centre));

    // Now recurse in every direction.
    for (int i = 0; i < 8; ++i)
    {
        const coord_def new_delta = delta + Compass[i];

        if (new_delta.rdist() > centre.rdist())
            continue;

        // If we were at a wall, only move to visible squares.
        coord_def caster_pos = actor_by_mid(source_id) ?
                                   actor_by_mid(source_id)->pos() :
                                   you.pos();

        if (at_wall && !cell_see_cell(caster_pos, loc + Compass[i], LOS_NO_TRANS))
            continue;

        int cadd = 5;
        // Circling around the center is always free.
        if (delta.rdist() == 1 && new_delta.rdist() == 1)
            cadd = 0;
        // Otherwise changing direction (e.g. looking around a wall) costs more.
        else if (delta.x * Compass[i].x < 0 || delta.y * Compass[i].y < 0)
            cadd = 17;

        // Is that cell already covered?
        if (m(new_delta + centre) <= count + cadd)
            continue;

        determine_affected_cells(m, new_delta, count + cadd, r,
                                 stop_at_statues, stop_at_walls);
    }
}

// Returns true if the beam is harmful ((mostly) ignoring monster
// resists) -- mon is given for 'special' cases where,
// for example, "Heal" might actually hurt undead, or
// "Holy Word" being ignored by holy monsters, etc.
//
// Only enchantments should need the actual monster type
// to determine this; non-enchantments are pretty
// straightforward.
bool bolt::nasty_to(const monster* mon) const
{
    // Cleansing flame.
    if (flavour == BEAM_HOLY)
        return mon->res_holy_energy() < 3;

    // Foul flame.
    if (flavour == BEAM_FOUL_FLAME)
        return mon->res_foul_flame() < 3;

    // The orbs are made of pure disintegration energy. This also has the side
    // effect of not stopping us from firing further orbs when the previous one
    // is still flying.
    if (flavour == BEAM_DESTRUCTION)
        return mon->type != MONS_ORB_OF_DESTRUCTION;

    // Take care of other non-enchantments.
    if (!is_enchantment())
        return true;

    // Positive effects.
    if (nice_to(monster_info(mon)))
        return false;

    switch (flavour)
    {
        // Don't immediately break Cause Fear when cast by a Dith worshipper)
        case BEAM_SHADOW_TORPOR:
        case BEAM_DIGGING:
            return false;
        case BEAM_INNER_FLAME:
            // Co-aligned inner flame is fine.
            return !mons_aligned(mon, agent());
        case BEAM_TELEPORT:
        case BEAM_BECKONING:
        case BEAM_INFESTATION:
        case BEAM_VILE_CLUTCH:
        case BEAM_ROOTS:
        case BEAM_SLOW:
        case BEAM_PARALYSIS:
        case BEAM_PETRIFY:
        case BEAM_POLYMORPH:
        case BEAM_DISPEL_UNDEAD:
        case BEAM_PAIN:
        case BEAM_AGONY:
        case BEAM_CURSE_OF_AGONY:
        case BEAM_HIBERNATION:
        case BEAM_MINDBURST:
        case BEAM_VAMPIRIC_DRAINING:
        case BEAM_ENFEEBLE:
        case BEAM_RIMEBLIGHT:
        case BEAM_CHARM:
            return ench_flavour_affects_monster(agent(), flavour, mon);
        case BEAM_TUKIMAS_DANCE:
            return tukima_affects(*mon); // XXX: move to ench_flavour_affects?
        case BEAM_UNRAVELLING:
            return monster_can_be_unravelled(*mon); // XXX: as tukima's
        default:
            break;
    }

    // everything else is considered nasty by everyone
    return true;
}

// Return true if the bolt is considered nice by mon.
// This is not the inverse of nasty_to(): the bolt needs to be
// actively positive.
bool bolt::nice_to(const monster_info& mi) const
{
    // Polymorphing a (very) ugly thing will mutate it into a different
    // (very) ugly thing.
    if (flavour == BEAM_POLYMORPH)
    {
        return mi.type == MONS_UGLY_THING
               || mi.type == MONS_VERY_UGLY_THING;
    }

    if (flavour == BEAM_HASTE
        || flavour == BEAM_HEALING
        || flavour == BEAM_DOUBLE_VIGOUR
        || flavour == BEAM_MIGHT
        || flavour == BEAM_AGILITY
        || flavour == BEAM_INVISIBILITY
        || flavour == BEAM_RESISTANCE
        || flavour == BEAM_CONCENTRATE_VENOM)
    {
        return true;
    }

    return false;
}

////////////////////////////////////////////////////////////////////////////
// bolt
// TODO: Eventually it'd be nice to have a proper factory for these things
// (extended from setup_mons_cast() and zapping() which act as limited ones).

killer_type bolt::killer() const
{
    if (flavour == BEAM_BANISH)
        return KILL_BANISHED;

    switch (thrower)
    {
    case KILL_YOU:
    case KILL_YOU_MISSILE:
        return (flavour == BEAM_PARALYSIS
                || flavour == BEAM_PETRIFY) ? KILL_YOU : KILL_YOU_MISSILE;

    case KILL_MON:
    case KILL_MON_MISSILE:
        return KILL_MON_MISSILE;

    case KILL_YOU_CONF:
        return KILL_YOU_CONF;

    default:
        return KILL_MON_MISSILE;
    }
}

void bolt::set_target(const dist &d)
{
    if (!d.isValid)
        return;

    target = d.target;

    chose_ray = d.choseRay;
    if (d.choseRay)
        ray = d.ray;

    if (d.isEndpoint)
        aimed_at_spot = true;
}

void bolt::setup_retrace()
{
    if (pos().x && pos().y)
        target = pos();

    swap(source, target);
    chose_ray        = false;
    affects_nothing  = true;
    aimed_at_spot    = true;
    extra_range_used = 0;
}

void bolt::set_agent(const actor *actor)
{
    // nullptr actor is fine by us.
    if (!actor)
        return;

    source_id = actor->mid;

    if (actor->is_player())
        thrower = KILL_YOU_MISSILE;
    else
        thrower = KILL_MON_MISSILE;
}

/**
 * Who caused this beam?
 *
 * @param ignore_reflection If true, look all the way back to the original
 *                          source; if false (the default), treat the latest
 *                          actor to reflect this as the source.
 * @returns The actor that can be treated as the source. May be null if
 *          it's a now-dead monster, or if neither the player nor a monster
 *          caused it (for example, divine retribution).
 */
actor* bolt::agent(bool ignore_reflection) const
{
    killer_type nominal_ktype = thrower;
    mid_t nominal_source = source_id;

    // If the beam was reflected report a different point of origin
    if (reflections > 0 && !ignore_reflection)
    {
        if (reflector == MID_PLAYER || source_id == MID_PLAYER)
            return &env.mons[YOU_FAULTLESS];
        nominal_source = reflector;
    }

    if (YOU_KILL(nominal_ktype))
        return &you;
    else
    {
        actor* act = actor_by_mid(nominal_source);
        if (act)
            return act;
        // If this is an explosion caused by a dead friendly monster, set its
        // agent to MID_ANON_FRIEND, so that the player gets XP attribution for
        // the damage.
        else if (monster* mon = cached_monster_copy_by_mid(nominal_source))
        {
            if (mon->friendly())
                return monster_by_mid(MID_ANON_FRIEND);
        }

        return nullptr;
    }
}

bool bolt::is_enchantment() const
{
    return flavour >= BEAM_FIRST_ENCHANTMENT
           && flavour <= BEAM_LAST_ENCHANTMENT;
}

string bolt::get_short_name() const
{
    if (!short_name.empty())
        return short_name;

    if (ranged_atk)
        return ranged_atk->projectile_name();

    if (real_flavour == BEAM_RANDOM
        || real_flavour == BEAM_CHAOS)
    {
        return _beam_type_name(real_flavour);
    }

    if (flavour == BEAM_FIRE
        && (origin_spell == SPELL_STICKY_FLAME
            || origin_spell == SPELL_PYRE_ARROW))
    {
        return "sticky fire";
    }

    if (flavour == BEAM_ELECTRICITY && pierce)
        return "lightning";

    if (name == "bolt of dispelling energy")
        return "dispelling energy";

    if (flavour == BEAM_NONE || flavour == BEAM_MISSILE
        || flavour == BEAM_MMISSILE)
    {
        return name;
    }

    return _beam_type_name(flavour);
}

static string _beam_type_name(beam_type type)
{
    switch (type)
    {
    case BEAM_NONE:                  return "none";
    case BEAM_MISSILE:               return "missile";
    case BEAM_MMISSILE:              return "magic missile";
    case BEAM_FIRE:                  return "fire";
    case BEAM_COLD:                  return "cold";
    case BEAM_WATER:                 return "water";
    case BEAM_MAGIC:                 return "magic";
    case BEAM_ELECTRICITY:           return "electricity";
    case BEAM_MEPHITIC:              return "noxious fumes";
    case BEAM_POISON:                return "poison";
    case BEAM_NEG:                   return "negative energy";
    case BEAM_ACID:                  return "acid";
    case BEAM_LIGHT:                 return "light";
    case BEAM_MIASMA:                return "miasma";
    case BEAM_SPORE:                 return "spores";
    case BEAM_POISON_ARROW:          return "poison sting";
    case BEAM_DAMNATION:             return "damnation";
    case BEAM_STICKY_FLAME:          return "sticky fire";
    case BEAM_STEAM:                 return "steam";
    case BEAM_ENERGY:                return "energy";
    case BEAM_HOLY:                  return "cleansing flame";
    case BEAM_FOUL_FLAME:            return "foul flame";
    case BEAM_FRAG:                  return "fragments";
    case BEAM_LAVA:                  return "magma";
    case BEAM_ICE:                   return "ice";
    case BEAM_THUNDER:               return "thunder";
    case BEAM_STUN_BOLT:             return "stunning bolt";
    case BEAM_DESTRUCTION:           return "destruction";
    case BEAM_RANDOM:                return "random";
    case BEAM_CHAOS:                 return "chaos";
    case BEAM_SLOW:                  return "slow";
    case BEAM_HASTE:                 return "haste";
    case BEAM_MIGHT:                 return "might";
    case BEAM_HEALING:               return "healing";
    case BEAM_PARALYSIS:             return "paralysis";
    case BEAM_CONFUSION:             return "confusion";
    case BEAM_INVISIBILITY:          return "invisibility";
    case BEAM_DIGGING:               return "digging";
    case BEAM_TELEPORT:              return "teleportation";
    case BEAM_POLYMORPH:             return "polymorph";
    case BEAM_MALMUTATE:             return "malmutation";
    case BEAM_CHARM:                 return "charming";
    case BEAM_BANISH:                return "banishment";
    case BEAM_PAIN:                  return "pain";
    case BEAM_AGONY:                 return "agony";
    case BEAM_CURSE_OF_AGONY:        return "curse of agony";
    case BEAM_DISPEL_UNDEAD:         return "dispel undead";
    case BEAM_MINDBURST:             return "mindburst";
    case BEAM_BLINK:                 return "blink";
    case BEAM_BLINK_CLOSE:           return "blink close";
    case BEAM_BECKONING:             return "beckoning";
    case BEAM_PETRIFY:               return "petrify";
    case BEAM_CORONA:                return "backlight";
    case BEAM_PORKALATOR:            return "porkalator";
    case BEAM_HIBERNATION:           return "hibernation";
    case BEAM_SLEEP:                 return "sleep";
    case BEAM_BERSERK:               return "berserk";
    case BEAM_VISUAL:                return "visual effects";
    case BEAM_TORMENT_DAMAGE:        return "torment damage";
    case BEAM_AIR:                   return "air";
    case BEAM_INNER_FLAME:           return "inner flame";
    case BEAM_PETRIFYING_CLOUD:      return "calcifying dust";
    case BEAM_ENSNARE:               return "magic web";
    case BEAM_SENTINEL_MARK:         return "sentinel's mark";
    case BEAM_DIMENSION_ANCHOR:      return "dimension anchor";
    case BEAM_VULNERABILITY:         return "vulnerability";
    case BEAM_MALIGN_OFFERING:       return "malign offering";
    case BEAM_VIRULENCE:             return "virulence";
    case BEAM_AGILITY:               return "agility";
    case BEAM_SAP_MAGIC:             return "sap magic";
    case BEAM_DRAIN_MAGIC:           return "drain magic";
    case BEAM_DIMINISH_SPELLS:       return "diminish spells";
    case BEAM_TUKIMAS_DANCE:         return "tukima's dance";
    case BEAM_DEATH_RATTLE:          return "breath of the dead";
    case BEAM_RESISTANCE:            return "resistance";
    case BEAM_UNRAVELLING:           return "unravelling";
    case BEAM_UNRAVELLED_MAGIC:      return "unravelled magic";
    case BEAM_SHARED_PAIN:           return "shared pain";
    case BEAM_IRRESISTIBLE_CONFUSION:return "confusion";
    case BEAM_INFESTATION:           return "infestation";
    case BEAM_VILE_CLUTCH:           return "vile clutch";
    case BEAM_VAMPIRIC_DRAINING:     return "vampiric draining";
    case BEAM_CONCENTRATE_VENOM:     return "concentrate venom";
    case BEAM_ENFEEBLE:              return "enfeeble";
    case BEAM_SOUL_SPLINTER:         return "soul splinter";
    case BEAM_ROOTS:                 return "roots";
    case BEAM_VITRIFY:               return "vitrification";
    case BEAM_VITRIFYING_GAZE:       return "vitrification";
    case BEAM_WEAKNESS:              return "weakness";
    case BEAM_DEVASTATION:           return "devastation";
    case BEAM_UMBRAL_TORCHLIGHT:     return "umbral torchlight";
    case BEAM_CRYSTALLISING:         return "crystallising";
    case BEAM_WARPING:               return "spatial disruption";
    case BEAM_QAZLAL:                return "upheaval targetter";
    case BEAM_RIMEBLIGHT:            return "rimeblight";
    case BEAM_SHADOW_TORPOR:         return "shadow torpor";
    case BEAM_HAEMOCLASM:            return "gore";
    case BEAM_BLOODRITE:             return "blood";
    case BEAM_DOUBLE_VIGOUR:         return "vigour-doubling";
    case BEAM_VEX:                   return "vexing";
    case BEAM_SEISMIC:               return "seismic shockwave";
    case BEAM_BOLAS:                 return "entwining bolas";
    case BEAM_MERCURY:               return "mercury";
    case BEAM_BAT_CLOUD:             return "cloud of bats";
    case BEAM_ILL_OMEN:              return "omen";
    case BEAM_WARP_BODY:             return "warp body";

    case NUM_BEAMS:                  die("invalid beam type");
    }
    die("unknown beam type");
}

string bolt::get_source_name() const
{
    if (!source_name.empty())
        return source_name;
    const actor *a = agent();
    if (a)
        return a->name(DESC_A, true);
    return "";
}

/**
 * Can this bolt potentially knock back a creature it hits?
 *
 * @param dam The damage dealt. If non-negative, check that dam > 0 for bolts
 *             like force lance that only push back upon damage.
 * @return True iff the bolt could knock something back.
*/
bool bolt::can_knockback(int dam) const
{
    switch (origin_spell)
    {
    case SPELL_PRIMAL_WAVE:
    case SPELL_MUD_BREATH:
        return true;
    case SPELL_FORCE_LANCE:
    case SPELL_ISKENDERUNS_MYSTIC_BLAST:
        return dam != 0;
    default:
        return false;
    }
}

/**
 * Can this bolt pull an actor?
 *
 * If a bolt is capable of pulling actors and the given actor can be pulled,
 * return true. May print messages.
 *
 * @param act The target actor. Check if the actor is non-stationary and not
 *            already adjacent.
 * @param dam The damage dealt. Check that dam > 0.
 * @return True if the bolt could pull the actor, false otherwise.
*/
bool bolt::can_pull(const actor &act, int dam) const
{
    if (act.is_stationary() || adjacent(source, act.pos()))
        return false;

    return origin_spell == SPELL_HARPOON_SHOT && dam;
}

ai_action::goodness targeting_tracer::good_to_fire(int foe_ratio) const
{
    // Use of foeRatio:
    // The higher this number, the more monsters will _avoid_ collateral
    // damage to their friends.
    // Setting this to zero will in fact have all monsters ignore their
    // friends when considering collateral damage.

    // Quick check - did we in fact hit anything?
    if (foe_info.count == 0)
        return ai_action::neutral();

    // If a friendly harmed a player in any way, always abort.
    if (abort_for_player)
        return ai_action::bad();

    const int total_pow = foe_info.power + friend_info.power;
    // Only fire if they do acceptably low collateral damage.
    if (!friend_info.power
        || foe_info.power >= div_round_up(foe_ratio * total_pow, 100))
    {
        return ai_action::good();
    }
    return ai_action::bad();
}

void clear_zap_info_on_exit()
{
    for (const zap_info &zap : zap_data)
    {
        delete zap.player_damage;
        delete zap.player_tohit;
        delete zap.monster_damage;
        delete zap.monster_tohit;
    }
}

int ench_power_stepdown(int pow)
{
    return stepdown(pow, 35);
}

/// Translate a given ench power to a duration, in aut, and randomize it.
int _ench_pow_to_dur(int pow)
{
    // ~15 turns at 25 pow, ~21 turns at 50 pow, ~27 turns at 100 pow
    int base_dur = stepdown(pow * BASELINE_DELAY, 70);

    return div_rand_round(base_dur, 2) + random2(1 + base_dur);
}

/**
 * Given some shield value, what is the chance that omnireflect will activate
 * on an AUTOMATIC_HIT attack?
 *
 * E.g., if 40 is returned, there is a SH in 40 chance of a given attack being
 * reflected.
 *
 * @param SH        The SH (shield) value of the omnireflect user.
 * @return          A denominator to the chance of omnireflect activating.
 */
int omnireflect_chance_denom(int SH)
{
    return SH + 20;
}

/// Set up a beam aiming from the given monster to their target.
bolt setup_targeting_beam(const monster &mons)
{
    bolt beem;

    beem.source    = mons.pos();
    beem.target    = mons.target;
    beem.source_id = mons.mid;

    return beem;
}

bool player_beam_tracer::has_hit_foe() noexcept
{
    return foe_count != 0;
}

bool player_beam_tracer::should_stop() noexcept
{
    return bad_attack_targets.empty() && bad_charm_target == nullptr
           && god_hated_target == nullptr;
}

void player_beam_tracer::actor_affected(bool friendly_fire, int power) noexcept
{
    UNUSED(power);
    if (!friendly_fire)
        ++foe_count;
}

void player_beam_tracer::player_hit(bool /*was_friendly*/) noexcept
{
    hit_self_count++;
}

void player_beam_tracer::monster_hit(const bolt& beam, const monster& mon)
{
    if (!you.can_see(mon))
        return;

    if (beam.is_harmless(&mon))
        return;

    bool penance = false;
    string adj, suffix;
    if (bad_attack(&mon, adj, suffix, penance))
    {
        bool found = false;
        for (bad_target target : bad_attack_targets)
        {
            if (target.mon == &mon)
            {
                found = true;
                break;
            }
        }
        if (!found)
        {
            bad_attack_targets.push_back({ &mon, std::move(adj),
                                           std::move(suffix), penance });
        }
    }
    else if (_stop_because_god_hates_target_prompt(&mon, beam.origin_spell, true))
        god_hated_target = &mon;
    // Handle charming monsters when a nasty dur is up: give a prompt for
    // attempting to charm monsters that might be affected.
    else if (beam.flavour == BEAM_CHARM)
    {
        monclass_flags_t flags = monster_info(&mon).airborne() ? M_FLIES
            : M_NO_FLAGS;
        resists_t mon_resists = monster_info(&mon).resists();
        const string reason = stop_summoning_reason(mon_resists, flags);
        if (!reason.empty())
            bad_charm_target = &mon;
    }
}

void player_beam_tracer::blocked(string message) noexcept
{
    blocked_message = std::move(message);
    blocked_count++;
}

// Returns true if there is anything this tracer might possibly want to prompt
// the player about.
bool player_beam_tracer::has_any_warnings() noexcept
{
    return blocked_count > 0
            || god_hated_target
            || bad_charm_target
            || hit_self_count > 0
            || !bad_attack_targets.empty();
}

bool targeting_tracer::has_hit_foe() noexcept
{
    return foe_info.count != 0;
}

void targeting_tracer::actor_affected(bool friendly_fire, int power) noexcept
{
    if (friendly_fire)
    {
        friend_info.count++;
        friend_info.power += power;
    }
    else
    {
        foe_info.count++;
        foe_info.power += power;
    }
}

void targeting_tracer::player_hit(bool was_friendly) noexcept
{
    if (was_friendly)
        abort_for_player = true;
    else
        hurt_player_count++;
}

int targeting_tracer::player_hit_count() noexcept
{
    return hurt_player_count;
}

// returns true if the player wishes to cancel firing the bolt, false otherwise
bool cancel_beam_prompt(const bolt& beam, const player_beam_tracer& tracer,
                        int beams_fired)
{
    ASSERT(beams_fired >= tracer.blocked_count);
    if (tracer.blocked_count >= beams_fired)
    {
        mpr(tracer.blocked_message);
        return true;
    }

    const spell_type spell = beam.origin_spell;

    if (tracer.god_hated_target
        && _stop_because_god_hates_target_prompt(tracer.god_hated_target,
                                                 spell))
    {
        return true;
    }

    if (tracer.bad_charm_target)
    {
        const monster* mon = tracer.bad_charm_target;
        monclass_flags_t flags = monster_info(mon).airborne() ? M_FLIES
                                                              : M_NO_FLAGS;
        const string verb = make_stringf("charm %s",
                                         mon->name(DESC_THE).c_str());
        if (stop_summoning_prompt(monster_info(mon).resists(), flags, verb))
            return true;
    }

    if (tracer.hit_self_count > 0)
    {
        string prompt;
        if (tracer.hit_self_count > 1)
        {
            prompt = make_stringf("That %s could hit you up to %d times."
                " Continue anyway?",
                beam.ranged_atk ? beam.ranged_atk->projectile_name().c_str() : "beam",
                tracer.hit_self_count);
        }
        else
        {
            prompt = make_stringf("That %s is likely to hit you."
                " Continue anyway?",
                beam.ranged_atk ? beam.ranged_atk->projectile_name().c_str() : "beam");
        }
        if (!yesno(prompt.c_str(), false, 'n'))
        {
            canned_msg(MSG_OK);
            return true;
        }
    }

    if (!tracer.bad_attack_targets.empty())
    {
        attacked_monster_list victims;
        for (auto& target : tracer.bad_attack_targets)
            victims.add(*target.mon, target.adj, target.suffix, target.penance);

        string mon_name = victims.describe();
        string verb = "fire ";
        if (tracer.bad_attack_targets.size() == 1)
        {
            const monster* mon = tracer.bad_attack_targets[0].mon;
            if (beam.target == mon->pos() && spell != SPELL_STARBURST)
                verb += "at ";
            else
            {
                verb += "in " + apostrophise(mon_name) + " direction";
                mon_name = "";
            }
        }
        else
            verb += "towards ";
        const bool penance = victims.penance();

        const string prompt = make_stringf("Really %s%s%s?%s",
                        verb.c_str(), mon_name.c_str(),
                        victims.suffix(),
                        penance ? " This would place you under penance!" : "");

        if (!yesno(prompt.c_str(), false, 'n'))
        {
            canned_msg(MSG_OK);
            return true;
        }
    }

    return false;
}

void bolt::set_is_tracer(bool value) noexcept
{
    if (!value)
    {
        tracer = nullptr;
        return;
    }

    static beam_tracer default_tracer;
    tracer = &default_tracer;
}

void bolt::do_ranged_attack(actor& targ)
{
    actor *ag = agent(true);
    // if the immediate agent is now dead, check to see if we can get a
    // usable agent by factoring in reflections.
    // At this point, it is possible that the agent is the dummy monster
    // associated with YOU_FAULTLESS. This case will cause
    // "INVALID YOU_FAULTLESS" to show up in dprfs and mess up the to-hit,
    // but it otherwise works.
    // TODO: is there a good way of handling the to-hit correctly? (And why
    // should the to-hit be affected by reflections at all?)
    // An alternative would be to stop the missile at this point.
    if (!ag)
        ag = agent(false);
    // if that didn't work, blanket fall back on YOU_FAULTLESS. This covers
    // a number of other weird penetration cases.
    if (!ag)
        ag = &env.mons[YOU_FAULTLESS];
    ASSERT(ag);

    // XXX: Use this to track if the player was firing in the direction of a
    //      real foe, for purposes of Coglin Rev and similar effects.
    //      (This takes advantage of the fact that foes_hurt is not used for
    //      any other purpose by ranged attack beams, but this is maybe a little
    //      brittle...)
    if (ag->is_player() && !targ.wont_attack() && !targ.is_firewood())
        foes_hurt++;

    ranged_attack attk(ag, &targ, ranged_atk->weapon, use_target_as_pos, agent());
    attk.will_mulch = ranged_atk->will_mulch;

    attk.attack();
    // XXX: hit_verb is used later to make Damnation bolts only explode on it.
    if (attk.ev_margin >= 0 && hit_verb.empty())
        hit_verb = attk.attack_verb;
    if (attk.reflected)
        reflect();
    extra_range_used += attk.range_used;
    if (attk.did_net())
        drop_item = false;
}

// Returns the effective willpower an actor with a given willpower would have
// against hexes from a given source.
int apply_willpower_bypass(const actor& source, int willpower)
{
    if (source.wearing_ego(OBJ_ARMOUR, SPARM_GUILE))
    {
        if (source.is_monster())
            willpower = max(0, willpower - 2 * WL_PIP);
        else
            willpower = max(0, willpower - guile_will_reduction());
    }

    if (source.is_player() && you.form == transformation::sphinx)
        willpower = max(0, willpower - WL_PIP);

    return willpower;
}

int apply_willpower_bypass(const monster_info& source, int willpower)
{
    if (source.inv[MSLOT_SHIELD]
        && get_armour_ego_type(*source.inv[MSLOT_SHIELD]) == SPARM_GUILE)
    {
        willpower = max(0, willpower - 2 * WL_PIP);
    }

    return willpower;
}

int guile_will_reduction(bool max)
{
    const int skill = max ? 27 : you.skill(SK_EVOCATIONS);
    return 20 + stepdown(skill * 7 / 2, 75);
}
