/** * @file * @brief Monster enchantments. **/ #include "AppHdr.h" #include #include "act-iter.h" #include "actor.h" #include "areas.h" #include "attitude-change.h" #include "bloodspatter.h" #include "cloud.h" #include "coordit.h" #include "delay.h" #include "dgn-shoals.h" #include "env.h" #include "fight.h" #include "hints.h" #include "itemprop.h" #include "items.h" #include "libutil.h" #include "mgen_data.h" #include "misc.h" #include "mon-abil.h" #include "mon-behv.h" #include "mon-cast.h" #include "mon-death.h" #include "mon-poly.h" #include "mon-place.h" #include "religion.h" #include "spl-clouds.h" #include "spl-damage.h" #include "spl-summoning.h" #include "state.h" #include "stepdown.h" #include "strings.h" #include "teleport.h" #include "terrain.h" #include "traps.h" #include "view.h" #include "xom.h" #ifdef DEBUG_DIAGNOSTICS bool monster::has_ench(enchant_type ench) const { mon_enchant e = get_ench(ench); if (e.ench == ench) { if (!ench_cache[ench]) { die("monster %s has ench '%s' not in cache", name(DESC_PLAIN).c_str(), string(e).c_str()); } } else if (e.ench == ENCH_NONE) { if (ench_cache[ench]) { die("monster %s has no ench '%s' but cache says it does", name(DESC_PLAIN).c_str(), string(mon_enchant(ench)).c_str()); } } else { die("get_ench returned '%s' when asked for '%s'", string(e).c_str(), string(mon_enchant(ench)).c_str()); } return ench_cache[ench]; } #endif bool monster::has_ench(enchant_type ench, enchant_type ench2) const { if (ench2 == ENCH_NONE) ench2 = ench; for (int i = ench; i <= ench2; ++i) if (has_ench(static_cast(i))) return true; return false; } mon_enchant monster::get_ench(enchant_type ench1, enchant_type ench2) const { if (ench2 == ENCH_NONE) ench2 = ench1; for (int e = ench1; e <= ench2; ++e) { mon_enchant_list::const_iterator i = enchantments.find(static_cast(e)); if (i != enchantments.end()) return i->second; } return mon_enchant(); } void monster::update_ench(const mon_enchant &ench) { if (ench.ench != ENCH_NONE) { mon_enchant_list::iterator i = enchantments.find(ench.ench); if (i != enchantments.end()) i->second = ench; } } bool monster::add_ench(const mon_enchant &ench) { // silliness if (ench.ench == ENCH_NONE) return false; if (ench.ench == ENCH_FEAR && (holiness() == MH_NONLIVING || berserk_or_insane())) { return false; } if (ench.ench == ENCH_FLIGHT && has_ench(ENCH_LIQUEFYING)) { del_ench(ENCH_LIQUEFYING); invalidate_agrid(); } mon_enchant_list::iterator i = enchantments.find(ench.ench); bool new_enchantment = false; mon_enchant *added = NULL; if (i == enchantments.end()) { new_enchantment = true; added = &(enchantments[ench.ench] = ench); ench_cache.set(ench.ench, true); } else { i->second += ench; added = &i->second; } // If the duration is not set, we must calculate it (depending on the // enchantment). if (!ench.duration) added->set_duration(this, new_enchantment ? NULL : &ench); if (new_enchantment) add_enchantment_effect(ench); if (ench.ench == ENCH_CHARM || ench.ench == ENCH_BRIBED || ench.ench == ENCH_PERMA_BRIBED) { this->align_avatars(true); } return true; } void monster::add_enchantment_effect(const mon_enchant &ench, bool quiet) { // Check for slow/haste. switch (ench.ench) { case ENCH_BERSERK: // Inflate hp. scale_hp(3, 2); // deliberate fall-through case ENCH_INSANE: if (has_ench(ENCH_SUBMERGED)) del_ench(ENCH_SUBMERGED); if (mons_is_lurking(this)) { behaviour = BEH_WANDER; behaviour_event(this, ME_EVAL); } calc_speed(); break; case ENCH_HASTE: calc_speed(); break; case ENCH_SLOW: calc_speed(); break; case ENCH_STONESKIN: { // player gets 2+earth/5 const int ac_bonus = get_hit_dice() / 2; ac += ac_bonus; // the monster may get drained or level up, we need to store the bonus props["stoneskin_ac"].get_byte() = ac_bonus; } break; case ENCH_OZOCUBUS_ARMOUR: { // player gets 4+ice/3 const int ac_bonus = 4 + get_hit_dice() / 3; ac += ac_bonus; // the monster may get drained or level up, we need to store the bonus props["ozocubus_ac"].get_byte() = ac_bonus; } break; case ENCH_SUBMERGED: mons_clear_trapping_net(this); // Don't worry about invisibility. You should be able to see if // something has submerged. if (!quiet && mons_near(this)) { if (type == MONS_AIR_ELEMENTAL) { mprf("%s merges itself into the air.", name(DESC_A, true).c_str()); } else if (type == MONS_TRAPDOOR_SPIDER) { mprf("%s hides itself under the floor.", name(DESC_A, true).c_str()); } else if (seen_context == SC_SURFACES) { // The monster surfaced and submerged in the same turn // without doing anything else. interrupt_activity(AI_SEE_MONSTER, activity_interrupt_data(this, SC_SURFACES_BRIEFLY)); // Why does this handle only land-capables? I'd imagine this // to happen mostly (only?) for fish. -- 1KB } else if (crawl_state.game_is_arena()) mprf("%s submerges.", name(DESC_A, true).c_str()); } // Pacified monsters leave the level when they submerge. if (pacified()) make_mons_leave_level(this); break; case ENCH_CONFUSION: case ENCH_MAD: if (type == MONS_TRAPDOOR_SPIDER && has_ench(ENCH_SUBMERGED)) del_ench(ENCH_SUBMERGED); if (mons_is_lurking(this)) { behaviour = BEH_WANDER; behaviour_event(this, ME_EVAL); } break; case ENCH_CHARM: case ENCH_BRIBED: case ENCH_PERMA_BRIBED: behaviour = BEH_SEEK; target = you.pos(); foe = MHITYOU; if (is_patrolling()) { // Enslaved monsters stop patrolling and forget their patrol // point; they're supposed to follow you now. patrol_point.reset(); firing_pos.reset(); } mons_att_changed(this); // clear any constrictions on/by you stop_constricting(MID_PLAYER, true); you.stop_constricting(mid, true); if (invisible() && mons_near(this) && !you.can_see_invisible() && !backlit() && !has_ench(ENCH_SUBMERGED)) { if (!quiet) { mprf("You detect the %s %s.", ench.ench == ENCH_CHARM ? "charmed" : "bribed", name(DESC_PLAIN, true).c_str()); } autotoggle_autopickup(false); handle_seen_interrupt(this); } // TODO -- and friends if (you.can_see(this)) learned_something_new(HINT_MONSTER_FRIENDLY, pos()); break; case ENCH_LIQUEFYING: case ENCH_SILENCE: invalidate_agrid(true); break; case ENCH_ROLLING: calc_speed(); break; case ENCH_ICEMAIL: ac += ICEMAIL_MAX; break; case ENCH_AGILE: ev += AGILITY_BONUS; break; case ENCH_FROZEN: calc_speed(); break; case ENCH_EPHEMERAL_INFUSION: { if (!props.exists("eph_amount")) { int amount = min((ench.degree / 2) + random2avg(ench.degree, 2), max_hit_points - hit_points); if (amount > 0 && heal(amount) && !quiet) simple_monster_message(this, " seems to gain new vigour!"); else amount = 0; props["eph_amount"].get_byte() = amount; } break; } case ENCH_INVIS: if (testbits(flags, MF_WAS_IN_VIEW)) { went_unseen_this_turn = true; unseen_pos = pos(); } break; default: break; } } static bool _prepare_del_ench(monster* mon, const mon_enchant &me) { if (me.ench != ENCH_SUBMERGED) return true; // Unbreathing stuff that can't swim stays on the bottom. if (grd(mon->pos()) == DNGN_DEEP_WATER && !mon->can_drown() && !monster_habitable_grid(mon, DNGN_DEEP_WATER)) { return false; } // Lurking monsters only unsubmerge when their foe is in sight if the foe // is right next to them. if (mons_is_lurking(mon)) { const actor* foe = mon->get_foe(); if (foe != NULL && mon->can_see(foe) && !adjacent(mon->pos(), foe->pos())) { return false; } } int midx = mon->mindex(); if (!monster_at(mon->pos())) mgrd(mon->pos()) = midx; if (mon->pos() != you.pos() && midx == mgrd(mon->pos())) return true; if (midx != mgrd(mon->pos())) { monster* other_mon = &menv[mgrd(mon->pos())]; if (other_mon->type == MONS_NO_MONSTER || other_mon->type == MONS_PROGRAM_BUG) { mgrd(mon->pos()) = midx; mprf(MSGCH_ERROR, "mgrd(%d,%d) points to %s monster, even " "though it contains submerged monster %s (see bug 2293518)", mon->pos().x, mon->pos().y, other_mon->type == MONS_NO_MONSTER ? "dead" : "buggy", mon->name(DESC_PLAIN, true).c_str()); if (mon->pos() != you.pos()) return true; } else mprf(MSGCH_ERROR, "%s tried to unsubmerge while on same square as " "%s (see bug 2293518)", mon->name(DESC_THE, true).c_str(), mon->name(DESC_A, true).c_str()); } // Monster un-submerging while under player or another monster. Try to // move to an adjacent square in which the monster could have been // submerged and have it unsubmerge from there. coord_def target_square; int okay_squares = 0; for (adjacent_iterator ai(mon->pos()); ai; ++ai) if (!actor_at(*ai) && monster_can_submerge(mon, grd(*ai)) && one_chance_in(++okay_squares)) { target_square = *ai; } if (okay_squares > 0) return mon->move_to_pos(target_square); // No available adjacent squares from which the monster could also // have unsubmerged. Can it just stay submerged where it is? if (monster_can_submerge(mon, grd(mon->pos()))) return false; // The terrain changed and the monster can't remain submerged. // Try to move to an adjacent square where it would be happy. for (adjacent_iterator ai(mon->pos()); ai; ++ai) { if (!actor_at(*ai) && monster_habitable_grid(mon, grd(*ai)) && !find_trap(*ai)) { if (one_chance_in(++okay_squares)) target_square = *ai; } } if (okay_squares > 0) return mon->move_to_pos(target_square); return true; } bool monster::del_ench(enchant_type ench, bool quiet, bool effect) { mon_enchant_list::iterator i = enchantments.find(ench); if (i == enchantments.end()) return false; const mon_enchant me = i->second; const enchant_type et = i->first; if (!_prepare_del_ench(this, me)) return false; enchantments.erase(et); ench_cache.set(et, false); if (effect) remove_enchantment_effect(me, quiet); return true; } void monster::remove_enchantment_effect(const mon_enchant &me, bool quiet) { switch (me.ench) { case ENCH_TIDE: shoals_release_tide(this); break; case ENCH_INSANE: attitude = static_cast(props["old_attitude"].get_short()); mons_att_changed(this); break; case ENCH_BERSERK: scale_hp(2, 3); calc_speed(); break; case ENCH_HASTE: calc_speed(); if (!quiet) simple_monster_message(this, " is no longer moving quickly."); break; case ENCH_SWIFT: if (!quiet) if (type == MONS_ALLIGATOR) simple_monster_message(this, " slows down."); else simple_monster_message(this, " is no longer moving somewhat quickly."); break; case ENCH_SILENCE: invalidate_agrid(); if (!quiet && !silenced(pos())) if (alive()) simple_monster_message(this, " becomes audible again."); else mprf("As %s dies, the sound returns.", name(DESC_THE).c_str()); break; case ENCH_MIGHT: if (!quiet) simple_monster_message(this, " no longer looks unusually strong."); break; case ENCH_SLOW: if (!quiet) simple_monster_message(this, " is no longer moving slowly."); calc_speed(); break; case ENCH_STONESKIN: if (props.exists("stoneskin_ac")) ac -= props["stoneskin_ac"].get_byte(); if (!quiet && you.can_see(this)) { mprf("%s skin looks tender.", apostrophise(name(DESC_THE)).c_str()); } break; case ENCH_OZOCUBUS_ARMOUR: if (props.exists("ozocubus_ac")) ac -= props["ozocubus_ac"].get_byte(); if (!quiet && you.can_see(this)) { mprf("%s icy armour evaporates.", apostrophise(name(DESC_THE)).c_str()); } break; case ENCH_PARALYSIS: if (!quiet) simple_monster_message(this, " is no longer paralysed."); behaviour_event(this, ME_EVAL); break; case ENCH_PETRIFIED: if (!quiet) simple_monster_message(this, " is no longer petrified."); del_ench(ENCH_PETRIFYING); behaviour_event(this, ME_EVAL); break; case ENCH_PETRIFYING: fully_petrify(me.agent(), quiet); if (alive()) // losing active flight over lava behaviour_event(this, ME_EVAL); break; case ENCH_FEAR: if (holiness() == MH_NONLIVING || berserk_or_insane()) { // This should only happen because of fleeing sanctuary snprintf(info, INFO_SIZE, " stops retreating."); } else if (!mons_is_tentacle_or_tentacle_segment(type)) { snprintf(info, INFO_SIZE, " seems to regain %s courage.", pronoun(PRONOUN_POSSESSIVE, true).c_str()); } if (!quiet) simple_monster_message(this, info); // Reevaluate behaviour. behaviour_event(this, ME_EVAL); break; case ENCH_CONFUSION: if (!quiet) simple_monster_message(this, " seems less confused."); // Reevaluate behaviour. behaviour_event(this, ME_EVAL); break; case ENCH_INVIS: // Note: Invisible monsters are not forced to stay invisible, so // that they can properly have their invisibility removed just // before being polymorphed into a non-invisible monster. if (mons_near(this) && !you.can_see_invisible() && !backlit() && !has_ench(ENCH_SUBMERGED) && !friendly() && !you.duration[DUR_TELEPATHY]) { if (!quiet) mprf("%s appears from thin air!", name(DESC_A, true).c_str()); autotoggle_autopickup(false); handle_seen_interrupt(this); } break; case ENCH_CHARM: case ENCH_BRIBED: case ENCH_PERMA_BRIBED: if (invisible() && mons_near(this) && !you.can_see_invisible() && !backlit() && !has_ench(ENCH_SUBMERGED)) { if (!quiet) { if (me.ench == ENCH_CHARM && props.exists("charmed_demon")) { mprf("%s breaks free of your control!", name(DESC_THE, true).c_str()); } else mprf("%s is no longer %s.", name(DESC_THE, true).c_str(), me.ench == ENCH_CHARM ? "charmed" : "bribed"); mprf("You can no longer detect the %s.", name(DESC_PLAIN, true).c_str()); } autotoggle_autopickup(true); } else { if (!quiet) { if (me.ench == ENCH_CHARM && props.exists("charmed_demon")) { simple_monster_message(this, " breaks free of your control!"); } else simple_monster_message(this, me.ench == ENCH_CHARM ? " is no longer charmed." : " is no longer bribed."); } } if (you.can_see(this)) { // and fire activity interrupts interrupt_activity(AI_SEE_MONSTER, activity_interrupt_data(this, SC_UNCHARM)); } if (is_patrolling()) { // Enslaved monsters stop patrolling and forget their patrol point, // in case they were on order to wait. patrol_point.reset(); } mons_att_changed(this); // If a greater demon is breaking free, give the player time to respond if (me.ench == ENCH_CHARM && props.exists("charmed_demon")) { speed_increment -= speed; props.erase("charmed_demon"); } // Reevaluate behaviour. behaviour_event(this, ME_EVAL); break; case ENCH_CORONA: case ENCH_SILVER_CORONA: if (!quiet) { if (visible_to(&you)) simple_monster_message(this, " stops glowing."); else if (has_ench(ENCH_INVIS) && mons_near(this)) { mprf("%s stops glowing and disappears.", name(DESC_THE, true).c_str()); } } break; case ENCH_STICKY_FLAME: if (!quiet) simple_monster_message(this, " stops burning."); break; case ENCH_POISON: if (!quiet) simple_monster_message(this, " looks more healthy."); break; case ENCH_ROT: if (!quiet) simple_monster_message(this, " is no longer rotting."); break; case ENCH_HELD: { int net = get_trapping_net(pos()); if (net != NON_ITEM) { free_stationary_net(net); if (props.exists(NEWLY_TRAPPED_KEY)) props.erase(NEWLY_TRAPPED_KEY); if (!quiet) simple_monster_message(this, " breaks free."); } break; } case ENCH_FAKE_ABJURATION: if (type == MONS_BATTLESPHERE) return end_battlesphere(this, false); case ENCH_ABJ: if (type == MONS_SPECTRAL_WEAPON) return end_spectral_weapon(this, false); // Set duration to -1 so that monster_die() and any of its // callees can tell that the monster ran out of time or was // abjured. add_ench(mon_enchant( (me.ench != ENCH_FAKE_ABJURATION) ? ENCH_ABJ : ENCH_FAKE_ABJURATION, 0, 0, -1)); if (berserk()) simple_monster_message(this, " is no longer berserk."); monster_die(this, (me.ench == ENCH_FAKE_ABJURATION) ? KILL_MISC : (quiet) ? KILL_DISMISSED : KILL_RESET, NON_MONSTER); break; case ENCH_SHORT_LIVED: // Conjured ball lightnings explode when they time out. monster_die(this, KILL_TIMEOUT, NON_MONSTER); break; case ENCH_SUBMERGED: if (mons_is_wandering(this) || mons_is_lurking(this)) { behaviour = BEH_SEEK; behaviour_event(this, ME_EVAL); } if (you.pos() == pos()) { // If, despite our best efforts, it unsubmerged on the same // square as the player, teleport it away. monster_teleport(this, true, false); if (you.pos() == pos()) { mprf(MSGCH_ERROR, "%s is on the same square as you!", name(DESC_A).c_str()); } } if (you.can_see(this)) { if (!mons_is_safe(this) && delay_is_run(current_delay_action())) { // Already set somewhere else. if (seen_context) return; if (!monster_habitable_grid(this, DNGN_FLOOR)) seen_context = SC_FISH_SURFACES; else seen_context = SC_SURFACES; } else if (!quiet) { msg_channel_type channel = MSGCH_PLAIN; if (!seen_context) { channel = MSGCH_WARN; seen_context = SC_JUST_SEEN; } if (type == MONS_AIR_ELEMENTAL) { mprf(channel, "%s forms itself from the air!", name(DESC_A, true).c_str()); } else if (type == MONS_TRAPDOOR_SPIDER) { mprf(channel, "%s leaps out from its hiding place under the floor!", name(DESC_A, true).c_str()); } else if (type == MONS_LOST_SOUL) { mprf(channel, "%s flickers into view.", name(DESC_A).c_str()); } else if (crawl_state.game_is_arena()) mprf("%s surfaces.", name(DESC_A, true).c_str()); } } else if (mons_near(this) && feat_compatible(grd(pos()), DNGN_DEEP_WATER)) { mpr("Something invisible bursts forth from the water."); interrupt_activity(AI_FORCE_INTERRUPT); } break; case ENCH_SOUL_RIPE: if (!quiet) { simple_monster_message(this, "'s soul is no longer ripe for the taking."); } break; case ENCH_AWAKEN_FOREST: env.forest_awoken_until = 0; if (!quiet) forest_message(pos(), "The forest calms down."); break; case ENCH_BLEED: if (!quiet) simple_monster_message(this, " is no longer bleeding."); break; case ENCH_WITHDRAWN: if (!quiet) simple_monster_message(this, " emerges from its shell."); break; case ENCH_LIQUEFYING: invalidate_agrid(); if (!quiet) simple_monster_message(this, " is no longer liquefying the ground."); break; case ENCH_FLIGHT: apply_location_effects(pos(), me.killer(), me.kill_agent()); break; case ENCH_DAZED: if (!quiet && alive()) simple_monster_message(this, " is no longer dazed."); break; case ENCH_INNER_FLAME: if (!quiet && alive()) simple_monster_message(this, "'s inner flame fades away."); break; case ENCH_ROLLING: calc_speed(); if (!quiet && alive()) simple_monster_message(this, " stops rolling."); break; //The following should never happen, but just in case... case ENCH_MUTE: if (!quiet && alive()) simple_monster_message(this, " is no longer mute."); break; case ENCH_BLIND: if (!quiet && alive()) simple_monster_message(this, " is no longer blind."); // Reevaluate behaviour. behaviour_event(this, ME_EVAL); break; case ENCH_DUMB: if (!quiet && alive()) simple_monster_message(this, " is no longer stupefied."); // Reevaluate behaviour. behaviour_event(this, ME_EVAL); break; case ENCH_MAD: if (!quiet && alive()) simple_monster_message(this, " is no longer mad."); // Reevaluate behaviour. behaviour_event(this, ME_EVAL); break; case ENCH_DEATHS_DOOR: if (!quiet) simple_monster_message(this, " is no longer invulnerable."); break; case ENCH_REGENERATION: if (!quiet) simple_monster_message(this, " is no longer regenerating."); break; case ENCH_WRETCHED: if (!quiet) { snprintf(info, INFO_SIZE, " seems to return to %s normal shape.", pronoun(PRONOUN_POSSESSIVE, true).c_str()); simple_monster_message(this, info); } break; case ENCH_FLAYED: heal_flayed_effect(this); break; case ENCH_HAUNTING: { mon_enchant abj = get_ench(ENCH_ABJ); abj.degree = 1; abj.duration = min(5 + random2(30), abj.duration); update_ench(abj); break; } case ENCH_WEAK: if (!quiet) simple_monster_message(this, " is no longer weakened."); break; case ENCH_AWAKEN_VINES: unawaken_vines(this, quiet); break; case ENCH_CONTROL_WINDS: if (!quiet && you.can_see(this)) mprf("The winds cease moving at %s will.", name(DESC_ITS).c_str()); break; case ENCH_TOXIC_RADIANCE: if (!quiet && you.can_see(this)) mprf("%s toxic aura wanes.", name(DESC_ITS).c_str()); break; case ENCH_GRASPING_ROOTS_SOURCE: if (!quiet && you.see_cell(pos())) mpr("The grasping roots settle back into the ground."); // Done here to avoid duplicate messages if (you.duration[DUR_GRASPING_ROOTS]) check_grasping_roots(&you, true); break; case ENCH_FIRE_VULN: if (!quiet) simple_monster_message(this, " is no longer more vulnerable to fire."); break; case ENCH_SIREN_SONG: props.erase("siren_call"); break; case ENCH_POISON_VULN: if (!quiet) simple_monster_message(this, " is no longer more vulnerable to poison."); break; case ENCH_ICEMAIL: ac -= ICEMAIL_MAX; if (!quiet && you.can_see(this)) { mprf("%s icy envelope dissipates!", apostrophise(name(DESC_THE)).c_str()); } break; case ENCH_AGILE: ev -= AGILITY_BONUS; if (!quiet) simple_monster_message(this, " is no longer unusually agile."); break; case ENCH_FROZEN: if (!quiet) simple_monster_message(this, " is no longer encased in ice."); calc_speed(); break; case ENCH_EPHEMERAL_INFUSION: { int dam = 0; if (props.exists("eph_amount")) { dam = props["eph_amount"].get_byte(); props.erase("eph_amount"); } dam = min(dam, hit_points - 1); if (dam > 0) hurt(NULL, dam); if (!quiet) simple_monster_message(this, " looks less vigorous."); break; } case ENCH_BLACK_MARK: if (!quiet) { simple_monster_message(this, " is no longer absorbing vital" " energies."); } calc_speed(); break; case ENCH_SAP_MAGIC: if (!quiet) simple_monster_message(this, " is no longer being sapped."); break; case ENCH_CORROSION: if (!quiet) simple_monster_message(this, " is no longer covered in acid."); break; case ENCH_GOLD_LUST: if (!quiet) simple_monster_message(this, " is no longer distracted by gold."); break; case ENCH_DRAINED: if (!quiet) simple_monster_message(this, " seems less drained."); break; default: break; } } bool monster::lose_ench_levels(const mon_enchant &e, int lev, bool infinite) { if (!lev) return false; if (e.duration >= INFINITE_DURATION && !infinite) return false; if (e.degree <= lev) { del_ench(e.ench); return true; } else { mon_enchant newe(e); newe.degree -= lev; update_ench(newe); return false; } } bool monster::lose_ench_duration(const mon_enchant &e, int dur) { if (!dur) return false; if (e.duration >= INFINITE_DURATION) return false; if (e.duration <= dur) { del_ench(e.ench); return true; } else { mon_enchant newe(e); newe.duration -= dur; update_ench(newe); return false; } } //--------------------------------------------------------------- // // timeout_enchantments // // Update a monster's enchantments when the player returns // to the level. // // Management for enchantments... problems with this are the oddities // (monster dying from poison several thousands of turns later), and // game balance. // // Consider: Poison/Sticky Flame a monster at range and leave, monster // dies but can't leave level to get to player (implied game balance of // the delayed damage is that the monster could be a danger before // it dies). This could be fixed by keeping some monsters active // off level and allowing them to take stairs (a very serious change). // // Compare this to the current abuse where the player gets // effectively extended duration of these effects (although only // the actual effects only occur on level, the player can leave // and heal up without having the effect disappear). // // This is a simple compromise between the two... the enchantments // go away, but the effects don't happen off level. -- bwr // //--------------------------------------------------------------- void monster::timeout_enchantments(int levels) { if (enchantments.empty()) return; const mon_enchant_list ec = enchantments; for (mon_enchant_list::const_iterator i = ec.begin(); i != ec.end(); ++i) { switch (i->first) { case ENCH_WITHDRAWN: if (hit_points >= (max_hit_points - max_hit_points / 4) && !one_chance_in(3)) { del_ench(i->first); break; } lose_ench_levels(i->second, levels); break; case ENCH_POISON: case ENCH_ROT: case ENCH_CORONA: case ENCH_STICKY_FLAME: case ENCH_ABJ: case ENCH_SHORT_LIVED: case ENCH_HASTE: case ENCH_MIGHT: case ENCH_FEAR: case ENCH_CHARM: case ENCH_SLEEP_WARY: case ENCH_SICK: case ENCH_PARALYSIS: case ENCH_PETRIFYING: case ENCH_PETRIFIED: case ENCH_SWIFT: case ENCH_BATTLE_FRENZY: case ENCH_SILENCE: case ENCH_LOWERED_MR: case ENCH_SOUL_RIPE: case ENCH_BLEED: case ENCH_ANTIMAGIC: case ENCH_FEAR_INSPIRING: case ENCH_REGENERATION: case ENCH_RAISED_MR: case ENCH_MIRROR_DAMAGE: case ENCH_STONESKIN: case ENCH_LIQUEFYING: case ENCH_SILVER_CORONA: case ENCH_DAZED: case ENCH_FAKE_ABJURATION: case ENCH_ROUSED: case ENCH_BREATH_WEAPON: case ENCH_DEATHS_DOOR: case ENCH_WRETCHED: case ENCH_SCREAMED: case ENCH_BLIND: case ENCH_WORD_OF_RECALL: case ENCH_INJURY_BOND: case ENCH_FLAYED: case ENCH_BARBS: case ENCH_AGILE: case ENCH_FROZEN: case ENCH_EPHEMERAL_INFUSION: case ENCH_BLACK_MARK: case ENCH_SAP_MAGIC: case ENCH_BRIBED: case ENCH_PERMA_BRIBED: case ENCH_CORROSION: case ENCH_GOLD_LUST: lose_ench_levels(i->second, levels); break; case ENCH_SLOW: if (torpor_slowed()) lose_ench_levels(i->second, min(levels, i->second.degree - 1)); else { lose_ench_levels(i->second, levels); if (props.exists(TORPOR_SLOWED_KEY)) props.erase(TORPOR_SLOWED_KEY); } break; case ENCH_INVIS: if (!mons_class_flag(type, M_INVIS)) lose_ench_levels(i->second, levels); break; case ENCH_INSANE: case ENCH_BERSERK: case ENCH_INNER_FLAME: case ENCH_ROLLING: case ENCH_SIREN_SONG: del_ench(i->first); break; case ENCH_FATIGUE: del_ench(i->first); del_ench(ENCH_SLOW); break; case ENCH_TP: teleport(true); del_ench(i->first); break; case ENCH_CONFUSION: if (!mons_class_flag(type, M_CONFUSED)) del_ench(i->first); if (!is_stationary()) monster_blink(this, true); break; case ENCH_HELD: del_ench(i->first); break; case ENCH_TIDE: { const int actdur = speed_to_duration(speed) * levels; lose_ench_duration(i->first, actdur); break; } case ENCH_SLOWLY_DYING: { const int actdur = speed_to_duration(speed) * levels; if (lose_ench_duration(i->first, actdur)) monster_die(this, KILL_MISC, NON_MONSTER, true); break; } default: break; } if (!alive()) break; } } string monster::describe_enchantments() const { ostringstream oss; for (mon_enchant_list::const_iterator i = enchantments.begin(); i != enchantments.end(); ++i) { if (i != enchantments.begin()) oss << ", "; oss << string(i->second); } return oss.str(); } bool monster::decay_enchantment(enchant_type en, bool decay_degree) { if (!has_ench(en)) return false; const mon_enchant& me(get_ench(en)); if (me.duration >= INFINITE_DURATION) return false; // Faster monsters can wiggle out of the net more quickly. const int spd = (me.ench == ENCH_HELD) ? speed : 10; const int actdur = speed_to_duration(spd); if (lose_ench_duration(me, actdur)) return true; if (!decay_degree) return false; // Decay degree so that higher degrees decay faster than lower // degrees, and a degree of 1 does not decay (it expires when the // duration runs out). const int level = me.degree; if (level <= 1) return false; const int decay_factor = level * (level + 1) / 2; if (me.duration < me.maxduration * (decay_factor - 1) / decay_factor) { mon_enchant newme = me; --newme.degree; newme.maxduration = newme.duration; if (newme.degree <= 0) { del_ench(me.ench); return true; } else update_ench(newme); } return false; } bool monster::clear_far_engulf(void) { if (you.duration[DUR_WATER_HOLD] && (mid_t) you.props["water_holder"].get_int() == mid) { you.clear_far_engulf(); } const mon_enchant& me = get_ench(ENCH_WATER_HOLD); if (me.ench == ENCH_NONE) return false; const bool nonadj = !me.agent() || !adjacent(me.agent()->pos(), pos()); if (nonadj) del_ench(ENCH_WATER_HOLD); return nonadj; } void monster::apply_enchantment(const mon_enchant &me) { enchant_type en = me.ench; switch (me.ench) { case ENCH_INSANE: if (decay_enchantment(en)) { simple_monster_message(this, " is no longer in an insane frenzy."); const int duration = random_range(70, 130); add_ench(mon_enchant(ENCH_FATIGUE, 0, 0, duration)); add_ench(mon_enchant(ENCH_SLOW, 0, 0, duration)); } break; case ENCH_BERSERK: if (decay_enchantment(en)) { simple_monster_message(this, " is no longer berserk."); const int duration = random_range(70, 130); add_ench(mon_enchant(ENCH_FATIGUE, 0, 0, duration)); add_ench(mon_enchant(ENCH_SLOW, 0, 0, duration)); } break; case ENCH_FATIGUE: if (decay_enchantment(en)) { simple_monster_message(this, " looks more energetic."); del_ench(ENCH_SLOW, true); } break; case ENCH_WITHDRAWN: if (hit_points >= (max_hit_points - max_hit_points / 4) && !one_chance_in(3)) { del_ench(ENCH_WITHDRAWN); break; } decay_enchantment(en); break; case ENCH_SLOW: case ENCH_HASTE: case ENCH_SWIFT: case ENCH_MIGHT: case ENCH_FEAR: case ENCH_PARALYSIS: case ENCH_PETRIFYING: case ENCH_PETRIFIED: case ENCH_SICK: case ENCH_CORONA: case ENCH_ABJ: case ENCH_CHARM: case ENCH_SLEEP_WARY: case ENCH_LOWERED_MR: case ENCH_SOUL_RIPE: case ENCH_TIDE: case ENCH_REGENERATION: case ENCH_RAISED_MR: case ENCH_STONESKIN: case ENCH_FEAR_INSPIRING: case ENCH_LIFE_TIMER: case ENCH_FLIGHT: case ENCH_DAZED: case ENCH_FAKE_ABJURATION: case ENCH_RECITE_TIMER: case ENCH_INNER_FLAME: case ENCH_MUTE: case ENCH_BLIND: case ENCH_DUMB: case ENCH_MAD: case ENCH_BREATH_WEAPON: case ENCH_WRETCHED: case ENCH_SCREAMED: case ENCH_WEAK: case ENCH_AWAKEN_VINES: case ENCH_FIRE_VULN: case ENCH_BARBS: case ENCH_POISON_VULN: case ENCH_DIMENSION_ANCHOR: case ENCH_AGILE: case ENCH_FROZEN: case ENCH_EPHEMERAL_INFUSION: case ENCH_SAP_MAGIC: case ENCH_CORROSION: case ENCH_GOLD_LUST: decay_enchantment(en); break; case ENCH_ANTIMAGIC: if (!has_ench(ENCH_SAP_MAGIC)) decay_enchantment(en); break; case ENCH_MIRROR_DAMAGE: if (decay_enchantment(en)) simple_monster_message(this, "'s dark mirror aura disappears."); break; case ENCH_SILENCE: case ENCH_LIQUEFYING: decay_enchantment(en); invalidate_agrid(); break; case ENCH_BATTLE_FRENZY: case ENCH_ROUSED: case ENCH_DRAINED: decay_enchantment(en, false); break; case ENCH_AQUATIC_LAND: // Aquatic monsters lose hit points every turn they spend on dry land. ASSERT(mons_habitat(this) == HT_WATER); if (feat_is_watery(grd(pos()))) { // The tide, water card or Fedhas gave us water. del_ench(ENCH_AQUATIC_LAND); break; } // Zombies don't take damage from flopping about on land. if (mons_is_zombified(this)) break; hurt(me.agent(), 1 + random2(5), BEAM_NONE); break; case ENCH_HELD: break; // handled in mon-act.cc:struggle_against_net() case ENCH_CONFUSION: if (!mons_class_flag(type, M_CONFUSED)) decay_enchantment(en); break; case ENCH_INVIS: if (!mons_class_flag(type, M_INVIS)) decay_enchantment(en); break; case ENCH_SUBMERGED: { // Don't unsubmerge into a harmful cloud if (!is_harmless_cloud(cloud_type_at(pos()))) break; // Air elementals are a special case, as their submerging in air // isn't up to choice. - bwr if (type == MONS_AIR_ELEMENTAL) { heal(1, one_chance_in(5)); if (one_chance_in(5)) del_ench(ENCH_SUBMERGED); break; } // Now we handle the others: const dungeon_feature_type grid = grd(pos()); if (!monster_can_submerge(this, grid)) { // unbreathing stuff can stay on the bottom if (grid != DNGN_DEEP_WATER || monster_habitable_grid(this, grid) || can_drown()) { del_ench(ENCH_SUBMERGED); // forced to surface } } else if (mons_landlubbers_in_reach(this)) { del_ench(ENCH_SUBMERGED); make_mons_stop_fleeing(this); } break; } case ENCH_POISON: { const int poisonval = me.degree; int dam = (poisonval >= 4) ? 1 : 0; if (coinflip()) dam += roll_dice(1, poisonval + 1); if (res_poison() < 0) dam += roll_dice(2, poisonval) - 1; if (dam > 0) { #ifdef DEBUG_DIAGNOSTICS // For debugging, we don't have this silent. simple_monster_message(this, " takes poison damage.", MSGCH_DIAGNOSTICS); mprf(MSGCH_DIAGNOSTICS, "poison damage: %d", dam); #endif hurt(me.agent(), dam, BEAM_POISON); } decay_enchantment(en, true); break; } case ENCH_ROT: { if (hit_points > 1 && one_chance_in(3)) { hurt(me.agent(), 1); if (hit_points < max_hit_points && coinflip()) --max_hit_points; } decay_enchantment(en, true); break; } // Assumption: monster::res_fire has already been checked. case ENCH_STICKY_FLAME: { if (feat_is_watery(grd(pos())) && (ground_level() || mons_intel(this) >= I_NORMAL && flight_mode())) { if (mons_near(this) && visible_to(&you)) { mprf(ground_level() ? "The flames covering %s go out." : "%s dips into the water, and the flames go out.", name(DESC_THE, false).c_str()); } del_ench(ENCH_STICKY_FLAME); break; } int dam = resist_adjust_damage(this, BEAM_FIRE, res_fire(), roll_dice(2, 4) - 1); if (dam > 0) { simple_monster_message(this, " burns!"); dprf("sticky flame damage: %d", dam); if (type == MONS_SHEEP) { for (adjacent_iterator ai(pos()); ai; ++ai) { monster *mon = monster_at(*ai); if (mon && mon->type == MONS_SHEEP && !mon->has_ench(ENCH_STICKY_FLAME) && coinflip()) { const int dur = me.degree/2 + 1 + random2(me.degree); mon->add_ench(mon_enchant(ENCH_STICKY_FLAME, dur, me.agent())); mon->add_ench(mon_enchant(ENCH_FEAR, dur + random2(20), me.agent())); if (visible_to(&you)) mprf("%s catches fire!", mon->name(DESC_A).c_str()); behaviour_event(mon, ME_SCARE, me.agent()); xom_is_stimulated(100); } } } hurt(me.agent(), dam, BEAM_STICKY_FLAME); } decay_enchantment(en, true); break; } case ENCH_SHORT_LIVED: // This should only be used for ball lightning -- bwr if (decay_enchantment(en)) suicide(); break; case ENCH_SLOWLY_DYING: // If you are no longer dying, you must be dead. if (decay_enchantment(en)) { if (you.can_see(this)) { if (type == MONS_PILLAR_OF_SALT) mprf("%s crumbles away.", name(DESC_THE, false).c_str()); else if (type == MONS_BLOCK_OF_ICE) mprf("%s melts away.", name(DESC_THE, false).c_str()); else { mprf("A nearby %s withers and dies.", name(DESC_PLAIN, false).c_str()); } } monster_die(this, KILL_MISC, NON_MONSTER, true); } break; case ENCH_SPORE_PRODUCTION: // Reduce the timer, if that means we lose the enchantment then // spawn a spore and re-add the enchantment. if (decay_enchantment(en)) { // Search for an open adjacent square to place a spore on int idx[] = {0, 1, 2, 3, 4, 5, 6, 7}; shuffle_array(idx, 8); bool re_add = true; for (unsigned i = 0; i < 8; ++i) { coord_def adjacent = pos() + Compass[idx[i]]; if (mons_class_can_pass(MONS_GIANT_SPORE, env.grid(adjacent)) && !actor_at(adjacent)) { beh_type plant_attitude = SAME_ATTITUDE(this); if (monster *plant = create_monster(mgen_data(MONS_GIANT_SPORE, plant_attitude, NULL, 0, 0, adjacent, MHITNOT, MG_FORCE_PLACE))) { if (mons_is_god_gift(this, GOD_FEDHAS)) { plant->flags |= MF_NO_REWARD; if (plant_attitude == BEH_FRIENDLY) { plant->flags |= MF_ATT_CHANGE_ATTEMPT; mons_make_god_gift(plant, GOD_FEDHAS); } } plant->behaviour = BEH_WANDER; plant->number = 20; if (you.see_cell(adjacent) && you.see_cell(pos())) mpr("A ballistomycete spawns a giant spore."); // Decrease the count and maybe become inactive // again. if (number) { number--; if (number == 0) { colour = MAGENTA; del_ench(ENCH_SPORE_PRODUCTION); re_add = false; } } } break; } } // Re-add the enchantment (this resets the spore production // timer). if (re_add) add_ench(ENCH_SPORE_PRODUCTION); } break; case ENCH_EXPLODING: { // Reduce the timer, if that means we lose the enchantment then // spawn a spore and re-add the enchantment if (decay_enchantment(en)) { monster_type mtype = type; bolt beam; setup_spore_explosion(beam, *this); beam.explode(); // The ballisto dying, then a spore being created in its slot // env.mons means we can appear to be alive, but in fact be // an entirely different monster. if (alive() && type == mtype) add_ench(ENCH_EXPLODING); } } break; case ENCH_PORTAL_TIMER: { if (decay_enchantment(en)) { coord_def base_position = props["base_position"].get_coord(); // Do a thing. if (you.see_cell(base_position)) mprf("The portal closes; %s is severed.", name(DESC_THE).c_str()); if (env.grid(base_position) == DNGN_MALIGN_GATEWAY) env.grid(base_position) = DNGN_FLOOR; maybe_bloodify_square(base_position); add_ench(ENCH_SEVERED); // Severed tentacles immediately become "hostile" to everyone (or insane) attitude = ATT_NEUTRAL; mons_att_changed(this); behaviour_event(this, ME_ALERT); } } break; case ENCH_PORTAL_PACIFIED: { if (decay_enchantment(en)) { if (has_ench(ENCH_SEVERED)) break; if (!friendly()) break; if (!silenced(you.pos())) { if (you.can_see(this)) simple_monster_message(this, " suddenly becomes enraged!"); else mpr("You hear a distant and violent thrashing sound."); } attitude = ATT_HOSTILE; mons_att_changed(this); behaviour_event(this, ME_ALERT, &you); } } break; case ENCH_SEVERED: { simple_monster_message(this, " writhes!"); coord_def base_position = props["base_position"].get_coord(); maybe_bloodify_square(base_position); hurt(me.agent(), 20); } break; case ENCH_GLOWING_SHAPESHIFTER: // This ench never runs out! // Number of actions is fine for shapeshifters. Don't change // shape while taking the stairs because monster_polymorph() has // an assert about it. -cao if (!(flags & MF_TAKING_STAIRS) && !(paralysed() || petrified() || petrifying() || asleep()) && (type == MONS_GLOWING_SHAPESHIFTER || one_chance_in(4))) { monster_polymorph(this, RANDOM_MONSTER); } break; case ENCH_SHAPESHIFTER: // This ench never runs out! if (!(flags & MF_TAKING_STAIRS) && !(paralysed() || petrified() || petrifying() || asleep()) && (type == MONS_SHAPESHIFTER || x_chance_in_y(1000 / (15 * max(1, get_hit_dice()) / 5), 1000))) { monster_polymorph(this, RANDOM_MONSTER); } break; case ENCH_TP: if (decay_enchantment(en, true) && !no_tele(true, false)) monster_teleport(this, true); break; case ENCH_EAT_ITEMS: break; case ENCH_AWAKEN_FOREST: forest_damage(this); decay_enchantment(en); break; case ENCH_TORNADO: tornado_damage(this, speed_to_duration(speed)); if (decay_enchantment(en)) { add_ench(ENCH_TORNADO_COOLDOWN); if (you.can_see(this)) { mprf("The winds around %s start to calm down.", name(DESC_THE).c_str()); } } break; case ENCH_BLEED: { // 3, 6, 9% of current hp int dam = div_rand_round(random2((1 + hit_points)*(me.degree * 3)),100); // location, montype, damage (1 hp = 5% chance), spatter, smell_alert bleed_onto_floor(pos(), type, 20, false, true); if (dam < hit_points) { hurt(me.agent(), dam); dprf("hit_points: %d ; bleed damage: %d ; degree: %d", hit_points, dam, me.degree); } decay_enchantment(en, true); break; } // This is like Corona, but if silver harms them, it has sticky // flame levels of damage. case ENCH_SILVER_CORONA: if (how_chaotic()) { int dam = roll_dice(2, 4) - 1; simple_monster_message(this, " is seared!"); dprf("Zin's Corona damage: %d", dam); hurt(me.agent(), dam); } decay_enchantment(en, true); break; case ENCH_WORD_OF_RECALL: // If we've gotten silenced or somehow incapacitated since we started, // cancel the recitation if (silenced(pos()) || paralysed() || petrified() || confused() || asleep() || has_ench(ENCH_FEAR) || has_ench(ENCH_BREATH_WEAPON) || has_ench(ENCH_WATER_HOLD) && !res_water_drowning() || has_ench(ENCH_MUTE)) { this->speed_increment += me.duration; del_ench(ENCH_WORD_OF_RECALL, true, false); if (you.can_see(this)) { mprf("%s word of recall is interrupted.", name(DESC_ITS).c_str()); } break; } if (decay_enchantment(en)) { mons_word_of_recall(this, 3 + random2(5)); // This is the same delay as vault sentinels. mon_enchant breath_timeout = mon_enchant(ENCH_BREATH_WEAPON, 1, this, (4 + random2(9)) * BASELINE_DELAY); add_ench(breath_timeout); } break; case ENCH_INJURY_BOND: // It's hard to absorb someone else's injuries when you're dead if (!me.agent() || !me.agent()->alive() || me.agent()->mindex() == ANON_FRIENDLY_MONSTER) { del_ench(ENCH_INJURY_BOND, true, false); } else decay_enchantment(en); break; case ENCH_WATER_HOLD: if (!clear_far_engulf()) { if (res_water_drowning() <= 0) { lose_ench_duration(me, -speed_to_duration(speed)); int dam = div_rand_round((50 + stepdown((float)me.duration, 30.0)) * speed_to_duration(speed), BASELINE_DELAY * 10); if (res_water_drowning() < 0) dam = dam * 3 / 2; hurt(me.agent(), dam); } } break; case ENCH_FLAYED: { bool near_ghost = false; for (monster_iterator mi; mi; ++mi) { if (mi->type == MONS_FLAYED_GHOST && !mons_aligned(this, *mi) && see_cell(mi->pos())) { near_ghost = true; break; } } if (!near_ghost) decay_enchantment(en); break; } case ENCH_HAUNTING: if (!me.agent() || !me.agent()->alive()) del_ench(ENCH_HAUNTING); break; case ENCH_CONTROL_WINDS: apply_control_winds(this); decay_enchantment(en); break; case ENCH_TOXIC_RADIANCE: toxic_radiance_effect(this, 1); decay_enchantment(en); break; case ENCH_GRASPING_ROOTS_SOURCE: if (!apply_grasping_roots(this)) decay_enchantment(en); break; case ENCH_GRASPING_ROOTS: check_grasping_roots(this); break; case ENCH_TORNADO_COOLDOWN: if (decay_enchantment(en)) { remove_tornado_clouds(mindex()); if (you.can_see(this)) mprf("The winds around %s calm down.", name(DESC_THE).c_str()); } break; case ENCH_DEATHS_DOOR: if (decay_enchantment(en)) { add_ench(mon_enchant(ENCH_FATIGUE, 0, 0, (1 + random2(3)) * BASELINE_DELAY)); } break; case ENCH_SIREN_SONG: // If we've gotten silenced or somehow incapacitated since we started, // cancel the song if (silenced(pos()) || paralysed() || petrified() || confused() || asleep() || has_ench(ENCH_FEAR)) { del_ench(ENCH_SIREN_SONG, true, false); if (you.can_see(this)) { mprf("%s song is interrupted.", name(DESC_ITS).c_str()); } break; } siren_song(this); // The siren will stop singing without her audience if (!see_cell_no_trans(you.pos())) decay_enchantment(en); break; case ENCH_GRAND_AVATAR: if (!me.agent() || !me.agent()->alive()) del_ench(ENCH_GRAND_AVATAR, true, false); break; default: break; } } void monster::mark_summoned(int longevity, bool mark_items, int summon_type, bool abj) { if (abj) add_ench(mon_enchant(ENCH_ABJ, longevity)); if (summon_type != 0) add_ench(mon_enchant(ENCH_SUMMON, summon_type, 0, INT_MAX)); if (mark_items) { for (int i = 0; i < NUM_MONSTER_SLOTS; ++i) { const int item = inv[i]; if (item != NON_ITEM) mitm[item].flags |= ISFLAG_SUMMONED; } } } bool monster::is_summoned(int* duration, int* summon_type) const { const mon_enchant abj = get_ench(ENCH_ABJ); if (abj.ench == ENCH_NONE) { if (duration != NULL) *duration = -1; if (summon_type != NULL) *summon_type = 0; return false; } if (duration != NULL) *duration = abj.duration; const mon_enchant summ = get_ench(ENCH_SUMMON); if (summ.ench == ENCH_NONE) { if (summon_type != NULL) *summon_type = 0; return true; } if (summon_type != NULL) *summon_type = summ.degree; if (mons_is_conjured(type)) return false; switch (summ.degree) { // Temporarily dancing weapons are really there. case SPELL_TUKIMAS_DANCE: // A corpse/skeleton which was temporarily animated. case SPELL_ANIMATE_DEAD: case SPELL_ANIMATE_SKELETON: // Conjured stuff (fire vortices, ball lightning, IOOD) is handled above. // Clones aren't really summoned (though their equipment might be). case MON_SUMM_CLONE: // Nor are body parts. case SPELL_CREATE_TENTACLES: // Some object which was animated, and thus not really summoned. case MON_SUMM_ANIMATE: return false; } return true; } bool monster::is_perm_summoned() const { return testbits(flags, MF_HARD_RESET | MF_NO_REWARD); } void monster::apply_enchantments() { if (enchantments.empty()) return; // We process an enchantment only if it existed both at the start of this // function and when getting to it in order; any enchantment can add, modify // or remove others -- or even itself. FixedBitVector ec = ench_cache; // The ordering in enchant_type makes sure that "super-enchantments" // like berserk time out before their parts. for (int i = 0; i < NUM_ENCHANTMENTS; ++i) if (ec[i] && has_ench(static_cast(i))) apply_enchantment(enchantments.find(static_cast(i))->second); } // Used to adjust time durations in calc_duration() for monster speed. static inline int _mod_speed(int val, int speed) { if (!speed) speed = 10; const int modded = val * 10 / speed; return modded? modded : 1; } ///////////////////////////////////////////////////////////////////////// // mon_enchant static const char *enchant_names[] = { "none", "berserk", "haste", "might", "fatigue", "slow", "fear", "confusion", "invis", "poison", "rot", "summon", "abj", "corona", "charm", "sticky_flame", "glowing_shapeshifter", "shapeshifter", "tp", "sleep_wary", "submerged", "short_lived", "paralysis", "sick", #if TAG_MAJOR_VERSION == 34 "sleepy", #endif "held", "battle_frenzy", #if TAG_MAJOR_VERSION == 34 "temp_pacif", #endif "petrifying", "petrified", "lowered_mr", "soul_ripe", "slowly_dying", "eat_items", "aquatic_land", "spore_production", #if TAG_MAJOR_VERSION == 34 "slouch", #endif "swift", "tide", "insane", "silenced", "awaken_forest", "exploding", "bleeding", "tethered", "severed", "antimagic", #if TAG_MAJOR_VERSION == 34 "fading_away", "preparing_resurrect", #endif "regen", "magic_res", "mirror_dam", "stoneskin", "fear inspiring", "temporarily pacified", "withdrawn", "attached", "guardian_timer", "flight", "liquefying", "tornado", "fake_abjuration", "dazed", "mute", "blind", "dumb", "mad", "silver_corona", "recite timer", "inner_flame", "roused", "breath timer", "deaths_door", "rolling", "ozocubus_armour", "wretched", "screamed", "rune_of_recall", "injury bond", "drowning", "flayed", "haunting", #if TAG_MAJOR_VERSION == 34 "retching", #endif "weak", "dimension_anchor", "awaken vines", "control_winds", #if TAG_MAJOR_VERSION == 34 "wind_aided", #endif "summon_capped", "toxic_radiance", "grasping_roots_source", "grasping_roots", "iood_charged", "fire_vuln", "tornado_cooldown", "siren_song", "barbs", #if TAG_MAJOR_VERSION == 34 "building_charge", #endif "poison_vuln", "icemail", "agile", "frozen", "ephemeral_infusion", "black_mark", "grand_avatar", "sap magic", "shroud", "phantom_mirror", "bribed", "permabribed", "corrosion", "gold_lust", "drained", "buggy", }; static const char *_mons_enchantment_name(enchant_type ench) { COMPILE_CHECK(ARRAYSZ(enchant_names) == NUM_ENCHANTMENTS+1); if (ench > NUM_ENCHANTMENTS) ench = NUM_ENCHANTMENTS; return enchant_names[ench]; } enchant_type name_to_ench(const char *name) { for (unsigned int i = ENCH_NONE; i < ARRAYSZ(enchant_names); i++) if (!strcmp(name, enchant_names[i])) return (enchant_type)i; return ENCH_NONE; } mon_enchant::mon_enchant(enchant_type e, int deg, const actor* a, int dur) : ench(e), degree(deg), duration(dur), maxduration(0) { if (a) { who = a->kill_alignment(); source = a->mid; } else { who = KC_OTHER; source = 0; } } mon_enchant::operator string () const { const actor *a = agent(); return make_stringf("%s (%d:%d%s %s)", _mons_enchantment_name(ench), degree, duration, kill_category_desc(who), source == MID_ANON_FRIEND ? "anon friend" : source == MID_YOU_FAULTLESS ? "you w/o fault" : a ? a->name(DESC_PLAIN, true).c_str() : "N/A"); } const char *mon_enchant::kill_category_desc(kill_category k) const { return k == KC_YOU ? " you" : k == KC_FRIENDLY ? " pet" : ""; } void mon_enchant::merge_killer(kill_category k, mid_t m) { if (who >= k) // prefer the new one who = k, source = m; } void mon_enchant::cap_degree() { // Sickness & draining are not capped. if (ench == ENCH_SICK || ench == ENCH_DRAINED) return; // Hard cap to simulate old enum behaviour, we should really throw this // out entirely. const int max = (ench == ENCH_ABJ || ench == ENCH_FAKE_ABJURATION) ? 6 : 4; if (degree > max) degree = max; } mon_enchant &mon_enchant::operator += (const mon_enchant &other) { if (ench == other.ench) { degree += other.degree; cap_degree(); duration += other.duration; if (duration > INFINITE_DURATION) duration = INFINITE_DURATION; merge_killer(other.who, other.source); } return *this; } mon_enchant mon_enchant::operator + (const mon_enchant &other) const { mon_enchant tmp(*this); tmp += other; return tmp; } killer_type mon_enchant::killer() const { return who == KC_YOU ? KILL_YOU : who == KC_FRIENDLY ? KILL_MON : KILL_MISC; } int mon_enchant::kill_agent() const { return who == KC_FRIENDLY ? ANON_FRIENDLY_MONSTER : 0; } actor* mon_enchant::agent() const { return find_agent(source, who); } int mon_enchant::modded_speed(const monster* mons, int hdplus) const { return _mod_speed(mons->get_hit_dice() + hdplus, mons->speed); } int mon_enchant::calc_duration(const monster* mons, const mon_enchant *added) const { int cturn = 0; const int newdegree = added ? added->degree : degree; const int deg = newdegree ? newdegree : 1; // Beneficial enchantments (like Haste) should not be throttled by // monster HD via modded_speed(). Use _mod_speed instead! switch (ench) { case ENCH_WITHDRAWN: cturn = 5000 / _mod_speed(25, mons->speed); break; case ENCH_SWIFT: cturn = 1000 / _mod_speed(25, mons->speed); break; case ENCH_HASTE: case ENCH_MIGHT: case ENCH_INVIS: case ENCH_FEAR_INSPIRING: case ENCH_STONESKIN: case ENCH_AGILE: case ENCH_BLACK_MARK: cturn = 1000 / _mod_speed(25, mons->speed); break; case ENCH_LIQUEFYING: case ENCH_SILENCE: case ENCH_REGENERATION: case ENCH_RAISED_MR: case ENCH_MIRROR_DAMAGE: case ENCH_DEATHS_DOOR: case ENCH_EPHEMERAL_INFUSION: case ENCH_SAP_MAGIC: cturn = 300 / _mod_speed(25, mons->speed); break; case ENCH_SLOW: case ENCH_CORROSION: cturn = 250 / (1 + modded_speed(mons, 10)); break; case ENCH_FEAR: cturn = 150 / (1 + modded_speed(mons, 5)); break; case ENCH_PARALYSIS: cturn = max(90 / modded_speed(mons, 5), 3); break; case ENCH_PETRIFIED: cturn = max(8, 150 / (1 + modded_speed(mons, 5))); break; case ENCH_DAZED: case ENCH_PETRIFYING: cturn = 50 / _mod_speed(10, mons->speed); break; case ENCH_CONFUSION: cturn = max(100 / modded_speed(mons, 5), 3); break; case ENCH_HELD: cturn = 120 / _mod_speed(25, mons->speed); break; case ENCH_POISON: cturn = 1000 * deg / _mod_speed(125, mons->speed); break; case ENCH_STICKY_FLAME: cturn = 1000 * deg / _mod_speed(200, mons->speed); break; case ENCH_ROT: if (deg > 1) cturn = 1000 * (deg - 1) / _mod_speed(333, mons->speed); cturn += 1000 / _mod_speed(250, mons->speed); break; case ENCH_CORONA: case ENCH_SILVER_CORONA: if (deg > 1) cturn = 1000 * (deg - 1) / _mod_speed(200, mons->speed); cturn += 1000 / _mod_speed(100, mons->speed); break; case ENCH_SHORT_LIVED: cturn = 1200 / _mod_speed(200, mons->speed); break; case ENCH_SLOWLY_DYING: // This may be a little too direct but the randomization at the end // of this function is excessive for toadstools. -cao return (2 * FRESHEST_CORPSE + random2(10)) * speed_to_duration(mons->speed); case ENCH_SPORE_PRODUCTION: // This is used as a simple timer, when the enchantment runs out // the monster will create a giant spore. return random_range(475, 525) * 10; case ENCH_EXPLODING: return random_range(3, 7) * 10; case ENCH_PORTAL_PACIFIED: // Must be set by spell. return 0; case ENCH_BREATH_WEAPON: // Must be set by creature. return 0; case ENCH_PORTAL_TIMER: cturn = 30 * 10 / _mod_speed(10, mons->speed); break; case ENCH_FAKE_ABJURATION: case ENCH_ABJ: // The duration is: // deg = 1 90 aut // deg = 2 180 aut // deg = 3 270 aut // deg = 4 360 aut // deg = 5 810 aut // deg = 6 1710 aut // with a large fuzz if (deg >= 6) cturn = 1000 / _mod_speed(10, mons->speed); if (deg >= 5) cturn += 1000 / _mod_speed(20, mons->speed); cturn += 1000 * min(4, deg) / _mod_speed(100, mons->speed); break; case ENCH_CHARM: cturn = 500 / modded_speed(mons, 10); break; case ENCH_TP: cturn = 1000 * deg / _mod_speed(1000, mons->speed); break; case ENCH_SLEEP_WARY: cturn = 1000 / _mod_speed(50, mons->speed); break; case ENCH_LIFE_TIMER: cturn = 10 * (4 + random2(4)) / _mod_speed(10, mons->speed); break; case ENCH_INNER_FLAME: return random_range(25, 35) * 10; case ENCH_BERSERK: return (16 + random2avg(13, 2)) * 10; case ENCH_ROLLING: cturn = 10000 / _mod_speed(25, mons->speed); break; case ENCH_WRETCHED: cturn = (20 + roll_dice(3, 10)) * 10 / _mod_speed(10, mons->speed); break; case ENCH_TORNADO_COOLDOWN: cturn = random_range(25, 35) * 10 / _mod_speed(10, mons->speed); break; case ENCH_FROZEN: cturn = 3 * BASELINE_DELAY; break; default: break; } cturn = max(2, cturn); int raw_duration = (cturn * speed_to_duration(mons->speed)); // Note: this fuzzing is _not_ symmetric, resulting in 90% of input // on the average. raw_duration = max(15, fuzz_value(raw_duration, 60, 40)); dprf("cturn: %d, raw_duration: %d", cturn, raw_duration); return raw_duration; } // Calculate the effective duration (in terms of normal player time - 10 // duration units being one normal player action) of this enchantment. void mon_enchant::set_duration(const monster* mons, const mon_enchant *added) { if (duration && !added) return; if (added && added->duration) duration += added->duration; else duration += calc_duration(mons, added); if (duration > maxduration) maxduration = duration; }