/**
 * @file player_reacts.cc
 * @brief Player functions called every turn, mostly handling enchantment durations/expirations.
 **/

#include "AppHdr.h"

#include "player-reacts.h"

#include <algorithm>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <list>
#include <sstream>
#include <string>

#ifndef TARGET_OS_WINDOWS
# include <langinfo.h>
#endif
#include <fcntl.h>
#ifdef USE_UNIX_SIGNALS
#include <csignal>
#endif

#include "abyss.h" // abyss_maybe_spawn_xp_exit
#include "act-iter.h"
#include "areas.h"
#include "artefact.h"
#include "beam.h"
#include "cloud.h"
#include "clua.h"
#include "colour.h"
#include "coord.h"
#include "coordit.h"
#include "database.h"
#include "dbg-util.h"
#include "delay.h"
#ifdef DGL_SIMPLE_MESSAGING
#include "dgl-message.h"
#endif
#include "dlua.h"
#include "dungeon.h"
#include "env.h"
#include "evoke.h"
#include "exercise.h"
#include "files.h"
#include "god-abil.h"
#include "god-companions.h"
#include "god-conduct.h"
#include "god-passive.h"
#include "invent.h"
#include "item-prop.h"
#include "item-use.h"
#include "level-state-type.h"
#include "libutil.h"
#include "maps.h"
#include "message.h"
#include "mon-abil.h"
#include "mon-act.h"
#include "mon-cast.h"
#include "mon-death.h"
#include "mon-place.h"
#include "mon-tentacle.h"
#include "mon-util.h"
#include "mutation.h"
#include "nearby-danger.h"
#include "ouch.h"
#include "player.h"
#include "player-stats.h"
#include "random.h"
#include "religion.h"
#include "shopping.h"
#include "shout.h"
#include "skills.h"
#include "spl-cast.h"
#include "spl-clouds.h"
#include "spl-damage.h"
#include "spl-goditem.h"
#include "spl-selfench.h"
#include "spl-other.h"
#include "spl-summoning.h"
#include "spl-transloc.h"
#include "spl-util.h"
#include "state.h"
#include "status.h"
#include "stepdown.h"
#include "stringutil.h"
#include "terrain.h"
#ifdef USE_TILE
#include "rltiles/tiledef-dngn.h"
#include "tilepick.h"
#endif
#include "transform.h"
#include "traps.h"
#include "travel.h"
#include "view.h"
#include "xom.h"
#include "zot.h" // bezotting

/**
 * Decrement a duration by the given delay.

 * The midloss value should be either 0 or a number of turns where the delay
 * from those turns at normal speed is less than the duration's midpoint. The
 * use of midloss prevents the player from knowing the exact remaining duration
 * when the midpoint message is displayed.
 *
 * @param dur The duration type to be decremented.
 * @param delay The delay aut amount by which to decrement the duration.
 * @param endmsg The message to be displayed when the duration ends.
 * @param midloss A number of normal-speed turns by which to further decrement
 *                the duration if we cross the duration's midpoint.
 * @param endmsg The message to be displayed when the duration is decremented
 *               to a value under its midpoint.
 * @param chan The channel where the endmsg will be printed if the duration
 *             ends.
 *
 * @return  True if the duration ended, false otherwise.
 */

static bool _decrement_a_duration(duration_type dur, int delay,
                                 const char* endmsg = nullptr,
                                 int exploss = 0,
                                 const char* expmsg = nullptr,
                                 msg_channel_type chan = MSGCH_DURATION)
{
    ASSERT(you.duration[dur] >= 0);
    if (you.duration[dur] == 0)
        return false;

    if (you.attempted_attack && duration_extended_by_attacks(dur))
        return false;

    ASSERT(!exploss || expmsg != nullptr);
    const int exppoint = duration_expire_point(dur);
    ASSERTM(!exploss || exploss * BASELINE_DELAY < exppoint,
            "expiration delay loss %d not less than duration expiration point %d",
            exploss * BASELINE_DELAY, exppoint);

    if (dur == DUR_SENTINEL_MARK && aura_is_active_on_player(OPHAN_MARK_KEY))
        return false;
    else if (dur == DUR_PHALANX_BARRIER && aura_is_active_on_player(PHALANX_BARRIER_KEY))
        return false;

    const int old_dur = you.duration[dur];
    you.duration[dur] -= delay;

    // Amulets of dissipation wear off negative statuses faster
    if (duration_dispellable(dur) && duration_negative(dur)
        && you.wearing_jewellery(AMU_DISSIPATION))
    {
        you.duration[dur] -= delay * you.wearing_jewellery(AMU_DISSIPATION);
    }

    // If we start expiring, handle exploss and print the exppoint message.
    if (you.duration[dur] <= exppoint && old_dur > exppoint)
    {
        you.duration[dur] -= exploss * BASELINE_DELAY;
        if (expmsg)
        {
            // Make sure the player has a turn to react to the midpoint
            // message.
            if (you.duration[dur] <= 0)
                you.duration[dur] = 1;
            if (need_expiration_warning(dur))
                mprf(MSGCH_DANGER, "Careful! %s", expmsg);
            else
                mprf(chan, "%s", expmsg);
        }
    }

    if (you.duration[dur] <= 0)
    {
        you.duration[dur] = 0;
        if (endmsg && *endmsg != '\0')
            mprf(chan, "%s", endmsg);
        return true;
    }

    return false;
}


static void _decrement_petrification(int delay)
{
    if (_decrement_a_duration(DUR_PETRIFIED, delay))
    {
        you.redraw_armour_class = true;
        you.redraw_evasion = true;
        // implicit assumption: all races that can be petrified are made of
        // flesh when not petrified. (Unfortunately, species::skin_name doesn't
        // really work here..)
        const string flesh_equiv = get_form()->flesh_equivalent.empty() ?
                                            "flesh" :
                                            get_form()->flesh_equivalent;

        mprf(MSGCH_DURATION, "You turn to %s%s.",
             flesh_equiv.c_str(),
             you.paralysed() ? "" : " and can act again");
    }

    if (you.duration[DUR_PETRIFYING])
    {
        int &dur = you.duration[DUR_PETRIFYING];
        int old_dur = dur;
        if ((dur -= delay) <= 0)
        {
            dur = 0;
            you.fully_petrify();
        }
        else if (dur < 15 && old_dur >= 15)
            mpr("Your limbs are stiffening.");
    }
}

