/** * @file * @brief Traps related functions. **/ #include "AppHdr.h" #include "traps.h" #include "trap_def.h" #include #include #include "areas.h" #include "artefact.h" #include "beam.h" #include "bloodspatter.h" #include "branch.h" #include "cloud.h" #include "clua.h" #include "coord.h" #include "coordit.h" #include "delay.h" #include "describe.h" #include "directn.h" #include "dungeon.h" #include "env.h" #include "exercise.h" #include "hints.h" #include "itemname.h" #include "itemprop.h" #include "items.h" #include "libutil.h" #include "makeitem.h" #include "map_knowledge.h" #include "mapmark.h" #include "message.h" #include "misc.h" #include "mon-death.h" #include "mon-transit.h" #include "mon-util.h" #include "ouch.h" #include "output.h" #include "player.h" #include "prompt.h" #include "religion.h" #include "shout.h" #include "skills.h" #include "spl-miscast.h" #include "spl-util.h" #include "stash.h" #include "state.h" #include "strings.h" #include "travel.h" #include "terrain.h" #include "transform.h" #include "view.h" #include "xom.h" bool trap_def::active() const { return type != TRAP_UNASSIGNED; } bool trap_def::type_has_ammo() const { switch (type) { #if TAG_MAJOR_VERSION == 34 case TRAP_DART: #endif case TRAP_ARROW: case TRAP_BOLT: case TRAP_NEEDLE: case TRAP_SPEAR: return true; default: break; } return false; } void trap_def::disarm() { if (type == TRAP_NET) { item_def trap_item = generate_trap_item(); copy_item_to_grid(trap_item, pos); } destroy(); } void trap_def::destroy(bool known) { if (!in_bounds(pos)) die("Trap position out of bounds!"); grd(pos) = DNGN_FLOOR; ammo_qty = 0; type = TRAP_UNASSIGNED; if (known) { env.map_knowledge(pos).set_feature(DNGN_FLOOR); StashTrack.update_stash(pos); } pos = coord_def(-1,-1); } void trap_def::hide() { grd(pos) = DNGN_UNDISCOVERED_TRAP; } void trap_def::prepare_ammo(int charges) { skill_rnd = random2(256); if (charges) { ammo_qty = charges; return; } switch (type) { case TRAP_ARROW: case TRAP_BOLT: case TRAP_NEEDLE: ammo_qty = 3 + random2avg(9, 3); break; case TRAP_SPEAR: ammo_qty = 2 + random2avg(6, 3); break; case TRAP_GOLUBRIA: // really, turns until it vanishes ammo_qty = 30 + random2(20); break; case TRAP_TELEPORT: if (crawl_state.game_is_zotdef()) ammo_qty = 2 + random2(2); else ammo_qty = 1; break; default: ammo_qty = 0; break; } // Zot def: traps have 10x as much ammo if (crawl_state.game_is_zotdef() && type != TRAP_GOLUBRIA) ammo_qty *= 10; } void trap_def::reveal() { grd(pos) = category(); } string trap_def::name(description_level_type desc) const { if (type >= NUM_TRAPS) return "buggy"; string basename = full_trap_name(type); if (desc == DESC_A) { string prefix = "a"; if (is_vowel(basename[0])) prefix += 'n'; prefix += ' '; return prefix + basename; } else if (desc == DESC_THE) return string("the ") + basename; else // everything else return basename; } bool trap_def::is_known(const actor* act) const { const bool player_knows = (grd(pos) != DNGN_UNDISCOVERED_TRAP); if (act == NULL || act->is_player()) return player_knows; else if (act->is_monster()) { const monster* mons = act->as_monster(); const int intel = mons_intel(mons); // Smarter trap handling for intelligent monsters // * monsters native to a branch can be assumed to know the trap // locations and thus be able to avoid them // * friendlies and good neutrals can be assumed to have been warned // by the player about all traps s/he knows about // * very intelligent monsters can be assumed to have a high T&D // skill (or have memorised part of the dungeon layout ;)) if (type == TRAP_SHAFT) { // Slightly different rules for shafts: // * Lower intelligence requirement for native monsters. // * Allied zombies won't fall through shafts. (No herding!) // * Highly intelligent monsters never fall through shafts. return intel >= I_HIGH || intel > I_PLANT && mons_is_native_in_branch(mons) || player_knows && mons->wont_attack(); } else { if (intel < I_NORMAL) return false; if (player_knows && mons->wont_attack()) return true; // This should ultimately be removed, but only after monsters // learn to make use of the trap being limited in ZotDef if // there is no other way. Golubria and Malign Gateway are a // problem, too... if (crawl_state.game_is_zotdef()) return false; return mons_is_native_in_branch(mons) || intel >= I_HIGH && one_chance_in(3); } } die("invalid actor type"); } bool trap_def::is_safe(actor* act) const { if (!act) act = &you; // Shaft and mechanical traps are safe when flying or clinging. if ((act->airborne() || act->can_cling_to(pos)) && ground_only()) return true; // TODO: For now, just assume they're safe; they don't damage outright, // and the messages get old very quickly if (category() == DNGN_TRAP_WEB) // && act->is_web_immune() return true; if (!act->is_player()) return false; // No prompt (teleport traps are ineffective if wearing an amulet of // stasis or a -Tele item) if ((type == TRAP_TELEPORT || type == TRAP_TELEPORT_PERMANENT) && you.no_tele(false)) { return true; } if (!is_known(act)) return false; if (type == TRAP_GOLUBRIA || type == TRAP_SHAFT || crawl_state.game_is_zotdef()) { return true; } #ifdef CLUA_BINDINGS // Prompt for any trap where you might not have enough hp // as defined in init.txt (see trapwalk.lua) if (clua.callbooleanfn(false, "ch_cross_trap", "s", name().c_str())) return true; #endif return false; } /** * Get the item index of the first net on the square. * * @param where The location. * @param trapped If true, the index of the stationary net (trapping a victim) * is returned. * @return The item index of the net. */ int get_trapping_net(const coord_def& where, bool trapped) { for (stack_iterator si(where); si; ++si) { if (si->base_type == OBJ_MISSILES && si->sub_type == MI_THROWING_NET && (!trapped || item_is_stationary_net(*si))) { return si->index(); } } return NON_ITEM; } /** * Return a string describing the reason a given actor is ensnared. (Since nets * & webs use the same status. * * @param actor The ensnared actor. * @return Either 'held in a net' or 'caught in a web'. */ const char* held_status(actor *act) { act = act ? act : &you; if (get_trapping_net(act->pos(), true) != NON_ITEM) return "held in a net"; else return "caught in a web"; } // If there are more than one net on this square // split off one of them for checking/setting values. static void _maybe_split_nets(item_def &item, const coord_def& where) { if (item.quantity == 1) { set_net_stationary(item); return; } item_def it; it.base_type = item.base_type; it.sub_type = item.sub_type; it.plus = item.plus; it.plus2 = item.plus2; it.flags = item.flags; it.special = item.special; it.quantity = --item.quantity; item_colour(it); item.quantity = 1; set_net_stationary(item); copy_item_to_grid(it, where); } static void _mark_net_trapping(const coord_def& where) { int net = get_trapping_net(where); if (net == NON_ITEM) { net = get_trapping_net(where, false); if (net != NON_ITEM) _maybe_split_nets(mitm[net], where); } } /** * Attempt to trap a monster in a net. * * @param mon The monster being trapped. * @param agent The entity doing the trapping. * @return Whether the monster was successfully trapped. */ bool monster_caught_in_net(monster* mon, actor* agent) { if (mon->body_size(PSIZE_BODY) >= SIZE_GIANT) { if (mons_near(mon) && !mon->visible_to(&you)) mpr("The net bounces off something gigantic!"); else simple_monster_message(mon, " is too large for the net to hold!"); return false; } if (mons_class_is_stationary(mon->type)) { if (you.see_cell(mon->pos())) { if (mon->visible_to(&you)) { mprf("The net is caught on %s!", mon->name(DESC_THE).c_str()); } else mpr("The net is caught on something unseen!"); } return false; } if (mon->is_insubstantial()) { if (you.can_see(mon)) { mprf("The net passes right through %s!", mon->name(DESC_THE).c_str()); } return false; } if (mon->type == MONS_OOZE) { simple_monster_message(mon, " oozes right through the net!"); return false; } if (!mon->caught() && mon->add_ench(ENCH_HELD)) { if (mons_near(mon) && !mon->visible_to(&you)) mpr("Something gets caught in the net!"); else simple_monster_message(mon, " is caught in the net!"); return true; } return false; } bool player_caught_in_net() { if (you.body_size(PSIZE_BODY) >= SIZE_GIANT) return false; if (!you.attribute[ATTR_HELD]) { mpr("You become entangled in the net!"); stop_running(); // Set the attribute after the mpr, otherwise the screen updates // and we get a glimpse of a web because there isn't a trapping net // item yet you.attribute[ATTR_HELD] = 10; stop_delay(true); // even stair delays return true; } return false; } void check_net_will_hold_monster(monster* mons) { if (mons->body_size(PSIZE_BODY) >= SIZE_GIANT) { int net = get_trapping_net(mons->pos()); if (net != NON_ITEM) destroy_item(net); if (you.see_cell(mons->pos())) { if (mons->visible_to(&you)) { mprf("The net rips apart, and %s comes free!", mons->name(DESC_THE).c_str()); } else mpr("All of a sudden the net rips apart!"); } } else if (mons->is_insubstantial() || mons->type == MONS_OOZE) { const int net = get_trapping_net(mons->pos()); if (net != NON_ITEM) free_stationary_net(net); if (mons->is_insubstantial()) { simple_monster_message(mons, " drifts right through the net!"); } else { simple_monster_message(mons, " oozes right through the net!"); } } else mons->add_ench(ENCH_HELD); } static bool _player_caught_in_web() { if (you.attribute[ATTR_HELD]) return false; you.attribute[ATTR_HELD] = 10; // No longer stop_running() and stop_delay(). redraw_screen(); // Account for changes in display. return true; } vector find_golubria_on_level() { vector ret; for (rectangle_iterator ri(coord_def(0, 0), coord_def(GXM-1, GYM-1)); ri; ++ri) { trap_def *trap = find_trap(*ri); if (trap && trap->type == TRAP_GOLUBRIA) ret.push_back(*ri); } return ret; } static bool _find_other_passage_side(coord_def& to) { vector passages = find_golubria_on_level(); vector clear_passages; for (unsigned int i = 0; i < passages.size(); i++) { if (passages[i] != to && !actor_at(passages[i])) clear_passages.push_back(passages[i]); } const int choices = clear_passages.size(); if (choices < 1) return false; to = clear_passages[random2(choices)]; return true; } // Returns a direction string from you.pos to the // specified position. If fuzz is true, may be wrong. // Returns an empty string if no direction could be // determined (if fuzz if false, this is only if // you.pos==pos). static string _direction_string(coord_def pos, bool fuzz) { int dx = you.pos().x - pos.x; if (fuzz) dx += random2avg(41,2) - 20; int dy = you.pos().y - pos.y; if (fuzz) dy += random2avg(41,2) - 20; const char *ew=((dx > 0) ? "west" : ((dx < 0) ? "east" : "")); const char *ns=((dy < 0) ? "south" : ((dy > 0) ? "north" : "")); if (abs(dy) > 2 * abs(dx)) ew=""; if (abs(dx) > 2 * abs(dy)) ns=""; return string(ns) + ew; } void trap_def::trigger(actor& triggerer, bool flat_footed) { const bool you_know = is_known(); const bool trig_knows = !flat_footed && is_known(&triggerer); const bool you_trigger = (triggerer.is_player()); const bool in_sight = you.see_cell(pos); // Zot def - player never sets off known traps if (crawl_state.game_is_zotdef() && you_trigger && you_know) { mpr("You step safely past the trap."); return; } // If set, the trap will be removed at the end of the // triggering process. bool trap_destroyed = false, know_trap_destroyed = false;; monster* m = triggerer.as_monster(); // Smarter monsters and those native to the level will simply // side-step known shafts. Unless they are already looking for // an exit, of course. if (type == TRAP_SHAFT && m) { if (!m->will_trigger_shaft() || trig_knows && !mons_is_fleeing(m) && !m->pacified()) { // No message for flying monsters to avoid message spam. if (you_know && triggerer.ground_level()) simple_monster_message(m, " carefully avoids the shaft."); return; } } // Zot def - friendly monsters never set off known traps if (crawl_state.game_is_zotdef() && m && m->friendly() && trig_knows) { simple_monster_message(m," carefully avoids a trap."); return; } // Only magical traps and webs affect flying critters. if (!triggerer.ground_level() && ground_only()) { if (you_know && m && triggerer.airborne()) simple_monster_message(m, " flies safely over a trap."); return; } // Anything stepping onto a trap almost always reveals it. // (We can rehide it later for the exceptions.) if (in_sight) reveal(); // Store the position now in case it gets cleared in between. const coord_def p(pos); if (type_has_ammo()) shoot_ammo(triggerer, trig_knows); else switch (type) { case TRAP_GOLUBRIA: { coord_def to = p; if (_find_other_passage_side(to)) { if (you_trigger) mpr("You enter the passage of Golubria."); else simple_monster_message(m, " enters the passage of Golubria."); if (triggerer.move_to_pos(to)) { if (you_trigger) place_cloud(CLOUD_TLOC_ENERGY, p, 1 + random2(3), &you); else place_cloud(CLOUD_TLOC_ENERGY, p, 1 + random2(3), m); trap_destroyed = true; know_trap_destroyed = you_trigger; } else mpr("But it is blocked!"); } break; } case TRAP_TELEPORT: case TRAP_TELEPORT_PERMANENT: // Never revealed by monsters. // except when it's in sight, it's pretty obvious what happened. -doy if (!you_trigger && !you_know && !in_sight) hide(); if (you_trigger) mprf("You enter %s!", name(DESC_A).c_str()); if (ammo_qty > 0 && !--ammo_qty) { // can't use trap_destroyed, as we might recurse into a shaft // or be banished by a Zot trap if (in_sight) { env.map_knowledge(pos).set_feature(DNGN_FLOOR); mprf("%s disappears.", name(DESC_THE).c_str()); } disarm(); } if (!triggerer.no_tele(true, you_know || you_trigger)) triggerer.teleport(true); break; case TRAP_ALARM: // In ZotDef, alarm traps don't go away after use. if (!crawl_state.game_is_zotdef()) trap_destroyed = true; if (silenced(pos)) { if (you_know && in_sight) { mprf("%s vibrates slightly, failing to make a sound.", name(DESC_THE).c_str()); } } else { string msg; if (you_trigger) { msg = make_stringf("%s emits a blaring wail!", name(DESC_THE).c_str()); } else { string dir = _direction_string(pos, !in_sight); msg = string("You hear a ") + ((in_sight) ? "" : "distant ") + "blaring wail " + (!dir.empty()? ("to the " + dir + ".") : "behind you!"); } // Monsters of normal or greater intelligence will realize that // they were the one to set off the trap. int source = !m ? you.mindex() : mons_intel(m) >= I_NORMAL ? m->mindex() : -1; noisy(40, pos, msg.c_str(), source, false); if (crawl_state.game_is_zotdef()) more(); } if (you_trigger) you.sentinel_mark(true); break; case TRAP_BLADE: if (you_trigger) { if (trig_knows && one_chance_in(3)) mpr("You avoid triggering a blade trap."); else if (random2limit(player_evasion(), 40) + (random2(you.dex()) / 3) + (trig_knows ? 3 : 0) > 8) { mpr("A huge blade swings just past you!"); } else { mpr("A huge blade swings out and slices into you!"); const int damage = you.apply_ac(48 + random2avg(29, 2)); string n = name(DESC_A) + " trap"; ouch(damage, NON_MONSTER, KILLED_BY_TRAP, n.c_str()); bleed_onto_floor(you.pos(), MONS_PLAYER, damage, true); } } else if (m) { if (one_chance_in(5) || (trig_knows && coinflip())) { // Trap doesn't trigger. Don't reveal it. if (you_know) { simple_monster_message(m, " fails to trigger a blade trap."); } else hide(); } else if (random2(m->ev) > 8 || (trig_knows && random2(m->ev) > 8)) { if (in_sight && !simple_monster_message(m, " avoids a huge, swinging blade.")) { mpr("A huge blade swings out!"); } } else { if (in_sight) { string msg = "A huge blade swings out"; if (m->visible_to(&you)) { msg += " and slices into "; msg += m->name(DESC_THE); } msg += "!"; mpr(msg.c_str()); } int damage_taken = m->apply_ac(10 + random2avg(29, 2)); if (!m->is_summoned()) bleed_onto_floor(m->pos(), m->type, damage_taken, true); m->hurt(NULL, damage_taken); if (in_sight && m->alive()) print_wounds(m); // zotdef: blade traps break eventually if (crawl_state.game_is_zotdef() && one_chance_in(200)) { if (in_sight) mpr("The blade breaks!"); disarm(); } } } break; case TRAP_NET: if (you_trigger) { if (trig_knows && one_chance_in(3)) mpr("A net swings high above you."); else { item_def item = generate_trap_item(); copy_item_to_grid(item, triggerer.pos()); if (random2limit(player_evasion(), 40) + (random2(you.dex()) / 3) + (trig_knows ? 3 : 0) > 12) { mpr("A net drops to the ground!"); } else { mpr("A large net falls onto you!"); if (player_caught_in_net()) { if (player_in_a_dangerous_place()) xom_is_stimulated(50); // Mark the item as trapping; after this it's // safe to update the view _mark_net_trapping(you.pos()); } } trap_destroyed = true; } } else if (m) { bool triggered = false; if (one_chance_in(3) || (trig_knows && coinflip())) { // Not triggered, trap stays. triggered = false; if (you_know) simple_monster_message(m, " fails to trigger a net trap."); else hide(); } else if (random2(m->ev) > 8 || (trig_knows && random2(m->ev) > 8)) { // Triggered but evaded. triggered = true; if (in_sight) { if (!simple_monster_message(m, " nimbly jumps out of the way " "of a falling net.")) { mpr("A large net falls down!"); } } } else { // Triggered and hit. triggered = true; if (in_sight) { if (m->visible_to(&you)) { mprf("A large net falls down onto %s!", m->name(DESC_THE).c_str()); } else mpr("A large net falls down!"); } // actually try to net the monster if (monster_caught_in_net(m, NULL)) { // Don't try to escape the net in the same turn m->props[NEWLY_TRAPPED_KEY] = true; } } if (triggered) { item_def item = generate_trap_item(); copy_item_to_grid(item, triggerer.pos()); if (m->caught()) _mark_net_trapping(m->pos()); trap_destroyed = true; } } break; case TRAP_WEB: if (triggerer.body_size(PSIZE_BODY) >= SIZE_GIANT) { trap_destroyed = true; if (you_trigger) mprf("You tear through %s web.", you_know ? "the" : "a"); else if (m) simple_monster_message(m, " tears through a web."); break; } if (triggerer.is_web_immune()) { if (m) { if (m->is_insubstantial()) simple_monster_message(m, " passes through a web."); else if (mons_genus(m->type) == MONS_JELLY) simple_monster_message(m, " oozes through a web."); // too spammy for spiders, and expected } break; } if (you_trigger) { if (trig_knows && one_chance_in(3)) mpr("You pick your way through the web."); else { mpr("You are caught in the web!"); if (_player_caught_in_web()) { check_monsters_sense(SENSE_WEB_VIBRATION, 100, you.pos()); if (player_in_a_dangerous_place()) xom_is_stimulated(50); } } } else if (m) { if (one_chance_in(3) || (trig_knows && coinflip())) { // Not triggered, trap stays. if (you_know) simple_monster_message(m, " evades a web."); else hide(); } else { // Triggered and hit. if (in_sight) { if (m->visible_to(&you)) simple_monster_message(m, " is caught in a web!"); else mpr("A web moves frantically as something is caught in it!"); } // If somehow already caught, make it worse. m->add_ench(ENCH_HELD); // Don't try to escape the web in the same turn m->props[NEWLY_TRAPPED_KEY] = true; // Alert monsters. check_monsters_sense(SENSE_WEB_VIBRATION, 100, triggerer.position); } } break; case TRAP_ZOT: if (you_trigger) { mpr((trig_knows) ? "You enter the Zot trap." : "Oh no! You have blundered into a Zot trap!"); if (!trig_knows) xom_is_stimulated(25); MiscastEffect(&you, ZOT_TRAP_MISCAST, SPTYP_RANDOM, 3, name(DESC_A)); } else if (m) { // Zot traps are out to get *the player*! Hostile monsters // benefit and friendly monsters suffer. Such is life. // The old code rehid the trap, but that's pure interface screw // in 99% of cases - a player can just watch who stepped where // and mark the trap on an external paper map. Not good. actor* targ = NULL; if (m->wont_attack() || crawl_state.game_is_arena()) targ = m; else if (you.see_cell_no_trans(pos) && one_chance_in(5)) targ = &you; // Give the player a chance to figure out what happened // to their friend. if (player_can_hear(pos) && (!targ || !in_sight)) { mprf(MSGCH_SOUND, "You hear a %s \"Zot\"!", in_sight ? "loud" : "distant"); } if (targ) { if (in_sight) { mprf("The power of Zot is invoked against %s!", targ->name(DESC_THE).c_str()); } MiscastEffect(targ, ZOT_TRAP_MISCAST, SPTYP_RANDOM, 3, "the power of Zot"); } } break; case TRAP_SHAFT: // Unknown shafts are traps triggered by walking onto them. // Known shafts are used as escape hatches // Paranoia if (!is_valid_shaft_level()) { if (you_know && in_sight) mpr("The shaft disappears in a puff of logic!"); trap_destroyed = true; break; } // If the shaft isn't known, don't reveal it. // The shafting code in downstairs() needs to know // whether it's undiscovered. if (!you_know) hide(); // Known shafts don't trigger as traps. if (trig_knows) break; // Depending on total (body + equipment) weight, give monsters // and player a chance to escape a shaft. if (x_chance_in_y(140, triggerer.body_weight())) break; // Fire away! triggerer.do_shaft(); // Player-used shafts are destroyed // after one use in down_stairs(), misc.cc if (!you_trigger) { if (in_sight) { mpr("The shaft crumbles and collapses."); know_trap_destroyed = true; } trap_destroyed = true; } break; #if TAG_MAJOR_VERSION == 34 case TRAP_GAS: if (in_sight && you_know) mpr("The gas trap seems to be inoperative."); trap_destroyed = true; break; #endif case TRAP_PLATE: dungeon_events.fire_position_event(DET_PRESSURE_PLATE, pos); break; default: break; } if (you_trigger) learned_something_new(HINT_SEEN_TRAP, p); if (trap_destroyed) destroy(know_trap_destroyed); } int trap_def::max_damage(const actor& act) { // Trap damage to monsters is a lot smaller, because they are fairly // stupid and tend to have fewer hp than players -- this choice prevents // traps from easily killing large monsters. bool mon = act.is_monster(); switch (type) { case TRAP_NEEDLE: return 0; case TRAP_ARROW: return mon ? 7 : 15; case TRAP_SPEAR: return mon ? 10 : 26; case TRAP_BOLT: return mon ? 18 : 40; case TRAP_BLADE: return mon ? 38 : 76; default: return 0; } return 0; } int trap_def::shot_damage(actor& act) { const int dam = max_damage(act); if (!dam) return 0; return random2(dam) + 1; } int trap_def::difficulty() { switch (type) { // To-hit and disarming: case TRAP_ARROW: return 7; case TRAP_SPEAR: return 10; case TRAP_BOLT: return 15; case TRAP_NET: return 5; case TRAP_NEEDLE: return 8; // Disarming only: case TRAP_BLADE: return 20; case TRAP_PLATE: return 15; case TRAP_WEB: return 12; #if TAG_MAJOR_VERSION == 34 case TRAP_GAS: return 15; case TRAP_DART: return 3; #endif // Irrelevant: default: return 0; } } int reveal_traps(const int range) { int traps_found = 0; for (int i = 0; i < MAX_TRAPS; i++) { trap_def& trap = env.trap[i]; if (!trap.active()) continue; if (distance2(you.pos(), trap.pos) < dist_range(range) && !trap.is_known()) { traps_found++; trap.reveal(); env.map_knowledge(trap.pos).set_feature(grd(trap.pos), 0, trap.type); set_terrain_mapped(trap.pos); } } return traps_found; } void destroy_trap(const coord_def& pos) { if (trap_def* ptrap = find_trap(pos)) ptrap->destroy(); } trap_def* find_trap(const coord_def& pos) { if (!feat_is_trap(grd(pos), true)) return NULL; unsigned short t = env.tgrid(pos); ASSERT(t != NON_ENTITY); ASSERT(t < MAX_TRAPS); ASSERT(env.trap[t].pos == pos); ASSERT(env.trap[t].type != TRAP_UNASSIGNED); return &env.trap[t]; } trap_type get_trap_type(const coord_def& pos) { if (trap_def* ptrap = find_trap(pos)) return ptrap->type; return TRAP_UNASSIGNED; } static bool _disarm_is_deadly(trap_def& trap) { int dam = trap.max_damage(you); if (trap.type == TRAP_NEEDLE && you.res_poison() <= 1) dam += 15; // arbitrary return you.hp <= dam; } void search_around() { ASSERT(!crawl_state.game_is_arena()); int base_skill = you.experience_level * 100 / 3; int skill = (2/(1+exp(-(base_skill+120)/325.0))-1) * 225 + (base_skill/200.0) + 15; if (you_worship(GOD_ASHENZARI) && !player_under_penance()) skill += you.piety * 2; int max_dist = div_rand_round(skill, 32); if (max_dist > 5) max_dist = 5; if (max_dist < 1) max_dist = 1; for (radius_iterator ri(you.pos(), max_dist, C_ROUND, LOS_NO_TRANS); ri; ++ri) { if (grd(*ri) != DNGN_UNDISCOVERED_TRAP) continue; int dist = ri->range(you.pos()); // Own square is not excluded; may be flying. // XXX: Currently, flying over a trap will always detect it. int effective = (dist <= 1) ? skill : skill / (dist * 2 - 1); trap_def* ptrap = find_trap(*ri); if (!ptrap) { // Maybe we shouldn't kill the trap for debugging // purposes - oh well. grd(*ri) = DNGN_FLOOR; dprf("You found a buggy trap! It vanishes!"); continue; } if (effective > ptrap->skill_rnd) { ptrap->reveal(); mprf("You found %s!", ptrap->name(DESC_A).c_str()); learned_something_new(HINT_SEEN_TRAP, *ri); } } } // where *must* point to a valid, discovered trap. void disarm_trap(const coord_def& where) { if (you.berserk()) { canned_msg(MSG_TOO_BERSERK); return; } trap_def& trap = *find_trap(where); switch (trap.category()) { case DNGN_TRAP_ALARM: // Zotdef - allow alarm traps to be disarmed if (crawl_state.game_is_zotdef()) break; case DNGN_TRAP_TELEPORT: case DNGN_TRAP_ZOT: mpr("You can't disarm magical traps."); return; case DNGN_PASSAGE_OF_GOLUBRIA: mpr("You can't disarm passages of Golubria."); return; case DNGN_TRAP_SHAFT: // Only shafts for now. mpr("You can't disarm shafts."); return; default: break; } // Prompt for any trap for which you might not survive setting it off. if (_disarm_is_deadly(trap)) { string prompt = make_stringf("Really try disarming that %s?", feature_description_at(where, false, DESC_BASENAME, false).c_str()); if (!yesno(prompt.c_str(), true, 'n')) { canned_msg(MSG_OK); return; } } // Make the actual attempt you.turn_is_over = true; if (random2(div_rand_round(you.experience_level, 3) + 2) <= random2(trap.difficulty() + 5)) { mpr("You failed to disarm the trap."); if (random2(you.dex()) <= 5 + random2(5 + trap.difficulty())) { if ((trap.type == TRAP_NET || trap.type == TRAP_WEB) && trap.pos != you.pos()) { if (coinflip()) { mpr("You stumble into the trap!"); move_player_to_grid(trap.pos, true); } } else trap.trigger(you, true); } } else { mpr("You have disarmed the trap."); trap.disarm(); } } // Decides whether you will try to tear the net (result <= 0) // or try to slip out of it (result > 0). // Both damage and escape could be 9 (more likely for damage) // but are capped at 5 (damage) and 4 (escape). static int damage_or_escape_net(int hold) { // Spriggan: little (+2) // Halfling, Kobold: small (+1) // Human, Elf, ...: medium (0) // Ogre, Troll, Centaur, Naga: large (-1) // transformations: spider, bat: tiny (+3); ice beast: large (-1) int escape = SIZE_MEDIUM - you.body_size(PSIZE_BODY); int damage = -escape; // your weapon may damage the net, max. bonus of 2 if (you.weapon()) { if (can_cut_meat(*you.weapon())) damage++; int brand = get_weapon_brand(*you.weapon()); if (brand == SPWPN_FLAMING || brand == SPWPN_VORPAL) damage++; } else if (you.form == TRAN_BLADE_HANDS) damage += 2; else if (you.has_usable_claws()) { int level = you.has_claws(); if (level == 1) damage += coinflip(); else damage += level - 1; } // Berserkers get a fighting bonus. if (you.berserk()) damage += 2; // Check stats. if (x_chance_in_y(you.strength(), 18)) damage++; if (x_chance_in_y(you.dex(), 12)) escape++; if (x_chance_in_y(player_evasion(), 20)) escape++; // Dangerous monsters around you add urgency. if (there_are_monsters_nearby(true)) { damage++; escape++; } // Confusion makes the whole thing somewhat harder // (less so for trying to escape). if (you.confused()) { if (escape > 1) escape--; else if (damage >= 2) damage -= 2; } // Damaged nets are easier to destroy. if (hold < 0) { damage += random2(-hold/3 + 1); // ... and easier to slip out of (but only if escape looks feasible). if (you.attribute[ATTR_HELD] < 5 || escape >= damage) escape += random2(-hold/2) + 1; } // If undecided, choose damaging approach (it's quicker). if (damage >= escape) return -damage; // negate value return escape; } // Calls the above function to decide on how to get free. // Note that usually the net will be damaged until trying to slip out // becomes feasible (for size etc.), so it may take even longer. void free_self_from_net() { int net = get_trapping_net(you.pos()); if (net == NON_ITEM) { trap_def *trap = find_trap(you.pos()); if (trap && trap->type == TRAP_WEB) { if (x_chance_in_y(40 - you.stat(STAT_STR), 66)) { mpr("You struggle to detach yourself from the web."); return; } maybe_destroy_web(&you); } you.attribute[ATTR_HELD] = 0; you.redraw_quiver = true; you.redraw_evasion = true; return; } int hold = mitm[net].plus; int do_what = damage_or_escape_net(hold); dprf("net.plus: %d, ATTR_HELD: %d, do_what: %d", hold, you.attribute[ATTR_HELD], do_what); if (do_what <= 0) // You try to destroy the net { // For previously undamaged nets this takes at least 2 and at most // 8 turns. bool can_slice = (you.form == TRAN_BLADE_HANDS) || (you.weapon() && can_cut_meat(*you.weapon())); int damage = -do_what; if (damage < 1) damage = 1; if (you.berserk()) damage *= 2; // Medium sized characters are at a disadvantage and sometimes // get a bonus. if (you.body_size(PSIZE_BODY) == SIZE_MEDIUM) damage += coinflip(); if (damage > 5) damage = 5; hold -= damage; mitm[net].plus = hold; if (hold < -7) { mprf("You %s the net and break free!", can_slice ? (damage >= 4? "slice" : "cut") : (damage >= 4? "shred" : "rip")); destroy_item(net); you.attribute[ATTR_HELD] = 0; you.redraw_quiver = true; you.redraw_evasion = true; return; } if (damage >= 4) { mprf("You %s into the net.", can_slice? "slice" : "tear a large gash"); } else mpr("You struggle against the net."); // Occasionally decrease duration a bit // (this is so switching from damage to escape does not hurt as much). if (you.attribute[ATTR_HELD] > 1 && coinflip()) { you.attribute[ATTR_HELD]--; if (you.attribute[ATTR_HELD] > 1 && hold < -random2(5)) you.attribute[ATTR_HELD]--; } } else { // You try to escape (takes at least 3 turns, and at most 10). int escape = do_what; if (you.duration[DUR_HASTE] || you.duration[DUR_BERSERK]) // extra bonus escape++; // Medium sized characters are at a disadvantage and sometimes // get a bonus. if (you.body_size(PSIZE_BODY) == SIZE_MEDIUM) escape += coinflip(); if (escape > 4) escape = 4; if (escape >= you.attribute[ATTR_HELD]) { if (escape >= 3) mpr("You slip out of the net!"); else mpr("You break free from the net!"); you.attribute[ATTR_HELD] = 0; you.redraw_quiver = true; you.redraw_evasion = true; free_stationary_net(net); return; } if (escape >= 3) mpr("You try to slip out of the net."); else mpr("You struggle to escape the net."); you.attribute[ATTR_HELD] -= escape; } } void mons_clear_trapping_net(monster* mon) { if (!mon->caught()) return; const int net = get_trapping_net(mon->pos()); if (net != NON_ITEM) free_stationary_net(net); mon->del_ench(ENCH_HELD, true); } void free_stationary_net(int item_index) { item_def &item = mitm[item_index]; if (item.base_type == OBJ_MISSILES && item.sub_type == MI_THROWING_NET) { const coord_def pos = item.pos; // Probabilistically mulch net based on damage done, otherwise // reset damage counter (ie: item.plus). if (x_chance_in_y(-item.plus, 9)) destroy_item(item_index); else { item.plus = 0; item.plus2 = 0; } // Make sure we don't leave a bad trapping net in the stash // FIXME: may leak info if a monster escapes an out-of-sight net. StashTrack.update_stash(pos); } } void clear_trapping_net() { if (!you.attribute[ATTR_HELD]) return; if (!in_bounds(you.pos())) return; const int net = get_trapping_net(you.pos()); if (net != NON_ITEM) free_stationary_net(net); you.attribute[ATTR_HELD] = 0; you.redraw_quiver = true; you.redraw_evasion = true; } item_def trap_def::generate_trap_item() { item_def item; object_class_type base; int sub; switch (type) { #if TAG_MAJOR_VERSION == 34 case TRAP_DART: base = OBJ_MISSILES; sub = MI_DART; break; #endif case TRAP_ARROW: base = OBJ_MISSILES; sub = MI_ARROW; break; case TRAP_BOLT: base = OBJ_MISSILES; sub = MI_BOLT; break; case TRAP_SPEAR: base = OBJ_WEAPONS; sub = WPN_SPEAR; break; case TRAP_NEEDLE: base = OBJ_MISSILES; sub = MI_NEEDLE; break; case TRAP_NET: base = OBJ_MISSILES; sub = MI_THROWING_NET; break; default: return item; } item.base_type = base; item.sub_type = sub; item.quantity = 1; if (base == OBJ_MISSILES) { set_item_ego_type(item, base, (sub == MI_NEEDLE) ? SPMSL_POISONED : SPMSL_NORMAL); } else set_item_ego_type(item, base, SPWPN_NORMAL); item_colour(item); return item; } // Shoot a single piece of ammo at the relevant actor. void trap_def::shoot_ammo(actor& act, bool was_known) { if (ammo_qty <= 0) { if (was_known && act.is_player()) mpr("The trap is out of ammunition!"); else if (player_can_hear(pos) && you.see_cell(pos)) mpr("You hear a soft click."); disarm(); return; } bool force_hit = (env.markers.property_at(pos, MAT_ANY, "force_hit") == "true"); if (act.is_player()) { if (!force_hit && (one_chance_in(5) || was_known && !one_chance_in(4))) { mprf("You avoid triggering %s.", name(DESC_A).c_str()); return; // no ammo generated either } } else if (!force_hit && one_chance_in(5)) { if (was_known && you.see_cell(pos) && you.can_see(&act)) { mprf("%s avoids triggering %s.", act.name(DESC_THE).c_str(), name(DESC_A).c_str()); } return; } item_def shot = generate_trap_item(); int trap_hit = (20 + (difficulty()*2)) * random2(200) / 100; if (int defl = act.missile_deflection()) trap_hit = random2(trap_hit / defl); const int con_block = random2(20 + act.shield_block_penalty()); const int pro_block = act.shield_bonus(); dprf("%s: hit %d EV %d, shield hit %d block %d", name(DESC_PLAIN).c_str(), trap_hit, act.melee_evasion(0), con_block, pro_block); // Determine whether projectile hits. if (!force_hit && trap_hit < act.melee_evasion(NULL)) { if (act.is_player()) { mprf("%s shoots out and misses you.", shot.name(DESC_A).c_str()); practise(EX_DODGE_TRAP); } else if (you.see_cell(act.pos())) { mprf("%s misses %s!", shot.name(DESC_A).c_str(), act.name(DESC_THE).c_str()); } } else if (!force_hit && pro_block >= con_block && you.see_cell(act.pos())) { string owner; if (act.is_player()) owner = "your"; else if (you.can_see(&act)) owner = apostrophise(act.name(DESC_THE)); else // "its" sounds abysmal; animals don't use shields owner = "someone's"; mprf("%s shoots out and hits %s shield.", shot.name(DESC_A).c_str(), owner.c_str()); act.shield_block_succeeded(0); } else // OK, we've been hit. { bool force_poison = (env.markers.property_at(pos, MAT_ANY, "poisoned_needle_trap") == "true"); bool poison = (type == TRAP_NEEDLE && (x_chance_in_y(50 - (3*act.armour_class()) / 2, 100) || force_poison)); int damage_taken = act.apply_ac(shot_damage(act)); if (act.is_player()) { mprf("%s shoots out and hits you!", shot.name(DESC_A).c_str()); string n = name(DESC_A) + " trap"; // Needle traps can poison. if (poison) poison_player(1 + roll_dice(2, 9), "", n); ouch(damage_taken, NON_MONSTER, KILLED_BY_TRAP, n.c_str()); } else { if (you.see_cell(act.pos())) { mprf("%s hits %s%s!", shot.name(DESC_A).c_str(), act.name(DESC_THE).c_str(), (damage_taken == 0 && !poison) ? ", but does no damage" : ""); } if (poison) act.poison(NULL, 3 + roll_dice(2, 5)); act.hurt(NULL, damage_taken); } } ammo_qty--; } // returns appropriate trap symbol dungeon_feature_type trap_def::category() const { return trap_category(type); } dungeon_feature_type trap_category(trap_type type) { switch (type) { case TRAP_WEB: return DNGN_TRAP_WEB; case TRAP_SHAFT: return DNGN_TRAP_SHAFT; case TRAP_TELEPORT: case TRAP_TELEPORT_PERMANENT: return DNGN_TRAP_TELEPORT; case TRAP_ALARM: return DNGN_TRAP_ALARM; case TRAP_ZOT: return DNGN_TRAP_ZOT; case TRAP_GOLUBRIA: return DNGN_PASSAGE_OF_GOLUBRIA; case TRAP_ARROW: case TRAP_SPEAR: case TRAP_BLADE: case TRAP_BOLT: case TRAP_NEEDLE: case TRAP_NET: #if TAG_MAJOR_VERSION == 34 case TRAP_GAS: case TRAP_DART: #endif case TRAP_PLATE: return DNGN_TRAP_MECHANICAL; default: die("placeholder trap type %d used", type); } } bool trap_def::ground_only() const { return type == TRAP_SHAFT || category() == DNGN_TRAP_MECHANICAL; } bool is_valid_shaft_level(const level_id &place) { if (crawl_state.test || crawl_state.game_is_sprint() || crawl_state.game_is_zotdef()) { return false; } if (!is_connected_branch(place)) return false; // Shafts are now allowed on the first two levels, as they have a // good chance of being detected. You'll also fall less deep. /* if (place == BRANCH_DUNGEON && you.depth < 3) return false; */ // Don't generate shafts in branches where teleport control // is prevented. Prevents player from going down levels without // reaching stairs, and also keeps player from getting stuck // on lower levels with the innability to use teleport control to // get back up. if (testbits(env.level_flags, LFLAG_NO_TELE_CONTROL)) return false; const Branch &branch = branches[place.branch]; // When generating levels, don't place a shaft on the level // immediately above the bottom of a branch if that branch is // significantly more dangerous than normal. int min_delta = 1; if (env.turns_on_level == -1 && branch.dangerous_bottom_level) min_delta = 2; return (brdepth[place.branch] - place.depth) >= min_delta; } static level_id _generic_shaft_dest(level_pos lpos, bool known = false) { level_id lid = lpos.id; if (!is_connected_branch(lid)) return lid; int curr_depth = lid.depth; int max_depth = brdepth[lid.branch]; // Shaft traps' behavior depends on whether it is entered intentionally. // Knowingly entering one is more likely to drop you 1 level. // Falling in unknowingly can drop you 1/2/3 levels with equal chance. if (known) { // Chances are 5/8s for 1 level, 2/8s for 2 levels, 1/8 for 3 levels int s = random2(8) + 1; if (s == 1) lid.depth += 3; else if (s <= 3) lid.depth += 2; else lid.depth += 1; } else { // 33.3% for 1, 2, 3 from D:3, less before lid.depth += 1 + random2(min(lid.depth, 3)); } if (lid.depth > max_depth) lid.depth = max_depth; if (lid.depth == curr_depth) return lid; // Only shafts on the level immediately above a dangerous branch // bottom will take you to that dangerous bottom, and shafts can't // be created during level generation time. // Include level 27 of the main dungeon here, but don't restrict // shaft creation (so don't set branch.dangerous_bottom_level). if (branches[lid.branch].dangerous_bottom_level && lid.depth == max_depth && (max_depth - curr_depth) > 1) { lid.depth--; } return lid; } level_id generic_shaft_dest(coord_def pos, bool known = false) { return _generic_shaft_dest(level_pos(level_id::current(), pos)); } /** * When a player falls through a shaft at a location, disperse items on the * target level. * * @param pos The location. * @param open_shaft If True and the location was seen, print a shaft opening * message, otherwise don't. */ void handle_items_on_shaft(const coord_def& pos, bool open_shaft) { if (!is_valid_shaft_level()) return; level_id dest = generic_shaft_dest(pos); if (dest == level_id::current()) return; int o = igrd(pos); if (o == NON_ITEM) return; bool need_open_message = env.map_knowledge(pos).seen() && open_shaft; while (o != NON_ITEM) { int next = mitm[o].link; if (mitm[o].defined() && !item_is_stationary_net(mitm[o])) { if (need_open_message) { mpr("A shaft opens up in the floor!"); grd(pos) = DNGN_TRAP_SHAFT; need_open_message = false; } if (env.map_knowledge(pos).visible()) { mprf("%s fall%s through the shaft.", mitm[o].name(DESC_INVENTORY).c_str(), mitm[o].quantity == 1 ? "s" : ""); env.map_knowledge(pos).clear_item(); StashTrack.update_stash(pos); } // Item will be randomly placed on the destination level. unlink_item(o); mitm[o].pos = INVALID_COORD; add_item_to_transit(dest, mitm[o]); mitm[o].base_type = OBJ_UNASSIGNED; mitm[o].quantity = 0; mitm[o].props.clear(); } o = next; } } /** * Get a number of traps to place on the current level. * * No traps are placed in either Temple or disconnected branches other than * Pandemonium. For other branches, we place 0-8 traps a level, averaged over * two dice. * @return A number of traps to be placed. */ int num_traps_for_place() { if (you.where_are_you == BRANCH_TEMPLE || (!player_in_connected_branch() && you.where_are_you != BRANCH_PANDEMONIUM)) { return 0; } return random2avg(9, 2); } static trap_type _random_trap_slime(int level_number) { trap_type type = NUM_TRAPS; if (random2(1 + level_number) > 14 && one_chance_in(3)) type = TRAP_ZOT; if (one_chance_in(5) && is_valid_shaft_level()) type = TRAP_SHAFT; if (one_chance_in(5) && !crawl_state.game_is_sprint()) type = TRAP_TELEPORT; if (one_chance_in(10)) type = TRAP_ALARM; return type; } static trap_type _random_trap_default(int level_number) { trap_type type = TRAP_ARROW; if ((random2(1 + level_number) > 1) && one_chance_in(4)) type = TRAP_NEEDLE; if (random2(1 + level_number) > 3) type = TRAP_SPEAR; if (type == TRAP_ARROW && one_chance_in(15)) type = TRAP_NET; if (random2(1 + level_number) > 7) type = TRAP_BOLT; if (random2(1 + level_number) > 14) type = TRAP_BLADE; if (random2(1 + level_number) > 14 && one_chance_in(3) || (player_in_branch(BRANCH_ZOT) && coinflip())) { type = TRAP_ZOT; } if (one_chance_in(20) && is_valid_shaft_level()) type = TRAP_SHAFT; if (one_chance_in(20) && !crawl_state.game_is_sprint()) type = TRAP_TELEPORT; if (one_chance_in(40) && level_number > 3) type = TRAP_ALARM; return type; } trap_type random_trap_for_place() { int level_number = env.absdepth0; if (player_in_branch(BRANCH_SLIME)) return _random_trap_slime(level_number); return _random_trap_default(level_number); } int count_traps(trap_type ttyp) { int num = 0; for (int tcount = 0; tcount < MAX_TRAPS; tcount++) if (env.trap[tcount].type == ttyp) num++; return num; } void place_webs(int num) { int slot = 0; for (int j = 0; j < num; j++) { for (;; slot++) { if (slot >= MAX_TRAPS) return; if (env.trap[slot].type == TRAP_UNASSIGNED) break; } trap_def& ts(env.trap[slot]); int tries; // this is hardly ever enough to place many webs, most of the time // it will fail prematurely. Which is fine. for (tries = 0; tries < 200; ++tries) { ts.pos.x = random2(GXM); ts.pos.y = random2(GYM); if (in_bounds(ts.pos) && grd(ts.pos) == DNGN_FLOOR && !map_masked(ts.pos, MMT_NO_TRAP)) { // Calculate weight. int weight = 0; for (adjacent_iterator ai(ts.pos); ai; ++ai) { // Solid wall? int solid_weight = 0; // Orthogonals weight three, diagonals 1. if (cell_is_solid(*ai)) { solid_weight = (ai->x == ts.pos.x || ai->y == ts.pos.y) ? 3 : 1; } weight += solid_weight; } // Maximum weight is 4*3+4*1 = 16 // *But* that would imply completely surrounded by rock (no point there) if (weight <= 16 && x_chance_in_y(weight + 2, 34)) break; } } if (tries >= 200) break; ts.type = TRAP_WEB; grd(ts.pos) = DNGN_UNDISCOVERED_TRAP; env.tgrid(ts.pos) = slot; ts.prepare_ammo(); // Reveal some webs if (coinflip()) ts.reveal(); } } bool maybe_destroy_web(actor *oaf) { trap_def *trap = find_trap(oaf->pos()); if (!trap || trap->type != TRAP_WEB) return false; if (coinflip()) { if (oaf->is_monster()) simple_monster_message(oaf->as_monster(), " pulls away from the web."); else mpr("You disentangle yourself."); return false; } if (oaf->is_monster()) simple_monster_message(oaf->as_monster(), " tears the web."); else mpr("The web tears apart."); destroy_trap(oaf->pos()); return true; } bool ensnare(actor *fly) { if (fly->is_web_immune()) return false; if (fly->caught()) { // currently webs are stateless so except for flavour it's a no-op if (fly->is_player()) mpr("You are even more entangled."); return false; } if (fly->body_size() >= SIZE_GIANT) { if (you.can_see(fly)) mprf("A web harmlessly splats on %s.", fly->name(DESC_THE).c_str()); return false; } // If we're over water, an open door, shop, portal, etc, the web will // fail to attach and you'll be released after a single turn. // Same if we're at max traps already. if (grd(fly->pos()) == DNGN_FLOOR && place_specific_trap(fly->pos(), TRAP_WEB) // succeeded, mark the web known discovered && grd(fly->pos()) == DNGN_UNDISCOVERED_TRAP && you.see_cell(fly->pos())) { grd(fly->pos()) = DNGN_TRAP_WEB; } if (fly->is_player()) { if (_player_caught_in_web()) // no fail, returns false if already held mpr("You are caught in a web!"); } else { simple_monster_message(fly->as_monster(), " is caught in a web!"); fly->as_monster()->add_ench(ENCH_HELD); } // Drowned? if (!fly->alive()) return true; check_monsters_sense(SENSE_WEB_VIBRATION, 100, fly->pos()); return true; }