static void _decrement_sleep_and_daze(int delay)
{
    const bool break_sleep = _decrement_a_duration(DUR_SLEEP, delay);
    const bool break_daze = _decrement_a_duration(DUR_DAZED, delay);

    // Add a tiny bit of duration back to the effects we're breaking, so that
    // you.wake_up will see them and print appropriate messages.
    if (break_sleep)
        you.duration[DUR_SLEEP] = 1;
    if (break_daze)
        you.duration[DUR_DAZED] = 1;

    you.wake_up(break_sleep, break_daze);
}

static void _decrement_attraction(int delay)
{
    if (!you.duration[DUR_ATTRACTIVE])
        return;

    attract_monsters(delay);
    if (_decrement_a_duration(DUR_ATTRACTIVE, delay))
        mpr("You feel less attractive to monsters.");
}

static void _decrement_paralysis(int delay)
{
    if (!you.duration[DUR_PARALYSIS])
        return;

    _decrement_a_duration(DUR_PARALYSIS, delay);

    if (you.duration[DUR_PARALYSIS])
        return;

    you.give_stun_immunity(random_range(1, 3));

    mprf(MSGCH_DURATION, "You can act again.");
    you.redraw_armour_class = true;
    you.redraw_evasion = true;
}

/**
 * Check whether the player's ice (Ozocubu's) armour was melted this turn.
 * If so, print the appropriate message and clear the flag.
 */
static void _maybe_melt_armour()
{
    // We have to do the messaging here, because a simple wand of flame will
    // call _maybe_melt_player_enchantments twice. It also avoids duplicate
    // messages when melting because of several heat sources.
    if (you.props.exists(MELT_ARMOUR_KEY))
    {
        you.props.erase(MELT_ARMOUR_KEY);
        mprf(MSGCH_DURATION, "The heat melts your icy armour.");
    }
}

// Give a two turn grace period for the sprites to lose interest when no
// valid enemies are in sight (and reset it if enemies come into sight again)
static void _handle_jinxbite_interest()
{
    if (you.duration[DUR_JINXBITE])
    {
        if (!jinxbite_targets_available())
        {
            if (!you.duration[DUR_JINXBITE_LOST_INTEREST])
                you.duration[DUR_JINXBITE_LOST_INTEREST] = 20;
        }
        else
            you.duration[DUR_JINXBITE_LOST_INTEREST] = 0;
    }
}

/**
 * How much horror does the player character feel in the current situation?
 *
 * (For Ru's `MUT_COWARDICE` and for the Sacred Labrys.)
 *
 * Penalties are based on the "scariness" (threat level) of monsters currently
 * visible.
 */
int current_horror_level()
{
    int horror_level = 0;

    for (monster_near_iterator mi(&you, LOS_NO_TRANS); mi; ++mi)
    {
        if (mons_aligned(*mi, &you)
            || !mons_is_threatening(**mi)
            || mons_is_tentacle_or_tentacle_segment(mi->type))
        {
            continue;
        }

        const mon_threat_level_type threat_level = mons_threat_level(**mi);
        if (threat_level == MTHRT_NASTY)
            horror_level += 3;
        else if (threat_level == MTHRT_TOUGH)
            horror_level += 1;
    }
    return horror_level;
}

/**
 * What was the player's most recent horror level?
 *
 * (For Ru's MUT_COWARDICE.)
 */
static int _old_horror_level()
{
    if (you.duration[DUR_HORROR])
        return you.props[HORROR_PENALTY_KEY].get_int();
    return 0;
}

/**
 * When the player should no longer be horrified, end the DUR_HORROR if it
 * exists & cleanup the corresponding prop.
 */
static void _end_horror()
{
    if (!you.duration[DUR_HORROR])
        return;

    you.props.erase(HORROR_PENALTY_KEY);
    you.set_duration(DUR_HORROR, 0);
}

/**
 * Update penalties for cowardice based on the current situation, if the player
 * has Ru's MUT_COWARDICE.
 */
static void _update_cowardice()
{
    if (!you.has_mutation(MUT_COWARDICE))
    {
        // If the player somehow becomes sane again, handle that
        _end_horror();
        return;
    }

    // Subtract one from the horror level so that you don't get a message
    // when a single tough monster appears.
    const int horror_level = max(0, current_horror_level() - 1);

    if (horror_level <= 0)
    {
        // If you were horrified before & aren't now, clean up.
        _end_horror();
        return;
    }

    // Lookup the old value before modifying it
    const int old_horror_level = _old_horror_level();

    // as long as there's still scary enemies, keep the horror going
    you.props[HORROR_PENALTY_KEY] = horror_level;
    you.set_duration(DUR_HORROR, 1);

    // only show a message on increase
    if (horror_level <= old_horror_level)
        return;

    if (horror_level >= HORROR_LVL_OVERWHELMING)
        mpr("Monsters! Monsters everywhere! You have to get out of here!");
    else if (horror_level >= HORROR_LVL_EXTREME)
        mpr("You reel with horror at the sight of these foes!");
    else
        mpr("You feel a twist of horror at the sight of this foe.");
}

static void _update_claustrophobia()
{
    if (!you.has_bane(BANE_CLAUSTROPHOBIA))
        return;

    int count = 0;
    for (adjacent_iterator ai(you.pos()); ai; ++ai)
    {
        if (feat_is_solid(env.grid(*ai)))
            ++count;
    }

    you.props[CLAUSTROPHOBIA_KEY] = count * 2;
}

// Uskayaw piety decays incredibly fast, but only to a baseline level of *.
// Using Uskayaw abilities can still take you under *.
static void _handle_uskayaw_piety(int time_taken)
{
    if (you.props[USKAYAW_NUM_MONSTERS_HURT].get_int() > 0)
    {
        int num_hurt = you.props[USKAYAW_NUM_MONSTERS_HURT];
        int hurt_val = you.props[USKAYAW_MONSTER_HURT_VALUE];
        int piety_gain = max(num_hurt, stepdown_value(hurt_val, 5, 10, 20, 40));

        gain_piety(piety_gain);
        you.props[USKAYAW_AUT_SINCE_PIETY_GAIN] = 0;
    }
    else if (you.piety() > piety_breakpoint(0))
    {
        // If we didn't do a dance action and we can lose piety, we're going
        // to lose piety proportional to the time since the last time we took
        // a dance action and hurt a monster.
        int time_since_gain = you.props[USKAYAW_AUT_SINCE_PIETY_GAIN].get_int();
        time_since_gain += time_taken;

        // Only start losing piety if it's been a few turns since we gained
        // piety, in order to give more tolerance for missing in combat.
        if (time_since_gain > 30)
        {
            int piety_lost = min(you.piety() - piety_breakpoint(0),
                    div_rand_round(time_since_gain, 10));

            if (piety_lost > 0)
                lose_piety(piety_lost);

        }
        you.props[USKAYAW_AUT_SINCE_PIETY_GAIN] = time_since_gain;
    }

    // Re-initialize Uskayaw piety variables
    you.props[USKAYAW_NUM_MONSTERS_HURT] = 0;
    you.props[USKAYAW_MONSTER_HURT_VALUE] = 0;
}

static void _handle_uskayaw_time(int time_taken)
{
    _handle_uskayaw_piety(time_taken);

    int audience_timer = you.props[USKAYAW_AUDIENCE_TIMER].get_int();
    int bond_timer = you.props[USKAYAW_BOND_TIMER].get_int();

    // For the timered abilities, if we set the timer to -1, that means we
    // need to trigger the abilities this turn. Otherwise we'll decrement the
    // timer down to a minimum of 0, at which point it becomes eligible to
    // trigger again.
    if (audience_timer == -1 || (you.piety() >= piety_breakpoint(2)
            && x_chance_in_y(time_taken, 100 + audience_timer)))
    {
        uskayaw_prepares_audience();
    }
    else
        you.props[USKAYAW_AUDIENCE_TIMER] = max(0, audience_timer - time_taken);

    if (bond_timer == -1 || (you.piety() >= piety_breakpoint(3)
            && x_chance_in_y(time_taken, 100 + bond_timer)))
    {
        uskayaw_bonds_audience();
    }
    else
        you.props[USKAYAW_BOND_TIMER] =  max(0, bond_timer - time_taken);
}

static void _handle_hoarding()
{
    if (you.has_mutation(MUT_RENOUNCE_POTIONS))
    {
        if (you.hp < you.hp_max / 2)
            you.props.erase(RENOUNCE_POTIONS_TIMER_KEY);
        else if (there_are_monsters_nearby(true, true, false))
            you.props[RENOUNCE_POTIONS_TIMER_KEY].get_int() = you.elapsed_time + 60;
        else if (you.elapsed_time > you.props[RENOUNCE_POTIONS_TIMER_KEY].get_int())
            you.props.erase(RENOUNCE_POTIONS_TIMER_KEY);
    }

    if (you.has_mutation(MUT_RENOUNCE_SCROLLS))
    {
        if (you.hp <= you.hp_max / 2)
            you.props.erase(RENOUNCE_SCROLLS_TIMER_KEY);
        else if (there_are_monsters_nearby(true, true, false))
            you.props[RENOUNCE_SCROLLS_TIMER_KEY].get_int() = you.elapsed_time + 60;
        else if (you.elapsed_time > you.props[RENOUNCE_SCROLLS_TIMER_KEY].get_int())
            you.props.erase(RENOUNCE_SCROLLS_TIMER_KEY);
    }
}

/**
 * Player reactions after monster and cloud activities in the turn are finished.
 */
void player_reacts_to_monsters()
{
    // In case Maurice managed to steal a needed item for example.
    if (!you_are_delayed())
        update_can_currently_train();

    check_monster_detect();

    if (have_passive(passive_t::detect_items) || you.has_mutation(MUT_JELLY_GROWTH)
        || you.get_mutation_level(MUT_TREASURE_SENSE) > 0)
    {
        detect_items(-1);
    }

    _decrement_a_duration(DUR_STUN_IMMUNITY, you.time_taken);
    _decrement_attraction(you.time_taken);
    _decrement_paralysis(you.time_taken);
    _decrement_petrification(you.time_taken);
    _decrement_sleep_and_daze(you.time_taken);

    if (_decrement_a_duration(DUR_CONSTRICTED, you.time_taken)
        && you.is_constricted())
    {
        if (you.constricted_type == CONSTRICT_ROOTS)
            mprf("The roots around you sink back into the ground.");
        else if (you.constricted_type == CONSTRICT_BVC)
            mprf("The zombie hands holding you return to the earth.");

        you.stop_being_constricted(true);
    }

    _handle_jinxbite_interest();

    sphinx_check_riddle();

    // If you have signalled your allies to stop attacking, cancel this order
    // once there are no longer any enemies in view for 50 consecutive aut
    if (you.pet_target == MHITYOU)
    {
        // Reset the timer if there are hostiles in sight
        if (there_are_monsters_nearby(true, true, false))
            you.duration[DUR_ALLY_RESET_TIMER] = 0;
        else
        {
            if (!you.duration[DUR_ALLY_RESET_TIMER])
                you.duration[DUR_ALLY_RESET_TIMER] = 50;
            else if (_decrement_a_duration(DUR_ALLY_RESET_TIMER, you.time_taken))
                you.pet_target = MHITNOT;
        }
    }

    // Expire Blood For Blood much faster when there are no enemies around for
    // the horde to fight (but not at the tail end of its duration, which could
    // otherwise make the expiry message misleading)
    if (you.duration[DUR_BLOOD_FOR_BLOOD] > 40
        && !there_are_monsters_nearby(true, true, false))
    {
        you.duration[DUR_BLOOD_FOR_BLOOD] -= you.time_taken * 3;
        if (you.duration[DUR_BLOOD_FOR_BLOOD] < 1)
            you.duration[DUR_BLOOD_FOR_BLOOD] = 1;
    }

    if (you.duration[DUR_STARDUST_COOLDOWN]
        && you.magic_points == you.max_magic_points
        && !there_are_monsters_nearby(true, true, false))
    {
        if (_decrement_a_duration(DUR_STARDUST_COOLDOWN, you.time_taken))
            mprf(MSGCH_DURATION, "Your orb has finished recharging its magic.");
    }


    if (_decrement_a_duration(DUR_PHALANX_BARRIER, you.time_taken))
        you.redraw_armour_class = true;

    _maybe_melt_armour();
    _update_cowardice();
    _update_claustrophobia();
    if (you_worship(GOD_USKAYAW))
        _handle_uskayaw_time(you.time_taken);

    announce_beogh_conversion_offer();

    if (player_in_branch(BRANCH_ARENA) && !okawaru_duel_active())
        okawaru_end_duel();

    if (you.duration[DUR_BLINKITIS])
    {
        uncontrolled_blink(false, 3);
        ouch(roll_dice(2, 2), KILLED_BY_BLINKING);
    }

    _decrement_a_duration(DUR_AUTODODGE, you.time_taken);

    _handle_hoarding();

    you.props.erase(PYROMANIA_TRIGGERED_KEY);
    you.shouted_pos.reset();

    // Cloud spreading or monster actions may have changed the player's LoS,
    // so check these again now.
    you.update_beholders();
    you.update_fearmongers();
}

static bool _check_recite()
{
    if (you.is_silenced()
        || you.paralysed()
        || you.confused()
        || you.asleep()
        || you.petrified()
        || you.berserk())
    {
        mprf(MSGCH_DURATION, "Your recitation is interrupted.");
        you.duration[DUR_RECITE] = 0;
        you.set_duration(DUR_RECITE_COOLDOWN, 1 + random2(10) + random2(30));
        return false;
    }
    return true;
}


static void _handle_recitation(int step)
{
    mprf("\"%s\"",
         zin_recite_text(you.attribute[ATTR_RECITE_SEED],
                         you.attribute[ATTR_RECITE_TYPE], step).c_str());

    if (apply_area_visible(zin_recite_to_single_monster, you.pos()))
    {
        viewwindow();
        update_screen();
    }

    // Recite trains more than once per use, because it has a
    // long timer in between uses and actually takes up multiple
    // turns.
    practise_using_ability(ABIL_ZIN_RECITE);

    noisy(you.shout_volume(), you.pos());

    if (step == 0)
    {
        ostringstream speech;
        speech << zin_recite_text(you.attribute[ATTR_RECITE_SEED],
                                  you.attribute[ATTR_RECITE_TYPE], -1);
        speech << '.';
        if (one_chance_in(27))
        {
            const string closure = getSpeakString("recite_closure");
            if (!closure.empty())
                speech << ' ' << closure;
        }
        mprf(MSGCH_DURATION, "You finish reciting %s", speech.str().c_str());
        you.set_duration(DUR_RECITE_COOLDOWN, 1 + random2(10) + random2(30));
    }
}

/**
 * Try to respawn the player's ancestor, if possible.
 */
static void _try_to_respawn_ancestor()
{
    monster *ancestor = create_monster(hepliaklqana_ancestor_gen_data());
    if (!ancestor)
        return;

    mprf("%s emerges from the mists of memory!",
         ancestor->name(DESC_YOUR).c_str());
    add_companion(ancestor);
    place_cloud(CLOUD_MIST, ancestor->pos(), random_range(1,2), ancestor); // ;)
}

static void _decrement_transform_duration(int delay)
{
    if (you.form == you.default_form || you.form == transformation::flux)
        return;

    // FIXME: [ds] Remove this once we've ensured durations can never go < 0?
    if (you.duration[DUR_TRANSFORMATION] <= 0
        && you.form != transformation::none)
    {
        you.duration[DUR_TRANSFORMATION] = 1;
    }

    if (form_can_fly()
        || form_can_swim() && feat_is_water(env.grid(you.pos())))
    {
        // Disable emergency flight if it was active
        you.props.erase(EMERGENCY_FLIGHT_KEY);
    }
    if (_decrement_a_duration(DUR_TRANSFORMATION, delay, nullptr, random2(3),
                                "Your transformation is almost over."))
    {
        return_to_default_form();
    }
}

static void _decrement_rampage_heal_duration(int delay)
{
    const int heal = you.props[RAMPAGE_HEAL_KEY].get_int();
    if (heal > 0 && _decrement_a_duration(DUR_RAMPAGE_HEAL, delay))
    {
        you.props[RAMPAGE_HEAL_KEY] = heal - 1;
        reset_rampage_heal_duration();
    }
}

static void _handle_trickster_decay(int delay)
{
    if (you.duration[DUR_TRICKSTER_GRACE] || delay == 0)
        return;

    if (!you.props.exists(TRICKSTER_POW_KEY))
        return;

    int& stacks = you.props[TRICKSTER_POW_KEY].get_int();

    // Decay at a rate of ~1 AC per 30 aut.
    const int reduction = div_rand_round(3, delay);
    stacks -= reduction;
    if (stacks <= 0)
    {
        you.props.erase(TRICKSTER_POW_KEY);
        mprf(MSGCH_DURATION, "You feel your existence waver again.");
    }

    if (reduction > 0)
        you.redraw_armour_class = true;
}

/**
 * Take a 'simple' duration, decrement it, and print messages as appropriate
 * when it hits 50% and 0% remaining.
 *
 * @param dur       The duration in question.
 * @param delay     How much to decrement the duration by.
 */
static void _decrement_simple_duration(duration_type dur, int delay)
{
    if (_decrement_a_duration(dur, delay, duration_end_message(dur),
                             duration_expire_offset(dur),
                             duration_expire_message(dur),
                             duration_expire_chan(dur)))
    {
        duration_end_effect(dur);
    }
}


/**
 * Decrement player durations based on how long the player's turn lasted in aut.
 */
static void _decrement_durations()
{
    const int delay = you.time_taken;

    if (you.duration[DUR_STICKY_FLAME])
        dec_sticky_flame_player(delay);

    const bool melted = you.props.exists(MELT_ARMOUR_KEY);
    if (_decrement_a_duration(DUR_ICY_ARMOUR, delay,
                              "Your icy armour evaporates.",
                              melted ? 0 : coinflip(),
                              melted ? nullptr
                              : "Your icy armour starts to melt."))
    {
        if (you.props.exists(ICY_ARMOUR_KEY))
            you.props.erase(ICY_ARMOUR_KEY);
        you.redraw_armour_class = true;
    }

    // Possible reduction of silence radius.
    if (you.duration[DUR_SILENCE])
        invalidate_agrid();
    // and liquefying radius.
    if (you.duration[DUR_LIQUEFYING])
        invalidate_agrid();

    _decrement_transform_duration(delay);

    if (you.attribute[ATTR_SWIFTNESS] >= 0)
    {
        if (_decrement_a_duration(DUR_SWIFTNESS, delay,
                                  "You feel sluggish.", coinflip(),
                                  "You start to feel a little slower."))
        {
            // Start anti-swiftness.
            you.duration[DUR_SWIFTNESS] = you.attribute[ATTR_SWIFTNESS];
            you.attribute[ATTR_SWIFTNESS] = -1;
        }
    }
    else
    {
        if (_decrement_a_duration(DUR_SWIFTNESS, delay,
                                  "You no longer feel sluggish.", coinflip(),
                                  "You start to feel a little faster."))
        {
            you.attribute[ATTR_SWIFTNESS] = 0;
        }
    }

    // Decrement Powered By Death strength
    int pbd_str = you.props[POWERED_BY_DEATH_KEY].get_int();
    if (pbd_str > 0 && _decrement_a_duration(DUR_POWERED_BY_DEATH, delay))
    {
        you.props[POWERED_BY_DEATH_KEY] = pbd_str - 1;
        reset_powered_by_death_duration();
    }

    _decrement_rampage_heal_duration(delay);

    dec_ambrosia_player(delay);
    dec_channel_player(delay);
    dec_slow_player(delay);
    dec_berserk_recovery_player(delay);
    dec_haste_player(delay);

    // Leak piety from the piety pool into actual piety.
    // Note that changes of religious status without corresponding actions
    // (killing monsters, offering items, ...) might be confusing for characters
    // of other religions.
    // For now, though, keep information about what happened hidden.
    if (you.raw_piety < MAX_PIETY && you.duration[DUR_PIETY_POOL] > 0
        && one_chance_in(5))
    {
        you.duration[DUR_PIETY_POOL]--;
        gain_piety(1, 1);

#if defined(DEBUG_DIAGNOSTICS) || defined(DEBUG_SACRIFICE) || defined(DEBUG_PIETY)
        mprf(MSGCH_DIAGNOSTICS, "Piety increases by 1 due to piety pool.");

        if (you.duration[DUR_PIETY_POOL] == 0)
            mprf(MSGCH_DIAGNOSTICS, "Piety pool is now empty.");
#endif
    }

    if (you.duration[DUR_DISJUNCTION])
        disjunction_spell();

    // Should expire before flight.
    if (you.duration[DUR_VORTEX])
    {
        polar_vortex_damage(&you, min(delay, you.duration[DUR_VORTEX]));
        if (_decrement_a_duration(DUR_VORTEX, delay,
                                  "The winds around you start to calm down."))
        {
            you.duration[DUR_VORTEX_COOLDOWN] = random_range(35, 45);
        }
    }

    if (you.duration[DUR_FORTRESS_BLAST_TIMER])
    {
        if (you.pos() != you.props[FORTRESS_BLAST_POS_KEY].get_coord())
        {
            mprf(MSGCH_DURATION, "Your fortress blast dissipates harmlessly.");
            you.duration[DUR_FORTRESS_BLAST_TIMER] = 0;
        }
        else
        {
            you.duration[DUR_FORTRESS_BLAST_TIMER] -= you.time_taken;
            if (you.duration[DUR_FORTRESS_BLAST_TIMER] <= 0)
            {
                unleash_fortress_blast(you);
                you.duration[DUR_FORTRESS_BLAST_TIMER] = 0;
            }
        }
    }

    if (you.duration[DUR_FLIGHT])
    {
        if (!you.permanent_flight())
        {
            if (_decrement_a_duration(DUR_FLIGHT, delay, nullptr, random2(6),
                                      "You are starting to lose your buoyancy."))
            {
                land_player();
            }
            else
            {
                // Disable emergency flight if it was active
                you.props.erase(EMERGENCY_FLIGHT_KEY);
            }
        }
        else if ((you.duration[DUR_FLIGHT] -= delay) <= 0)
        {
            // Just time out potions/spells/miscasts.
            you.duration[DUR_FLIGHT] = 0;
            you.props.erase(EMERGENCY_FLIGHT_KEY);
        }
    }

    if (you.duration[DUR_DEATHS_DOOR]
        && you.attribute[ATTR_DEATHS_DOOR_HP] > 0
        && you.hp > you.attribute[ATTR_DEATHS_DOOR_HP])
    {
        set_hp(you.attribute[ATTR_DEATHS_DOOR_HP]);
        you.redraw_hit_points = true;
    }
    else if (!you.duration[DUR_DEATHS_DOOR]
             && you.attribute[ATTR_DEATHS_DOOR_HP] > 0)
    {
        you.attribute[ATTR_DEATHS_DOOR_HP] = 0;
    }

    if (_decrement_a_duration(DUR_CLOUD_TRAIL, delay,
            "Your trail of clouds dissipates."))
    {
        you.props.erase(XOM_CLOUD_TRAIL_TYPE_KEY);
    }

    _decrement_a_duration(DUR_DETONATION_CATALYST, delay,
        "Your catalyst becomes inert.");

    if (you.duration[DUR_FLOODED])
    {
        // eg: if you turned into a tree.
        if (you.res_water_drowning())
            you.duration[DUR_FLOODED] = 0;
        else if (_decrement_a_duration(DUR_FLOODED, delay))
        {
            mprf(MSGCH_RECOVERY, "You finish coughing all the %s out of your lungs.",
                 you.props[WATER_HOLD_SUBSTANCE_KEY].get_string().c_str());
            you.duration[DUR_FLOODED_IMMUNITY] = you.time_taken + 1;
        }
        else
        {
            mprf(MSGCH_WARN, "Your lungs strain for air.");
            const int dmg = roll_dice(2, 5);
            ouch(div_rand_round(dmg * delay, BASELINE_DELAY), KILLED_BY_WATER,
                                                you.props[WATER_HOLDER_KEY].get_int());
        }
    }

    if (you.duration[DUR_FLAYED])
    {
        bool near_ghost = false;
        for (monster_iterator mi; mi; ++mi)
        {
            if (mi->type == MONS_FLAYED_GHOST && !mi->wont_attack()
                && you.see_cell(mi->pos()))
            {
                near_ghost = true;
                break;
            }
        }
        if (!near_ghost)
        {
            if (_decrement_a_duration(DUR_FLAYED, delay))
                heal_flayed_effect(&you);
        }
        else if (you.duration[DUR_FLAYED] < 80)
            you.duration[DUR_FLAYED] += div_rand_round(delay, 2);
    }

    if (you.duration[DUR_TOXIC_RADIANCE])
        toxic_radiance_effect(&you, min(delay, you.duration[DUR_TOXIC_RADIANCE]));

    if (you.duration[DUR_FATHOMLESS_SHACKLES])
        yred_fathomless_shackles_effect(min(delay, you.duration[DUR_FATHOMLESS_SHACKLES]));

    if (you.duration[DUR_RECITE] && _check_recite())
    {
        const int old_recite =
            (you.duration[DUR_RECITE] + BASELINE_DELAY - 1) / BASELINE_DELAY;
        _decrement_a_duration(DUR_RECITE, delay);
        const int new_recite =
            (you.duration[DUR_RECITE] + BASELINE_DELAY - 1) / BASELINE_DELAY;
        if (old_recite != new_recite)
            _handle_recitation(new_recite);
    }

    if (you.duration[DUR_DRAGON_CALL])
        do_dragon_call(delay);

    if (you.duration[DUR_INFERNAL_LEGION])
        makhleb_infernal_legion_tick(delay);

    if (you.duration[DUR_OBLIVION_HOWL])
        oblivion_howl(min(delay, you.duration[DUR_OBLIVION_HOWL]));

    dec_elixir_player(delay);
    dec_frozen_ramparts(delay);

    if (!you.cannot_act()
        && !you.confused())
    {
        extract_barbs(
            make_stringf("You %s the barbed spikes from your body.",
                you.berserk() ? "rip and tear" : "carefully extract").c_str());
    }

    if (you.wearing_jewellery(AMU_WILDSHAPE))
        did_god_conduct(DID_CHAOS, 1);

    if (you.wearing_ego(OBJ_ARMOUR, SPARM_DEATH))
        did_god_conduct(DID_EVIL, 1);

    if (!you.duration[DUR_ANCESTOR_DELAY]
        && have_passive(passive_t::frail)
        && hepliaklqana_ancestor() == MID_NOBODY)
    {
        _try_to_respawn_ancestor();
    }

    if (you.form == transformation::sun_scarab
        && !get_solar_ember()
        && you.elapsed_time >= you.props[SOLAR_EMBER_REVIVAL_KEY].get_int())
    {
        sun_scarab_spawn_ember(false);
    }

    const bool sanguine_armour_is_valid = sanguine_armour_valid();
    if (sanguine_armour_is_valid)
        activate_sanguine_armour();
    else if (!sanguine_armour_is_valid && you.duration[DUR_SANGUINE_ARMOUR])
        you.duration[DUR_SANGUINE_ARMOUR] = 1; // expire
    refresh_meek_bonus();

    if (you.props.exists(WU_JIAN_HEAVENLY_STORM_KEY))
    {
        ASSERT(you.duration[DUR_HEAVENLY_STORM]);
        wu_jian_heaven_tick();
    }

    if (you.duration[DUR_TEMP_CLOUD_IMMUNITY])
        _decrement_a_duration(DUR_TEMP_CLOUD_IMMUNITY, delay);

    if (you.duration[DUR_BLOOD_FOR_BLOOD])
        beogh_blood_for_blood_tick(delay);

    if (you.duration[DUR_CACOPHONY])
    {
        // Check if every haunted armour is already dead. If so, end the
        // effect early.
        bool found = false;
        for (monster_iterator mi; mi; ++mi)
        {
            if (mi->was_created_by(MON_SUMM_CACOPHONY))
            {
                found = true;
                break;
            }
        }
        if (!found)
        {
            mprf(MSGCH_DURATION, "As the last of your armour is driven back to "
                  "you, your cacophony ends.");
            you.duration[DUR_CACOPHONY] = 0;
        }
        else if (_decrement_a_duration(DUR_CACOPHONY, delay,
                "Your cacophony subsides and your armour settles down once more."))
        {
            for (monster_iterator mi; mi; ++mi)
                if (mi->was_created_by(MON_SUMM_CACOPHONY))
                    monster_die(**mi, KILL_RESET, NON_MONSTER, true);
        }
        else
            noisy(20, you.pos());
    }

    if (you.duration[DUR_FUSILLADE] && you.time_taken > 0)
        fire_fusillade();

    if (you.duration[DUR_CELEBRANT_COOLDOWN] && you.hp == you.hp_max)
    {
        mprf(MSGCH_DURATION, "You are ready to perform a blood rite again.");
        you.duration[DUR_CELEBRANT_COOLDOWN] = 0;
    }

    if (you.duration[DUR_TIME_WARPED_BLOOD_COOLDOWN] && you.hp == you.hp_max)
    {
        // Don't print it the message if the mutation is lost
        // before the cooldown wears off.
        if (you.get_mutation_level(MUT_TIME_WARPED_BLOOD))
            mprf(MSGCH_DURATION, "Your time-warped blood is ready to ripple again.");

        you.duration[DUR_TIME_WARPED_BLOOD_COOLDOWN] = 0;
    }

    if (you.duration[DUR_HIVE_COOLDOWN] && you.hp == you.hp_max)
    {
        mprf(MSGCH_DURATION, "The buzzing within you returns to its normal rhythm.");
        you.duration[DUR_HIVE_COOLDOWN] = 0;
    }

    if (you.duration[DUR_MEDUSA_COOLDOWN] && you.hp == you.hp_max)
    {
        mprf(MSGCH_DURATION, "You feel your defenses recover.");
        you.duration[DUR_MEDUSA_COOLDOWN] = 0;
    }

    if (you.duration[DUR_SPITEFUL_BLOOD_COOLDOWN] && you.hp == you.hp_max)
        you.duration[DUR_SPITEFUL_BLOOD_COOLDOWN] = 0;

    if (you.duration[DUR_EELJOLT_COOLDOWN] && you.hp == you.hp_max)
    {
        mprf(MSGCH_DURATION, "Your %s have fully recharged.", you.hand_name(true).c_str());
        you.duration[DUR_EELJOLT_COOLDOWN] = 0;
    }

    if (you.has_mutation(MUT_TRICKSTER))
        _handle_trickster_decay(delay);

    // these should be after decr_ambrosia, transforms, liquefying, etc.
    for (int i = 0; i < NUM_DURATIONS; ++i)
        if (duration_decrements_normally((duration_type) i))
            _decrement_simple_duration((duration_type) i, delay);
}

static void _handle_emergency_flight()
{
    ASSERT(you.props[EMERGENCY_FLIGHT_KEY].get_bool());

    if (!is_feat_dangerous(orig_terrain(you.pos()), true, false))
    {
        you.props.erase(EMERGENCY_FLIGHT_KEY);
        land_player();
    }
    else
    {
        const int drain = div_rand_round(15 * you.time_taken, BASELINE_DELAY);
        drain_player(drain, true, true);
    }
}

// Regeneration and Magic Regeneration items only work if the player has reached
// max hp/mp while they are being worn. This scans and updates such items when
// the player refills their hp/mp.
static void _maybe_attune_regen_items()
{
    bool attune_regen = you.check_hp_regen_attunement;
    bool attune_mana_regen = you.check_mp_regen_attunement;
    you.check_hp_regen_attunement = false;
    you.check_mp_regen_attunement = false;

    if (!attune_regen && !attune_mana_regen)
        return;

    vector<string> eq_list;
    bool plural = false;

    bool gained_regen = false;
    bool gained_mana_regen = false;
    bool gained_chemistry = false;

    for (player_equip_entry& entry : you.equipment.items)
    {
        if (entry.melded || entry.attuned || entry.is_overflow)
            continue;
        const item_def &arm = entry.get_item();

        if ((attune_regen && is_regen_item(arm)
             && (you.magic_points == you.max_magic_points || !is_mana_regen_item(arm)))
            || (attune_mana_regen && is_mana_regen_item(arm)
                && (you.hp == you.hp_max || !is_regen_item(arm))))
        {
            // Track which properties we should notify the player they have gained.
            if (!gained_regen && is_regen_item(arm))
                gained_regen = true;
            if (arm.is_type(OBJ_JEWELLERY, AMU_CHEMISTRY))
                gained_chemistry = true;
            else if (!gained_mana_regen && is_mana_regen_item(arm))
                gained_mana_regen = true;

            eq_list.push_back(is_artefact(arm) ? get_artefact_name(arm) :
                entry.slot == SLOT_AMULET ? "amulet" :
                    lowercase_string(equip_slot_name(entry.slot, true)));

            if (entry.slot == SLOT_BOOTS || entry.slot == SLOT_GLOVES)
                plural = true;
            entry.attuned = true;
        }
    }

    if (eq_list.empty())
        return;

    vector<string> msgs;

    if (gained_regen || gained_mana_regen)
    {
        msgs.emplace_back(make_stringf("regenerate%s more quickly",
                gained_regen && gained_mana_regen ? " health and magic"
                : (gained_regen ? "" : " magic")));
    }
    if (gained_chemistry)
        msgs.emplace_back("extract magic from the potions you drink");

    plural = plural || eq_list.size() > 1;
    string eq_str = comma_separated_line(eq_list.begin(), eq_list.end());
    string msg_str = comma_separated_line(msgs.begin(), msgs.end());
    mprf("Your %s attune%s to your body, and you begin to %s.",
         eq_str.c_str(), plural ? " themselves" : "s itself",
         msg_str.c_str());
}

// cjo: Handles player hp and mp regeneration. If the counter
// you.hit_points_regeneration is over 100, a loop restores 1 hp and decreases
// the counter by 100 (so you can regen more than 1 hp per turn). If the counter
// is below 100, it is increased by a variable calculated from delay,
// BASELINE_DELAY, and your regeneration rate. MP regeneration happens
// similarly, but the countup depends on delay, BASELINE_DELAY, and
// you.max_magic_points
static void _regenerate_hp_and_mp(int delay)
{
    if (crawl_state.disables[DIS_PLAYER_REGEN])
        return;

    const int old_hp = you.hp;
    const int old_mp = you.magic_points;

    // HP Regeneration
    if (!you.duration[DUR_DEATHS_DOOR])
    {
        const int base_val = player_regen();
        you.hit_points_regeneration += div_rand_round(base_val * delay, BASELINE_DELAY);
    }

    while (you.hit_points_regeneration >= 100)
    {
        // at low mp, "mana link" restores mp in place of hp
        if (you.has_mutation(MUT_MANA_LINK)
            && !x_chance_in_y(you.magic_points, you.max_magic_points))
        {
            inc_mp(1);
        }
        else // standard hp regeneration
            inc_hp(1);
        you.hit_points_regeneration -= 100;
    }

    ASSERT_RANGE(you.hit_points_regeneration, 0, 100);

    // MP Regeneration
    if (player_regenerates_mp())
    {
        if (you.magic_points < you.max_magic_points)
        {
            const int base_val = player_mp_regen();
            int mp_regen_countup = div_rand_round(base_val * delay, BASELINE_DELAY);
            you.magic_points_regeneration += mp_regen_countup;
        }

        while (you.magic_points_regeneration >= 100)
        {
            inc_mp(1);
            you.magic_points_regeneration -= 100;
        }

        ASSERT_RANGE(you.magic_points_regeneration, 0, 100);
    }
}

static void _handle_fugue(int delay)
{
    if (you.duration[DUR_FUGUE]
        && x_chance_in_y(you.props[FUGUE_KEY].get_int() * delay,
                         9 * BASELINE_DELAY)
        && !silenced(you.pos()))
    {
        // Keep the spam down
        if (you.props[FUGUE_KEY].get_int() < 3 || one_chance_in(5))
            mpr("The wailing of tortured souls fills the air!");
        noisy(spell_effect_noise(SPELL_FUGUE_OF_THE_FALLEN), you.pos());
    }
}

static void _do_eel_flavour_msg()
{
    // No time for play while there's enemies to zap!
    if (there_are_monsters_nearby(true, true, false))
        return;

    string msg;
    if (you.arm_count() == 1)
        msg = getSpeakString("eel hand solo actions");
    else
        msg = getSpeakString("eel hand actions");

    // XXX: Apologies for the ad hoc string replacement. We really need a
    //      centralised place to do this...
    msg = replace_all(msg, "@head@",
                        you.has_mutation(MUT_FORMLESS) ? "form" : "head");

    msg = replace_all(msg, "@skin@", species::skin_name(you.species).c_str());

    mprf(MSGCH_TALK, "%s", msg.c_str());
}

void player_reacts()
{
    // don't allow reactions while stair peeking in descent mode
    if (crawl_state.game_is_descent() && !env.properties.exists(DESCENT_STAIRS_KEY))
        return;

    // This happens as close as possible after the player acts, for better messaging
    if (you_worship(GOD_BEOGH))
        beogh_ally_healing();

    unrand_reacts();

    if (you.form == transformation::eel_hands && one_chance_in(500))
        _do_eel_flavour_msg();

    _handle_fugue(you.time_taken);
    if (you.has_mutation(MUT_WARMUP_STRIKES))
        you.rev_down(you.time_taken);

    if (x_chance_in_y(you.time_taken, 10 * BASELINE_DELAY))
    {
        const int teleportitis_level = get_teleportitis_level();

        if (teleportitis_level > 0
            && !you.duration[DUR_TELEPORT]
            && one_chance_in(40 / teleportitis_level)
            && hostile_teleport_is_possible()
            && you.elapsed_time >= you.props[TELEPORTITIS_COOLDOWN_KEY].get_int())
        {
            you_teleport(true, MID_PLAYER);
            you.props[TELEPORTITIS_COOLDOWN_KEY] = you.elapsed_time + random_range(500, 1000);
        }
        else if (player_in_branch(BRANCH_ABYSS) && one_chance_in(80)
                 && (!map_masked(you.pos(), MMT_VAULT) || one_chance_in(3)))
        {
            you_teleport_now(); // to new area of the Abyss

            // It's effectively a new level, make a checkpoint save so eventual
            // crashes lose less of the player's progress (and fresh new bad
            // mutations).
            if (!crawl_state.disables[DIS_SAVE_CHECKPOINTS])
                save_game(false);
        }
    }

    abyss_maybe_spawn_xp_exit();
    if (player_in_branch(BRANCH_ABYSS) && you.depth >= 5 && you.skill_cost_level == 27)
        you.props[ABYSS_LOITERING_TIME_KEY].get_int() += you.time_taken;

    actor_apply_cloud(&you);
    // Immunity due to just casting Volatile Blastmotes. Only lasts for one
    // turn, so erase it just after we apply clouds for the turn (above).
    if (you.props.exists(BLASTMOTE_IMMUNE_KEY))
        you.props.erase(BLASTMOTE_IMMUNE_KEY);

    actor_apply_toxic_bog(&you);

    if (you.duration[DUR_SPIKE_LAUNCHER_ACTIVE])
        handle_spike_launcher(you.time_taken);

    if (you.duration[DUR_RIME_YAK_AURA])
        frigid_walls_damage(you.time_taken);

    _regenerate_hp_and_mp(you.time_taken);

    // Update attunement of regeneration items if our hp/mp has refilled.
    _maybe_attune_regen_items();

    _decrement_durations();

    if (you.attempted_attack)
        update_parrying_status();
    else
        you.attribute[ATTR_SUNDERING_CHARGE] = 0;

    // Translocations and possibly other duration decrements can
    // escape a player from beholders and fearmongers. These should
    // update after.
    you.update_beholders();
    you.update_fearmongers();

    you.handle_constriction();

    if (you.duration[DUR_POISONING])
        handle_player_poison(you.time_taken);

    if (you.form == transformation::bat_swarm)
    {
        const int num_clouds = random_range(1, 5);
        big_cloud(CLOUD_BATS, &you, you.pos(), 8, num_clouds);
    }

    // safety first: make absolutely sure that there's no mimic underfoot.
    // (this can happen with e.g. apport.)
    discover_mimic(you.pos());

    // Player stealth check.
    seen_monsters_react();

    // XOM now ticks from here, to increase his reaction time to tension.
    if (you_worship(GOD_XOM))
        xom_tick();
    else if (you_worship(GOD_QAZLAL))
        qazlal_storm_clouds();
    else if (you_worship(GOD_ASHENZARI))
        ash_scrying();

    if (you.props[EMERGENCY_FLIGHT_KEY].get_bool())
        _handle_emergency_flight();

    if (you.duration[DUR_PRIMORDIAL_NIGHTFALL])
        update_vision_range();

    incr_gem_clock();
    incr_zot_clock();
}

void extract_barbs(const char* endmsg)
{
    if (_decrement_a_duration(DUR_BARBS, you.time_taken, endmsg))
    {
        // Note: When this is called in _move player(), ATTR_BARBS_POW
        // has already been used to calculated damage for the player.
        // Otherwise, this prevents the damage.

        you.attribute[ATTR_BARBS_POW] = 0;

        you.props.erase(BARBS_MOVE_KEY);

        // somewhat hacky: ensure that a rest delay can get the right interrupt
        // check when barbs are removed, and all other rest stop conditions are
        // satisfied
        if (you.is_sufficiently_rested())
            interrupt_activity(activity_interrupt::full_hp);
    }
}
